@herbertgao/pi-extensions 2026.9.12 → 2026.9.13
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/README.md +4 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
- package/node_modules/@narumitw/pi-btw/README.md +21 -4
- package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
- package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
- package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
- package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
- package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
- package/node_modules/pi-multi-account/README.md +38 -11
- package/node_modules/pi-multi-account/index.ts +371 -96
- package/node_modules/pi-multi-account/package.json +5 -4
- package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
- package/node_modules/pi-multi-account/usage.ts +31 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
- package/node_modules/pi-typesafe/dist/auth.js +13 -7
- package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
- package/node_modules/pi-typesafe/dist/backends.js +33 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
- package/node_modules/pi-typesafe/dist/client.js +63 -39
- package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
- package/node_modules/pi-typesafe/dist/credentials.js +15 -8
- package/node_modules/pi-typesafe/dist/extension.js +4 -1
- package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
- package/node_modules/pi-typesafe/dist/login.js +17 -8
- package/node_modules/pi-typesafe/dist/schema.js +19 -13
- package/node_modules/pi-typesafe/package.json +1 -1
- package/package.json +5 -5
|
@@ -440,6 +440,53 @@ function escapeTerminalControls(text) {
|
|
|
440
440
|
}).join("");
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// src/conversation-context.ts
|
|
444
|
+
var MAX_CONTEXT_CHARS = 4e4;
|
|
445
|
+
function buildConversationContext(entries) {
|
|
446
|
+
const sections = [];
|
|
447
|
+
for (const entry of entries) {
|
|
448
|
+
if (entry.type !== "message" || !entry.message?.role) continue;
|
|
449
|
+
const role = entry.message.role;
|
|
450
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
451
|
+
const contentLines = extractContentLines(entry.message.content);
|
|
452
|
+
if (contentLines.length === 0) continue;
|
|
453
|
+
const label = role === "user" ? "User" : "Assistant";
|
|
454
|
+
const status = entry.message.stopReason && entry.message.stopReason !== "stop" ? ` (${entry.message.stopReason})` : "";
|
|
455
|
+
sections.push(`${label}${status}: ${contentLines.join("\n")}`);
|
|
456
|
+
}
|
|
457
|
+
return truncateFromStart(sections.join("\n\n"), MAX_CONTEXT_CHARS);
|
|
458
|
+
}
|
|
459
|
+
function extractContentLines(content) {
|
|
460
|
+
if (typeof content === "string") return [content.trim()].filter(Boolean);
|
|
461
|
+
if (!Array.isArray(content)) return [];
|
|
462
|
+
const lines = [];
|
|
463
|
+
for (const part of content) {
|
|
464
|
+
if (!part || typeof part !== "object") continue;
|
|
465
|
+
const block = part;
|
|
466
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
467
|
+
lines.push(block.text.trim());
|
|
468
|
+
} else if (block.type === "toolCall" && typeof block.name === "string") {
|
|
469
|
+
lines.push(`Tool call: ${block.name}(${formatJson(block.arguments)})`);
|
|
470
|
+
} else if (block.type === "toolResult" && typeof block.name === "string") {
|
|
471
|
+
lines.push(`Tool result from ${block.name}: ${formatJson(block.result)}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return lines.filter(Boolean);
|
|
475
|
+
}
|
|
476
|
+
function formatJson(value) {
|
|
477
|
+
if (value === void 0) return "";
|
|
478
|
+
try {
|
|
479
|
+
return JSON.stringify(value);
|
|
480
|
+
} catch {
|
|
481
|
+
return String(value);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function truncateFromStart(text, maxChars) {
|
|
485
|
+
if (text.length <= maxChars) return text;
|
|
486
|
+
return `[Earlier context omitted; showing the last ${maxChars} characters.]
|
|
487
|
+
${text.slice(-maxChars)}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
443
490
|
// src/fullscreen-ui.ts
|
|
444
491
|
import { spawn } from "node:child_process";
|
|
445
492
|
import {
|
|
@@ -451,7 +498,7 @@ import {
|
|
|
451
498
|
Key as Key2,
|
|
452
499
|
parseKey,
|
|
453
500
|
TuiAltScreen,
|
|
454
|
-
truncateToWidth as
|
|
501
|
+
truncateToWidth as truncateToWidth3
|
|
455
502
|
} from "@earendil-works/pi-tui";
|
|
456
503
|
|
|
457
504
|
// src/keybindings.ts
|
|
@@ -689,977 +736,1653 @@ var BtwPasteGuard = class {
|
|
|
689
736
|
}
|
|
690
737
|
};
|
|
691
738
|
|
|
692
|
-
// src/
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
theme,
|
|
717
|
-
keybindings,
|
|
718
|
-
ctx,
|
|
719
|
-
run,
|
|
720
|
-
(value) => {
|
|
721
|
-
try {
|
|
722
|
-
liveEditorText = ctx.ui.getEditorText();
|
|
723
|
-
restoreEditor = true;
|
|
724
|
-
} catch {
|
|
725
|
-
}
|
|
726
|
-
done(value);
|
|
727
|
-
},
|
|
728
|
-
createTui,
|
|
729
|
-
options
|
|
730
|
-
);
|
|
731
|
-
return host;
|
|
732
|
-
},
|
|
733
|
-
{
|
|
734
|
-
overlay: true,
|
|
735
|
-
onHandle: (handle) => host?.setParentOverlay(handle)
|
|
736
|
-
}
|
|
739
|
+
// src/workspace-layout.ts
|
|
740
|
+
import {
|
|
741
|
+
HStack,
|
|
742
|
+
isFocusable,
|
|
743
|
+
ScrollView,
|
|
744
|
+
truncateToWidth as truncateToWidth2,
|
|
745
|
+
visibleWidth as visibleWidth2
|
|
746
|
+
} from "@earendil-works/pi-tui";
|
|
747
|
+
|
|
748
|
+
// src/settings.ts
|
|
749
|
+
import { randomUUID } from "node:crypto";
|
|
750
|
+
import { constants } from "node:fs";
|
|
751
|
+
import { mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
752
|
+
import { basename, dirname, join } from "node:path";
|
|
753
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
754
|
+
|
|
755
|
+
// src/side-thread.ts
|
|
756
|
+
var BTW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
757
|
+
function createSideThread(conversationContext) {
|
|
758
|
+
return { conversationContext, turns: [] };
|
|
759
|
+
}
|
|
760
|
+
function buildSideThreadMessages(thread, question) {
|
|
761
|
+
const answeredTurns = thread.turns.filter(
|
|
762
|
+
(turn) => turn.kind === "answered"
|
|
737
763
|
);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
}
|
|
764
|
+
const messages = [];
|
|
765
|
+
if (answeredTurns.length === 0) {
|
|
766
|
+
messages.push(createUserMessage(buildUserPrompt(question, thread.conversationContext)));
|
|
767
|
+
return messages;
|
|
743
768
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
function dispatchBtwInput(listeners, data) {
|
|
749
|
-
let current = data;
|
|
750
|
-
for (const group of [listeners.beforeAll, listeners.beforeViewport, listeners.regular]) {
|
|
751
|
-
for (const listener of group) {
|
|
752
|
-
const result = listener(current);
|
|
753
|
-
if (result?.consume) return result;
|
|
754
|
-
if (result?.data !== void 0) current = result.data;
|
|
755
|
-
}
|
|
769
|
+
const [first, ...rest] = answeredTurns;
|
|
770
|
+
messages.push(createUserMessage(buildUserPrompt(first.question, thread.conversationContext)), first.response);
|
|
771
|
+
for (const turn of rest) {
|
|
772
|
+
messages.push(createUserMessage(buildFollowUpPrompt(turn.question)), turn.response);
|
|
756
773
|
}
|
|
757
|
-
|
|
774
|
+
messages.push(createUserMessage(buildFollowUpPrompt(question)));
|
|
775
|
+
return messages;
|
|
758
776
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
777
|
+
async function completeSideThreadTurn({
|
|
778
|
+
thread,
|
|
779
|
+
model,
|
|
780
|
+
question,
|
|
781
|
+
thinkingLevel,
|
|
782
|
+
auth,
|
|
783
|
+
signal,
|
|
784
|
+
completeSimple,
|
|
785
|
+
sessionId
|
|
786
|
+
}) {
|
|
787
|
+
if (signal?.aborted) return { kind: "aborted" };
|
|
788
|
+
try {
|
|
789
|
+
const response = await completeSimple(
|
|
790
|
+
model,
|
|
791
|
+
{ systemPrompt: SYSTEM_PROMPT, messages: buildSideThreadMessages(thread, question) },
|
|
792
|
+
buildStreamOptions(
|
|
793
|
+
auth,
|
|
794
|
+
{ thinkingLevel, signal, model, sessionId },
|
|
795
|
+
completeSimple.appliesRequestHeaderTransforms === true
|
|
796
|
+
)
|
|
797
|
+
);
|
|
798
|
+
if (signal?.aborted || response?.stopReason === "aborted") return { kind: "aborted" };
|
|
799
|
+
if (!isAssistantMessage(response)) {
|
|
800
|
+
return { kind: "error", message: "The side model returned a malformed response." };
|
|
774
801
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (!listeners) return super.addInputListener(listener);
|
|
781
|
-
listeners.beforeAll.add(listener);
|
|
782
|
-
return () => listeners.beforeAll.delete(listener);
|
|
783
|
-
}
|
|
784
|
-
addInputListenerBeforeViewport(listener) {
|
|
785
|
-
const listeners = btwInputListeners.get(this);
|
|
786
|
-
if (!listeners) return super.addInputListener(listener);
|
|
787
|
-
listeners.beforeViewport.add(listener);
|
|
788
|
-
return () => listeners.beforeViewport.delete(listener);
|
|
789
|
-
}
|
|
790
|
-
removeInputListener(listener) {
|
|
791
|
-
const listeners = btwInputListeners.get(this);
|
|
792
|
-
if (!listeners) {
|
|
793
|
-
super.removeInputListener(listener);
|
|
794
|
-
return;
|
|
802
|
+
if (response.stopReason === "error") {
|
|
803
|
+
return {
|
|
804
|
+
kind: "error",
|
|
805
|
+
message: response.errorMessage ?? "The side model returned an error."
|
|
806
|
+
};
|
|
795
807
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
808
|
+
const answer = extractAssistantText(response) || "No response received.";
|
|
809
|
+
thread.turns.push({ kind: "answered", question, answer, response });
|
|
810
|
+
return { kind: "answered", response, answer };
|
|
811
|
+
} catch (error) {
|
|
812
|
+
if (signal?.aborted) return { kind: "aborted" };
|
|
813
|
+
return { kind: "error", message: formatError(error) };
|
|
799
814
|
}
|
|
800
|
-
};
|
|
801
|
-
var BRACKETED_PASTE_START = "\x1B[200~";
|
|
802
|
-
var BRACKETED_PASTE_END = "\x1B[201~";
|
|
803
|
-
var ALT_SCREEN_ACTIONS_BEFORE_BOTTOM = [
|
|
804
|
-
"tui.altScreen.search",
|
|
805
|
-
"tui.altScreen.searchNext",
|
|
806
|
-
"tui.altScreen.searchPrevious",
|
|
807
|
-
"tui.altScreen.searchClose",
|
|
808
|
-
"tui.altScreen.pageUp",
|
|
809
|
-
"tui.altScreen.pageDown",
|
|
810
|
-
"tui.altScreen.halfPageUp",
|
|
811
|
-
"tui.altScreen.halfPageDown",
|
|
812
|
-
"tui.altScreen.lineUp",
|
|
813
|
-
"tui.altScreen.lineDown",
|
|
814
|
-
"tui.altScreen.previousPrompt",
|
|
815
|
-
"tui.altScreen.nextPrompt",
|
|
816
|
-
"tui.altScreen.top"
|
|
817
|
-
];
|
|
818
|
-
var KEY_MODIFIER_ORDER = ["shift", "ctrl", "alt", "super"];
|
|
819
|
-
var MATCHABLE_SPECIAL_KEYS = /* @__PURE__ */ new Set([
|
|
820
|
-
"space",
|
|
821
|
-
"tab",
|
|
822
|
-
"enter",
|
|
823
|
-
"backspace",
|
|
824
|
-
"delete",
|
|
825
|
-
"insert",
|
|
826
|
-
"home",
|
|
827
|
-
"end",
|
|
828
|
-
"pageup",
|
|
829
|
-
"pagedown",
|
|
830
|
-
"up",
|
|
831
|
-
"down",
|
|
832
|
-
"left",
|
|
833
|
-
"right"
|
|
834
|
-
]);
|
|
835
|
-
var MATCHABLE_SYMBOL_KEYS = new Set("`-=[]\\;',./!@#$%^&*()_+|~{}:<>?");
|
|
836
|
-
function normalizedKeyId(key) {
|
|
837
|
-
const parts = key.toLowerCase().split("+");
|
|
838
|
-
const base = parts.at(-1);
|
|
839
|
-
if (!base) return "";
|
|
840
|
-
const normalizedBase = base === "esc" ? "escape" : base === "return" ? "enter" : base;
|
|
841
|
-
const modifiers = KEY_MODIFIER_ORDER.filter((modifier) => parts.includes(modifier));
|
|
842
|
-
return [...modifiers, normalizedBase].join("+");
|
|
843
815
|
}
|
|
844
|
-
function
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
if (base === "pagedown") parts[parts.length - 1] = "pageDown";
|
|
849
|
-
return formatKeyLabel2(parts.join("+"));
|
|
816
|
+
function extractAssistantText(response) {
|
|
817
|
+
return response.content.filter(
|
|
818
|
+
(content) => content !== null && typeof content === "object" && content.type === "text" && typeof content.text === "string"
|
|
819
|
+
).map((content) => content.text).join("\n").trim();
|
|
850
820
|
}
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
if (base === "escape") return modifiers.length === 0;
|
|
856
|
-
if (base === "clear") {
|
|
857
|
-
return modifiers.length === 0 || modifiers.length === 1 && (modifiers[0] === "shift" || modifiers[0] === "ctrl");
|
|
858
|
-
}
|
|
859
|
-
if (/^f(?:[1-9]|1[0-2])$/u.test(base)) return modifiers.length === 0;
|
|
860
|
-
return MATCHABLE_SPECIAL_KEYS.has(base) || base.length === 1 && (/^[a-z0-9]$/u.test(base) || MATCHABLE_SYMBOL_KEYS.has(base));
|
|
821
|
+
function isAssistantMessage(value) {
|
|
822
|
+
if (value === null || typeof value !== "object") return false;
|
|
823
|
+
const candidate = value;
|
|
824
|
+
return candidate.role === "assistant" && Array.isArray(candidate.content) && typeof candidate.stopReason === "string";
|
|
861
825
|
}
|
|
862
|
-
function
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
826
|
+
function buildUserPrompt(question, conversationContext) {
|
|
827
|
+
return [
|
|
828
|
+
"Answer this side question without modifying the main conversation.",
|
|
829
|
+
"",
|
|
830
|
+
"<side_question>",
|
|
831
|
+
question,
|
|
832
|
+
"</side_question>",
|
|
833
|
+
"",
|
|
834
|
+
"<conversation_context>",
|
|
835
|
+
conversationContext || "No prior conversation context was available.",
|
|
836
|
+
"</conversation_context>"
|
|
837
|
+
].join("\n");
|
|
867
838
|
}
|
|
868
|
-
function
|
|
869
|
-
|
|
870
|
-
const base = parts.at(-1) ?? "";
|
|
871
|
-
if (parts.length === 2 && parts[0] === "ctrl") return rawCtrlInput(base);
|
|
872
|
-
if (isKittyProtocolActive2()) return void 0;
|
|
873
|
-
if (parts.length === 2 && parts[0] === "alt" && base.length === 1) return `\x1B${base}`;
|
|
874
|
-
if (parts.length === 3 && parts[0] === "ctrl" && parts[1] === "alt") {
|
|
875
|
-
const input = rawCtrlInput(base);
|
|
876
|
-
return input ? `\x1B${input}` : void 0;
|
|
877
|
-
}
|
|
878
|
-
return void 0;
|
|
839
|
+
function buildFollowUpPrompt(question) {
|
|
840
|
+
return ["Continue the same side conversation.", "", "<side_question>", question, "</side_question>"].join("\n");
|
|
879
841
|
}
|
|
880
|
-
function
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
842
|
+
function createUserMessage(text) {
|
|
843
|
+
return {
|
|
844
|
+
role: "user",
|
|
845
|
+
content: [{ type: "text", text }],
|
|
846
|
+
timestamp: Date.now()
|
|
847
|
+
};
|
|
884
848
|
}
|
|
885
|
-
function
|
|
886
|
-
|
|
849
|
+
function getOpencodeSessionHeaders(model, sessionId) {
|
|
850
|
+
if (!sessionId || model.provider !== "opencode" && model.provider !== "opencode-go") return void 0;
|
|
851
|
+
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
|
887
852
|
}
|
|
888
|
-
function
|
|
889
|
-
if (!
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
for (const actionKey of keybindings.getKeys(action)) {
|
|
903
|
-
unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
if (!copyOnSelect) {
|
|
907
|
-
for (const copyKey of keybindings.getKeys("app.message.copy")) {
|
|
908
|
-
unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
const key = keybindings.getKeys("tui.altScreen.bottom").map((candidate) => keyInputIdentity(String(candidate))).find(
|
|
912
|
-
(identity) => identity && canMatchKeyInput(identity) && !unavailableKeyIdentities.has(identity) && formatEffectiveKeyLabel(identity)
|
|
913
|
-
);
|
|
914
|
-
const label = theme.fg("text", " \u2193 Jump to latest message");
|
|
915
|
-
const shortcut = key ? theme.fg("muted", ` \xB7 ${formatEffectiveKeyLabel(key)}`) : "";
|
|
916
|
-
return theme.bg("selectedBg", `${label}${shortcut} `);
|
|
917
|
-
},
|
|
918
|
-
searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
|
|
919
|
-
openUrl,
|
|
920
|
-
copySelection: async (text) => {
|
|
921
|
-
try {
|
|
922
|
-
await copyToClipboard2(text);
|
|
923
|
-
return true;
|
|
924
|
-
} catch {
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
});
|
|
929
|
-
if (!copyOnSelect) {
|
|
930
|
-
let isInBracketedPaste = false;
|
|
931
|
-
fullscreen.addInputListenerBeforeViewport((data) => {
|
|
932
|
-
const wasInBracketedPaste = isInBracketedPaste;
|
|
933
|
-
const startsBracketedPaste = data.includes(BRACKETED_PASTE_START);
|
|
934
|
-
if (startsBracketedPaste) isInBracketedPaste = true;
|
|
935
|
-
if (isInBracketedPaste && data.includes(BRACKETED_PASTE_END)) {
|
|
936
|
-
isInBracketedPaste = false;
|
|
937
|
-
}
|
|
938
|
-
if (wasInBracketedPaste || startsBracketedPaste || fullscreen.hasFocusedOverlay() || isKeyRelease2(data) || !keybindings.matches(data, "app.message.copy")) {
|
|
939
|
-
return void 0;
|
|
940
|
-
}
|
|
941
|
-
if (!fullscreen.hasActiveSelection()) {
|
|
942
|
-
fullscreen.flash("No selection to copy");
|
|
943
|
-
return { consume: true };
|
|
944
|
-
}
|
|
945
|
-
void fullscreen.copyActiveSelectionToClipboard().catch(() => fullscreen.flash("Copy failed"));
|
|
946
|
-
return { consume: true };
|
|
947
|
-
});
|
|
853
|
+
function mergeSessionHeaders(authHeaders, sessionHeaders) {
|
|
854
|
+
if (!sessionHeaders && !authHeaders) return void 0;
|
|
855
|
+
return { ...sessionHeaders, ...authHeaders };
|
|
856
|
+
}
|
|
857
|
+
function buildStreamOptions(auth, { thinkingLevel, signal, model, sessionId }, applyRequestHeaderTransforms) {
|
|
858
|
+
const sessionHeaders = model ? getOpencodeSessionHeaders(model, sessionId) : void 0;
|
|
859
|
+
const options = {
|
|
860
|
+
apiKey: auth?.apiKey,
|
|
861
|
+
headers: applyRequestHeaderTransforms ? auth?.headers : mergeSessionHeaders(auth?.headers, sessionHeaders),
|
|
862
|
+
env: auth?.env,
|
|
863
|
+
signal
|
|
864
|
+
};
|
|
865
|
+
if (applyRequestHeaderTransforms && sessionHeaders) {
|
|
866
|
+
options.transformHeaders = (headers) => mergeSessionHeaders(headers, sessionHeaders) ?? {};
|
|
948
867
|
}
|
|
949
|
-
|
|
868
|
+
if (thinkingLevel !== "off") options.reasoning = thinkingLevel;
|
|
869
|
+
return options;
|
|
950
870
|
}
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => {
|
|
954
|
-
}).unref();
|
|
871
|
+
function formatError(error) {
|
|
872
|
+
return error instanceof Error ? error.message : String(error);
|
|
955
873
|
}
|
|
956
|
-
var
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
874
|
+
var SYSTEM_PROMPT = `You answer quick side questions for a coding-agent user.
|
|
875
|
+
|
|
876
|
+
Use the provided conversation context only as background. Answer the user's side question directly and concisely. Do not claim to have changed files, run tools, or affected the main task. If the context is insufficient, say what is unknown and give the best next step.`;
|
|
877
|
+
|
|
878
|
+
// src/settings.ts
|
|
879
|
+
var BTW_SETTINGS_FILE = "pi-btw.json";
|
|
880
|
+
var BTW_LAYOUTS = ["fullscreen", "left-pane", "right-pane"];
|
|
881
|
+
var DEFAULT_BTW_LAYOUT = "fullscreen";
|
|
882
|
+
var DEFAULT_FULLSCREEN_COPY_ON_SELECT = true;
|
|
883
|
+
var DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES = true;
|
|
884
|
+
var DEFAULT_BTW_SIDE_PANE_RATIO = 0.5;
|
|
885
|
+
var MIN_BTW_SIDE_PANE_RATIO = 0.2;
|
|
886
|
+
var MAX_BTW_SIDE_PANE_RATIO = 0.8;
|
|
887
|
+
var MAX_SETTINGS_BYTES = 64 * 1024;
|
|
888
|
+
var mutationQueues = /* @__PURE__ */ new Map();
|
|
889
|
+
function btwSettingsPath() {
|
|
890
|
+
return join(getAgentDir(), BTW_SETTINGS_FILE);
|
|
891
|
+
}
|
|
892
|
+
function normalizeBtwSettings(value) {
|
|
893
|
+
if (!isSettingsDocument(value)) return void 0;
|
|
894
|
+
const settings = {};
|
|
895
|
+
if (Object.hasOwn(value, "keybindings")) {
|
|
896
|
+
const keys = value.keybindings;
|
|
897
|
+
if (!isSettingsDocument(keys)) return void 0;
|
|
898
|
+
settings.keybindings = {};
|
|
899
|
+
for (const action of BTW_SHORTCUT_ACTIONS) {
|
|
900
|
+
if (!Object.hasOwn(keys, action)) continue;
|
|
901
|
+
const key = normalizeBtwKey(keys[action]);
|
|
902
|
+
if (!key) return void 0;
|
|
903
|
+
settings.keybindings[action] = key;
|
|
904
|
+
}
|
|
967
905
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
run;
|
|
973
|
-
done;
|
|
974
|
-
createTui;
|
|
975
|
-
options;
|
|
976
|
-
fullscreen;
|
|
977
|
-
parentOverlay;
|
|
978
|
-
cancelActiveCustom;
|
|
979
|
-
hardCancelActiveCustom;
|
|
980
|
-
removeHardCancelListener;
|
|
981
|
-
removeUpstreamAbortListener;
|
|
982
|
-
started = false;
|
|
983
|
-
disposed = false;
|
|
984
|
-
finished = false;
|
|
985
|
-
parentStopped = false;
|
|
986
|
-
parentRestoreAttempted = false;
|
|
987
|
-
fullscreenCreated = false;
|
|
988
|
-
fullscreenStopped = false;
|
|
989
|
-
parentRestoreQueued = false;
|
|
990
|
-
parentRestorePromise;
|
|
991
|
-
cleanupError;
|
|
992
|
-
lifetimeController = new AbortController();
|
|
993
|
-
setParentOverlay(overlay) {
|
|
994
|
-
this.parentOverlay = overlay;
|
|
906
|
+
if (Object.hasOwn(value, "model")) {
|
|
907
|
+
const model = Reflect.get(value, "model");
|
|
908
|
+
if (typeof model !== "string" || !parseBtwModelReference(model)) return void 0;
|
|
909
|
+
settings.model = model;
|
|
995
910
|
}
|
|
996
|
-
|
|
997
|
-
|
|
911
|
+
if (Object.hasOwn(value, "thinkingLevel")) {
|
|
912
|
+
const thinkingLevel = Reflect.get(value, "thinkingLevel");
|
|
913
|
+
if (!isBtwThinkingLevel(thinkingLevel)) return void 0;
|
|
914
|
+
settings.thinkingLevel = thinkingLevel;
|
|
998
915
|
}
|
|
999
|
-
|
|
916
|
+
if (Object.hasOwn(value, "rememberThinkingLevelChanges")) {
|
|
917
|
+
const remember = Reflect.get(value, "rememberThinkingLevelChanges");
|
|
918
|
+
if (typeof remember !== "boolean") return void 0;
|
|
919
|
+
settings.rememberThinkingLevelChanges = remember;
|
|
1000
920
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
this.cancelActiveCustom?.();
|
|
921
|
+
if (Object.hasOwn(value, "fullscreenCopyOnSelect")) {
|
|
922
|
+
const copyOnSelect = Reflect.get(value, "fullscreenCopyOnSelect");
|
|
923
|
+
if (typeof copyOnSelect !== "boolean") return void 0;
|
|
924
|
+
settings.fullscreenCopyOnSelect = copyOnSelect;
|
|
1006
925
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
let outcome;
|
|
1012
|
-
try {
|
|
1013
|
-
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
1014
|
-
this.parent.stop({ preserveScreen: true });
|
|
1015
|
-
this.parentStopped = true;
|
|
1016
|
-
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
1017
|
-
this.fullscreen = this.createTui(this.parent, this.theme, this.keybindings, this.options);
|
|
1018
|
-
this.fullscreenCreated = true;
|
|
1019
|
-
this.fullscreen.start();
|
|
1020
|
-
const shortcuts = resolveBtwShortcuts(
|
|
1021
|
-
this.options.keybindings,
|
|
1022
|
-
this.keybindings,
|
|
1023
|
-
this.options.copyOnSelect ?? true
|
|
1024
|
-
);
|
|
1025
|
-
setBtwShortcuts(this.fullscreen, shortcuts);
|
|
1026
|
-
let previousWarnings = [];
|
|
1027
|
-
const reportWarnings = () => {
|
|
1028
|
-
const warnings = shortcuts.warnings;
|
|
1029
|
-
for (const warning of warnings) {
|
|
1030
|
-
if (previousWarnings.includes(warning)) continue;
|
|
1031
|
-
try {
|
|
1032
|
-
this.ctx.ui.notify(`Pi BTW: ${warning}`, "warning");
|
|
1033
|
-
} catch {
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
previousWarnings = warnings;
|
|
1037
|
-
};
|
|
1038
|
-
const pasteGuard = new BtwPasteGuard();
|
|
1039
|
-
const addHardCancelListener = this.fullscreen.addInputListenerBeforeAll?.bind(this.fullscreen) ?? this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ?? this.fullscreen.addInputListener.bind(this.fullscreen);
|
|
1040
|
-
this.removeHardCancelListener = addHardCancelListener((data) => {
|
|
1041
|
-
reportWarnings();
|
|
1042
|
-
if (pasteGuard.consume(data) || !shortcuts.matches(data, "exit")) return void 0;
|
|
1043
|
-
this.disposed = true;
|
|
1044
|
-
this.lifetimeController.abort();
|
|
1045
|
-
try {
|
|
1046
|
-
this.hardCancelActiveCustom?.();
|
|
1047
|
-
} finally {
|
|
1048
|
-
this.queueParentRestore();
|
|
1049
|
-
}
|
|
1050
|
-
return { consume: true };
|
|
1051
|
-
});
|
|
1052
|
-
outcome = { kind: "completed", value: await this.run(this.createContext()) };
|
|
1053
|
-
} catch (error) {
|
|
1054
|
-
outcome = { kind: "failed", error };
|
|
1055
|
-
}
|
|
1056
|
-
try {
|
|
1057
|
-
this.cancelActiveCustom?.();
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
this.cleanupError ??= error;
|
|
1060
|
-
}
|
|
1061
|
-
if (this.parentRestorePromise) await this.parentRestorePromise;
|
|
1062
|
-
else this.restoreParent();
|
|
1063
|
-
if (this.cleanupError !== void 0) outcome = { kind: "failed", error: this.cleanupError };
|
|
1064
|
-
this.finished = true;
|
|
1065
|
-
this.done(outcome);
|
|
926
|
+
if (Object.hasOwn(value, "layout")) {
|
|
927
|
+
const layout = Reflect.get(value, "layout");
|
|
928
|
+
if (!isBtwLayout(layout)) return void 0;
|
|
929
|
+
settings.layout = layout;
|
|
1066
930
|
}
|
|
1067
|
-
|
|
1068
|
-
const
|
|
1069
|
-
if (!
|
|
1070
|
-
|
|
1071
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1072
|
-
this.removeUpstreamAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
1073
|
-
if (signal.aborted) onAbort();
|
|
931
|
+
if (Object.hasOwn(value, "sidePaneRatio")) {
|
|
932
|
+
const sidePaneRatio = Reflect.get(value, "sidePaneRatio");
|
|
933
|
+
if (!isBtwSidePaneRatio(sidePaneRatio)) return void 0;
|
|
934
|
+
settings.sidePaneRatio = sidePaneRatio;
|
|
1074
935
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
936
|
+
return settings;
|
|
937
|
+
}
|
|
938
|
+
function parseBtwModelReference(reference) {
|
|
939
|
+
if (/[\s\p{Cc}]/u.test(reference)) return void 0;
|
|
940
|
+
const separator = reference.indexOf("/");
|
|
941
|
+
if (separator <= 0 || separator === reference.length - 1) return void 0;
|
|
942
|
+
return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
|
|
943
|
+
}
|
|
944
|
+
function effectiveBtwLayout(settings) {
|
|
945
|
+
return settings.layout ?? DEFAULT_BTW_LAYOUT;
|
|
946
|
+
}
|
|
947
|
+
function effectiveFullscreenCopyOnSelect(settings) {
|
|
948
|
+
return settings.fullscreenCopyOnSelect ?? DEFAULT_FULLSCREEN_COPY_ON_SELECT;
|
|
949
|
+
}
|
|
950
|
+
function effectiveRememberThinkingLevelChanges(settings) {
|
|
951
|
+
return settings.rememberThinkingLevelChanges ?? DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES;
|
|
952
|
+
}
|
|
953
|
+
function effectiveBtwSidePaneRatio(settings) {
|
|
954
|
+
return isBtwSidePaneRatio(settings.sidePaneRatio) ? settings.sidePaneRatio : DEFAULT_BTW_SIDE_PANE_RATIO;
|
|
955
|
+
}
|
|
956
|
+
async function readBtwSettings(settingsPath = btwSettingsPath()) {
|
|
957
|
+
await awaitBtwSettingsWrites(settingsPath);
|
|
958
|
+
return readBtwSettingsUncoordinated(settingsPath);
|
|
959
|
+
}
|
|
960
|
+
function updateBtwSettings(patch, options = {}) {
|
|
961
|
+
const settingsPath = options.settingsPath ?? btwSettingsPath();
|
|
962
|
+
return enqueueMutation(settingsPath, async () => {
|
|
963
|
+
options.signal?.throwIfAborted();
|
|
964
|
+
const current = await readSettingsDocumentForUpdate(settingsPath);
|
|
965
|
+
options.signal?.throwIfAborted();
|
|
966
|
+
options.validateCurrent?.(normalizeBtwSettings(current) ?? {});
|
|
967
|
+
const updated = applyBtwSettingsPatch(current, patch);
|
|
968
|
+
const settings = normalizeBtwSettings(updated);
|
|
969
|
+
if (!settings) throw invalidSettingsError(settingsPath, "invalid settings shape");
|
|
970
|
+
await publishSettings(settingsPath, updated, options.signal, options.beforeRename);
|
|
971
|
+
return settings;
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
async function awaitBtwSettingsWrites(settingsPath = btwSettingsPath()) {
|
|
975
|
+
await mutationQueues.get(settingsPath);
|
|
976
|
+
}
|
|
977
|
+
function enqueueMutation(settingsPath, mutation) {
|
|
978
|
+
const previous = mutationQueues.get(settingsPath) ?? Promise.resolve();
|
|
979
|
+
const result = previous.then(mutation, mutation);
|
|
980
|
+
const settled = result.then(
|
|
981
|
+
() => void 0,
|
|
982
|
+
() => void 0
|
|
983
|
+
);
|
|
984
|
+
mutationQueues.set(settingsPath, settled);
|
|
985
|
+
void settled.finally(() => {
|
|
986
|
+
if (mutationQueues.get(settingsPath) === settled) mutationQueues.delete(settingsPath);
|
|
987
|
+
});
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
async function readBtwSettingsUncoordinated(settingsPath) {
|
|
991
|
+
let contents;
|
|
992
|
+
try {
|
|
993
|
+
contents = await readSettingsContents(settingsPath);
|
|
994
|
+
} catch (error) {
|
|
995
|
+
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
996
|
+
return { kind: "invalid", reason: `${settingsPath}: ${formatError2(error)}` };
|
|
1087
997
|
}
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
998
|
+
try {
|
|
999
|
+
const settings = normalizeBtwSettings(JSON.parse(contents));
|
|
1000
|
+
return settings ? { kind: "loaded", settings } : { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
|
|
1001
|
+
} catch {
|
|
1002
|
+
return { kind: "invalid", reason: `${settingsPath}: invalid JSON` };
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async function readSettingsDocumentForUpdate(settingsPath) {
|
|
1006
|
+
let contents;
|
|
1007
|
+
try {
|
|
1008
|
+
contents = await readSettingsContents(settingsPath);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (isNodeError(error) && error.code === "ENOENT") return {};
|
|
1011
|
+
throw invalidSettingsError(settingsPath, formatError2(error));
|
|
1012
|
+
}
|
|
1013
|
+
let parsed;
|
|
1014
|
+
try {
|
|
1015
|
+
parsed = JSON.parse(contents);
|
|
1016
|
+
} catch {
|
|
1017
|
+
throw invalidSettingsError(settingsPath, "invalid JSON");
|
|
1018
|
+
}
|
|
1019
|
+
if (!isSettingsDocument(parsed) || !normalizeBtwSettings(parsed)) {
|
|
1020
|
+
throw invalidSettingsError(settingsPath, "invalid settings shape");
|
|
1021
|
+
}
|
|
1022
|
+
return parsed;
|
|
1023
|
+
}
|
|
1024
|
+
async function readSettingsContents(settingsPath) {
|
|
1025
|
+
const flags = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
|
|
1026
|
+
const handle = await open(settingsPath, flags);
|
|
1027
|
+
try {
|
|
1028
|
+
const descriptorStats = await handle.stat();
|
|
1029
|
+
if (!descriptorStats.isFile()) throw new Error("settings path is not a regular file");
|
|
1030
|
+
if (descriptorStats.size > MAX_SETTINGS_BYTES) {
|
|
1031
|
+
throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
1102
1032
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1033
|
+
const buffer = Buffer.alloc(MAX_SETTINGS_BYTES + 1);
|
|
1034
|
+
let offset = 0;
|
|
1035
|
+
while (offset < buffer.byteLength) {
|
|
1036
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
|
|
1037
|
+
if (bytesRead === 0) break;
|
|
1038
|
+
offset += bytesRead;
|
|
1110
1039
|
}
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1113
|
-
this.parentOverlay = void 0;
|
|
1114
|
-
try {
|
|
1115
|
-
parentOverlay?.setHidden(true);
|
|
1116
|
-
} catch (error) {
|
|
1117
|
-
this.cleanupError ??= error;
|
|
1040
|
+
if (offset > MAX_SETTINGS_BYTES) {
|
|
1041
|
+
throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
1118
1042
|
}
|
|
1119
1043
|
try {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
} catch (error) {
|
|
1124
|
-
this.cleanupError ??= error;
|
|
1044
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, offset));
|
|
1045
|
+
} catch {
|
|
1046
|
+
throw new Error("settings file is not valid UTF-8");
|
|
1125
1047
|
}
|
|
1048
|
+
} finally {
|
|
1049
|
+
await handle.close();
|
|
1126
1050
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
return new Proxy(this.ctx, {
|
|
1146
|
-
get: (target, property) => {
|
|
1147
|
-
if (property === "ui") return ui;
|
|
1148
|
-
if (property === "signal") return signal;
|
|
1149
|
-
return Reflect.get(target, property, target);
|
|
1150
|
-
}
|
|
1051
|
+
}
|
|
1052
|
+
async function publishSettings(settingsPath, document, signal, beforeRename) {
|
|
1053
|
+
signal?.throwIfAborted();
|
|
1054
|
+
const contents = `${JSON.stringify(document, null, 2)}
|
|
1055
|
+
`;
|
|
1056
|
+
if (Buffer.byteLength(contents, "utf8") > MAX_SETTINGS_BYTES) {
|
|
1057
|
+
throw new Error(`settings document exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
1058
|
+
}
|
|
1059
|
+
const directory = dirname(settingsPath);
|
|
1060
|
+
await mkdir(directory, { recursive: true });
|
|
1061
|
+
signal?.throwIfAborted();
|
|
1062
|
+
const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
1063
|
+
try {
|
|
1064
|
+
await writeFile(temporaryPath, contents, {
|
|
1065
|
+
encoding: "utf8",
|
|
1066
|
+
flag: "wx",
|
|
1067
|
+
mode: 384,
|
|
1068
|
+
signal
|
|
1151
1069
|
});
|
|
1070
|
+
await beforeRename?.(temporaryPath, settingsPath);
|
|
1071
|
+
signal?.throwIfAborted();
|
|
1072
|
+
await rename(temporaryPath, settingsPath);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
1075
|
+
throw error;
|
|
1152
1076
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1077
|
+
}
|
|
1078
|
+
function applyBtwSettingsPatch(current, patch) {
|
|
1079
|
+
const updated = { ...current };
|
|
1080
|
+
if (patch.keybindings) {
|
|
1081
|
+
const keys = isSettingsDocument(current.keybindings) ? { ...current.keybindings } : {};
|
|
1082
|
+
for (const action of BTW_SHORTCUT_ACTIONS) {
|
|
1083
|
+
if (!Object.hasOwn(patch.keybindings, action)) continue;
|
|
1084
|
+
if (patch.keybindings[action] === void 0) delete keys[action];
|
|
1085
|
+
else keys[action] = patch.keybindings[action];
|
|
1160
1086
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
let overlay;
|
|
1164
|
-
let mounted = false;
|
|
1165
|
-
let layoutMounted = false;
|
|
1166
|
-
let factorySettled = false;
|
|
1167
|
-
let closed = false;
|
|
1168
|
-
let promiseSettled = false;
|
|
1169
|
-
let componentDisposed = false;
|
|
1170
|
-
let pendingValue;
|
|
1171
|
-
let hasPendingValue = false;
|
|
1172
|
-
const disposeComponent = () => {
|
|
1173
|
-
if (!component || componentDisposed) return;
|
|
1174
|
-
componentDisposed = true;
|
|
1175
|
-
try {
|
|
1176
|
-
component.dispose?.();
|
|
1177
|
-
} catch {
|
|
1178
|
-
}
|
|
1179
|
-
};
|
|
1180
|
-
const unmount = () => {
|
|
1181
|
-
let cleanupError;
|
|
1182
|
-
try {
|
|
1183
|
-
if (overlay) overlay.hide();
|
|
1184
|
-
else if (mounted && layoutMounted) fullscreen.setLayoutRoot(void 0);
|
|
1185
|
-
else if (mounted && component) fullscreen.removeChild(component);
|
|
1186
|
-
} catch (error) {
|
|
1187
|
-
cleanupError = error;
|
|
1188
|
-
}
|
|
1189
|
-
if (overlay || mounted) {
|
|
1190
|
-
try {
|
|
1191
|
-
fullscreen.setFocus(null);
|
|
1192
|
-
fullscreen.requestRender();
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
cleanupError ??= error;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
disposeComponent();
|
|
1198
|
-
if (cleanupError !== void 0) throw cleanupError;
|
|
1199
|
-
};
|
|
1200
|
-
const complete = () => {
|
|
1201
|
-
if (promiseSettled || !hasPendingValue) return;
|
|
1202
|
-
promiseSettled = true;
|
|
1203
|
-
this.cancelActiveCustom = void 0;
|
|
1204
|
-
this.hardCancelActiveCustom = void 0;
|
|
1205
|
-
if (!factorySettled) {
|
|
1206
|
-
resolve(pendingValue);
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
try {
|
|
1210
|
-
unmount();
|
|
1211
|
-
resolve(pendingValue);
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
reject(error);
|
|
1214
|
-
}
|
|
1215
|
-
};
|
|
1216
|
-
const close = (value) => {
|
|
1217
|
-
if (closed || promiseSettled) return;
|
|
1218
|
-
closed = true;
|
|
1219
|
-
pendingValue = value;
|
|
1220
|
-
hasPendingValue = true;
|
|
1221
|
-
complete();
|
|
1222
|
-
};
|
|
1223
|
-
const fail = (error) => {
|
|
1224
|
-
if (promiseSettled) return;
|
|
1225
|
-
closed = true;
|
|
1226
|
-
promiseSettled = true;
|
|
1227
|
-
this.cancelActiveCustom = void 0;
|
|
1228
|
-
this.hardCancelActiveCustom = void 0;
|
|
1229
|
-
try {
|
|
1230
|
-
unmount();
|
|
1231
|
-
reject(error);
|
|
1232
|
-
} catch (cleanupError) {
|
|
1233
|
-
reject(cleanupError);
|
|
1234
|
-
}
|
|
1235
|
-
};
|
|
1236
|
-
this.cancelActiveCustom = () => {
|
|
1237
|
-
if (promiseSettled) return;
|
|
1238
|
-
disposeComponent();
|
|
1239
|
-
if (!promiseSettled) fail(new FullscreenUiDisposedError());
|
|
1240
|
-
};
|
|
1241
|
-
this.hardCancelActiveCustom = () => {
|
|
1242
|
-
if (promiseSettled) return;
|
|
1243
|
-
try {
|
|
1244
|
-
component?.handleInput?.("");
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
fail(error);
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
this.cancelActiveCustom?.();
|
|
1250
|
-
};
|
|
1251
|
-
let created;
|
|
1252
|
-
try {
|
|
1253
|
-
created = factory(fullscreen, this.theme, this.keybindings, close);
|
|
1254
|
-
} catch (error) {
|
|
1255
|
-
factorySettled = true;
|
|
1256
|
-
fail(error);
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
Promise.resolve(created).then((value) => {
|
|
1260
|
-
component = value;
|
|
1261
|
-
factorySettled = true;
|
|
1262
|
-
if (promiseSettled) {
|
|
1263
|
-
disposeComponent();
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
if (closed) {
|
|
1267
|
-
complete();
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
if (options?.overlay) {
|
|
1271
|
-
const overlayOptions = typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions;
|
|
1272
|
-
overlay = fullscreen.showOverlay(component, overlayOptions);
|
|
1273
|
-
options.onHandle?.(overlay);
|
|
1274
|
-
} else {
|
|
1275
|
-
fullscreen.clear();
|
|
1276
|
-
mounted = true;
|
|
1277
|
-
if (isFullscreenLayoutComponent(component)) {
|
|
1278
|
-
layoutMounted = true;
|
|
1279
|
-
fullscreen.setLayoutRoot(component.getFullscreenLayout());
|
|
1280
|
-
} else {
|
|
1281
|
-
fullscreen.addChild(component);
|
|
1282
|
-
}
|
|
1283
|
-
fullscreen.setFocus(component);
|
|
1284
|
-
fullscreen.requestRender();
|
|
1285
|
-
}
|
|
1286
|
-
}).catch(fail);
|
|
1287
|
-
});
|
|
1087
|
+
if (Object.keys(keys).length) updated.keybindings = keys;
|
|
1088
|
+
else delete updated.keybindings;
|
|
1288
1089
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1090
|
+
if (Object.hasOwn(patch, "model")) {
|
|
1091
|
+
if (patch.model === void 0) delete updated.model;
|
|
1092
|
+
else updated.model = patch.model;
|
|
1093
|
+
}
|
|
1094
|
+
if (Object.hasOwn(patch, "thinkingLevel")) {
|
|
1095
|
+
if (patch.thinkingLevel === void 0) delete updated.thinkingLevel;
|
|
1096
|
+
else updated.thinkingLevel = patch.thinkingLevel;
|
|
1097
|
+
}
|
|
1098
|
+
if (Object.hasOwn(patch, "rememberThinkingLevelChanges")) {
|
|
1099
|
+
updated.rememberThinkingLevelChanges = patch.rememberThinkingLevelChanges;
|
|
1100
|
+
}
|
|
1101
|
+
if (Object.hasOwn(patch, "fullscreenCopyOnSelect")) {
|
|
1102
|
+
if (patch.fullscreenCopyOnSelect === void 0) delete updated.fullscreenCopyOnSelect;
|
|
1103
|
+
else updated.fullscreenCopyOnSelect = patch.fullscreenCopyOnSelect;
|
|
1104
|
+
}
|
|
1105
|
+
if (Object.hasOwn(patch, "layout")) {
|
|
1106
|
+
if (patch.layout === void 0) delete updated.layout;
|
|
1107
|
+
else updated.layout = patch.layout;
|
|
1108
|
+
}
|
|
1109
|
+
if (Object.hasOwn(patch, "sidePaneRatio")) {
|
|
1110
|
+
if (patch.sidePaneRatio === void 0) delete updated.sidePaneRatio;
|
|
1111
|
+
else updated.sidePaneRatio = patch.sidePaneRatio;
|
|
1112
|
+
}
|
|
1113
|
+
return updated;
|
|
1292
1114
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
import {
|
|
1296
|
-
copyToClipboard,
|
|
1297
|
-
TreeSelectorComponent
|
|
1298
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1299
|
-
import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
|
|
1300
|
-
|
|
1301
|
-
// src/menu.ts
|
|
1302
|
-
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1303
|
-
import {
|
|
1304
|
-
BorderedLoader
|
|
1305
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1306
|
-
|
|
1307
|
-
// src/settings.ts
|
|
1308
|
-
import { randomUUID } from "node:crypto";
|
|
1309
|
-
import { constants } from "node:fs";
|
|
1310
|
-
import { mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
1311
|
-
import { basename, dirname, join } from "node:path";
|
|
1312
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
1313
|
-
|
|
1314
|
-
// src/side-thread.ts
|
|
1315
|
-
var BTW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1316
|
-
function createSideThread(conversationContext) {
|
|
1317
|
-
return { conversationContext, turns: [] };
|
|
1115
|
+
function isSettingsDocument(value) {
|
|
1116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1318
1117
|
}
|
|
1319
|
-
function
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1118
|
+
function isBtwThinkingLevel(value) {
|
|
1119
|
+
return BTW_THINKING_LEVELS.includes(value);
|
|
1120
|
+
}
|
|
1121
|
+
function isBtwLayout(value) {
|
|
1122
|
+
return BTW_LAYOUTS.includes(value);
|
|
1123
|
+
}
|
|
1124
|
+
function isBtwSidePaneRatio(value) {
|
|
1125
|
+
return typeof value === "number" && Number.isFinite(value) && value >= MIN_BTW_SIDE_PANE_RATIO && value <= MAX_BTW_SIDE_PANE_RATIO;
|
|
1126
|
+
}
|
|
1127
|
+
function invalidSettingsError(settingsPath, reason) {
|
|
1128
|
+
return new Error(`pi-btw settings at ${settingsPath} are invalid: ${reason}`);
|
|
1129
|
+
}
|
|
1130
|
+
function isNodeError(error) {
|
|
1131
|
+
return error instanceof Error && "code" in error;
|
|
1132
|
+
}
|
|
1133
|
+
function formatError2(error) {
|
|
1134
|
+
return error instanceof Error ? error.message : String(error);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/workspace-layout.ts
|
|
1138
|
+
var MIN_BTW_SPLIT_COLUMNS = 80;
|
|
1139
|
+
var PANE_DIVIDER_COLUMNS = 1;
|
|
1140
|
+
var SGR_MOUSE_PRESS_PATTERN = new RegExp("^\\u001b\\[<(\\d+);(\\d+);\\d+M$");
|
|
1141
|
+
var BtwMainThreadInput = class {
|
|
1142
|
+
constructor(initialTarget, resolveTarget, requestRender) {
|
|
1143
|
+
this.resolveTarget = resolveTarget;
|
|
1144
|
+
this.requestRender = requestRender;
|
|
1145
|
+
this.target = hasInput(initialTarget) ? initialTarget : void 0;
|
|
1146
|
+
}
|
|
1147
|
+
resolveTarget;
|
|
1148
|
+
requestRender;
|
|
1149
|
+
target;
|
|
1150
|
+
_focused = false;
|
|
1151
|
+
disposed = false;
|
|
1152
|
+
get focused() {
|
|
1153
|
+
return this._focused;
|
|
1154
|
+
}
|
|
1155
|
+
set focused(value) {
|
|
1156
|
+
this._focused = value;
|
|
1157
|
+
if (this.target && isFocusable(this.target)) this.target.focused = value;
|
|
1158
|
+
}
|
|
1159
|
+
get wantsKeyRelease() {
|
|
1160
|
+
return this.target?.wantsKeyRelease ?? false;
|
|
1161
|
+
}
|
|
1162
|
+
render() {
|
|
1163
|
+
return [];
|
|
1164
|
+
}
|
|
1165
|
+
handleInput(data) {
|
|
1166
|
+
if (this.disposed) return;
|
|
1167
|
+
this.refreshTarget();
|
|
1168
|
+
this.target?.handleInput?.(data);
|
|
1169
|
+
this.refreshTarget();
|
|
1170
|
+
this.requestRender();
|
|
1171
|
+
}
|
|
1172
|
+
refreshTarget() {
|
|
1173
|
+
if (this.disposed) return;
|
|
1174
|
+
const next = this.resolveTarget();
|
|
1175
|
+
if (!hasInput(next) || next === this.target) return;
|
|
1176
|
+
if (this._focused && this.target && isFocusable(this.target)) this.target.focused = false;
|
|
1177
|
+
this.target = next;
|
|
1178
|
+
if (this._focused && isFocusable(next)) next.focused = true;
|
|
1179
|
+
}
|
|
1180
|
+
invalidate() {
|
|
1181
|
+
}
|
|
1182
|
+
dispose() {
|
|
1183
|
+
if (this.disposed) return;
|
|
1184
|
+
this.disposed = true;
|
|
1185
|
+
if (this._focused && this.target && isFocusable(this.target)) this.target.focused = false;
|
|
1186
|
+
this.target = void 0;
|
|
1187
|
+
this._focused = false;
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
var BtwSplitPane = class {
|
|
1191
|
+
constructor(options) {
|
|
1192
|
+
this.options = options;
|
|
1193
|
+
this.sidePaneRatio = clampSidePaneRatio(options.sidePaneRatio ?? DEFAULT_BTW_SIDE_PANE_RATIO);
|
|
1194
|
+
this.persistedSidePaneRatio = this.sidePaneRatio;
|
|
1195
|
+
this.mainPane = new MainThreadPane(options.mainThread, options.mainLayout, options.theme, options.terminalRows);
|
|
1196
|
+
this.viewportRouter = new PaneViewportRouter(
|
|
1197
|
+
options.sideScrollView ?? findPrimaryScrollView(options.sideLayout),
|
|
1198
|
+
this.mainPane.getPrimaryScrollView(),
|
|
1199
|
+
options.setViewportTarget
|
|
1200
|
+
);
|
|
1201
|
+
this.viewportRouter.activate("side");
|
|
1202
|
+
const separator = {
|
|
1203
|
+
render: (width) => Array.from({ length: Math.max(1, options.terminalRows()) }, () => truncateToWidth2(this.renderDivider(), width)),
|
|
1204
|
+
handleMouse: (event) => this.handleDividerMouse(event),
|
|
1205
|
+
invalidate() {
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
const side = options.sideLayout;
|
|
1209
|
+
const main = this.mainPane.getLayout();
|
|
1210
|
+
this.layoutRoot = options.layout === "left-pane" ? new ResponsivePaneRow(
|
|
1211
|
+
side,
|
|
1212
|
+
separator,
|
|
1213
|
+
main,
|
|
1214
|
+
0,
|
|
1215
|
+
() => this.sidePaneRatio,
|
|
1216
|
+
(width) => this.handleViewportWidth(width)
|
|
1217
|
+
) : new ResponsivePaneRow(
|
|
1218
|
+
main,
|
|
1219
|
+
separator,
|
|
1220
|
+
side,
|
|
1221
|
+
2,
|
|
1222
|
+
() => this.sidePaneRatio,
|
|
1223
|
+
(width) => this.handleViewportWidth(width)
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
options;
|
|
1227
|
+
pasteGuard = new BtwPasteGuard();
|
|
1228
|
+
mainPane;
|
|
1229
|
+
layoutRoot;
|
|
1230
|
+
viewportRouter;
|
|
1231
|
+
activePane = "side";
|
|
1232
|
+
sidePaneRatio;
|
|
1233
|
+
persistedSidePaneRatio;
|
|
1234
|
+
dividerDragStartRatio;
|
|
1235
|
+
focusGeneration = 0;
|
|
1236
|
+
ratioSaveGeneration = 0;
|
|
1237
|
+
confirmedRatioSaveGeneration = 0;
|
|
1238
|
+
failedRatioSaveGenerationDuringDrag;
|
|
1239
|
+
disposed = false;
|
|
1240
|
+
getFullscreenLayout() {
|
|
1241
|
+
return this.layoutRoot;
|
|
1242
|
+
}
|
|
1243
|
+
handleTerminalInput(data) {
|
|
1244
|
+
if (this.disposed) return false;
|
|
1245
|
+
const pasted = this.pasteGuard.consume(data);
|
|
1246
|
+
if (this.options.hasFocusedOverlay()) return false;
|
|
1247
|
+
const width = this.terminalColumns();
|
|
1248
|
+
if (width < MIN_BTW_SPLIT_COLUMNS && this.activePane !== "side") this.activatePane("side");
|
|
1249
|
+
if (pasted) {
|
|
1250
|
+
this.forwardPastedInput(data);
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
if (width < MIN_BTW_SPLIT_COLUMNS) return false;
|
|
1254
|
+
const pane = paneForMouseClick(data, width, this.options.layout, this.sidePaneRatio);
|
|
1255
|
+
if (pane) this.queuePaneFocus(pane);
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
render(width) {
|
|
1259
|
+
if (width <= 0) return [];
|
|
1260
|
+
const safeWidth = Math.max(1, width);
|
|
1261
|
+
this.handleViewportWidth(safeWidth);
|
|
1262
|
+
if (safeWidth < MIN_BTW_SPLIT_COLUMNS) {
|
|
1263
|
+
return this.options.sideComponent.render(safeWidth).map((line) => truncateToWidth2(line, safeWidth));
|
|
1264
|
+
}
|
|
1265
|
+
const { sideWidth, mainWidth } = paneWidths(safeWidth, this.options.layout, this.sidePaneRatio);
|
|
1266
|
+
const sideLines = this.options.sideComponent.render(sideWidth);
|
|
1267
|
+
const mainLines = this.mainPane.render(mainWidth);
|
|
1268
|
+
const rows = Math.max(1, this.options.terminalRows());
|
|
1269
|
+
const separator = this.renderDivider();
|
|
1270
|
+
const lines = [];
|
|
1271
|
+
for (let index = 0; index < rows; index += 1) {
|
|
1272
|
+
const sideLine = padLine(sideLines[index] ?? "", sideWidth);
|
|
1273
|
+
const mainLine = padLine(mainLines[index] ?? "", mainWidth);
|
|
1274
|
+
lines.push(
|
|
1275
|
+
this.options.layout === "left-pane" ? `${sideLine}${separator}${mainLine}` : `${mainLine}${separator}${sideLine}`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
return lines.map((line) => truncateToWidth2(line, safeWidth));
|
|
1327
1279
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
for (const turn of rest) {
|
|
1331
|
-
messages.push(createUserMessage(buildFollowUpPrompt(turn.question)), turn.response);
|
|
1280
|
+
invalidate() {
|
|
1281
|
+
this.layoutRoot.invalidate();
|
|
1332
1282
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
const response = await completeSimple(
|
|
1349
|
-
model,
|
|
1350
|
-
{ systemPrompt: SYSTEM_PROMPT, messages: buildSideThreadMessages(thread, question) },
|
|
1351
|
-
buildStreamOptions(
|
|
1352
|
-
auth,
|
|
1353
|
-
{ thinkingLevel, signal, model, sessionId },
|
|
1354
|
-
completeSimple.appliesRequestHeaderTransforms === true
|
|
1355
|
-
)
|
|
1356
|
-
);
|
|
1357
|
-
if (signal?.aborted || response?.stopReason === "aborted") return { kind: "aborted" };
|
|
1358
|
-
if (!isAssistantMessage(response)) {
|
|
1359
|
-
return { kind: "error", message: "The side model returned a malformed response." };
|
|
1283
|
+
dispose() {
|
|
1284
|
+
if (this.disposed) return;
|
|
1285
|
+
this.disposed = true;
|
|
1286
|
+
this.focusGeneration += 1;
|
|
1287
|
+
this.dividerDragStartRatio = void 0;
|
|
1288
|
+
this.failedRatioSaveGenerationDuringDrag = void 0;
|
|
1289
|
+
this.viewportRouter.dispose();
|
|
1290
|
+
}
|
|
1291
|
+
renderDivider() {
|
|
1292
|
+
return this.options.theme.fg("borderMuted", "\u2502");
|
|
1293
|
+
}
|
|
1294
|
+
handleDividerMouse(event) {
|
|
1295
|
+
if (this.disposed) return void 0;
|
|
1296
|
+
if (this.options.hasFocusedOverlay()) {
|
|
1297
|
+
return { handled: true, render: this.cancelDividerDrag() };
|
|
1360
1298
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
return
|
|
1363
|
-
|
|
1364
|
-
|
|
1299
|
+
if (event.type === "press" && event.button === "left") {
|
|
1300
|
+
if (this.terminalColumns() < MIN_BTW_SPLIT_COLUMNS) return void 0;
|
|
1301
|
+
this.focusGeneration += 1;
|
|
1302
|
+
this.dividerDragStartRatio = this.sidePaneRatio;
|
|
1303
|
+
return { handled: true, capture: true, render: false };
|
|
1304
|
+
}
|
|
1305
|
+
if (this.dividerDragStartRatio === void 0) return void 0;
|
|
1306
|
+
if (event.type === "drag") {
|
|
1307
|
+
const changed = this.updateSidePaneRatio(event.screenX);
|
|
1308
|
+
return { handled: true, render: changed };
|
|
1309
|
+
}
|
|
1310
|
+
if (event.type === "release") {
|
|
1311
|
+
const changed = this.updateSidePaneRatio(event.screenX);
|
|
1312
|
+
const startRatio = this.dividerDragStartRatio;
|
|
1313
|
+
const failedDuringDrag = this.failedRatioSaveGenerationDuringDrag === this.ratioSaveGeneration;
|
|
1314
|
+
this.dividerDragStartRatio = void 0;
|
|
1315
|
+
this.failedRatioSaveGenerationDuringDrag = void 0;
|
|
1316
|
+
if (failedDuringDrag && this.sidePaneRatio === this.persistedSidePaneRatio) {
|
|
1317
|
+
return { handled: true, render: changed };
|
|
1318
|
+
}
|
|
1319
|
+
if (this.sidePaneRatio !== startRatio) {
|
|
1320
|
+
this.persistCurrentSidePaneRatio();
|
|
1321
|
+
return { handled: true, render: changed };
|
|
1322
|
+
}
|
|
1323
|
+
const restored = failedDuringDrag && this.restorePersistedSidePaneRatio();
|
|
1324
|
+
return { handled: true, render: changed || restored };
|
|
1325
|
+
}
|
|
1326
|
+
return { handled: true, render: false };
|
|
1327
|
+
}
|
|
1328
|
+
updateSidePaneRatio(dividerColumn) {
|
|
1329
|
+
const width = this.terminalColumns();
|
|
1330
|
+
if (width < MIN_BTW_SPLIT_COLUMNS) return false;
|
|
1331
|
+
const contentWidth = width - PANE_DIVIDER_COLUMNS;
|
|
1332
|
+
const leftWidth = clamp(Math.floor(dividerColumn), 1, contentWidth - 1);
|
|
1333
|
+
const sideWidth = this.options.layout === "left-pane" ? leftWidth : contentWidth - leftWidth;
|
|
1334
|
+
const ratio = clampSidePaneRatio(roundPaneRatio(sideWidth / contentWidth));
|
|
1335
|
+
if (ratio === this.sidePaneRatio) return false;
|
|
1336
|
+
this.sidePaneRatio = ratio;
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1339
|
+
cancelDividerDrag() {
|
|
1340
|
+
const startRatio = this.dividerDragStartRatio;
|
|
1341
|
+
if (startRatio === void 0) return false;
|
|
1342
|
+
const failedDuringDrag = this.failedRatioSaveGenerationDuringDrag === this.ratioSaveGeneration;
|
|
1343
|
+
this.dividerDragStartRatio = void 0;
|
|
1344
|
+
this.failedRatioSaveGenerationDuringDrag = void 0;
|
|
1345
|
+
return this.setSidePaneRatio(failedDuringDrag ? this.persistedSidePaneRatio : startRatio);
|
|
1346
|
+
}
|
|
1347
|
+
persistCurrentSidePaneRatio() {
|
|
1348
|
+
const persist = this.options.persistSidePaneRatio;
|
|
1349
|
+
const ratio = this.sidePaneRatio;
|
|
1350
|
+
if (!persist) {
|
|
1351
|
+
this.persistedSidePaneRatio = ratio;
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
this.failedRatioSaveGenerationDuringDrag = void 0;
|
|
1355
|
+
const saveGeneration = ++this.ratioSaveGeneration;
|
|
1356
|
+
void Promise.resolve().then(() => persist(ratio)).then(() => {
|
|
1357
|
+
if (saveGeneration <= this.confirmedRatioSaveGeneration) return;
|
|
1358
|
+
this.confirmedRatioSaveGeneration = saveGeneration;
|
|
1359
|
+
this.persistedSidePaneRatio = ratio;
|
|
1360
|
+
}).catch(() => {
|
|
1361
|
+
if (this.disposed || saveGeneration !== this.ratioSaveGeneration) return;
|
|
1362
|
+
const dragActive = this.dividerDragStartRatio !== void 0;
|
|
1363
|
+
const changed = this.restorePersistedSidePaneRatio();
|
|
1364
|
+
if (dragActive) this.failedRatioSaveGenerationDuringDrag = saveGeneration;
|
|
1365
|
+
if (changed) this.options.requestRender();
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
restorePersistedSidePaneRatio() {
|
|
1369
|
+
return this.setSidePaneRatio(this.persistedSidePaneRatio);
|
|
1370
|
+
}
|
|
1371
|
+
setSidePaneRatio(ratio) {
|
|
1372
|
+
if (ratio === this.sidePaneRatio) return false;
|
|
1373
|
+
this.sidePaneRatio = ratio;
|
|
1374
|
+
this.layoutRoot.invalidate();
|
|
1375
|
+
return true;
|
|
1376
|
+
}
|
|
1377
|
+
queuePaneFocus(pane) {
|
|
1378
|
+
const generation = ++this.focusGeneration;
|
|
1379
|
+
queueMicrotask(() => {
|
|
1380
|
+
if (this.disposed || generation !== this.focusGeneration || this.options.hasFocusedOverlay()) return;
|
|
1381
|
+
this.activatePane(this.terminalColumns() < MIN_BTW_SPLIT_COLUMNS ? "side" : pane);
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
terminalColumns() {
|
|
1385
|
+
return Math.max(1, Math.floor(this.options.terminalColumns()));
|
|
1386
|
+
}
|
|
1387
|
+
handleViewportWidth(width) {
|
|
1388
|
+
if (this.disposed || width >= MIN_BTW_SPLIT_COLUMNS || this.activePane === "side" || this.options.hasFocusedOverlay()) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
this.activatePane("side");
|
|
1392
|
+
}
|
|
1393
|
+
activatePane(pane) {
|
|
1394
|
+
if (this.disposed) return;
|
|
1395
|
+
this.activePane = pane;
|
|
1396
|
+
this.viewportRouter.activate(pane);
|
|
1397
|
+
this.options.setFocus(pane === "side" ? this.options.sideComponent : this.options.mainInput);
|
|
1398
|
+
this.options.requestRender();
|
|
1399
|
+
}
|
|
1400
|
+
forwardPastedInput(data) {
|
|
1401
|
+
const target = this.activePane === "side" ? this.options.sideComponent : this.options.mainInput;
|
|
1402
|
+
target.handleInput?.(data);
|
|
1403
|
+
this.options.requestRender();
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
var ResponsivePaneRow = class extends HStack {
|
|
1407
|
+
constructor(left, separator, right, sideIndex, sidePaneRatio, onViewportWidth) {
|
|
1408
|
+
super([
|
|
1409
|
+
{ component: left, basis: 1, grow: 0, shrink: 0, minSize: 1 },
|
|
1410
|
+
{
|
|
1411
|
+
component: separator,
|
|
1412
|
+
basis: PANE_DIVIDER_COLUMNS,
|
|
1413
|
+
grow: 0,
|
|
1414
|
+
shrink: 0,
|
|
1415
|
+
minSize: PANE_DIVIDER_COLUMNS
|
|
1416
|
+
},
|
|
1417
|
+
{ component: right, basis: 1, grow: 0, shrink: 0, minSize: 1 }
|
|
1418
|
+
]);
|
|
1419
|
+
this.sideIndex = sideIndex;
|
|
1420
|
+
this.sidePaneRatio = sidePaneRatio;
|
|
1421
|
+
this.onViewportWidth = onViewportWidth;
|
|
1422
|
+
for (const [index, entry] of this.entries.entries()) {
|
|
1423
|
+
entry.visible = (viewport) => {
|
|
1424
|
+
if (index === 0) this.resize(viewport.width);
|
|
1425
|
+
return index === this.sideIndex || viewport.width >= MIN_BTW_SPLIT_COLUMNS;
|
|
1365
1426
|
};
|
|
1366
1427
|
}
|
|
1367
|
-
const answer = extractAssistantText(response) || "No response received.";
|
|
1368
|
-
thread.turns.push({ kind: "answered", question, answer, response });
|
|
1369
|
-
return { kind: "answered", response, answer };
|
|
1370
|
-
} catch (error) {
|
|
1371
|
-
if (signal?.aborted) return { kind: "aborted" };
|
|
1372
|
-
return { kind: "error", message: formatError(error) };
|
|
1373
1428
|
}
|
|
1429
|
+
sideIndex;
|
|
1430
|
+
sidePaneRatio;
|
|
1431
|
+
onViewportWidth;
|
|
1432
|
+
resize(width) {
|
|
1433
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
1434
|
+
this.onViewportWidth(safeWidth);
|
|
1435
|
+
if (safeWidth < MIN_BTW_SPLIT_COLUMNS) {
|
|
1436
|
+
for (const [index, entry] of this.entries.entries()) {
|
|
1437
|
+
entry.basis = index === this.sideIndex ? safeWidth : 1;
|
|
1438
|
+
}
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const layout = this.sideIndex === 0 ? "left-pane" : "right-pane";
|
|
1442
|
+
const { leftWidth, rightWidth } = paneWidths(safeWidth, layout, this.sidePaneRatio());
|
|
1443
|
+
const left = this.entries[0];
|
|
1444
|
+
const separator = this.entries[1];
|
|
1445
|
+
const right = this.entries[2];
|
|
1446
|
+
if (left) left.basis = leftWidth;
|
|
1447
|
+
if (separator) separator.basis = PANE_DIVIDER_COLUMNS;
|
|
1448
|
+
if (right) right.basis = rightWidth;
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
var MainThreadPane = class {
|
|
1452
|
+
constructor(mainThread, mainLayout, theme, terminalRows) {
|
|
1453
|
+
this.terminalRows = terminalRows;
|
|
1454
|
+
this.body = {
|
|
1455
|
+
render: (width) => mainThread.render(Math.max(1, width)).map((line) => truncateToWidth2(line, Math.max(1, width))),
|
|
1456
|
+
invalidate() {
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
if (mainLayout) {
|
|
1460
|
+
this.layout = mainLayout;
|
|
1461
|
+
this.scroll = findPrimaryScrollView(mainLayout);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
this.scroll = new ScrollView(this.body, {
|
|
1465
|
+
follow: "end",
|
|
1466
|
+
scrollbar: "auto",
|
|
1467
|
+
scrollbarTrackStyle: (text) => theme.fg("borderMuted", text),
|
|
1468
|
+
scrollbarThumbStyle: (text) => theme.fg("muted", text)
|
|
1469
|
+
});
|
|
1470
|
+
this.layout = this.scroll;
|
|
1471
|
+
}
|
|
1472
|
+
terminalRows;
|
|
1473
|
+
body;
|
|
1474
|
+
layout;
|
|
1475
|
+
scroll;
|
|
1476
|
+
getLayout() {
|
|
1477
|
+
return this.layout;
|
|
1478
|
+
}
|
|
1479
|
+
getPrimaryScrollView() {
|
|
1480
|
+
return this.scroll;
|
|
1481
|
+
}
|
|
1482
|
+
render(width) {
|
|
1483
|
+
if (width <= 0) return [];
|
|
1484
|
+
const safeWidth = Math.max(1, width);
|
|
1485
|
+
const rows = Math.max(1, this.terminalRows());
|
|
1486
|
+
const visible = this.body.render(safeWidth).slice(-rows);
|
|
1487
|
+
return [...Array.from({ length: Math.max(0, rows - visible.length) }, () => ""), ...visible];
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
var PaneViewportRouter = class {
|
|
1491
|
+
constructor(side, main, setViewportTarget) {
|
|
1492
|
+
this.side = side;
|
|
1493
|
+
this.main = main;
|
|
1494
|
+
this.setViewportTarget = setViewportTarget;
|
|
1495
|
+
for (const scrollView of [side, main]) {
|
|
1496
|
+
if (scrollView) this.originalPrimary.set(scrollView, scrollView.primary);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
side;
|
|
1500
|
+
main;
|
|
1501
|
+
setViewportTarget;
|
|
1502
|
+
originalPrimary = /* @__PURE__ */ new Map();
|
|
1503
|
+
disposed = false;
|
|
1504
|
+
activate(pane) {
|
|
1505
|
+
if (this.disposed) return;
|
|
1506
|
+
const target = pane === "side" ? this.side : this.main;
|
|
1507
|
+
for (const scrollView of this.originalPrimary.keys()) setScrollViewPrimary(scrollView, scrollView === target);
|
|
1508
|
+
this.setViewportTarget(target);
|
|
1509
|
+
}
|
|
1510
|
+
dispose() {
|
|
1511
|
+
if (this.disposed) return;
|
|
1512
|
+
this.disposed = true;
|
|
1513
|
+
for (const [scrollView, primary] of this.originalPrimary) setScrollViewPrimary(scrollView, primary);
|
|
1514
|
+
this.setViewportTarget(void 0);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
function setScrollViewPrimary(scrollView, primary) {
|
|
1518
|
+
Reflect.set(scrollView, "primary", primary);
|
|
1519
|
+
}
|
|
1520
|
+
function findPrimaryScrollView(root) {
|
|
1521
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1522
|
+
let fallback;
|
|
1523
|
+
let primary;
|
|
1524
|
+
const visit = (component) => {
|
|
1525
|
+
if (visited.has(component)) return;
|
|
1526
|
+
visited.add(component);
|
|
1527
|
+
if (component instanceof ScrollView) {
|
|
1528
|
+
fallback ??= component;
|
|
1529
|
+
if (component.primary) primary = component;
|
|
1530
|
+
}
|
|
1531
|
+
if (!("children" in component) || !Array.isArray(component.children)) return;
|
|
1532
|
+
for (const child of component.children) visit(child);
|
|
1533
|
+
};
|
|
1534
|
+
visit(root);
|
|
1535
|
+
return primary ?? fallback;
|
|
1536
|
+
}
|
|
1537
|
+
function paneForMouseClick(data, terminalColumns, layout, sidePaneRatio) {
|
|
1538
|
+
const match = SGR_MOUSE_PRESS_PATTERN.exec(data);
|
|
1539
|
+
if (!match) return void 0;
|
|
1540
|
+
const button = Number.parseInt(match[1] ?? "", 10);
|
|
1541
|
+
if ((button & 32) !== 0 || (button & 64) !== 0 || (button & 3) !== 0) return void 0;
|
|
1542
|
+
const column = Number.parseInt(match[2] ?? "", 10) - 1;
|
|
1543
|
+
if (!Number.isFinite(column) || column < 0 || column >= terminalColumns) return void 0;
|
|
1544
|
+
const { leftWidth } = paneWidths(terminalColumns, layout, sidePaneRatio);
|
|
1545
|
+
if (column >= leftWidth && column < leftWidth + PANE_DIVIDER_COLUMNS) return void 0;
|
|
1546
|
+
const clickedLeft = column < leftWidth;
|
|
1547
|
+
if (layout === "left-pane") return clickedLeft ? "side" : "main";
|
|
1548
|
+
return clickedLeft ? "main" : "side";
|
|
1549
|
+
}
|
|
1550
|
+
function paneWidths(width, layout, sidePaneRatio) {
|
|
1551
|
+
const contentWidth = Math.max(2, width - PANE_DIVIDER_COLUMNS);
|
|
1552
|
+
const sideWidth = clamp(
|
|
1553
|
+
Math.round(contentWidth * clampSidePaneRatio(sidePaneRatio)),
|
|
1554
|
+
1,
|
|
1555
|
+
Math.max(1, contentWidth - 1)
|
|
1556
|
+
);
|
|
1557
|
+
const mainWidth = Math.max(1, contentWidth - sideWidth);
|
|
1558
|
+
const leftWidth = layout === "left-pane" ? sideWidth : mainWidth;
|
|
1559
|
+
const rightWidth = layout === "left-pane" ? mainWidth : sideWidth;
|
|
1560
|
+
return { leftWidth, rightWidth, sideWidth, mainWidth };
|
|
1374
1561
|
}
|
|
1375
|
-
function
|
|
1376
|
-
return
|
|
1377
|
-
(content) => content !== null && typeof content === "object" && content.type === "text" && typeof content.text === "string"
|
|
1378
|
-
).map((content) => content.text).join("\n").trim();
|
|
1379
|
-
}
|
|
1380
|
-
function isAssistantMessage(value) {
|
|
1381
|
-
if (value === null || typeof value !== "object") return false;
|
|
1382
|
-
const candidate = value;
|
|
1383
|
-
return candidate.role === "assistant" && Array.isArray(candidate.content) && typeof candidate.stopReason === "string";
|
|
1384
|
-
}
|
|
1385
|
-
function buildUserPrompt(question, conversationContext) {
|
|
1386
|
-
return [
|
|
1387
|
-
"Answer this side question without modifying the main conversation.",
|
|
1388
|
-
"",
|
|
1389
|
-
"<side_question>",
|
|
1390
|
-
question,
|
|
1391
|
-
"</side_question>",
|
|
1392
|
-
"",
|
|
1393
|
-
"<conversation_context>",
|
|
1394
|
-
conversationContext || "No prior conversation context was available.",
|
|
1395
|
-
"</conversation_context>"
|
|
1396
|
-
].join("\n");
|
|
1562
|
+
function clampSidePaneRatio(ratio) {
|
|
1563
|
+
return Number.isFinite(ratio) ? clamp(ratio, MIN_BTW_SIDE_PANE_RATIO, MAX_BTW_SIDE_PANE_RATIO) : DEFAULT_BTW_SIDE_PANE_RATIO;
|
|
1397
1564
|
}
|
|
1398
|
-
function
|
|
1399
|
-
return
|
|
1565
|
+
function roundPaneRatio(ratio) {
|
|
1566
|
+
return Math.round(ratio * 1e4) / 1e4;
|
|
1400
1567
|
}
|
|
1401
|
-
function
|
|
1402
|
-
return
|
|
1403
|
-
role: "user",
|
|
1404
|
-
content: [{ type: "text", text }],
|
|
1405
|
-
timestamp: Date.now()
|
|
1406
|
-
};
|
|
1568
|
+
function clamp(value, minimum, maximum) {
|
|
1569
|
+
return Math.max(minimum, Math.min(maximum, value));
|
|
1407
1570
|
}
|
|
1408
|
-
function
|
|
1409
|
-
|
|
1410
|
-
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
|
1571
|
+
function hasInput(component) {
|
|
1572
|
+
return component !== null && typeof component.handleInput === "function";
|
|
1411
1573
|
}
|
|
1412
|
-
function
|
|
1413
|
-
|
|
1414
|
-
return {
|
|
1574
|
+
function padLine(line, width) {
|
|
1575
|
+
const truncated = truncateToWidth2(line, width);
|
|
1576
|
+
return `${truncated}${" ".repeat(Math.max(0, width - visibleWidth2(truncated)))}`;
|
|
1415
1577
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
signal
|
|
1423
|
-
};
|
|
1424
|
-
if (applyRequestHeaderTransforms && sessionHeaders) {
|
|
1425
|
-
options.transformHeaders = (headers) => mergeSessionHeaders(headers, sessionHeaders) ?? {};
|
|
1578
|
+
|
|
1579
|
+
// src/fullscreen-ui.ts
|
|
1580
|
+
var FullscreenUiDisposedError = class extends Error {
|
|
1581
|
+
constructor() {
|
|
1582
|
+
super("The dedicated pi-btw UI was disposed.");
|
|
1583
|
+
this.name = "FullscreenUiDisposedError";
|
|
1426
1584
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1585
|
+
};
|
|
1586
|
+
async function runBtwFullscreen(ctx, run, options = {}, dependencies = {}) {
|
|
1587
|
+
const createTui = dependencies.createTui ?? ((parent, theme, keybindings, fullscreenOptions) => createBtwFullscreenTui(
|
|
1588
|
+
parent,
|
|
1589
|
+
theme,
|
|
1590
|
+
keybindings,
|
|
1591
|
+
fullscreenOptions.copyOnSelect ?? true,
|
|
1592
|
+
dependencies.manualSelectionCopySupported ?? hasManualSelectionCopyApi(),
|
|
1593
|
+
dependencies.openUrl ?? openUrlInBrowser,
|
|
1594
|
+
dependencies.copyToClipboard ?? copyToHostClipboard
|
|
1595
|
+
));
|
|
1596
|
+
let liveEditorText = ctx.ui.getEditorText();
|
|
1597
|
+
let restoreEditor = false;
|
|
1598
|
+
let host;
|
|
1599
|
+
const outcome = await ctx.ui.custom(
|
|
1600
|
+
(parent, theme, keybindings, done) => {
|
|
1601
|
+
host = new BtwFullscreenHost(
|
|
1602
|
+
parent,
|
|
1603
|
+
theme,
|
|
1604
|
+
keybindings,
|
|
1605
|
+
ctx,
|
|
1606
|
+
run,
|
|
1607
|
+
(value) => {
|
|
1608
|
+
try {
|
|
1609
|
+
liveEditorText = ctx.ui.getEditorText();
|
|
1610
|
+
restoreEditor = true;
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1613
|
+
done(value);
|
|
1614
|
+
},
|
|
1615
|
+
createTui,
|
|
1616
|
+
options
|
|
1617
|
+
);
|
|
1618
|
+
return host;
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
overlay: true,
|
|
1622
|
+
onHandle: (handle) => host?.setParentOverlay(handle)
|
|
1623
|
+
}
|
|
1624
|
+
);
|
|
1625
|
+
if (restoreEditor) {
|
|
1626
|
+
try {
|
|
1627
|
+
if (ctx.ui.getEditorText() !== liveEditorText) ctx.ui.setEditorText(liveEditorText);
|
|
1628
|
+
} catch {
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (outcome.kind === "failed") throw outcome.error;
|
|
1632
|
+
return outcome.value;
|
|
1432
1633
|
}
|
|
1433
|
-
var
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
return join(getAgentDir(), BTW_SETTINGS_FILE);
|
|
1634
|
+
var btwInputListeners = /* @__PURE__ */ new WeakMap();
|
|
1635
|
+
function dispatchBtwInput(listeners, data) {
|
|
1636
|
+
let current = data;
|
|
1637
|
+
for (const group of [listeners.beforeAll, listeners.beforeViewport, listeners.regular]) {
|
|
1638
|
+
for (const listener of group) {
|
|
1639
|
+
const result = listener(current);
|
|
1640
|
+
if (result?.consume) return result;
|
|
1641
|
+
if (result?.data !== void 0) current = result.data;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return current === data ? void 0 : { data: current };
|
|
1445
1645
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (!
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1646
|
+
var BtwTuiAltScreen = class extends TuiAltScreen {
|
|
1647
|
+
hasFocusedOverlay() {
|
|
1648
|
+
return this.isOverlayFocused();
|
|
1649
|
+
}
|
|
1650
|
+
setViewportTarget(scrollView) {
|
|
1651
|
+
if (!scrollView) return;
|
|
1652
|
+
const layout = Reflect.get(this, "currentLayout");
|
|
1653
|
+
if (layout) layout.primaryScrollView = scrollView;
|
|
1654
|
+
}
|
|
1655
|
+
addInputListener(listener) {
|
|
1656
|
+
let listeners = btwInputListeners.get(this);
|
|
1657
|
+
if (!listeners) {
|
|
1658
|
+
const registeredListeners = {
|
|
1659
|
+
beforeAll: /* @__PURE__ */ new Set(),
|
|
1660
|
+
beforeViewport: /* @__PURE__ */ new Set(),
|
|
1661
|
+
regular: /* @__PURE__ */ new Set()
|
|
1662
|
+
};
|
|
1663
|
+
btwInputListeners.set(this, registeredListeners);
|
|
1664
|
+
super.addInputListener((data) => dispatchBtwInput(registeredListeners, data));
|
|
1665
|
+
listeners = registeredListeners;
|
|
1458
1666
|
}
|
|
1667
|
+
listeners.regular.add(listener);
|
|
1668
|
+
return () => listeners.regular.delete(listener);
|
|
1459
1669
|
}
|
|
1460
|
-
|
|
1461
|
-
const
|
|
1462
|
-
if (
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (Object.hasOwn(value, "thinkingLevel")) {
|
|
1466
|
-
const thinkingLevel = Reflect.get(value, "thinkingLevel");
|
|
1467
|
-
if (!isBtwThinkingLevel(thinkingLevel)) return void 0;
|
|
1468
|
-
settings.thinkingLevel = thinkingLevel;
|
|
1670
|
+
addInputListenerBeforeAll(listener) {
|
|
1671
|
+
const listeners = btwInputListeners.get(this);
|
|
1672
|
+
if (!listeners) return super.addInputListener(listener);
|
|
1673
|
+
listeners.beforeAll.add(listener);
|
|
1674
|
+
return () => listeners.beforeAll.delete(listener);
|
|
1469
1675
|
}
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
if (
|
|
1473
|
-
|
|
1676
|
+
addInputListenerBeforeViewport(listener) {
|
|
1677
|
+
const listeners = btwInputListeners.get(this);
|
|
1678
|
+
if (!listeners) return super.addInputListener(listener);
|
|
1679
|
+
listeners.beforeViewport.add(listener);
|
|
1680
|
+
return () => listeners.beforeViewport.delete(listener);
|
|
1474
1681
|
}
|
|
1475
|
-
|
|
1476
|
-
const
|
|
1477
|
-
if (
|
|
1478
|
-
|
|
1682
|
+
removeInputListener(listener) {
|
|
1683
|
+
const listeners = btwInputListeners.get(this);
|
|
1684
|
+
if (!listeners) {
|
|
1685
|
+
super.removeInputListener(listener);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
listeners.beforeAll.delete(listener);
|
|
1689
|
+
listeners.beforeViewport.delete(listener);
|
|
1690
|
+
listeners.regular.delete(listener);
|
|
1479
1691
|
}
|
|
1480
|
-
|
|
1692
|
+
};
|
|
1693
|
+
var BRACKETED_PASTE_START = "\x1B[200~";
|
|
1694
|
+
var BRACKETED_PASTE_END = "\x1B[201~";
|
|
1695
|
+
var ALT_SCREEN_ACTIONS_BEFORE_BOTTOM = [
|
|
1696
|
+
"tui.altScreen.search",
|
|
1697
|
+
"tui.altScreen.searchNext",
|
|
1698
|
+
"tui.altScreen.searchPrevious",
|
|
1699
|
+
"tui.altScreen.searchClose",
|
|
1700
|
+
"tui.altScreen.pageUp",
|
|
1701
|
+
"tui.altScreen.pageDown",
|
|
1702
|
+
"tui.altScreen.halfPageUp",
|
|
1703
|
+
"tui.altScreen.halfPageDown",
|
|
1704
|
+
"tui.altScreen.lineUp",
|
|
1705
|
+
"tui.altScreen.lineDown",
|
|
1706
|
+
"tui.altScreen.previousPrompt",
|
|
1707
|
+
"tui.altScreen.nextPrompt",
|
|
1708
|
+
"tui.altScreen.top"
|
|
1709
|
+
];
|
|
1710
|
+
var KEY_MODIFIER_ORDER = ["shift", "ctrl", "alt", "super"];
|
|
1711
|
+
var MATCHABLE_SPECIAL_KEYS = /* @__PURE__ */ new Set([
|
|
1712
|
+
"space",
|
|
1713
|
+
"tab",
|
|
1714
|
+
"enter",
|
|
1715
|
+
"backspace",
|
|
1716
|
+
"delete",
|
|
1717
|
+
"insert",
|
|
1718
|
+
"home",
|
|
1719
|
+
"end",
|
|
1720
|
+
"pageup",
|
|
1721
|
+
"pagedown",
|
|
1722
|
+
"up",
|
|
1723
|
+
"down",
|
|
1724
|
+
"left",
|
|
1725
|
+
"right"
|
|
1726
|
+
]);
|
|
1727
|
+
var MATCHABLE_SYMBOL_KEYS = new Set("`-=[]\\;',./!@#$%^&*()_+|~{}:<>?");
|
|
1728
|
+
function normalizedKeyId(key) {
|
|
1729
|
+
const parts = key.toLowerCase().split("+");
|
|
1730
|
+
const base = parts.at(-1);
|
|
1731
|
+
if (!base) return "";
|
|
1732
|
+
const normalizedBase = base === "esc" ? "escape" : base === "return" ? "enter" : base;
|
|
1733
|
+
const modifiers = KEY_MODIFIER_ORDER.filter((modifier) => parts.includes(modifier));
|
|
1734
|
+
return [...modifiers, normalizedBase].join("+");
|
|
1481
1735
|
}
|
|
1482
|
-
function
|
|
1483
|
-
|
|
1484
|
-
const
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
1736
|
+
function formatEffectiveKeyLabel(key) {
|
|
1737
|
+
const parts = key.split("+");
|
|
1738
|
+
const base = parts.at(-1);
|
|
1739
|
+
if (base === "pageup") parts[parts.length - 1] = "pageUp";
|
|
1740
|
+
if (base === "pagedown") parts[parts.length - 1] = "pageDown";
|
|
1741
|
+
return formatKeyLabel2(parts.join("+"));
|
|
1487
1742
|
}
|
|
1488
|
-
function
|
|
1489
|
-
|
|
1743
|
+
function canMatchKeyInput(key) {
|
|
1744
|
+
const parts = key.split("+");
|
|
1745
|
+
const base = parts.at(-1) ?? "";
|
|
1746
|
+
const modifiers = parts.slice(0, -1);
|
|
1747
|
+
if (base === "escape") return modifiers.length === 0;
|
|
1748
|
+
if (base === "clear") {
|
|
1749
|
+
return modifiers.length === 0 || modifiers.length === 1 && (modifiers[0] === "shift" || modifiers[0] === "ctrl");
|
|
1750
|
+
}
|
|
1751
|
+
if (/^f(?:[1-9]|1[0-2])$/u.test(base)) return modifiers.length === 0;
|
|
1752
|
+
return MATCHABLE_SPECIAL_KEYS.has(base) || base.length === 1 && (/^[a-z0-9]$/u.test(base) || MATCHABLE_SYMBOL_KEYS.has(base));
|
|
1490
1753
|
}
|
|
1491
|
-
function
|
|
1492
|
-
|
|
1754
|
+
function rawCtrlInput(base) {
|
|
1755
|
+
if (base.length !== 1) return void 0;
|
|
1756
|
+
const rawBase = base === "-" ? "_" : base;
|
|
1757
|
+
if (!"abcdefghijklmnopqrstuvwxyz[\\]_".includes(rawBase)) return void 0;
|
|
1758
|
+
return String.fromCharCode(rawBase.charCodeAt(0) & 31);
|
|
1493
1759
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1760
|
+
function legacyRawInput(key) {
|
|
1761
|
+
const parts = key.split("+");
|
|
1762
|
+
const base = parts.at(-1) ?? "";
|
|
1763
|
+
if (parts.length === 2 && parts[0] === "ctrl") return rawCtrlInput(base);
|
|
1764
|
+
if (isKittyProtocolActive2()) return void 0;
|
|
1765
|
+
if (parts.length === 2 && parts[0] === "alt" && base.length === 1) return `\x1B${base}`;
|
|
1766
|
+
if (parts.length === 3 && parts[0] === "ctrl" && parts[1] === "alt") {
|
|
1767
|
+
const input = rawCtrlInput(base);
|
|
1768
|
+
return input ? `\x1B${input}` : void 0;
|
|
1769
|
+
}
|
|
1770
|
+
return void 0;
|
|
1497
1771
|
}
|
|
1498
|
-
function
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
const current = await readSettingsDocumentForUpdate(settingsPath);
|
|
1503
|
-
options.signal?.throwIfAborted();
|
|
1504
|
-
options.validateCurrent?.(normalizeBtwSettings(current) ?? {});
|
|
1505
|
-
const updated = applyBtwSettingsPatch(current, patch);
|
|
1506
|
-
const settings = normalizeBtwSettings(updated);
|
|
1507
|
-
if (!settings) throw invalidSettingsError(settingsPath, "invalid settings shape");
|
|
1508
|
-
await publishSettings(settingsPath, updated, options.signal, options.beforeRename);
|
|
1509
|
-
return settings;
|
|
1510
|
-
});
|
|
1772
|
+
function keyInputIdentity(key) {
|
|
1773
|
+
const identity = normalizedKeyId(key);
|
|
1774
|
+
const input = legacyRawInput(identity);
|
|
1775
|
+
return input ? normalizedKeyId(parseKey(input) ?? identity) : identity;
|
|
1511
1776
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1777
|
+
function hasManualSelectionCopyApi() {
|
|
1778
|
+
return typeof TuiAltScreen.prototype.hasActiveSelection === "function" && typeof TuiAltScreen.prototype.copyActiveSelectionToClipboard === "function";
|
|
1514
1779
|
}
|
|
1515
|
-
function
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
);
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1780
|
+
function createBtwFullscreenTui(parent, theme, keybindings, copyOnSelect, manualSelectionCopySupported, openUrl, copyToClipboard2) {
|
|
1781
|
+
if (!copyOnSelect && !manualSelectionCopySupported) {
|
|
1782
|
+
throw new Error(
|
|
1783
|
+
"Manual fullscreen selection copying is unavailable in this Pi version; update Pi or enable automatic selection copying."
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
const styleSearchMatch = (text) => theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
|
|
1787
|
+
const fullscreen = new BtwTuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), void 0, {
|
|
1788
|
+
mouse: true,
|
|
1789
|
+
copyOnSelect,
|
|
1790
|
+
searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
|
|
1791
|
+
scrollToEndIndicator: () => {
|
|
1792
|
+
const unavailableKeyIdentities = /* @__PURE__ */ new Set([keyInputIdentity(Key2.ctrl("c"))]);
|
|
1793
|
+
for (const action of ALT_SCREEN_ACTIONS_BEFORE_BOTTOM) {
|
|
1794
|
+
for (const actionKey of keybindings.getKeys(action)) {
|
|
1795
|
+
unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (!copyOnSelect) {
|
|
1799
|
+
for (const copyKey of keybindings.getKeys("app.message.copy")) {
|
|
1800
|
+
unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const key = keybindings.getKeys("tui.altScreen.bottom").map((candidate) => keyInputIdentity(String(candidate))).find(
|
|
1804
|
+
(identity) => identity && canMatchKeyInput(identity) && !unavailableKeyIdentities.has(identity) && formatEffectiveKeyLabel(identity)
|
|
1805
|
+
);
|
|
1806
|
+
const label = theme.fg("text", " \u2193 Jump to latest message");
|
|
1807
|
+
const shortcut = key ? theme.fg("muted", ` \xB7 ${formatEffectiveKeyLabel(key)}`) : "";
|
|
1808
|
+
return theme.bg("selectedBg", `${label}${shortcut} `);
|
|
1809
|
+
},
|
|
1810
|
+
searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
|
|
1811
|
+
openUrl,
|
|
1812
|
+
copySelection: async (text) => {
|
|
1813
|
+
try {
|
|
1814
|
+
await copyToClipboard2(text);
|
|
1815
|
+
return true;
|
|
1816
|
+
} catch {
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1525
1820
|
});
|
|
1526
|
-
|
|
1821
|
+
if (!copyOnSelect) {
|
|
1822
|
+
let isInBracketedPaste = false;
|
|
1823
|
+
fullscreen.addInputListenerBeforeViewport((data) => {
|
|
1824
|
+
const wasInBracketedPaste = isInBracketedPaste;
|
|
1825
|
+
const startsBracketedPaste = data.includes(BRACKETED_PASTE_START);
|
|
1826
|
+
if (startsBracketedPaste) isInBracketedPaste = true;
|
|
1827
|
+
if (isInBracketedPaste && data.includes(BRACKETED_PASTE_END)) {
|
|
1828
|
+
isInBracketedPaste = false;
|
|
1829
|
+
}
|
|
1830
|
+
if (wasInBracketedPaste || startsBracketedPaste || fullscreen.hasFocusedOverlay() || isKeyRelease2(data) || !keybindings.matches(data, "app.message.copy")) {
|
|
1831
|
+
return void 0;
|
|
1832
|
+
}
|
|
1833
|
+
if (!fullscreen.hasActiveSelection()) {
|
|
1834
|
+
fullscreen.flash("No selection to copy");
|
|
1835
|
+
return { consume: true };
|
|
1836
|
+
}
|
|
1837
|
+
void fullscreen.copyActiveSelectionToClipboard().catch(() => fullscreen.flash("Copy failed"));
|
|
1838
|
+
return { consume: true };
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
return fullscreen;
|
|
1527
1842
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1843
|
+
function openUrlInBrowser(target) {
|
|
1844
|
+
const [command, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", target]] : ["xdg-open", [target]];
|
|
1845
|
+
spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => {
|
|
1846
|
+
}).unref();
|
|
1847
|
+
}
|
|
1848
|
+
var BtwFullscreenHost = class {
|
|
1849
|
+
constructor(parent, theme, keybindings, ctx, run, done, createTui, options) {
|
|
1850
|
+
this.parent = parent;
|
|
1851
|
+
this.theme = theme;
|
|
1852
|
+
this.keybindings = keybindings;
|
|
1853
|
+
this.ctx = ctx;
|
|
1854
|
+
this.run = run;
|
|
1855
|
+
this.done = done;
|
|
1856
|
+
this.createTui = createTui;
|
|
1857
|
+
this.options = options;
|
|
1858
|
+
const initialMainInput = getFocusedComponent(parent);
|
|
1859
|
+
this.mainThreadInput = new BtwMainThreadInput(
|
|
1860
|
+
initialMainInput,
|
|
1861
|
+
() => {
|
|
1862
|
+
const target = getFocusedComponent(parent);
|
|
1863
|
+
return target === this ? initialMainInput : target;
|
|
1864
|
+
},
|
|
1865
|
+
() => this.fullscreen?.requestRender()
|
|
1866
|
+
);
|
|
1867
|
+
this.sidePaneRatio = options.sidePaneRatio;
|
|
1868
|
+
queueMicrotask(() => void this.start());
|
|
1869
|
+
}
|
|
1870
|
+
parent;
|
|
1871
|
+
theme;
|
|
1872
|
+
keybindings;
|
|
1873
|
+
ctx;
|
|
1874
|
+
run;
|
|
1875
|
+
done;
|
|
1876
|
+
createTui;
|
|
1877
|
+
options;
|
|
1878
|
+
fullscreen;
|
|
1879
|
+
parentOverlay;
|
|
1880
|
+
cancelActiveCustom;
|
|
1881
|
+
hardCancelActiveCustom;
|
|
1882
|
+
removeHardCancelListener;
|
|
1883
|
+
removeUpstreamAbortListener;
|
|
1884
|
+
started = false;
|
|
1885
|
+
disposed = false;
|
|
1886
|
+
finished = false;
|
|
1887
|
+
parentStopped = false;
|
|
1888
|
+
parentRestoreAttempted = false;
|
|
1889
|
+
fullscreenCreated = false;
|
|
1890
|
+
fullscreenStopped = false;
|
|
1891
|
+
parentRestoreQueued = false;
|
|
1892
|
+
parentRestorePromise;
|
|
1893
|
+
cleanupError;
|
|
1894
|
+
removeMainThreadUpdateListener;
|
|
1895
|
+
mainThreadRefreshTimer;
|
|
1896
|
+
mainThreadInput;
|
|
1897
|
+
lifetimeController = new AbortController();
|
|
1898
|
+
pendingSidePaneWrites = /* @__PURE__ */ new Set();
|
|
1899
|
+
sidePaneRatio;
|
|
1900
|
+
sidePaneWriteGeneration = 0;
|
|
1901
|
+
confirmedSidePaneWriteGeneration = 0;
|
|
1902
|
+
setParentOverlay(overlay) {
|
|
1903
|
+
this.parentOverlay = overlay;
|
|
1535
1904
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
return settings ? { kind: "loaded", settings } : { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
|
|
1539
|
-
} catch {
|
|
1540
|
-
return { kind: "invalid", reason: `${settingsPath}: invalid JSON` };
|
|
1905
|
+
render(width) {
|
|
1906
|
+
return [truncateToWidth3(this.theme.fg("muted", "Opening btw side thread\u2026"), width)];
|
|
1541
1907
|
}
|
|
1542
|
-
|
|
1543
|
-
async function readSettingsDocumentForUpdate(settingsPath) {
|
|
1544
|
-
let contents;
|
|
1545
|
-
try {
|
|
1546
|
-
contents = await readSettingsContents(settingsPath);
|
|
1547
|
-
} catch (error) {
|
|
1548
|
-
if (isNodeError(error) && error.code === "ENOENT") return {};
|
|
1549
|
-
throw invalidSettingsError(settingsPath, formatError2(error));
|
|
1908
|
+
invalidate() {
|
|
1550
1909
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1910
|
+
dispose() {
|
|
1911
|
+
if (this.disposed || this.finished) return;
|
|
1912
|
+
this.disposed = true;
|
|
1913
|
+
this.lifetimeController.abort();
|
|
1914
|
+
this.cancelActiveCustom?.();
|
|
1556
1915
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1916
|
+
async start() {
|
|
1917
|
+
if (this.started || this.finished) return;
|
|
1918
|
+
this.started = true;
|
|
1919
|
+
this.watchUpstreamCancellation();
|
|
1920
|
+
let outcome;
|
|
1921
|
+
try {
|
|
1922
|
+
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
1923
|
+
this.parent.stop({ preserveScreen: true });
|
|
1924
|
+
this.parentStopped = true;
|
|
1925
|
+
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
1926
|
+
this.fullscreen = this.createTui(this.parent, this.theme, this.keybindings, this.options);
|
|
1927
|
+
this.fullscreenCreated = true;
|
|
1928
|
+
this.fullscreen.start();
|
|
1929
|
+
this.watchMainThreadUpdates();
|
|
1930
|
+
const shortcuts = resolveBtwShortcuts(
|
|
1931
|
+
this.options.keybindings,
|
|
1932
|
+
this.keybindings,
|
|
1933
|
+
this.options.copyOnSelect ?? true
|
|
1934
|
+
);
|
|
1935
|
+
setBtwShortcuts(this.fullscreen, shortcuts);
|
|
1936
|
+
let previousWarnings = [];
|
|
1937
|
+
const reportWarnings = () => {
|
|
1938
|
+
const warnings = shortcuts.warnings;
|
|
1939
|
+
for (const warning of warnings) {
|
|
1940
|
+
if (previousWarnings.includes(warning)) continue;
|
|
1941
|
+
try {
|
|
1942
|
+
this.ctx.ui.notify(`Pi BTW: ${warning}`, "warning");
|
|
1943
|
+
} catch {
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
previousWarnings = warnings;
|
|
1947
|
+
};
|
|
1948
|
+
const pasteGuard = new BtwPasteGuard();
|
|
1949
|
+
const addHardCancelListener = this.fullscreen.addInputListenerBeforeAll?.bind(this.fullscreen) ?? this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ?? this.fullscreen.addInputListener.bind(this.fullscreen);
|
|
1950
|
+
this.removeHardCancelListener = addHardCancelListener((data) => {
|
|
1951
|
+
reportWarnings();
|
|
1952
|
+
if (pasteGuard.consume(data) || !shortcuts.matches(data, "exit")) return void 0;
|
|
1953
|
+
this.disposed = true;
|
|
1954
|
+
this.lifetimeController.abort();
|
|
1955
|
+
try {
|
|
1956
|
+
this.hardCancelActiveCustom?.();
|
|
1957
|
+
} finally {
|
|
1958
|
+
this.queueParentRestore();
|
|
1959
|
+
}
|
|
1960
|
+
return { consume: true };
|
|
1961
|
+
});
|
|
1962
|
+
const value = await this.run(this.createContext());
|
|
1963
|
+
await this.waitForSidePaneWrites();
|
|
1964
|
+
outcome = { kind: "completed", value };
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
await this.waitForSidePaneWrites();
|
|
1967
|
+
outcome = { kind: "failed", error };
|
|
1968
|
+
}
|
|
1969
|
+
try {
|
|
1970
|
+
this.cancelActiveCustom?.();
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
this.cleanupError ??= error;
|
|
1973
|
+
}
|
|
1974
|
+
if (this.parentRestorePromise) await this.parentRestorePromise;
|
|
1975
|
+
else this.restoreParent();
|
|
1976
|
+
if (this.cleanupError !== void 0) outcome = { kind: "failed", error: this.cleanupError };
|
|
1977
|
+
this.finished = true;
|
|
1978
|
+
this.done(outcome);
|
|
1559
1979
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1980
|
+
watchUpstreamCancellation() {
|
|
1981
|
+
const signal = this.ctx.signal;
|
|
1982
|
+
if (!signal) return;
|
|
1983
|
+
const onAbort = () => this.dispose();
|
|
1984
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1985
|
+
this.removeUpstreamAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
1986
|
+
if (signal.aborted) onAbort();
|
|
1987
|
+
}
|
|
1988
|
+
queueParentRestore() {
|
|
1989
|
+
if (this.parentRestoreQueued || this.parentRestoreAttempted) return;
|
|
1990
|
+
this.parentRestoreQueued = true;
|
|
1991
|
+
this.parentRestorePromise = Promise.resolve().then(async () => {
|
|
1992
|
+
try {
|
|
1993
|
+
await this.fullscreen?.terminal.drainInput?.();
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
this.cleanupError ??= error;
|
|
1996
|
+
}
|
|
1997
|
+
this.parentRestoreQueued = false;
|
|
1998
|
+
this.restoreParent();
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
watchMainThreadUpdates() {
|
|
2002
|
+
if ((this.options.layout ?? "fullscreen") === "fullscreen") return;
|
|
2003
|
+
this.removeMainThreadUpdateListener = this.options.subscribeMainThreadUpdates?.(() => {
|
|
2004
|
+
this.mainThreadInput.refreshTarget();
|
|
2005
|
+
if (this.mainThreadRefreshTimer || this.disposed || this.finished) return;
|
|
2006
|
+
this.mainThreadRefreshTimer = setTimeout(() => {
|
|
2007
|
+
this.mainThreadRefreshTimer = void 0;
|
|
2008
|
+
if (!this.disposed && !this.finished) this.fullscreen?.requestRender();
|
|
2009
|
+
}, 0);
|
|
2010
|
+
this.mainThreadRefreshTimer.unref();
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
restoreParent() {
|
|
2014
|
+
if (this.mainThreadRefreshTimer) {
|
|
2015
|
+
clearTimeout(this.mainThreadRefreshTimer);
|
|
2016
|
+
this.mainThreadRefreshTimer = void 0;
|
|
1570
2017
|
}
|
|
1571
|
-
const
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2018
|
+
const removeMainThreadUpdateListener = this.removeMainThreadUpdateListener;
|
|
2019
|
+
this.removeMainThreadUpdateListener = void 0;
|
|
2020
|
+
try {
|
|
2021
|
+
removeMainThreadUpdateListener?.();
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
this.cleanupError ??= error;
|
|
1577
2024
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
2025
|
+
this.mainThreadInput.dispose();
|
|
2026
|
+
const removeUpstreamAbortListener = this.removeUpstreamAbortListener;
|
|
2027
|
+
this.removeUpstreamAbortListener = void 0;
|
|
2028
|
+
try {
|
|
2029
|
+
removeUpstreamAbortListener?.();
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
this.cleanupError ??= error;
|
|
1580
2032
|
}
|
|
2033
|
+
const removeHardCancelListener = this.removeHardCancelListener;
|
|
2034
|
+
this.removeHardCancelListener = void 0;
|
|
1581
2035
|
try {
|
|
1582
|
-
|
|
1583
|
-
} catch {
|
|
1584
|
-
|
|
2036
|
+
removeHardCancelListener?.();
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
this.cleanupError ??= error;
|
|
2039
|
+
}
|
|
2040
|
+
if (this.fullscreenCreated && !this.fullscreenStopped) {
|
|
2041
|
+
this.fullscreenStopped = true;
|
|
2042
|
+
try {
|
|
2043
|
+
this.fullscreen?.stop({ preserveScreen: true });
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
this.cleanupError ??= error;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (!this.parentStopped || this.parentRestoreAttempted) return;
|
|
2049
|
+
const parentOverlay = this.parentOverlay;
|
|
2050
|
+
this.parentOverlay = void 0;
|
|
2051
|
+
try {
|
|
2052
|
+
parentOverlay?.setHidden(true);
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
this.cleanupError ??= error;
|
|
2055
|
+
}
|
|
2056
|
+
try {
|
|
2057
|
+
this.parentRestoreAttempted = true;
|
|
2058
|
+
this.parent.start();
|
|
2059
|
+
this.parent.renderNow(false);
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
this.cleanupError ??= error;
|
|
1585
2062
|
}
|
|
1586
|
-
} finally {
|
|
1587
|
-
await handle.close();
|
|
1588
2063
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
2064
|
+
createContext() {
|
|
2065
|
+
const ui = new Proxy(this.ctx.ui, {
|
|
2066
|
+
get: (target, property) => {
|
|
2067
|
+
if (property === "custom") {
|
|
2068
|
+
return (factory, options) => this.showCustom(factory, options);
|
|
2069
|
+
}
|
|
2070
|
+
if (property === "notify") {
|
|
2071
|
+
return (message, level) => {
|
|
2072
|
+
target.notify(message, level);
|
|
2073
|
+
const display = sanitizeSingleLine(message);
|
|
2074
|
+
if (display) this.fullscreen?.flash?.(display);
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
const value = Reflect.get(target, property, target);
|
|
2078
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
const signal = this.ctx.signal ? AbortSignal.any([this.ctx.signal, this.lifetimeController.signal]) : this.lifetimeController.signal;
|
|
2082
|
+
return new Proxy(this.ctx, {
|
|
2083
|
+
get: (target, property) => {
|
|
2084
|
+
if (property === "ui") return ui;
|
|
2085
|
+
if (property === "signal") return signal;
|
|
2086
|
+
return Reflect.get(target, property, target);
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
1596
2089
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2090
|
+
showCustom(factory, options) {
|
|
2091
|
+
const fullscreen = this.fullscreen;
|
|
2092
|
+
if (!fullscreen || this.disposed || this.finished) {
|
|
2093
|
+
return Promise.reject(new FullscreenUiDisposedError());
|
|
2094
|
+
}
|
|
2095
|
+
if (this.cancelActiveCustom) {
|
|
2096
|
+
return Promise.reject(new Error("pi-btw attempted to open overlapping custom UI."));
|
|
2097
|
+
}
|
|
2098
|
+
return new Promise((resolve, reject) => {
|
|
2099
|
+
let component;
|
|
2100
|
+
let overlay;
|
|
2101
|
+
let splitPane;
|
|
2102
|
+
let removePaneFocusListener;
|
|
2103
|
+
let mounted = false;
|
|
2104
|
+
let layoutMounted = false;
|
|
2105
|
+
let factorySettled = false;
|
|
2106
|
+
let closed = false;
|
|
2107
|
+
let promiseSettled = false;
|
|
2108
|
+
let componentDisposed = false;
|
|
2109
|
+
let pendingValue;
|
|
2110
|
+
let hasPendingValue = false;
|
|
2111
|
+
const disposeComponent = () => {
|
|
2112
|
+
if (!component || componentDisposed) return;
|
|
2113
|
+
componentDisposed = true;
|
|
2114
|
+
try {
|
|
2115
|
+
component.dispose?.();
|
|
2116
|
+
} catch {
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
const unmount = () => {
|
|
2120
|
+
let cleanupError;
|
|
2121
|
+
const removeFocusListener = removePaneFocusListener;
|
|
2122
|
+
removePaneFocusListener = void 0;
|
|
2123
|
+
try {
|
|
2124
|
+
removeFocusListener?.();
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
cleanupError = error;
|
|
2127
|
+
}
|
|
2128
|
+
splitPane?.dispose();
|
|
2129
|
+
splitPane = void 0;
|
|
2130
|
+
try {
|
|
2131
|
+
if (overlay) overlay.hide();
|
|
2132
|
+
else if (mounted && layoutMounted) fullscreen.setLayoutRoot(void 0);
|
|
2133
|
+
else if (mounted && component) fullscreen.removeChild(component);
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
cleanupError ??= error;
|
|
2136
|
+
}
|
|
2137
|
+
if (overlay || mounted) {
|
|
2138
|
+
try {
|
|
2139
|
+
fullscreen.setFocus(null);
|
|
2140
|
+
fullscreen.requestRender();
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
cleanupError ??= error;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
disposeComponent();
|
|
2146
|
+
if (cleanupError !== void 0) throw cleanupError;
|
|
2147
|
+
};
|
|
2148
|
+
const complete = () => {
|
|
2149
|
+
if (promiseSettled || !hasPendingValue) return;
|
|
2150
|
+
promiseSettled = true;
|
|
2151
|
+
this.cancelActiveCustom = void 0;
|
|
2152
|
+
this.hardCancelActiveCustom = void 0;
|
|
2153
|
+
if (!factorySettled) {
|
|
2154
|
+
resolve(pendingValue);
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
try {
|
|
2158
|
+
unmount();
|
|
2159
|
+
resolve(pendingValue);
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
reject(error);
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
const close = (value) => {
|
|
2165
|
+
if (closed || promiseSettled) return;
|
|
2166
|
+
closed = true;
|
|
2167
|
+
pendingValue = value;
|
|
2168
|
+
hasPendingValue = true;
|
|
2169
|
+
complete();
|
|
2170
|
+
};
|
|
2171
|
+
const fail = (error) => {
|
|
2172
|
+
if (promiseSettled) return;
|
|
2173
|
+
closed = true;
|
|
2174
|
+
promiseSettled = true;
|
|
2175
|
+
this.cancelActiveCustom = void 0;
|
|
2176
|
+
this.hardCancelActiveCustom = void 0;
|
|
2177
|
+
try {
|
|
2178
|
+
unmount();
|
|
2179
|
+
reject(error);
|
|
2180
|
+
} catch (cleanupError) {
|
|
2181
|
+
reject(cleanupError);
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
this.cancelActiveCustom = () => {
|
|
2185
|
+
if (promiseSettled) return;
|
|
2186
|
+
disposeComponent();
|
|
2187
|
+
if (!promiseSettled) fail(new FullscreenUiDisposedError());
|
|
2188
|
+
};
|
|
2189
|
+
this.hardCancelActiveCustom = () => {
|
|
2190
|
+
if (promiseSettled) return;
|
|
2191
|
+
try {
|
|
2192
|
+
component?.handleInput?.("");
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
fail(error);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
this.cancelActiveCustom?.();
|
|
2198
|
+
};
|
|
2199
|
+
let created;
|
|
2200
|
+
try {
|
|
2201
|
+
created = factory(fullscreen, this.theme, this.keybindings, close);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
factorySettled = true;
|
|
2204
|
+
fail(error);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
Promise.resolve(created).then(async (value) => {
|
|
2208
|
+
component = value;
|
|
2209
|
+
factorySettled = true;
|
|
2210
|
+
if (promiseSettled) {
|
|
2211
|
+
disposeComponent();
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
if (closed) {
|
|
2215
|
+
complete();
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
const workspaceLayout = this.options.layout ?? "fullscreen";
|
|
2219
|
+
if (!options?.overlay && workspaceLayout !== "fullscreen") {
|
|
2220
|
+
await this.waitForSidePaneWrites();
|
|
2221
|
+
if (promiseSettled) {
|
|
2222
|
+
disposeComponent();
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
if (closed) {
|
|
2226
|
+
complete();
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
if (this.disposed || this.finished || this.fullscreen !== fullscreen) {
|
|
2230
|
+
fail(new FullscreenUiDisposedError());
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
if (options?.overlay) {
|
|
2235
|
+
const overlayOptions = typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions;
|
|
2236
|
+
overlay = fullscreen.showOverlay(component, overlayOptions);
|
|
2237
|
+
options.onHandle?.(overlay);
|
|
2238
|
+
} else {
|
|
2239
|
+
fullscreen.clear();
|
|
2240
|
+
mounted = true;
|
|
2241
|
+
if (workspaceLayout !== "fullscreen") {
|
|
2242
|
+
layoutMounted = true;
|
|
2243
|
+
const sideLayout = isFullscreenLayoutComponent(component) ? component.getFullscreenLayout() : component;
|
|
2244
|
+
splitPane = new BtwSplitPane({
|
|
2245
|
+
sideComponent: component,
|
|
2246
|
+
sideLayout,
|
|
2247
|
+
mainThread: this.parent,
|
|
2248
|
+
mainLayout: getParentFullscreenLayout(this.parent),
|
|
2249
|
+
mainInput: this.mainThreadInput,
|
|
2250
|
+
sideScrollView: isFullscreenLayoutComponent(component) ? component.getPrimaryScrollView?.() : void 0,
|
|
2251
|
+
layout: workspaceLayout,
|
|
2252
|
+
theme: this.theme,
|
|
2253
|
+
terminalColumns: () => fullscreen.terminal.columns,
|
|
2254
|
+
terminalRows: () => fullscreen.terminal.rows,
|
|
2255
|
+
hasFocusedOverlay: () => fullscreen.hasFocusedOverlay?.() ?? false,
|
|
2256
|
+
setFocus: (target) => fullscreen.setFocus(target),
|
|
2257
|
+
setViewportTarget: (target) => fullscreen.setViewportTarget?.(target),
|
|
2258
|
+
requestRender: () => fullscreen.requestRender(),
|
|
2259
|
+
sidePaneRatio: this.sidePaneRatio,
|
|
2260
|
+
...this.options.persistSidePaneRatio ? { persistSidePaneRatio: (ratio) => this.persistSidePaneRatio(ratio) } : {}
|
|
2261
|
+
});
|
|
2262
|
+
const addPaneFocusListener = fullscreen.addInputListenerBeforeViewport?.bind(fullscreen) ?? fullscreen.addInputListener.bind(fullscreen);
|
|
2263
|
+
removePaneFocusListener = addPaneFocusListener((data) => {
|
|
2264
|
+
const consumed = splitPane?.handleTerminalInput(data);
|
|
2265
|
+
return consumed ? { consume: true } : void 0;
|
|
2266
|
+
});
|
|
2267
|
+
fullscreen.setLayoutRoot(splitPane.getFullscreenLayout());
|
|
2268
|
+
} else if (isFullscreenLayoutComponent(component)) {
|
|
2269
|
+
layoutMounted = true;
|
|
2270
|
+
fullscreen.setLayoutRoot(component.getFullscreenLayout());
|
|
2271
|
+
} else {
|
|
2272
|
+
fullscreen.addChild(component);
|
|
2273
|
+
}
|
|
2274
|
+
fullscreen.setFocus(component);
|
|
2275
|
+
fullscreen.requestRender();
|
|
2276
|
+
}
|
|
2277
|
+
}).catch(fail);
|
|
1607
2278
|
});
|
|
1608
|
-
await beforeRename?.(temporaryPath, settingsPath);
|
|
1609
|
-
signal?.throwIfAborted();
|
|
1610
|
-
await rename(temporaryPath, settingsPath);
|
|
1611
|
-
} catch (error) {
|
|
1612
|
-
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
1613
|
-
throw error;
|
|
1614
2279
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2280
|
+
persistSidePaneRatio(ratio) {
|
|
2281
|
+
const persist = this.options.persistSidePaneRatio;
|
|
2282
|
+
if (!persist) {
|
|
2283
|
+
this.sidePaneRatio = ratio;
|
|
2284
|
+
return Promise.resolve();
|
|
2285
|
+
}
|
|
2286
|
+
const writeGeneration = ++this.sidePaneWriteGeneration;
|
|
2287
|
+
let task;
|
|
2288
|
+
task = Promise.resolve().then(() => persist(ratio, this.lifetimeController.signal)).then(() => {
|
|
2289
|
+
if (this.lifetimeController.signal.aborted || writeGeneration <= this.confirmedSidePaneWriteGeneration) {
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
this.confirmedSidePaneWriteGeneration = writeGeneration;
|
|
2293
|
+
this.sidePaneRatio = ratio;
|
|
2294
|
+
}).catch((error) => {
|
|
2295
|
+
if (!this.lifetimeController.signal.aborted) {
|
|
2296
|
+
const message = sanitizeSingleLine(
|
|
2297
|
+
`Pi BTW pane width was not saved; the previous value remains active: ${formatError3(error)}`
|
|
2298
|
+
);
|
|
2299
|
+
try {
|
|
2300
|
+
this.ctx.ui.notify(message, "error");
|
|
2301
|
+
} catch {
|
|
2302
|
+
}
|
|
2303
|
+
this.fullscreen?.flash?.(message);
|
|
2304
|
+
}
|
|
2305
|
+
throw error;
|
|
2306
|
+
}).finally(() => this.pendingSidePaneWrites.delete(task));
|
|
2307
|
+
this.pendingSidePaneWrites.add(task);
|
|
2308
|
+
return task;
|
|
2309
|
+
}
|
|
2310
|
+
async waitForSidePaneWrites() {
|
|
2311
|
+
while (this.pendingSidePaneWrites.size > 0) {
|
|
2312
|
+
await Promise.allSettled([...this.pendingSidePaneWrites]);
|
|
1624
2313
|
}
|
|
1625
|
-
if (Object.keys(keys).length) updated.keybindings = keys;
|
|
1626
|
-
else delete updated.keybindings;
|
|
1627
|
-
}
|
|
1628
|
-
if (Object.hasOwn(patch, "model")) {
|
|
1629
|
-
if (patch.model === void 0) delete updated.model;
|
|
1630
|
-
else updated.model = patch.model;
|
|
1631
|
-
}
|
|
1632
|
-
if (Object.hasOwn(patch, "thinkingLevel")) {
|
|
1633
|
-
if (patch.thinkingLevel === void 0) delete updated.thinkingLevel;
|
|
1634
|
-
else updated.thinkingLevel = patch.thinkingLevel;
|
|
1635
|
-
}
|
|
1636
|
-
if (Object.hasOwn(patch, "rememberThinkingLevelChanges")) {
|
|
1637
|
-
updated.rememberThinkingLevelChanges = patch.rememberThinkingLevelChanges;
|
|
1638
|
-
}
|
|
1639
|
-
if (Object.hasOwn(patch, "fullscreenCopyOnSelect")) {
|
|
1640
|
-
if (patch.fullscreenCopyOnSelect === void 0) delete updated.fullscreenCopyOnSelect;
|
|
1641
|
-
else updated.fullscreenCopyOnSelect = patch.fullscreenCopyOnSelect;
|
|
1642
2314
|
}
|
|
1643
|
-
|
|
2315
|
+
};
|
|
2316
|
+
function formatError3(error) {
|
|
2317
|
+
return error instanceof Error ? error.message : String(error);
|
|
1644
2318
|
}
|
|
1645
|
-
function
|
|
1646
|
-
return
|
|
2319
|
+
function getFocusedComponent(tui) {
|
|
2320
|
+
return tui.getFocusedComponent?.() ?? null;
|
|
1647
2321
|
}
|
|
1648
|
-
function
|
|
1649
|
-
|
|
2322
|
+
function getParentFullscreenLayout(tui) {
|
|
2323
|
+
if (tui.mode !== "fullscreen") return void 0;
|
|
2324
|
+
const layoutRoot = Reflect.get(tui, "layoutRoot");
|
|
2325
|
+
return isComponent(layoutRoot) ? layoutRoot : void 0;
|
|
1650
2326
|
}
|
|
1651
|
-
function
|
|
1652
|
-
return
|
|
2327
|
+
function isComponent(value) {
|
|
2328
|
+
return typeof value === "object" && value !== null && "render" in value && typeof value.render === "function" && "invalidate" in value && typeof value.invalidate === "function";
|
|
1653
2329
|
}
|
|
1654
|
-
function
|
|
1655
|
-
return
|
|
2330
|
+
function isFullscreenLayoutComponent(component) {
|
|
2331
|
+
return "getFullscreenLayout" in component && typeof component.getFullscreenLayout === "function";
|
|
1656
2332
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
2333
|
+
|
|
2334
|
+
// src/main-thread-updates.ts
|
|
2335
|
+
function registerBtwMainThreadUpdates(pi) {
|
|
2336
|
+
const listeners = /* @__PURE__ */ new WeakMap();
|
|
2337
|
+
const notify = (ctx) => {
|
|
2338
|
+
for (const listener of listeners.get(ctx.sessionManager) ?? []) listener();
|
|
2339
|
+
};
|
|
2340
|
+
pi.on("session_info_changed", (_event, ctx) => notify(ctx));
|
|
2341
|
+
pi.on("session_compact", (_event, ctx) => notify(ctx));
|
|
2342
|
+
pi.on("session_tree", (_event, ctx) => notify(ctx));
|
|
2343
|
+
pi.on("agent_start", (_event, ctx) => notify(ctx));
|
|
2344
|
+
pi.on("agent_end", (_event, ctx) => notify(ctx));
|
|
2345
|
+
pi.on("agent_settled", (_event, ctx) => notify(ctx));
|
|
2346
|
+
pi.on("turn_start", (_event, ctx) => notify(ctx));
|
|
2347
|
+
pi.on("turn_end", (_event, ctx) => notify(ctx));
|
|
2348
|
+
pi.on("message_start", (_event, ctx) => notify(ctx));
|
|
2349
|
+
pi.on("message_update", (_event, ctx) => notify(ctx));
|
|
2350
|
+
pi.on("message_end", (_event, ctx) => notify(ctx));
|
|
2351
|
+
pi.on("tool_execution_start", (_event, ctx) => notify(ctx));
|
|
2352
|
+
pi.on("tool_execution_update", (_event, ctx) => notify(ctx));
|
|
2353
|
+
pi.on("tool_execution_end", (_event, ctx) => notify(ctx));
|
|
2354
|
+
pi.on("model_select", (_event, ctx) => notify(ctx));
|
|
2355
|
+
pi.on("thinking_level_select", (_event, ctx) => notify(ctx));
|
|
2356
|
+
return (sessionManager, listener) => {
|
|
2357
|
+
const active = listeners.get(sessionManager) ?? /* @__PURE__ */ new Set();
|
|
2358
|
+
active.add(listener);
|
|
2359
|
+
listeners.set(sessionManager, active);
|
|
2360
|
+
return () => {
|
|
2361
|
+
active.delete(listener);
|
|
2362
|
+
if (active.size === 0) listeners.delete(sessionManager);
|
|
2363
|
+
};
|
|
2364
|
+
};
|
|
1659
2365
|
}
|
|
1660
2366
|
|
|
2367
|
+
// src/main-tree-picker.ts
|
|
2368
|
+
import {
|
|
2369
|
+
copyToClipboard,
|
|
2370
|
+
TreeSelectorComponent
|
|
2371
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2372
|
+
import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
|
|
2373
|
+
|
|
1661
2374
|
// src/menu.ts
|
|
2375
|
+
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
2376
|
+
import {
|
|
2377
|
+
BorderedLoader
|
|
2378
|
+
} from "@earendil-works/pi-coding-agent";
|
|
1662
2379
|
var SAME_AS_MAIN_THREAD = "Same as main thread";
|
|
2380
|
+
var BTW_LAYOUT_LABELS = {
|
|
2381
|
+
fullscreen: "Fullscreen",
|
|
2382
|
+
"left-pane": "Side thread left",
|
|
2383
|
+
"right-pane": "Side thread right"
|
|
2384
|
+
};
|
|
2385
|
+
var BTW_LAYOUT_VALUES = Object.values(BTW_LAYOUT_LABELS);
|
|
1663
2386
|
async function showBtwCommandMenu(ctx, options) {
|
|
1664
2387
|
if (ctx.mode !== "tui") return "closed";
|
|
1665
2388
|
const { defineMenu, runMenu, sanitizeTerminalText } = await import("@narumitw/pi-tui-kit");
|
|
@@ -1875,7 +2598,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1875
2598
|
lines: [
|
|
1876
2599
|
`Model: ${displayModelValue(state.settings)}`,
|
|
1877
2600
|
`Thinking: ${displayThinkingSummary(state.settings)} \xB7 Remember changes: ${displayRememberSummary(state.settings)}`,
|
|
1878
|
-
`Copy on select: ${effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off"}`
|
|
2601
|
+
`Layout: ${BTW_LAYOUT_LABELS[effectiveBtwLayout(state.settings)]} \xB7 Copy on select: ${effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off"}`
|
|
1879
2602
|
],
|
|
1880
2603
|
items: [
|
|
1881
2604
|
{
|
|
@@ -1901,7 +2624,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1901
2624
|
{
|
|
1902
2625
|
id: "settings",
|
|
1903
2626
|
label: "Settings",
|
|
1904
|
-
description: "Choose model, thinking, keybindings, and selection copying",
|
|
2627
|
+
description: "Choose model, thinking, layout, keybindings, and selection copying",
|
|
1905
2628
|
to: state.kind === "invalid" ? "invalid" : "settings"
|
|
1906
2629
|
}
|
|
1907
2630
|
],
|
|
@@ -1956,6 +2679,14 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1956
2679
|
values: ["On", "Off"],
|
|
1957
2680
|
action: "set-fullscreen-copy"
|
|
1958
2681
|
},
|
|
2682
|
+
{
|
|
2683
|
+
id: "layout",
|
|
2684
|
+
label: "Side-thread layout",
|
|
2685
|
+
description: "Use the full workspace or place BTW beside the live, click-to-focus main thread.",
|
|
2686
|
+
currentValue: BTW_LAYOUT_LABELS[effectiveBtwLayout(state.settings)],
|
|
2687
|
+
values: BTW_LAYOUT_VALUES,
|
|
2688
|
+
action: "set-layout"
|
|
2689
|
+
},
|
|
1959
2690
|
...BTW_SHORTCUT_ACTIONS.map((action) => ({
|
|
1960
2691
|
id: action,
|
|
1961
2692
|
label: shortcutLabels[action],
|
|
@@ -2089,6 +2820,19 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
2089
2820
|
if (!signal.aborted) notifySaveFailure(ctx, error);
|
|
2090
2821
|
return { kind: "rejected" };
|
|
2091
2822
|
}
|
|
2823
|
+
},
|
|
2824
|
+
"set-layout": async ({ value, signal }) => {
|
|
2825
|
+
const layout = Object.entries(BTW_LAYOUT_LABELS).find(([, label]) => label === value)?.[0];
|
|
2826
|
+
if (!layout) return { kind: "rejected" };
|
|
2827
|
+
try {
|
|
2828
|
+
await updateSettings({ layout }, { settingsPath, signal });
|
|
2829
|
+
if (signal.aborted) return { kind: "rejected" };
|
|
2830
|
+
notifySafely(ctx, `Pi BTW layout: ${BTW_LAYOUT_LABELS[layout]}. Applies when BTW next opens.`, "info");
|
|
2831
|
+
return { kind: "stay" };
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
if (!signal.aborted) notifySaveFailure(ctx, error);
|
|
2834
|
+
return { kind: "rejected" };
|
|
2835
|
+
}
|
|
2092
2836
|
}
|
|
2093
2837
|
}
|
|
2094
2838
|
});
|
|
@@ -2187,7 +2931,7 @@ function clampToAvailableThinkingLevel(requested, available) {
|
|
|
2187
2931
|
function notifySaveFailure(ctx, error) {
|
|
2188
2932
|
notifySafely(
|
|
2189
2933
|
ctx,
|
|
2190
|
-
`Pi BTW settings were not saved; the previous value remains active: ${
|
|
2934
|
+
`Pi BTW settings were not saved; the previous value remains active: ${formatError4(error)}`,
|
|
2191
2935
|
"error"
|
|
2192
2936
|
);
|
|
2193
2937
|
}
|
|
@@ -2197,7 +2941,7 @@ function notifySafely(ctx, message, level) {
|
|
|
2197
2941
|
} catch {
|
|
2198
2942
|
}
|
|
2199
2943
|
}
|
|
2200
|
-
function
|
|
2944
|
+
function formatError4(error) {
|
|
2201
2945
|
return error instanceof Error ? error.message : String(error);
|
|
2202
2946
|
}
|
|
2203
2947
|
|
|
@@ -2290,7 +3034,7 @@ async function pickMainEntry(pi, ctx, dependencies = {}) {
|
|
|
2290
3034
|
if (!settled) notifySafely2(ctx, "Copied selected message", "info");
|
|
2291
3035
|
}).catch((error) => {
|
|
2292
3036
|
if (!settled && !controller.signal.aborted) {
|
|
2293
|
-
notifySafely2(ctx, `Could not copy selected message: ${
|
|
3037
|
+
notifySafely2(ctx, `Could not copy selected message: ${formatError5(error)}`, "error");
|
|
2294
3038
|
}
|
|
2295
3039
|
}).finally(() => {
|
|
2296
3040
|
copyControllers.delete(controller);
|
|
@@ -2318,7 +3062,7 @@ async function pickMainEntry(pi, ctx, dependencies = {}) {
|
|
|
2318
3062
|
tui.requestRender();
|
|
2319
3063
|
} catch (error) {
|
|
2320
3064
|
restoreLabel(entryId);
|
|
2321
|
-
notifySafely2(ctx, `Could not update tree label: ${
|
|
3065
|
+
notifySafely2(ctx, `Could not update tree label: ${formatError5(error)}`, "error");
|
|
2322
3066
|
}
|
|
2323
3067
|
};
|
|
2324
3068
|
selector = createSelector({
|
|
@@ -2505,7 +3249,7 @@ function notifySafely2(ctx, message, level) {
|
|
|
2505
3249
|
} catch {
|
|
2506
3250
|
}
|
|
2507
3251
|
}
|
|
2508
|
-
function
|
|
3252
|
+
function formatError5(error) {
|
|
2509
3253
|
return error instanceof Error ? error.message : String(error);
|
|
2510
3254
|
}
|
|
2511
3255
|
|
|
@@ -2565,16 +3309,16 @@ import {
|
|
|
2565
3309
|
Loader,
|
|
2566
3310
|
Markdown,
|
|
2567
3311
|
matchesKey as matchesKey4,
|
|
2568
|
-
ScrollView,
|
|
2569
|
-
truncateToWidth as
|
|
3312
|
+
ScrollView as ScrollView2,
|
|
3313
|
+
truncateToWidth as truncateToWidth4,
|
|
2570
3314
|
VStack,
|
|
2571
|
-
visibleWidth as
|
|
3315
|
+
visibleWidth as visibleWidth3
|
|
2572
3316
|
} from "@earendil-works/pi-tui";
|
|
2573
3317
|
var TRANSCRIPT_CHROME_LINES = 2;
|
|
2574
3318
|
var MAX_STEERING_DISPLAY_LINES = 3;
|
|
2575
3319
|
var OSC133_MARKERS = ["\x1B]133;A\x07", "\x1B]133;B\x07", "\x1B]133;C\x07"];
|
|
2576
3320
|
var RESERVED_APP_LINES = 3;
|
|
2577
|
-
var PreservingScrollView = class extends
|
|
3321
|
+
var PreservingScrollView = class extends ScrollView2 {
|
|
2578
3322
|
updateLayout(contentHeight, viewportHeight, requestRender) {
|
|
2579
3323
|
const preserveManualPosition = !this.isFollowingEnd;
|
|
2580
3324
|
super.updateLayout(contentHeight, viewportHeight, requestRender);
|
|
@@ -2655,6 +3399,9 @@ var BtwTranscriptPager = class {
|
|
|
2655
3399
|
getFullscreenLayout() {
|
|
2656
3400
|
return this.layoutRoot;
|
|
2657
3401
|
}
|
|
3402
|
+
getPrimaryScrollView() {
|
|
3403
|
+
return this.scrollView;
|
|
3404
|
+
}
|
|
2658
3405
|
render(width) {
|
|
2659
3406
|
if (width <= 0) return [];
|
|
2660
3407
|
const safeWidth = Math.max(1, width);
|
|
@@ -2670,7 +3417,7 @@ var BtwTranscriptPager = class {
|
|
|
2670
3417
|
this.renderFooter(safeWidth),
|
|
2671
3418
|
editorLines,
|
|
2672
3419
|
availableRows
|
|
2673
|
-
).map((line) =>
|
|
3420
|
+
).map((line) => truncateToWidth4(line, safeWidth));
|
|
2674
3421
|
}
|
|
2675
3422
|
handleInput(data) {
|
|
2676
3423
|
if (this.finished) return;
|
|
@@ -2728,7 +3475,7 @@ var BtwTranscriptPager = class {
|
|
|
2728
3475
|
const bringKey = this.shortcuts.label("bringToMain");
|
|
2729
3476
|
if (this.warning) {
|
|
2730
3477
|
const warning = width < 32 ? `Empty \u2022 ${exit}` : `${this.warning} \u2022 ${exit} exit`;
|
|
2731
|
-
return
|
|
3478
|
+
return truncateToWidth4(this.theme.fg("warning", warning), width);
|
|
2732
3479
|
}
|
|
2733
3480
|
const scrollable = this.getMaxScrollOffset() > 0;
|
|
2734
3481
|
const thinking = this.options.thinking;
|
|
@@ -2738,24 +3485,24 @@ var BtwTranscriptPager = class {
|
|
|
2738
3485
|
const fallbackBase = `btw \u2022 Enter \u2022 ${exit}`;
|
|
2739
3486
|
const compactBase = bring ? `btw \u2022 Enter \u2022 ${bringKey} \u2022 ${exit}` : fallbackBase;
|
|
2740
3487
|
const compactWithThinking = `${compactBase}${cycleHint}`;
|
|
2741
|
-
let hints =
|
|
3488
|
+
let hints = visibleWidth3(fullBase) <= width ? fullBase : visibleWidth3(compactWithThinking) <= width ? compactWithThinking : visibleWidth3(compactBase) <= width ? compactBase : fallbackBase;
|
|
2742
3489
|
if (scrollable) {
|
|
2743
3490
|
const history = ` \u2022 ${this.scrollView.scrollTop > 0 ? "\u2191 older" : "\u2193 newer"} \u2022 PgUp/PgDn history`;
|
|
2744
3491
|
const compactHistory = " \u2022 PgUp/PgDn";
|
|
2745
3492
|
const compactScrollable = bring ? `Enter \u2022 ${bringKey} \u2022 ${exit} \u2022 PgUp/PgDn` : `${fallbackBase}${compactHistory}`;
|
|
2746
|
-
if (
|
|
3493
|
+
if (visibleWidth3(`${hints}${history}`) <= width) {
|
|
2747
3494
|
hints += history;
|
|
2748
|
-
} else if (
|
|
3495
|
+
} else if (visibleWidth3(`${compactBase}${history}`) <= width) {
|
|
2749
3496
|
hints = `${compactBase}${history}`;
|
|
2750
|
-
} else if (
|
|
3497
|
+
} else if (visibleWidth3(`${hints}${compactHistory}`) <= width) {
|
|
2751
3498
|
hints += compactHistory;
|
|
2752
|
-
} else if (
|
|
3499
|
+
} else if (visibleWidth3(`${compactBase}${compactHistory}`) <= width) {
|
|
2753
3500
|
hints = `${compactBase}${compactHistory}`;
|
|
2754
|
-
} else if (
|
|
3501
|
+
} else if (visibleWidth3(compactScrollable) <= width) {
|
|
2755
3502
|
hints = compactScrollable;
|
|
2756
3503
|
}
|
|
2757
3504
|
}
|
|
2758
|
-
return
|
|
3505
|
+
return truncateToWidth4(this.theme.fg("muted", hints), width);
|
|
2759
3506
|
}
|
|
2760
3507
|
createHeaderComponent() {
|
|
2761
3508
|
return {
|
|
@@ -2878,6 +3625,9 @@ var BtwAnsweringView = class {
|
|
|
2878
3625
|
getFullscreenLayout() {
|
|
2879
3626
|
return this.layoutRoot;
|
|
2880
3627
|
}
|
|
3628
|
+
getPrimaryScrollView() {
|
|
3629
|
+
return this.scrollView;
|
|
3630
|
+
}
|
|
2881
3631
|
render(width) {
|
|
2882
3632
|
if (width <= 0) return [];
|
|
2883
3633
|
const safeWidth = Math.max(1, width);
|
|
@@ -2904,7 +3654,7 @@ var BtwAnsweringView = class {
|
|
|
2904
3654
|
editorLines,
|
|
2905
3655
|
availableRows,
|
|
2906
3656
|
steeringLines
|
|
2907
|
-
).map((line) =>
|
|
3657
|
+
).map((line) => truncateToWidth4(line, safeWidth));
|
|
2908
3658
|
}
|
|
2909
3659
|
handleInput(data) {
|
|
2910
3660
|
if (this.finished) return;
|
|
@@ -2967,7 +3717,7 @@ var BtwAnsweringView = class {
|
|
|
2967
3717
|
const exit = this.shortcuts.label("exit");
|
|
2968
3718
|
if (this.warning) {
|
|
2969
3719
|
const warning = width < 32 ? `Empty \u2022 ${exit}` : `${this.warning} \u2022 ${exit} cancel`;
|
|
2970
|
-
return
|
|
3720
|
+
return truncateToWidth4(this.theme.fg("warning", warning), width);
|
|
2971
3721
|
}
|
|
2972
3722
|
const baseHint = this.editor ? `Enter steer \u2022 ${exit} cancel` : `${exit} cancel`;
|
|
2973
3723
|
const thinking = this.options.steering?.thinking;
|
|
@@ -2975,10 +3725,10 @@ var BtwAnsweringView = class {
|
|
|
2975
3725
|
const scrollHint = this.getMaxScrollOffset() > 0 ? " \u2022 PgUp/PgDn history" : "";
|
|
2976
3726
|
const hints = `${baseHint}${cycleHint}${scrollHint}`;
|
|
2977
3727
|
const compactHints = this.editor ? `Enter \u2022 ${exit}` : exit;
|
|
2978
|
-
const selectedHints =
|
|
2979
|
-
const loaderWidth = Math.max(1, width -
|
|
3728
|
+
const selectedHints = visibleWidth3(hints) <= width ? hints : compactHints;
|
|
3729
|
+
const loaderWidth = Math.max(1, width - visibleWidth3(selectedHints) - 3);
|
|
2980
3730
|
const loaderLine = this.loader.render(loaderWidth).at(-1) ?? "Answering\u2026";
|
|
2981
|
-
return
|
|
3731
|
+
return truncateToWidth4(`${loaderLine} \u2022 ${this.theme.fg("muted", selectedHints)}`, width);
|
|
2982
3732
|
}
|
|
2983
3733
|
createHeaderComponent() {
|
|
2984
3734
|
return {
|
|
@@ -3050,8 +3800,8 @@ function renderTranscriptLines(components, width) {
|
|
|
3050
3800
|
}
|
|
3051
3801
|
function renderSideThreadHeader(width, theme, thinkingLevel) {
|
|
3052
3802
|
const thinking = thinkingLevel ? ` \xB7 thinking ${thinkingLevel}` : "";
|
|
3053
|
-
const title =
|
|
3054
|
-
const ruleWidth = Math.max(0, width -
|
|
3803
|
+
const title = truncateToWidth4(`\u2500 btw \xB7 side thread${thinking} `, width);
|
|
3804
|
+
const ruleWidth = Math.max(0, width - visibleWidth3(title));
|
|
3055
3805
|
return theme.fg("muted", `${title}${"\u2500".repeat(ruleWidth)}`);
|
|
3056
3806
|
}
|
|
3057
3807
|
function fitComposerLayout(header, contentLines, footer, editorLines, availableRows, statusLines = []) {
|
|
@@ -3074,7 +3824,7 @@ function renderSteeringLines(questions, width, theme, maxLines) {
|
|
|
3074
3824
|
const formatQuestion = (question) => sanitizeSingleLine(question) || "(non-printing message)";
|
|
3075
3825
|
if (maxLines === 1 && questions.length > 1) {
|
|
3076
3826
|
return [
|
|
3077
|
-
|
|
3827
|
+
truncateToWidth4(
|
|
3078
3828
|
theme.fg("dim", `Steering (+${questions.length - 1} more): ${formatQuestion(questions[0] ?? "")}`),
|
|
3079
3829
|
width
|
|
3080
3830
|
)
|
|
@@ -3082,9 +3832,9 @@ function renderSteeringLines(questions, width, theme, maxLines) {
|
|
|
3082
3832
|
}
|
|
3083
3833
|
const hasOverflow = questions.length > maxLines;
|
|
3084
3834
|
const questionLimit = hasOverflow ? Math.max(1, maxLines - 1) : maxLines;
|
|
3085
|
-
const lines = questions.slice(0, questionLimit).map((question) =>
|
|
3835
|
+
const lines = questions.slice(0, questionLimit).map((question) => truncateToWidth4(theme.fg("dim", `Steering: ${formatQuestion(question)}`), width));
|
|
3086
3836
|
if (hasOverflow) {
|
|
3087
|
-
lines.push(
|
|
3837
|
+
lines.push(truncateToWidth4(theme.fg("dim", `Steering: \u2026 +${questions.length - questionLimit} more`), width));
|
|
3088
3838
|
}
|
|
3089
3839
|
return lines;
|
|
3090
3840
|
}
|
|
@@ -3104,7 +3854,6 @@ function escapeTerminalControls2(text) {
|
|
|
3104
3854
|
}
|
|
3105
3855
|
|
|
3106
3856
|
// src/btw.ts
|
|
3107
|
-
var MAX_CONTEXT_CHARS = 4e4;
|
|
3108
3857
|
function createModelRegistryCompleteSimple(modelRegistry) {
|
|
3109
3858
|
const completeSimple = async (model, context, options) => modelRegistry.streamSimple(model, context, options).result();
|
|
3110
3859
|
completeSimple.appliesRequestHeaderTransforms = true;
|
|
@@ -3140,7 +3889,7 @@ async function resolveBtwModel({
|
|
|
3140
3889
|
}
|
|
3141
3890
|
return currentModel && isAvailable(currentModel) ? { model: currentModel } : void 0;
|
|
3142
3891
|
}
|
|
3143
|
-
function
|
|
3892
|
+
function formatError6(error) {
|
|
3144
3893
|
return error instanceof Error ? error.message : String(error);
|
|
3145
3894
|
}
|
|
3146
3895
|
function readBtwSessionId(ctx) {
|
|
@@ -3162,6 +3911,7 @@ function btw(pi, dependencies = {}) {
|
|
|
3162
3911
|
const resolveModel = dependencies.resolveModel ?? resolveBtwModelForCommand;
|
|
3163
3912
|
const runThread = dependencies.runThread ?? runBtwThread;
|
|
3164
3913
|
const runFullscreen = dependencies.runFullscreen ?? runBtwFullscreen;
|
|
3914
|
+
const subscribeMainThreadUpdates = registerBtwMainThreadUpdates(pi);
|
|
3165
3915
|
const resumableThreads = /* @__PURE__ */ new Map();
|
|
3166
3916
|
let nextThreadNumber = 1;
|
|
3167
3917
|
const listResumeThreads = () => [...resumableThreads.values()].reverse().filter((state) => state.thread.turns.length > 0 && state.title).sort((first, second) => second.updatedAt - first.updatedAt || second.createdAt - first.createdAt).map((state) => ({
|
|
@@ -3250,6 +4000,10 @@ function btw(pi, dependencies = {}) {
|
|
|
3250
4000
|
},
|
|
3251
4001
|
{
|
|
3252
4002
|
copyOnSelect: effectiveFullscreenCopyOnSelect(settings),
|
|
4003
|
+
layout: effectiveBtwLayout(settings),
|
|
4004
|
+
sidePaneRatio: effectiveBtwSidePaneRatio(settings),
|
|
4005
|
+
persistSidePaneRatio: (ratio, signal) => updateBtwSettings({ sidePaneRatio: ratio }, { signal }).then(() => void 0),
|
|
4006
|
+
subscribeMainThreadUpdates: (listener) => subscribeMainThreadUpdates(ctx.sessionManager, listener),
|
|
3253
4007
|
...settings.keybindings ? { keybindings: settings.keybindings } : {}
|
|
3254
4008
|
}
|
|
3255
4009
|
);
|
|
@@ -3323,7 +4077,7 @@ async function runBtwThread({
|
|
|
3323
4077
|
write = Promise.resolve().then(() => persistThinkingLevel(level)).then(() => void 0).catch((error) => {
|
|
3324
4078
|
notifySafely3(
|
|
3325
4079
|
ctx,
|
|
3326
|
-
`Thinking level changed to ${level}, but could not be remembered in pi-btw.json: ${
|
|
4080
|
+
`Thinking level changed to ${level}, but could not be remembered in pi-btw.json: ${formatError6(error)}`,
|
|
3327
4081
|
"warning"
|
|
3328
4082
|
);
|
|
3329
4083
|
}).finally(() => pendingWrites.delete(write));
|
|
@@ -3659,50 +4413,6 @@ async function showThreadComposer(thread, startAtBottom, ctx, initialQuestion, t
|
|
|
3659
4413
|
})
|
|
3660
4414
|
);
|
|
3661
4415
|
}
|
|
3662
|
-
function buildConversationContext(entries) {
|
|
3663
|
-
const sections = [];
|
|
3664
|
-
for (const entry of entries) {
|
|
3665
|
-
if (entry.type !== "message" || !entry.message?.role) continue;
|
|
3666
|
-
const role = entry.message.role;
|
|
3667
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
3668
|
-
const contentLines = extractContentLines(entry.message.content);
|
|
3669
|
-
if (contentLines.length === 0) continue;
|
|
3670
|
-
const label = role === "user" ? "User" : "Assistant";
|
|
3671
|
-
const status = entry.message.stopReason && entry.message.stopReason !== "stop" ? ` (${entry.message.stopReason})` : "";
|
|
3672
|
-
sections.push(`${label}${status}: ${contentLines.join("\n")}`);
|
|
3673
|
-
}
|
|
3674
|
-
return truncateFromStart(sections.join("\n\n"), MAX_CONTEXT_CHARS);
|
|
3675
|
-
}
|
|
3676
|
-
function extractContentLines(content) {
|
|
3677
|
-
if (typeof content === "string") return [content.trim()].filter(Boolean);
|
|
3678
|
-
if (!Array.isArray(content)) return [];
|
|
3679
|
-
const lines = [];
|
|
3680
|
-
for (const part of content) {
|
|
3681
|
-
if (!part || typeof part !== "object") continue;
|
|
3682
|
-
const block = part;
|
|
3683
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
3684
|
-
lines.push(block.text.trim());
|
|
3685
|
-
} else if (block.type === "toolCall" && typeof block.name === "string") {
|
|
3686
|
-
lines.push(`Tool call: ${block.name}(${formatJson(block.arguments)})`);
|
|
3687
|
-
} else if (block.type === "toolResult" && typeof block.name === "string") {
|
|
3688
|
-
lines.push(`Tool result from ${block.name}: ${formatJson(block.result)}`);
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
return lines.filter(Boolean);
|
|
3692
|
-
}
|
|
3693
|
-
function formatJson(value) {
|
|
3694
|
-
if (value === void 0) return "";
|
|
3695
|
-
try {
|
|
3696
|
-
return JSON.stringify(value);
|
|
3697
|
-
} catch {
|
|
3698
|
-
return String(value);
|
|
3699
|
-
}
|
|
3700
|
-
}
|
|
3701
|
-
function truncateFromStart(text, maxChars) {
|
|
3702
|
-
if (text.length <= maxChars) return text;
|
|
3703
|
-
return `[Earlier context omitted; showing the last ${maxChars} characters.]
|
|
3704
|
-
${text.slice(-maxChars)}`;
|
|
3705
|
-
}
|
|
3706
4416
|
export {
|
|
3707
4417
|
btw as default
|
|
3708
4418
|
};
|