@narumitw/pi-btw 0.58.1 → 0.60.0
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 +8 -1
- package/dist/index.ts +490 -385
- package/dist/index.ts.map +4 -4
- package/package.json +52 -53
- package/src/bring-to-main.ts +494 -547
- package/src/btw.ts +803 -856
- package/src/fullscreen-ui.ts +673 -674
- package/src/keybindings.ts +228 -270
- package/src/main-tree-picker.ts +309 -318
- package/src/menu.ts +645 -454
- package/src/settings.ts +208 -219
- package/src/side-thread.ts +173 -201
- package/src/text.ts +21 -23
- package/src/transcript-markdown.ts +65 -0
- package/src/transcript-pager.ts +611 -662
package/dist/index.ts
CHANGED
|
@@ -4,25 +4,16 @@
|
|
|
4
4
|
// src/btw.ts
|
|
5
5
|
import {
|
|
6
6
|
clampThinkingLevel,
|
|
7
|
-
getSupportedThinkingLevels
|
|
7
|
+
getSupportedThinkingLevels as getSupportedThinkingLevels2
|
|
8
8
|
} from "@earendil-works/pi-ai";
|
|
9
|
-
import {
|
|
10
|
-
BorderedLoader
|
|
11
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { BorderedLoader as BorderedLoader2 } from "@earendil-works/pi-coding-agent";
|
|
12
10
|
|
|
13
11
|
// src/bring-to-main.ts
|
|
14
|
-
import {
|
|
15
|
-
Key,
|
|
16
|
-
matchesKey,
|
|
17
|
-
truncateToWidth,
|
|
18
|
-
visibleWidth
|
|
19
|
-
} from "@earendil-works/pi-tui";
|
|
12
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
20
13
|
var RESERVED_APP_ROWS = 3;
|
|
21
14
|
var GRAPHEME_SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
22
15
|
function getAnsweredTurns(turns) {
|
|
23
|
-
return turns.filter(
|
|
24
|
-
(turn) => turn.kind === "answered"
|
|
25
|
-
);
|
|
16
|
+
return turns.filter((turn) => turn.kind === "answered");
|
|
26
17
|
}
|
|
27
18
|
function buildQuickBringToMainSegments(turns, scope) {
|
|
28
19
|
const answered = getAnsweredTurns(turns);
|
|
@@ -82,9 +73,7 @@ function segmentsFromTextRange(lines, anchor, cursor) {
|
|
|
82
73
|
return segments;
|
|
83
74
|
}
|
|
84
75
|
function estimateBringToMainTokens(segments) {
|
|
85
|
-
return Math.ceil(
|
|
86
|
-
Buffer.byteLength(segments.map((segment) => segment.text).join("\n"), "utf8") / 4
|
|
87
|
-
);
|
|
76
|
+
return Math.ceil(Buffer.byteLength(segments.map((segment) => segment.text).join("\n"), "utf8") / 4);
|
|
88
77
|
}
|
|
89
78
|
function summarizeBringToMain(segments) {
|
|
90
79
|
return {
|
|
@@ -94,10 +83,8 @@ function summarizeBringToMain(segments) {
|
|
|
94
83
|
};
|
|
95
84
|
}
|
|
96
85
|
function formatBtwBringToMain(segments) {
|
|
97
|
-
const body = segments.map(
|
|
98
|
-
|
|
99
|
-
${escapeBringToMainText(segment.text)}`
|
|
100
|
-
).join("\n\n");
|
|
86
|
+
const body = segments.map((segment) => `${segment.role === "user" ? "User" : "Assistant"}:
|
|
87
|
+
${escapeBringToMainText(segment.text)}`).join("\n\n");
|
|
101
88
|
return [
|
|
102
89
|
"The following context was brought back from a /btw side discussion.",
|
|
103
90
|
"Treat it as discussion context, not as work already completed.",
|
|
@@ -151,10 +138,7 @@ var BtwTextRangeSelector = class {
|
|
|
151
138
|
const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
152
139
|
const showStatus = availableRows >= 4;
|
|
153
140
|
const showFooter = availableRows >= 3;
|
|
154
|
-
const viewportHeight = Math.max(
|
|
155
|
-
1,
|
|
156
|
-
availableRows - 1 - (showStatus ? 1 : 0) - (showFooter ? 1 : 0)
|
|
157
|
-
);
|
|
141
|
+
const viewportHeight = Math.max(1, availableRows - 1 - (showStatus ? 1 : 0) - (showFooter ? 1 : 0));
|
|
158
142
|
this.keepCursorVisible(viewportHeight);
|
|
159
143
|
const textWidth = Math.max(1, safeWidth - visibleWidth("\u25CF> Assistant \u2502 "));
|
|
160
144
|
this.keepCursorHorizontallyVisible(textWidth);
|
|
@@ -185,20 +169,10 @@ var BtwTextRangeSelector = class {
|
|
|
185
169
|
const footer = visibleWidth(detailedFooter) <= safeWidth ? detailedFooter : criticalFooter;
|
|
186
170
|
return fitRows(
|
|
187
171
|
[
|
|
188
|
-
truncateToWidth(
|
|
189
|
-
this.theme.fg("accent", this.theme.bold("Select text to bring to main")),
|
|
190
|
-
safeWidth,
|
|
191
|
-
""
|
|
192
|
-
),
|
|
172
|
+
truncateToWidth(this.theme.fg("accent", this.theme.bold("Select text to bring to main")), safeWidth, ""),
|
|
193
173
|
...showStatus ? [truncateToWidth(this.theme.fg("muted", status), safeWidth, "")] : [],
|
|
194
174
|
...rows,
|
|
195
|
-
...showFooter ? [
|
|
196
|
-
truncateToWidth(
|
|
197
|
-
this.theme.fg(this.warning ? "warning" : "muted", footer),
|
|
198
|
-
safeWidth,
|
|
199
|
-
""
|
|
200
|
-
)
|
|
201
|
-
] : []
|
|
175
|
+
...showFooter ? [truncateToWidth(this.theme.fg(this.warning ? "warning" : "muted", footer), safeWidth, "")] : []
|
|
202
176
|
],
|
|
203
177
|
availableRows
|
|
204
178
|
);
|
|
@@ -383,9 +357,7 @@ var BtwTextRangeSelector = class {
|
|
|
383
357
|
}
|
|
384
358
|
keepCursorHorizontallyVisible(width) {
|
|
385
359
|
const characters = splitGraphemes(this.lines[this.cursor.line]?.text ?? "");
|
|
386
|
-
const displayWidths = characters.map(
|
|
387
|
-
(character) => visibleWidth(escapeTerminalControls(character))
|
|
388
|
-
);
|
|
360
|
+
const displayWidths = characters.map((character) => visibleWidth(escapeTerminalControls(character)));
|
|
389
361
|
const currentWidth = displayWidths[this.cursor.column] ?? 0;
|
|
390
362
|
let usedWidth = 1 + Math.min(currentWidth, Math.max(0, width - 1));
|
|
391
363
|
let offset = this.cursor.column;
|
|
@@ -460,10 +432,7 @@ function escapeBringToMainText(text) {
|
|
|
460
432
|
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
461
433
|
}
|
|
462
434
|
return character;
|
|
463
|
-
}).join("").replace(/<btw_context(?=[ \t\r\n>])/g, "<btw_context").replace(
|
|
464
|
-
/<\/btw_context[ \t\r\n]*>/g,
|
|
465
|
-
(terminator) => terminator.replaceAll("<", "<").replaceAll(">", ">")
|
|
466
|
-
);
|
|
435
|
+
}).join("").replace(/<btw_context(?=[ \t\r\n>])/g, "<btw_context").replace(/<\/btw_context[ \t\r\n]*>/g, (terminator) => terminator.replaceAll("<", "<").replaceAll(">", ">"));
|
|
467
436
|
}
|
|
468
437
|
function escapeTerminalControls(text) {
|
|
469
438
|
return [...text].map((character) => {
|
|
@@ -539,20 +508,7 @@ var SPECIAL = {
|
|
|
539
508
|
up: 57419,
|
|
540
509
|
down: 57420
|
|
541
510
|
};
|
|
542
|
-
var FUNCTION_INPUTS = [
|
|
543
|
-
"OP",
|
|
544
|
-
"OQ",
|
|
545
|
-
"OR",
|
|
546
|
-
"OS",
|
|
547
|
-
"[15~",
|
|
548
|
-
"[17~",
|
|
549
|
-
"[18~",
|
|
550
|
-
"[19~",
|
|
551
|
-
"[20~",
|
|
552
|
-
"[21~",
|
|
553
|
-
"[23~",
|
|
554
|
-
"[24~"
|
|
555
|
-
];
|
|
511
|
+
var FUNCTION_INPUTS = ["OP", "OQ", "OR", "OS", "[15~", "[17~", "[18~", "[19~", "[20~", "[21~", "[23~", "[24~"];
|
|
556
512
|
var LEGACY_INPUTS = [
|
|
557
513
|
...Array.from({ length: 128 }, (_, code) => String.fromCharCode(code)),
|
|
558
514
|
...Array.from({ length: 128 }, (_, code) => `\x1B${String.fromCharCode(code)}`),
|
|
@@ -574,8 +530,7 @@ function normalizeBtwKey(value) {
|
|
|
574
530
|
return void 0;
|
|
575
531
|
if (!Object.hasOwn(SPECIAL, base) && base !== "clear" && !/^f(?:[1-9]|1[0-2])$/u.test(base) && !(base.length === 1 && (/^[a-z0-9]$/u.test(base) || SYMBOLS.includes(base))))
|
|
576
532
|
return void 0;
|
|
577
|
-
if ((base === "escape" || base.startsWith("f") && base.length > 1) && parts.length)
|
|
578
|
-
return void 0;
|
|
533
|
+
if ((base === "escape" || base.startsWith("f") && base.length > 1) && parts.length) return void 0;
|
|
579
534
|
if (base === "clear" && (parts.length > 1 || parts.length === 1 && !["ctrl", "shift"].includes(parts[0] ?? "")))
|
|
580
535
|
return void 0;
|
|
581
536
|
return [...MODIFIERS.filter((part) => parts.includes(part)), base].join("+");
|
|
@@ -583,10 +538,7 @@ function normalizeBtwKey(value) {
|
|
|
583
538
|
function inputsFor(key) {
|
|
584
539
|
const parts = key.split("+");
|
|
585
540
|
const base = parts.pop() ?? "";
|
|
586
|
-
const modifier = MODIFIERS.reduce(
|
|
587
|
-
(mask, part, bit) => mask | (parts.includes(part) ? 1 << bit : 0),
|
|
588
|
-
0
|
|
589
|
-
);
|
|
541
|
+
const modifier = MODIFIERS.reduce((mask, part, bit) => mask | (parts.includes(part) ? 1 << bit : 0), 0);
|
|
590
542
|
const code = SPECIAL[base] ?? (base.length === 1 ? base.charCodeAt(0) : void 0);
|
|
591
543
|
const inputs = code === void 0 ? LEGACY_INPUTS : [...LEGACY_INPUTS, `\x1B[${code};${modifier + 1}u`];
|
|
592
544
|
return inputs.filter((input) => matchesKey2(input, key));
|
|
@@ -613,9 +565,7 @@ function reservedKeys(keybindings, copyOnSelect) {
|
|
|
613
565
|
"ctrl+j",
|
|
614
566
|
"pageUp",
|
|
615
567
|
"pageDown",
|
|
616
|
-
...Object.keys(TUI_KEYBINDINGS).flatMap(
|
|
617
|
-
(id) => keybindings.getKeys(id)
|
|
618
|
-
),
|
|
568
|
+
...Object.keys(TUI_KEYBINDINGS).flatMap((id) => keybindings.getKeys(id)),
|
|
619
569
|
...!copyOnSelect ? keybindings.getKeys("app.message.copy") : []
|
|
620
570
|
];
|
|
621
571
|
}
|
|
@@ -660,9 +610,7 @@ function resolveShortcutSnapshot(overrides = {}, keybindings, copyOnSelect = tru
|
|
|
660
610
|
const inputs = inputsFor(key);
|
|
661
611
|
if (!inputs.length) return void 0;
|
|
662
612
|
if (action === "exit" && key === "ctrl+c") return key;
|
|
663
|
-
if (reserved.some(
|
|
664
|
-
(other) => typeof other === "string" && inputs.some((input) => matchesKey2(input, other))
|
|
665
|
-
))
|
|
613
|
+
if (reserved.some((other) => typeof other === "string" && inputs.some((input) => matchesKey2(input, other))))
|
|
666
614
|
return void 0;
|
|
667
615
|
return key;
|
|
668
616
|
};
|
|
@@ -710,8 +658,7 @@ function resolveShortcutSnapshot(overrides = {}, keybindings, copyOnSelect = tru
|
|
|
710
658
|
}
|
|
711
659
|
function validateBtwShortcutEdit(action, value, overrides, keybindings, copyOnSelect) {
|
|
712
660
|
if (value === void 0) return void 0;
|
|
713
|
-
if (!normalizeBtwKey(value))
|
|
714
|
-
return "Invalid key combination. Use a Pi key name such as ctrl+q or f6.";
|
|
661
|
+
if (!normalizeBtwKey(value)) return "Invalid key combination. Use a Pi key name such as ctrl+q or f6.";
|
|
715
662
|
const next = { ...overrides, [action]: value };
|
|
716
663
|
const resolved = resolveBtwShortcuts(next, keybindings, copyOnSelect);
|
|
717
664
|
const previous = resolveBtwShortcuts(overrides, keybindings, copyOnSelect);
|
|
@@ -949,45 +896,40 @@ function createBtwFullscreenTui(parent, theme, keybindings, copyOnSelect, manual
|
|
|
949
896
|
);
|
|
950
897
|
}
|
|
951
898
|
const styleSearchMatch = (text) => theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
|
|
952
|
-
const fullscreen = new BtwTuiAltScreen(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
{
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const unavailableKeyIdentities = /* @__PURE__ */ new Set([keyInputIdentity(Key2.ctrl("c"))]);
|
|
962
|
-
for (const action of ALT_SCREEN_ACTIONS_BEFORE_BOTTOM) {
|
|
963
|
-
for (const actionKey of keybindings.getKeys(action)) {
|
|
964
|
-
unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
|
|
965
|
-
}
|
|
899
|
+
const fullscreen = new BtwTuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), void 0, {
|
|
900
|
+
mouse: true,
|
|
901
|
+
copyOnSelect,
|
|
902
|
+
searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
|
|
903
|
+
scrollToEndIndicator: () => {
|
|
904
|
+
const unavailableKeyIdentities = /* @__PURE__ */ new Set([keyInputIdentity(Key2.ctrl("c"))]);
|
|
905
|
+
for (const action of ALT_SCREEN_ACTIONS_BEFORE_BOTTOM) {
|
|
906
|
+
for (const actionKey of keybindings.getKeys(action)) {
|
|
907
|
+
unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
|
|
966
908
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
const key = keybindings.getKeys("tui.altScreen.bottom").map((candidate) => keyInputIdentity(String(candidate))).find(
|
|
973
|
-
(identity) => identity && canMatchKeyInput(identity) && !unavailableKeyIdentities.has(identity) && formatEffectiveKeyLabel(identity)
|
|
974
|
-
);
|
|
975
|
-
const label = theme.fg("text", " \u2193 Jump to latest message");
|
|
976
|
-
const shortcut = key ? theme.fg("muted", ` \xB7 ${formatEffectiveKeyLabel(key)}`) : "";
|
|
977
|
-
return theme.bg("selectedBg", `${label}${shortcut} `);
|
|
978
|
-
},
|
|
979
|
-
searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
|
|
980
|
-
openUrl,
|
|
981
|
-
copySelection: async (text) => {
|
|
982
|
-
try {
|
|
983
|
-
await copyToClipboard2(text);
|
|
984
|
-
return true;
|
|
985
|
-
} catch {
|
|
986
|
-
return false;
|
|
909
|
+
}
|
|
910
|
+
if (!copyOnSelect) {
|
|
911
|
+
for (const copyKey of keybindings.getKeys("app.message.copy")) {
|
|
912
|
+
unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
|
|
987
913
|
}
|
|
988
914
|
}
|
|
915
|
+
const key = keybindings.getKeys("tui.altScreen.bottom").map((candidate) => keyInputIdentity(String(candidate))).find(
|
|
916
|
+
(identity) => identity && canMatchKeyInput(identity) && !unavailableKeyIdentities.has(identity) && formatEffectiveKeyLabel(identity)
|
|
917
|
+
);
|
|
918
|
+
const label = theme.fg("text", " \u2193 Jump to latest message");
|
|
919
|
+
const shortcut = key ? theme.fg("muted", ` \xB7 ${formatEffectiveKeyLabel(key)}`) : "";
|
|
920
|
+
return theme.bg("selectedBg", `${label}${shortcut} `);
|
|
921
|
+
},
|
|
922
|
+
searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
|
|
923
|
+
openUrl,
|
|
924
|
+
copySelection: async (text) => {
|
|
925
|
+
try {
|
|
926
|
+
await copyToClipboard2(text);
|
|
927
|
+
return true;
|
|
928
|
+
} catch {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
989
931
|
}
|
|
990
|
-
);
|
|
932
|
+
});
|
|
991
933
|
if (!copyOnSelect) {
|
|
992
934
|
let isInBracketedPaste = false;
|
|
993
935
|
fullscreen.addInputListenerBeforeViewport((data) => {
|
|
@@ -1040,6 +982,7 @@ var BtwFullscreenHost = class {
|
|
|
1040
982
|
cancelActiveCustom;
|
|
1041
983
|
hardCancelActiveCustom;
|
|
1042
984
|
removeHardCancelListener;
|
|
985
|
+
removeUpstreamAbortListener;
|
|
1043
986
|
started = false;
|
|
1044
987
|
disposed = false;
|
|
1045
988
|
finished = false;
|
|
@@ -1050,6 +993,7 @@ var BtwFullscreenHost = class {
|
|
|
1050
993
|
parentRestoreQueued = false;
|
|
1051
994
|
parentRestorePromise;
|
|
1052
995
|
cleanupError;
|
|
996
|
+
lifetimeController = new AbortController();
|
|
1053
997
|
setParentOverlay(overlay) {
|
|
1054
998
|
this.parentOverlay = overlay;
|
|
1055
999
|
}
|
|
@@ -1061,11 +1005,13 @@ var BtwFullscreenHost = class {
|
|
|
1061
1005
|
dispose() {
|
|
1062
1006
|
if (this.disposed || this.finished) return;
|
|
1063
1007
|
this.disposed = true;
|
|
1008
|
+
this.lifetimeController.abort();
|
|
1064
1009
|
this.cancelActiveCustom?.();
|
|
1065
1010
|
}
|
|
1066
1011
|
async start() {
|
|
1067
1012
|
if (this.started || this.finished) return;
|
|
1068
1013
|
this.started = true;
|
|
1014
|
+
this.watchUpstreamCancellation();
|
|
1069
1015
|
let outcome;
|
|
1070
1016
|
try {
|
|
1071
1017
|
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
@@ -1099,6 +1045,7 @@ var BtwFullscreenHost = class {
|
|
|
1099
1045
|
reportWarnings();
|
|
1100
1046
|
if (pasteGuard.consume(data) || !shortcuts.matches(data, "exit")) return void 0;
|
|
1101
1047
|
this.disposed = true;
|
|
1048
|
+
this.lifetimeController.abort();
|
|
1102
1049
|
try {
|
|
1103
1050
|
this.hardCancelActiveCustom?.();
|
|
1104
1051
|
} finally {
|
|
@@ -1121,6 +1068,14 @@ var BtwFullscreenHost = class {
|
|
|
1121
1068
|
this.finished = true;
|
|
1122
1069
|
this.done(outcome);
|
|
1123
1070
|
}
|
|
1071
|
+
watchUpstreamCancellation() {
|
|
1072
|
+
const signal = this.ctx.signal;
|
|
1073
|
+
if (!signal) return;
|
|
1074
|
+
const onAbort = () => this.dispose();
|
|
1075
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1076
|
+
this.removeUpstreamAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
1077
|
+
if (signal.aborted) onAbort();
|
|
1078
|
+
}
|
|
1124
1079
|
queueParentRestore() {
|
|
1125
1080
|
if (this.parentRestoreQueued || this.parentRestoreAttempted) return;
|
|
1126
1081
|
this.parentRestoreQueued = true;
|
|
@@ -1135,6 +1090,13 @@ var BtwFullscreenHost = class {
|
|
|
1135
1090
|
});
|
|
1136
1091
|
}
|
|
1137
1092
|
restoreParent() {
|
|
1093
|
+
const removeUpstreamAbortListener = this.removeUpstreamAbortListener;
|
|
1094
|
+
this.removeUpstreamAbortListener = void 0;
|
|
1095
|
+
try {
|
|
1096
|
+
removeUpstreamAbortListener?.();
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
this.cleanupError ??= error;
|
|
1099
|
+
}
|
|
1138
1100
|
const removeHardCancelListener = this.removeHardCancelListener;
|
|
1139
1101
|
this.removeHardCancelListener = void 0;
|
|
1140
1102
|
try {
|
|
@@ -1183,8 +1145,13 @@ var BtwFullscreenHost = class {
|
|
|
1183
1145
|
return typeof value === "function" ? value.bind(target) : value;
|
|
1184
1146
|
}
|
|
1185
1147
|
});
|
|
1148
|
+
const signal = this.ctx.signal ? AbortSignal.any([this.ctx.signal, this.lifetimeController.signal]) : this.lifetimeController.signal;
|
|
1186
1149
|
return new Proxy(this.ctx, {
|
|
1187
|
-
get: (target, property) =>
|
|
1150
|
+
get: (target, property) => {
|
|
1151
|
+
if (property === "ui") return ui;
|
|
1152
|
+
if (property === "signal") return signal;
|
|
1153
|
+
return Reflect.get(target, property, target);
|
|
1154
|
+
}
|
|
1188
1155
|
});
|
|
1189
1156
|
}
|
|
1190
1157
|
showCustom(factory, options) {
|
|
@@ -1335,6 +1302,12 @@ import {
|
|
|
1335
1302
|
} from "@earendil-works/pi-coding-agent";
|
|
1336
1303
|
import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
|
|
1337
1304
|
|
|
1305
|
+
// src/menu.ts
|
|
1306
|
+
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1307
|
+
import {
|
|
1308
|
+
BorderedLoader
|
|
1309
|
+
} from "@earendil-works/pi-coding-agent";
|
|
1310
|
+
|
|
1338
1311
|
// src/settings.ts
|
|
1339
1312
|
import { randomUUID } from "node:crypto";
|
|
1340
1313
|
import { constants } from "node:fs";
|
|
@@ -1343,15 +1316,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
1343
1316
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
1344
1317
|
|
|
1345
1318
|
// src/side-thread.ts
|
|
1346
|
-
var BTW_THINKING_LEVELS = [
|
|
1347
|
-
"off",
|
|
1348
|
-
"minimal",
|
|
1349
|
-
"low",
|
|
1350
|
-
"medium",
|
|
1351
|
-
"high",
|
|
1352
|
-
"xhigh",
|
|
1353
|
-
"max"
|
|
1354
|
-
];
|
|
1319
|
+
var BTW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1355
1320
|
function createSideThread(conversationContext) {
|
|
1356
1321
|
return { conversationContext, turns: [] };
|
|
1357
1322
|
}
|
|
@@ -1365,10 +1330,7 @@ function buildSideThreadMessages(thread, question) {
|
|
|
1365
1330
|
return messages;
|
|
1366
1331
|
}
|
|
1367
1332
|
const [first, ...rest] = answeredTurns;
|
|
1368
|
-
messages.push(
|
|
1369
|
-
createUserMessage(buildUserPrompt(first.question, thread.conversationContext)),
|
|
1370
|
-
first.response
|
|
1371
|
-
);
|
|
1333
|
+
messages.push(createUserMessage(buildUserPrompt(first.question, thread.conversationContext)), first.response);
|
|
1372
1334
|
for (const turn of rest) {
|
|
1373
1335
|
messages.push(createUserMessage(buildFollowUpPrompt(turn.question)), turn.response);
|
|
1374
1336
|
}
|
|
@@ -1434,13 +1396,7 @@ function buildUserPrompt(question, conversationContext) {
|
|
|
1434
1396
|
].join("\n");
|
|
1435
1397
|
}
|
|
1436
1398
|
function buildFollowUpPrompt(question) {
|
|
1437
|
-
return [
|
|
1438
|
-
"Continue the same side conversation.",
|
|
1439
|
-
"",
|
|
1440
|
-
"<side_question>",
|
|
1441
|
-
question,
|
|
1442
|
-
"</side_question>"
|
|
1443
|
-
].join("\n");
|
|
1399
|
+
return ["Continue the same side conversation.", "", "<side_question>", question, "</side_question>"].join("\n");
|
|
1444
1400
|
}
|
|
1445
1401
|
function createUserMessage(text) {
|
|
1446
1402
|
return {
|
|
@@ -1632,9 +1588,7 @@ async function readSettingsContents(settingsPath) {
|
|
|
1632
1588
|
throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
1633
1589
|
}
|
|
1634
1590
|
try {
|
|
1635
|
-
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
1636
|
-
buffer.subarray(0, offset)
|
|
1637
|
-
);
|
|
1591
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, offset));
|
|
1638
1592
|
} catch {
|
|
1639
1593
|
throw new Error("settings file is not valid UTF-8");
|
|
1640
1594
|
}
|
|
@@ -1652,10 +1606,7 @@ async function publishSettings(settingsPath, document, signal, beforeRename) {
|
|
|
1652
1606
|
const directory = dirname(settingsPath);
|
|
1653
1607
|
await mkdir(directory, { recursive: true });
|
|
1654
1608
|
signal?.throwIfAborted();
|
|
1655
|
-
const temporaryPath = join(
|
|
1656
|
-
directory,
|
|
1657
|
-
`.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`
|
|
1658
|
-
);
|
|
1609
|
+
const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
1659
1610
|
try {
|
|
1660
1611
|
await writeFile(temporaryPath, contents, {
|
|
1661
1612
|
encoding: "utf8",
|
|
@@ -1683,6 +1634,10 @@ function applyBtwSettingsPatch(current, patch) {
|
|
|
1683
1634
|
if (Object.keys(keys).length) updated.keybindings = keys;
|
|
1684
1635
|
else delete updated.keybindings;
|
|
1685
1636
|
}
|
|
1637
|
+
if (Object.hasOwn(patch, "model")) {
|
|
1638
|
+
if (patch.model === void 0) delete updated.model;
|
|
1639
|
+
else updated.model = patch.model;
|
|
1640
|
+
}
|
|
1686
1641
|
if (Object.hasOwn(patch, "thinkingLevel")) {
|
|
1687
1642
|
if (patch.thinkingLevel === void 0) delete updated.thinkingLevel;
|
|
1688
1643
|
else updated.thinkingLevel = patch.thinkingLevel;
|
|
@@ -1716,12 +1671,30 @@ function formatError2(error) {
|
|
|
1716
1671
|
var SAME_AS_MAIN_THREAD = "Same as main thread";
|
|
1717
1672
|
async function showBtwCommandMenu(ctx, options) {
|
|
1718
1673
|
if (ctx.mode !== "tui") return "closed";
|
|
1719
|
-
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
1674
|
+
const { defineMenu, runMenu, sanitizeTerminalText } = await import("@narumitw/pi-tui-kit");
|
|
1720
1675
|
if (ctx.signal?.aborted) return "closed";
|
|
1721
1676
|
const settingsPath = options.settingsPath ?? btwSettingsPath();
|
|
1722
1677
|
const readSettings = options.readSettings ?? readBtwSettings;
|
|
1723
1678
|
const updateSettings = options.updateSettings ?? updateBtwSettings;
|
|
1724
|
-
const
|
|
1679
|
+
const getAvailable = ctx.modelRegistry.getAvailable;
|
|
1680
|
+
const allAvailableModels = deduplicateModels(
|
|
1681
|
+
options.availableModels ?? (typeof getAvailable === "function" ? getAvailable.call(ctx.modelRegistry) : ctx.modelRegistry.getAll())
|
|
1682
|
+
);
|
|
1683
|
+
const currentModel = options.currentModel ?? ctx.model;
|
|
1684
|
+
const scopedModels = options.scopedModels ?? ctx.scopedModels ?? [];
|
|
1685
|
+
const selectableModels = availableModelsInScope(allAvailableModels, scopedModels);
|
|
1686
|
+
const modelItemIds = new Map(selectableModels.map((model, index) => [model, `btw-settings-model:${index}`]));
|
|
1687
|
+
const modelsByItemId = new Map(selectableModels.map((model) => [modelItemIds.get(model), model]));
|
|
1688
|
+
const safeModelMetadata = (value, fallback) => {
|
|
1689
|
+
const safe = sanitizeTerminalText(value).trim() || fallback;
|
|
1690
|
+
return [...safe].slice(0, 512).join("");
|
|
1691
|
+
};
|
|
1692
|
+
const displayModelReference = (model) => `${safeModelMetadata(model.id, "unknown model")} [${safeModelMetadata(model.provider, "unknown provider")}]`;
|
|
1693
|
+
const rawModelReference = (model) => {
|
|
1694
|
+
const reference = `${model.provider}/${model.id}`;
|
|
1695
|
+
const parsed = parseBtwModelReference(reference);
|
|
1696
|
+
return parsed?.provider === model.provider && parsed.modelId === model.id ? reference : void 0;
|
|
1697
|
+
};
|
|
1725
1698
|
const displaySettingsPath = sanitizeSingleLine(settingsPath);
|
|
1726
1699
|
const resumeThreads = options.resumeThreads ?? [];
|
|
1727
1700
|
let startSelected = false;
|
|
@@ -1736,11 +1709,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1736
1709
|
};
|
|
1737
1710
|
const shortcutValue = (settings, action) => {
|
|
1738
1711
|
if (!keybindings) return "Default";
|
|
1739
|
-
const effective = resolveBtwShortcuts(
|
|
1740
|
-
settings.keybindings,
|
|
1741
|
-
keybindings,
|
|
1742
|
-
effectiveFullscreenCopyOnSelect(settings)
|
|
1743
|
-
);
|
|
1712
|
+
const effective = resolveBtwShortcuts(settings.keybindings, keybindings, effectiveFullscreenCopyOnSelect(settings));
|
|
1744
1713
|
const configured = settings.keybindings?.[action];
|
|
1745
1714
|
if (configured !== void 0 && !effective.keys[action].includes(configured)) {
|
|
1746
1715
|
return `Fallback (${effective.label(action)}; saved ${formatKeyLabel2(configured)})`;
|
|
@@ -1749,8 +1718,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1749
1718
|
return `${source} (${effective.label(action)})`;
|
|
1750
1719
|
};
|
|
1751
1720
|
const saveShortcut = async (state, value, signal) => {
|
|
1752
|
-
if (!keybindings || state.kind !== "valid" || signal.aborted)
|
|
1753
|
-
return { kind: "rejected" };
|
|
1721
|
+
if (!keybindings || state.kind !== "valid" || signal.aborted) return { kind: "rejected" };
|
|
1754
1722
|
const action = shortcut;
|
|
1755
1723
|
const manager = keybindings;
|
|
1756
1724
|
const validate = (settings) => validateBtwShortcutEdit(
|
|
@@ -1785,6 +1753,48 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1785
1753
|
return { kind: "rejected" };
|
|
1786
1754
|
}
|
|
1787
1755
|
};
|
|
1756
|
+
const saveModel = async (model, signal) => {
|
|
1757
|
+
const modelReference = model ? rawModelReference(model) : void 0;
|
|
1758
|
+
if (model && !modelReference) return { kind: "cancelled" };
|
|
1759
|
+
const result2 = await ctx.ui.custom(
|
|
1760
|
+
(tui, theme, _keybindings, done) => {
|
|
1761
|
+
const loader = new BorderedLoader(tui, theme, "Saving Pi BTW model...");
|
|
1762
|
+
const ownerController = new AbortController();
|
|
1763
|
+
const saveSignal = AbortSignal.any([signal, loader.signal, ownerController.signal]);
|
|
1764
|
+
let settled = false;
|
|
1765
|
+
const finish = (value) => {
|
|
1766
|
+
if (settled) return;
|
|
1767
|
+
settled = true;
|
|
1768
|
+
done(value);
|
|
1769
|
+
};
|
|
1770
|
+
const cancel = () => finish({ kind: "cancelled" });
|
|
1771
|
+
loader.onAbort = cancel;
|
|
1772
|
+
saveSignal.addEventListener("abort", cancel, { once: true });
|
|
1773
|
+
queueMicrotask(() => {
|
|
1774
|
+
void (async () => {
|
|
1775
|
+
await Promise.resolve();
|
|
1776
|
+
if (saveSignal.aborted) return;
|
|
1777
|
+
try {
|
|
1778
|
+
await updateSettings({ model: modelReference }, { settingsPath, signal: saveSignal });
|
|
1779
|
+
finish({ kind: "saved" });
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
finish(saveSignal.aborted ? { kind: "cancelled" } : { kind: "failed", error });
|
|
1782
|
+
}
|
|
1783
|
+
})();
|
|
1784
|
+
});
|
|
1785
|
+
return {
|
|
1786
|
+
render: (width) => loader.render(width),
|
|
1787
|
+
invalidate: () => loader.invalidate(),
|
|
1788
|
+
handleInput: (data) => loader.handleInput(data),
|
|
1789
|
+
dispose() {
|
|
1790
|
+
ownerController.abort(new DOMException("Pi BTW model save disposed", "AbortError"));
|
|
1791
|
+
loader.dispose();
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
);
|
|
1796
|
+
return result2 ?? { kind: "cancelled" };
|
|
1797
|
+
};
|
|
1788
1798
|
const loadState = async () => {
|
|
1789
1799
|
const loaded = await readSettings(settingsPath);
|
|
1790
1800
|
if (loaded.kind === "invalid") {
|
|
@@ -1792,16 +1802,79 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1792
1802
|
}
|
|
1793
1803
|
return { kind: "valid", settings: loaded.kind === "loaded" ? loaded.settings : {} };
|
|
1794
1804
|
};
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
const
|
|
1805
|
+
const configuredModel = (settings) => {
|
|
1806
|
+
if (!settings.model) return void 0;
|
|
1807
|
+
const reference = parseBtwModelReference(settings.model);
|
|
1808
|
+
return reference ? allAvailableModels.find((model) => model.provider === reference.provider && model.id === reference.modelId) : void 0;
|
|
1809
|
+
};
|
|
1810
|
+
const selectableConfiguredModel = (settings) => {
|
|
1811
|
+
const configured = configuredModel(settings);
|
|
1812
|
+
return configured ? selectableModels.find((model) => sameModel(model, configured)) : void 0;
|
|
1813
|
+
};
|
|
1814
|
+
const thinkingLevels = (settings) => {
|
|
1815
|
+
const overridden = options.availableThinkingLevels;
|
|
1816
|
+
const effectiveModel = configuredModel(settings) ?? currentModel;
|
|
1817
|
+
const available = overridden && overridden.length > 0 ? overridden : effectiveModel ? getSupportedThinkingLevels(effectiveModel) : BTW_THINKING_LEVELS;
|
|
1818
|
+
return available.length > 0 ? [...available] : ["off"];
|
|
1819
|
+
};
|
|
1820
|
+
const currentMainThinkingLevel = (settings) => clampToAvailableThinkingLevel(options.currentThinkingLevel, thinkingLevels(settings));
|
|
1821
|
+
const displayThinkingLevel = (settings) => settings.thinkingLevel === void 0 ? SAME_AS_MAIN_THREAD : clampToAvailableThinkingLevel(settings.thinkingLevel, thinkingLevels(settings));
|
|
1822
|
+
const displayThinkingSummary = (settings) => settings.thinkingLevel === void 0 ? `${SAME_AS_MAIN_THREAD} (currently ${currentMainThinkingLevel(settings)})` : displayThinkingLevel(settings);
|
|
1801
1823
|
const displayRememberSummary = (settings) => {
|
|
1802
1824
|
const value = effectiveRememberThinkingLevelChanges(settings) ? "On" : "Off";
|
|
1803
1825
|
return settings.thinkingLevel === void 0 ? `${value} (fixed levels only)` : value;
|
|
1804
1826
|
};
|
|
1827
|
+
const displayModelValue = (settings) => {
|
|
1828
|
+
if (!settings.model) {
|
|
1829
|
+
return currentModel ? `${SAME_AS_MAIN_THREAD} (${displayModelReference(currentModel)})` : SAME_AS_MAIN_THREAD;
|
|
1830
|
+
}
|
|
1831
|
+
const available = configuredModel(settings);
|
|
1832
|
+
if (!available) return `${SAME_AS_MAIN_THREAD} \xB7 ${safeModelMetadata(settings.model, "unknown model")} unavailable`;
|
|
1833
|
+
const reference = displayModelReference(available);
|
|
1834
|
+
return selectableConfiguredModel(settings) ? reference : `${reference} \xB7 outside current scope`;
|
|
1835
|
+
};
|
|
1836
|
+
const modelItems = (settings) => {
|
|
1837
|
+
const configured = configuredModel(settings);
|
|
1838
|
+
const selectable = selectableConfiguredModel(settings);
|
|
1839
|
+
const retained = settings.model && !selectable;
|
|
1840
|
+
return [
|
|
1841
|
+
{
|
|
1842
|
+
id: "same-as-main",
|
|
1843
|
+
label: SAME_AS_MAIN_THREAD,
|
|
1844
|
+
description: currentModel ? `Currently ${displayModelReference(currentModel)}` : "Use the main thread model when /btw starts."
|
|
1845
|
+
},
|
|
1846
|
+
...selectableModels.map((model) => {
|
|
1847
|
+
const reference = displayModelReference(model);
|
|
1848
|
+
const name = safeModelMetadata(model.name ?? "", "");
|
|
1849
|
+
const validReference = rawModelReference(model);
|
|
1850
|
+
return {
|
|
1851
|
+
id: modelItemIds.get(model),
|
|
1852
|
+
label: reference,
|
|
1853
|
+
...name ? { details: [`Model Name: ${name}`] } : {},
|
|
1854
|
+
searchText: [reference, name].filter(Boolean).join(" "),
|
|
1855
|
+
...!validReference ? {
|
|
1856
|
+
disabled: true,
|
|
1857
|
+
disabledReason: "This model identity cannot be stored in pi-btw.json."
|
|
1858
|
+
} : {}
|
|
1859
|
+
};
|
|
1860
|
+
}),
|
|
1861
|
+
...retained ? [
|
|
1862
|
+
{
|
|
1863
|
+
id: "configured-model",
|
|
1864
|
+
label: configured ? displayModelReference(configured) : safeModelMetadata(settings.model, "unknown model"),
|
|
1865
|
+
...configured ? { description: "Configured outside the current model scope; retained until changed." } : {
|
|
1866
|
+
disabled: true,
|
|
1867
|
+
disabledReason: "Configured model is unavailable; /btw falls back to the main model."
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
] : []
|
|
1871
|
+
];
|
|
1872
|
+
};
|
|
1873
|
+
const selectedModelItemId = (settings) => {
|
|
1874
|
+
const selected = selectableConfiguredModel(settings);
|
|
1875
|
+
return selected ? modelItemIds.get(selected) : settings.model ? "configured-model" : "same-as-main";
|
|
1876
|
+
};
|
|
1877
|
+
const currentModelItemId = (settings) => settings.model && configuredModel(settings) ? selectedModelItemId(settings) : "same-as-main";
|
|
1805
1878
|
const menu = defineMenu({
|
|
1806
1879
|
start: "main",
|
|
1807
1880
|
screens: {
|
|
@@ -1809,6 +1882,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1809
1882
|
kind: "actions",
|
|
1810
1883
|
title: "Pi BTW",
|
|
1811
1884
|
lines: [
|
|
1885
|
+
`Model: ${displayModelValue(state.settings)}`,
|
|
1812
1886
|
`Thinking: ${displayThinkingSummary(state.settings)} \xB7 Remember changes: ${displayRememberSummary(state.settings)}`,
|
|
1813
1887
|
`Copy on select: ${effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off"}`
|
|
1814
1888
|
],
|
|
@@ -1836,7 +1910,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1836
1910
|
{
|
|
1837
1911
|
id: "settings",
|
|
1838
1912
|
label: "Settings",
|
|
1839
|
-
description: "Choose thinking, keybindings, and selection copying",
|
|
1913
|
+
description: "Choose model, thinking, keybindings, and selection copying",
|
|
1840
1914
|
to: state.kind === "invalid" ? "invalid" : "settings"
|
|
1841
1915
|
}
|
|
1842
1916
|
],
|
|
@@ -1860,12 +1934,19 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1860
1934
|
title: "Pi BTW Settings",
|
|
1861
1935
|
lines: [`User settings \xB7 ${displaySettingsPath}`],
|
|
1862
1936
|
items: [
|
|
1937
|
+
{
|
|
1938
|
+
id: "model",
|
|
1939
|
+
label: "Model",
|
|
1940
|
+
description: "Choose the model for future pi-btw side threads without changing the main session.",
|
|
1941
|
+
currentValue: displayModelValue(state.settings),
|
|
1942
|
+
action: "open-model"
|
|
1943
|
+
},
|
|
1863
1944
|
{
|
|
1864
1945
|
id: "thinkingLevel",
|
|
1865
1946
|
label: "Thinking level",
|
|
1866
|
-
description: `Set the starting level for future pi-btw side threads. Currently ${currentMainThinkingLevel}.`,
|
|
1947
|
+
description: `Set the starting level for future pi-btw side threads. Currently ${currentMainThinkingLevel(state.settings)}.`,
|
|
1867
1948
|
currentValue: displayThinkingLevel(state.settings),
|
|
1868
|
-
values: [SAME_AS_MAIN_THREAD, ...
|
|
1949
|
+
values: [SAME_AS_MAIN_THREAD, ...thinkingLevels(state.settings)],
|
|
1869
1950
|
action: "set-thinking"
|
|
1870
1951
|
},
|
|
1871
1952
|
{
|
|
@@ -1893,6 +1974,21 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1893
1974
|
}))
|
|
1894
1975
|
]
|
|
1895
1976
|
}),
|
|
1977
|
+
model: ({ state }) => ({
|
|
1978
|
+
kind: "choice",
|
|
1979
|
+
title: "Pi BTW Model",
|
|
1980
|
+
lines: [
|
|
1981
|
+
"Same as main thread is the default and fallback when a configured model is unavailable.",
|
|
1982
|
+
...state.settings.model && !selectableConfiguredModel(state.settings) ? [`Configured: ${displayModelValue(state.settings)}`] : []
|
|
1983
|
+
],
|
|
1984
|
+
items: modelItems(state.settings),
|
|
1985
|
+
action: "set-model",
|
|
1986
|
+
currentItemId: currentModelItemId(state.settings),
|
|
1987
|
+
initialItemId: selectedModelItemId(state.settings),
|
|
1988
|
+
enableSearch: true,
|
|
1989
|
+
viewportSize: 10,
|
|
1990
|
+
hint: "back"
|
|
1991
|
+
}),
|
|
1896
1992
|
shortcut: ({ state }) => ({
|
|
1897
1993
|
kind: "actions",
|
|
1898
1994
|
title: shortcutLabels[shortcut],
|
|
@@ -1923,13 +2019,32 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1923
2019
|
},
|
|
1924
2020
|
actions: {
|
|
1925
2021
|
"edit-shortcut": ({ itemId }) => {
|
|
1926
|
-
if (!BTW_SHORTCUT_ACTIONS.includes(itemId))
|
|
1927
|
-
return { kind: "rejected" };
|
|
2022
|
+
if (!BTW_SHORTCUT_ACTIONS.includes(itemId)) return { kind: "rejected" };
|
|
1928
2023
|
shortcut = itemId;
|
|
1929
2024
|
return { kind: "to", screen: "shortcut" };
|
|
1930
2025
|
},
|
|
1931
2026
|
"save-shortcut": ({ state, value, signal }) => saveShortcut(state, value?.trim() ?? "", signal),
|
|
1932
2027
|
"reset-shortcut": ({ state, signal }) => saveShortcut(state, void 0, signal),
|
|
2028
|
+
"open-model": async () => ({ kind: "to", screen: "model" }),
|
|
2029
|
+
"set-model": async ({ state, itemId, signal }) => {
|
|
2030
|
+
if (itemId === "configured-model" && configuredModel(state.settings)) {
|
|
2031
|
+
return { kind: "back" };
|
|
2032
|
+
}
|
|
2033
|
+
const model = itemId ? modelsByItemId.get(itemId) : void 0;
|
|
2034
|
+
if (itemId !== "same-as-main" && (!model || !rawModelReference(model))) return { kind: "rejected" };
|
|
2035
|
+
const result2 = await saveModel(model, signal);
|
|
2036
|
+
if (result2.kind === "failed") {
|
|
2037
|
+
notifySaveFailure(ctx, result2.error);
|
|
2038
|
+
return { kind: "rejected" };
|
|
2039
|
+
}
|
|
2040
|
+
if (result2.kind === "cancelled" || signal.aborted) return { kind: "close" };
|
|
2041
|
+
notifySafely(
|
|
2042
|
+
ctx,
|
|
2043
|
+
model ? `Pi BTW model: ${displayModelReference(model)}.` : `Pi BTW model: ${SAME_AS_MAIN_THREAD}.`,
|
|
2044
|
+
"info"
|
|
2045
|
+
);
|
|
2046
|
+
return { kind: "back" };
|
|
2047
|
+
},
|
|
1933
2048
|
start: async () => {
|
|
1934
2049
|
startSelected = true;
|
|
1935
2050
|
return { kind: "close" };
|
|
@@ -1945,8 +2060,9 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1945
2060
|
resumedThreadId = itemId;
|
|
1946
2061
|
return { kind: "close" };
|
|
1947
2062
|
},
|
|
1948
|
-
"set-thinking": async ({ value, signal }) => {
|
|
2063
|
+
"set-thinking": async ({ state, value, signal }) => {
|
|
1949
2064
|
if (!value) return { kind: "rejected" };
|
|
2065
|
+
const levels = thinkingLevels(state.settings);
|
|
1950
2066
|
const patch = value === SAME_AS_MAIN_THREAD ? { thinkingLevel: void 0 } : levels.includes(value) ? { thinkingLevel: value } : void 0;
|
|
1951
2067
|
if (!patch) return { kind: "rejected" };
|
|
1952
2068
|
try {
|
|
@@ -1962,10 +2078,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1962
2078
|
"set-remember": async ({ value, signal }) => {
|
|
1963
2079
|
if (value !== "On" && value !== "Off") return { kind: "rejected" };
|
|
1964
2080
|
try {
|
|
1965
|
-
await updateSettings(
|
|
1966
|
-
{ rememberThinkingLevelChanges: value === "On" },
|
|
1967
|
-
{ settingsPath, signal }
|
|
1968
|
-
);
|
|
2081
|
+
await updateSettings({ rememberThinkingLevelChanges: value === "On" }, { settingsPath, signal });
|
|
1969
2082
|
if (signal.aborted) return { kind: "rejected" };
|
|
1970
2083
|
notifySafely(ctx, `Remember thinking level changes: ${value}.`, "info");
|
|
1971
2084
|
return { kind: "stay" };
|
|
@@ -1977,10 +2090,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1977
2090
|
"set-fullscreen-copy": async ({ value, signal }) => {
|
|
1978
2091
|
if (value !== "On" && value !== "Off") return { kind: "rejected" };
|
|
1979
2092
|
try {
|
|
1980
|
-
await updateSettings(
|
|
1981
|
-
{ fullscreenCopyOnSelect: value === "On" },
|
|
1982
|
-
{ settingsPath, signal }
|
|
1983
|
-
);
|
|
2093
|
+
await updateSettings({ fullscreenCopyOnSelect: value === "On" }, { settingsPath, signal });
|
|
1984
2094
|
if (signal.aborted) return { kind: "rejected" };
|
|
1985
2095
|
notifySafely(ctx, `Copy selection automatically: ${value}.`, "info");
|
|
1986
2096
|
return { kind: "stay" };
|
|
@@ -2055,6 +2165,21 @@ async function runBtwMenuPreservingEditor(ctx, run, onKeybindings) {
|
|
|
2055
2165
|
}
|
|
2056
2166
|
return result;
|
|
2057
2167
|
}
|
|
2168
|
+
function deduplicateModels(models) {
|
|
2169
|
+
return models.filter((model, index) => models.findIndex((candidate) => sameModel(candidate, model)) === index);
|
|
2170
|
+
}
|
|
2171
|
+
function availableModelsInScope(availableModels, scopedModels) {
|
|
2172
|
+
if (scopedModels.length === 0) return [...availableModels];
|
|
2173
|
+
return deduplicateModels(
|
|
2174
|
+
scopedModels.flatMap((entry) => {
|
|
2175
|
+
const available = availableModels.find((model) => sameModel(model, entry.model));
|
|
2176
|
+
return available ? [available] : [];
|
|
2177
|
+
})
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
function sameModel(left, right) {
|
|
2181
|
+
return left.provider === right.provider && left.id === right.id;
|
|
2182
|
+
}
|
|
2058
2183
|
function clampToAvailableThinkingLevel(requested, available) {
|
|
2059
2184
|
if (available.includes(requested)) return requested;
|
|
2060
2185
|
const requestedIndex = BTW_THINKING_LEVELS.indexOf(requested);
|
|
@@ -2145,80 +2270,77 @@ async function pickMainEntry(pi, ctx, dependencies = {}) {
|
|
|
2145
2270
|
controller.abort(new Error("The main-thread tree picker closed"));
|
|
2146
2271
|
}
|
|
2147
2272
|
};
|
|
2148
|
-
const result = await showBtwCustomPreservingEditor(
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
const
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2273
|
+
const result = await showBtwCustomPreservingEditor(ctx, (tui, _theme, _keybindings, done) => {
|
|
2274
|
+
let settled = false;
|
|
2275
|
+
let selector;
|
|
2276
|
+
const finish = (value) => {
|
|
2277
|
+
if (settled) return;
|
|
2278
|
+
settled = true;
|
|
2279
|
+
abortCopies();
|
|
2280
|
+
done(value);
|
|
2281
|
+
};
|
|
2282
|
+
const onCopy = (entryId, displayText) => {
|
|
2283
|
+
if (settled) return;
|
|
2284
|
+
const text = entryId ? rawCopyText.get(entryId) : displayText;
|
|
2285
|
+
if (!text) {
|
|
2286
|
+
notifySafely2(ctx, "Selected entry has no text to copy", "warning");
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const controller = new AbortController();
|
|
2290
|
+
copyControllers.add(controller);
|
|
2291
|
+
let operation;
|
|
2292
|
+
try {
|
|
2293
|
+
operation = copy(text, controller.signal);
|
|
2294
|
+
} catch (error) {
|
|
2295
|
+
operation = Promise.reject(error);
|
|
2296
|
+
}
|
|
2297
|
+
let task;
|
|
2298
|
+
task = operation.then(() => {
|
|
2299
|
+
if (!settled) notifySafely2(ctx, "Copied selected message", "info");
|
|
2300
|
+
}).catch((error) => {
|
|
2301
|
+
if (!settled && !controller.signal.aborted) {
|
|
2302
|
+
notifySafely2(ctx, `Could not copy selected message: ${formatError4(error)}`, "error");
|
|
2173
2303
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
selector?.setViewLabel?.(entryId, previous?.label, previous?.labelTimestamp);
|
|
2190
|
-
tui.requestRender();
|
|
2191
|
-
};
|
|
2192
|
-
const onLabelChange = (entryId, label) => {
|
|
2193
|
-
if (settled) return;
|
|
2194
|
-
try {
|
|
2195
|
-
if (!ctx.sessionManager.getEntry(entryId)) {
|
|
2196
|
-
restoreLabel(entryId);
|
|
2197
|
-
notifySafely2(ctx, "The selected main-thread entry is no longer available", "warning");
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
const persistedLabel = label === void 0 ? void 0 : sanitizeSingleLine(label);
|
|
2201
|
-
pi.setLabel(entryId, persistedLabel);
|
|
2202
|
-
savedLabels.set(entryId, { label: persistedLabel });
|
|
2203
|
-
selector?.setViewLabel?.(entryId, persistedLabel);
|
|
2204
|
-
tui.requestRender();
|
|
2205
|
-
} catch (error) {
|
|
2304
|
+
}).finally(() => {
|
|
2305
|
+
copyControllers.delete(controller);
|
|
2306
|
+
copyTasks.delete(task);
|
|
2307
|
+
});
|
|
2308
|
+
copyTasks.add(task);
|
|
2309
|
+
};
|
|
2310
|
+
const restoreLabel = (entryId) => {
|
|
2311
|
+
const previous = savedLabels.get(entryId);
|
|
2312
|
+
selector?.setViewLabel?.(entryId, previous?.label, previous?.labelTimestamp);
|
|
2313
|
+
tui.requestRender();
|
|
2314
|
+
};
|
|
2315
|
+
const onLabelChange = (entryId, label) => {
|
|
2316
|
+
if (settled) return;
|
|
2317
|
+
try {
|
|
2318
|
+
if (!ctx.sessionManager.getEntry(entryId)) {
|
|
2206
2319
|
restoreLabel(entryId);
|
|
2207
|
-
notifySafely2(ctx,
|
|
2320
|
+
notifySafely2(ctx, "The selected main-thread entry is no longer available", "warning");
|
|
2321
|
+
return;
|
|
2208
2322
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2323
|
+
const persistedLabel = label === void 0 ? void 0 : sanitizeSingleLine(label);
|
|
2324
|
+
pi.setLabel(entryId, persistedLabel);
|
|
2325
|
+
savedLabels.set(entryId, { label: persistedLabel });
|
|
2326
|
+
selector?.setViewLabel?.(entryId, persistedLabel);
|
|
2327
|
+
tui.requestRender();
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
restoreLabel(entryId);
|
|
2330
|
+
notifySafely2(ctx, `Could not update tree label: ${formatError4(error)}`, "error");
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
selector = createSelector({
|
|
2334
|
+
tree,
|
|
2335
|
+
currentLeafId,
|
|
2336
|
+
terminalRows: tui.terminal.rows,
|
|
2337
|
+
onSelect: (entryId) => finish({ kind: "selected", entryId }),
|
|
2338
|
+
onCancel: () => finish({ kind: "back" }),
|
|
2339
|
+
onCopy,
|
|
2340
|
+
onLabelChange
|
|
2341
|
+
});
|
|
2342
|
+
return new MainThreadTreePickerComponent(selector, () => finish({ kind: "closed" }));
|
|
2343
|
+
});
|
|
2222
2344
|
abortCopies();
|
|
2223
2345
|
await Promise.allSettled([...copyTasks]);
|
|
2224
2346
|
return result ?? { kind: "closed" };
|
|
@@ -2254,8 +2376,7 @@ function sanitizeEntryForDisplay(entry) {
|
|
|
2254
2376
|
switch (entry.type) {
|
|
2255
2377
|
case "message": {
|
|
2256
2378
|
const message = { ...entry.message };
|
|
2257
|
-
if ("content" in entry.message)
|
|
2258
|
-
message.content = sanitizeDisplayContent(entry.message.content);
|
|
2379
|
+
if ("content" in entry.message) message.content = sanitizeDisplayContent(entry.message.content);
|
|
2259
2380
|
for (const key of ["role", "errorMessage", "command", "toolName"]) {
|
|
2260
2381
|
const value = message[key];
|
|
2261
2382
|
if (typeof value === "string") message[key] = sanitizeSingleLine(value);
|
|
@@ -2389,6 +2510,49 @@ function formatError4(error) {
|
|
|
2389
2510
|
return error instanceof Error ? error.message : String(error);
|
|
2390
2511
|
}
|
|
2391
2512
|
|
|
2513
|
+
// src/transcript-markdown.ts
|
|
2514
|
+
var MERMAID_MARKDOWN_MODULE = "@narumitw/pi-tui-kit/markdown";
|
|
2515
|
+
var noMarkdownTransformers = () => [];
|
|
2516
|
+
async function prepareBtwTranscriptMarkdown(turns, pendingQuestion, signal) {
|
|
2517
|
+
const documents = turns.flatMap(
|
|
2518
|
+
(turn) => turn.kind === "answered" ? [turn.question, turn.answer] : [turn.question]
|
|
2519
|
+
);
|
|
2520
|
+
if (pendingQuestion) documents.push(pendingQuestion);
|
|
2521
|
+
if (!documents.some((document) => /mermaid/iu.test(document))) return noMarkdownTransformers;
|
|
2522
|
+
if (signal?.aborted) return void 0;
|
|
2523
|
+
const markdownModule = await settleUnlessAborted(
|
|
2524
|
+
import(MERMAID_MARKDOWN_MODULE),
|
|
2525
|
+
signal
|
|
2526
|
+
);
|
|
2527
|
+
if (!markdownModule || signal?.aborted) return void 0;
|
|
2528
|
+
const { createMermaidMarkdownTransformer, prepareMermaidMarkdownRenderer } = markdownModule;
|
|
2529
|
+
const preparations = /* @__PURE__ */ new Set();
|
|
2530
|
+
for (const document of documents) {
|
|
2531
|
+
const preparation = prepareMermaidMarkdownRenderer(document);
|
|
2532
|
+
if (preparation) preparations.add(preparation);
|
|
2533
|
+
}
|
|
2534
|
+
if (preparations.size > 0 && !await settleUnlessAborted(Promise.all(preparations), signal)) return void 0;
|
|
2535
|
+
if (signal?.aborted) return void 0;
|
|
2536
|
+
return (theme) => {
|
|
2537
|
+
const transformer = createMermaidMarkdownTransformer(theme);
|
|
2538
|
+
return transformer ? [transformer] : [];
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
async function settleUnlessAborted(operation, signal) {
|
|
2542
|
+
if (!signal) return operation;
|
|
2543
|
+
let onAbort;
|
|
2544
|
+
const aborted = new Promise((resolve) => {
|
|
2545
|
+
onAbort = () => resolve(void 0);
|
|
2546
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2547
|
+
if (signal.aborted) onAbort();
|
|
2548
|
+
});
|
|
2549
|
+
try {
|
|
2550
|
+
return await Promise.race([operation, aborted]);
|
|
2551
|
+
} finally {
|
|
2552
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2392
2556
|
// src/transcript-pager.ts
|
|
2393
2557
|
import {
|
|
2394
2558
|
AssistantMessageComponent,
|
|
@@ -2427,7 +2591,7 @@ var BtwTranscriptPager = class {
|
|
|
2427
2591
|
this.onAction = onAction;
|
|
2428
2592
|
this.options = options;
|
|
2429
2593
|
this.shortcuts = getBtwShortcuts(tui, options.thinking?.keybindings);
|
|
2430
|
-
this.transcriptComponents = buildTranscriptComponents(turns, this.theme);
|
|
2594
|
+
this.transcriptComponents = buildTranscriptComponents(turns, this.theme, void 0, options.markdownTransformers);
|
|
2431
2595
|
this.canBringToMain = turns.some((turn) => turn.kind === "answered");
|
|
2432
2596
|
this.thinkingLevel = options.thinking?.level;
|
|
2433
2597
|
const editorTheme = {
|
|
@@ -2497,17 +2661,10 @@ var BtwTranscriptPager = class {
|
|
|
2497
2661
|
const safeWidth = Math.max(1, width);
|
|
2498
2662
|
const editorLines = this.editor.render(safeWidth);
|
|
2499
2663
|
const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_LINES);
|
|
2500
|
-
const viewportHeight = Math.max(
|
|
2501
|
-
0,
|
|
2502
|
-
availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES
|
|
2503
|
-
);
|
|
2664
|
+
const viewportHeight = Math.max(0, availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES);
|
|
2504
2665
|
const contentLines = renderTranscriptLines(this.transcriptComponents, safeWidth);
|
|
2505
2666
|
this.lastContentLineCount = contentLines.length;
|
|
2506
|
-
this.scrollView.updateLayout(
|
|
2507
|
-
contentLines.length,
|
|
2508
|
-
viewportHeight,
|
|
2509
|
-
() => this.tui.requestRender()
|
|
2510
|
-
);
|
|
2667
|
+
this.scrollView.updateLayout(contentLines.length, viewportHeight, () => this.tui.requestRender());
|
|
2511
2668
|
return fitComposerLayout(
|
|
2512
2669
|
renderSideThreadHeader(safeWidth, this.theme, this.thinkingLevel),
|
|
2513
2670
|
contentLines.slice(this.scrollView.scrollTop, this.scrollView.scrollTop + viewportHeight),
|
|
@@ -2638,7 +2795,12 @@ var BtwAnsweringView = class {
|
|
|
2638
2795
|
this.onCancel = onCancel;
|
|
2639
2796
|
this.options = options;
|
|
2640
2797
|
this.shortcuts = getBtwShortcuts(tui, options.steering?.thinking?.keybindings);
|
|
2641
|
-
this.transcriptComponents = buildTranscriptComponents(
|
|
2798
|
+
this.transcriptComponents = buildTranscriptComponents(
|
|
2799
|
+
turns,
|
|
2800
|
+
this.theme,
|
|
2801
|
+
pendingQuestion,
|
|
2802
|
+
options.markdownTransformers
|
|
2803
|
+
);
|
|
2642
2804
|
this.thinkingLevel = options.steering?.thinking?.level ?? thinkingLevel;
|
|
2643
2805
|
this.loader = new Loader(
|
|
2644
2806
|
this.tui,
|
|
@@ -2722,10 +2884,7 @@ var BtwAnsweringView = class {
|
|
|
2722
2884
|
const safeWidth = Math.max(1, width);
|
|
2723
2885
|
const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_LINES);
|
|
2724
2886
|
const editorLines = this.editor?.render(safeWidth) ?? [];
|
|
2725
|
-
const steeringCapacity = Math.max(
|
|
2726
|
-
0,
|
|
2727
|
-
availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES
|
|
2728
|
-
);
|
|
2887
|
+
const steeringCapacity = Math.max(0, availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES);
|
|
2729
2888
|
const steeringLines = renderSteeringLines(
|
|
2730
2889
|
this.options.steering?.questions ?? [],
|
|
2731
2890
|
safeWidth,
|
|
@@ -2738,11 +2897,7 @@ var BtwAnsweringView = class {
|
|
|
2738
2897
|
);
|
|
2739
2898
|
const contentLines = renderTranscriptLines(this.transcriptComponents, safeWidth);
|
|
2740
2899
|
this.lastContentLineCount = contentLines.length;
|
|
2741
|
-
this.scrollView.updateLayout(
|
|
2742
|
-
contentLines.length,
|
|
2743
|
-
viewportHeight,
|
|
2744
|
-
() => this.tui.requestRender()
|
|
2745
|
-
);
|
|
2900
|
+
this.scrollView.updateLayout(contentLines.length, viewportHeight, () => this.tui.requestRender());
|
|
2746
2901
|
return fitComposerLayout(
|
|
2747
2902
|
renderSideThreadHeader(safeWidth, this.theme, this.thinkingLevel),
|
|
2748
2903
|
contentLines.slice(this.scrollView.scrollTop, this.scrollView.scrollTop + viewportHeight),
|
|
@@ -2847,12 +3002,7 @@ var BtwAnsweringView = class {
|
|
|
2847
3002
|
}
|
|
2848
3003
|
createSteeringComponent() {
|
|
2849
3004
|
return {
|
|
2850
|
-
render: (width) => renderSteeringLines(
|
|
2851
|
-
this.options.steering?.questions ?? [],
|
|
2852
|
-
width,
|
|
2853
|
-
this.theme,
|
|
2854
|
-
MAX_STEERING_DISPLAY_LINES
|
|
2855
|
-
),
|
|
3005
|
+
render: (width) => renderSteeringLines(this.options.steering?.questions ?? [], width, this.theme, MAX_STEERING_DISPLAY_LINES),
|
|
2856
3006
|
invalidate() {
|
|
2857
3007
|
}
|
|
2858
3008
|
};
|
|
@@ -2867,21 +3017,18 @@ var BtwAnsweringView = class {
|
|
|
2867
3017
|
return Math.max(0, this.lastContentLineCount - this.scrollView.viewportHeight);
|
|
2868
3018
|
}
|
|
2869
3019
|
};
|
|
2870
|
-
function buildTranscriptComponents(turns, theme, pendingQuestion) {
|
|
3020
|
+
function buildTranscriptComponents(turns, theme, pendingQuestion, markdownTransformers = []) {
|
|
2871
3021
|
const components = turns.flatMap((turn) => {
|
|
2872
3022
|
const question = new UserMessageComponent(
|
|
2873
3023
|
escapeTerminalControls2(turn.question),
|
|
2874
3024
|
getMarkdownTheme(),
|
|
2875
|
-
1
|
|
3025
|
+
1,
|
|
3026
|
+
markdownTransformers
|
|
2876
3027
|
);
|
|
2877
3028
|
if (turn.kind === "error") {
|
|
2878
|
-
const error = new Markdown(
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
1,
|
|
2882
|
-
getMarkdownTheme(),
|
|
2883
|
-
{ color: (text) => theme.fg("error", text) }
|
|
2884
|
-
);
|
|
3029
|
+
const error = new Markdown(`Error: ${escapeTerminalControls2(turn.answer)}`, 1, 1, getMarkdownTheme(), {
|
|
3030
|
+
color: (text) => theme.fg("error", text)
|
|
3031
|
+
});
|
|
2885
3032
|
return [question, error];
|
|
2886
3033
|
}
|
|
2887
3034
|
const response = {
|
|
@@ -2890,11 +3037,11 @@ function buildTranscriptComponents(turns, theme, pendingQuestion) {
|
|
|
2890
3037
|
stopReason: "stop",
|
|
2891
3038
|
errorMessage: void 0
|
|
2892
3039
|
};
|
|
2893
|
-
return [question, new AssistantMessageComponent(response, true, getMarkdownTheme(), "", 1)];
|
|
3040
|
+
return [question, new AssistantMessageComponent(response, true, getMarkdownTheme(), "", 1, markdownTransformers)];
|
|
2894
3041
|
});
|
|
2895
3042
|
if (pendingQuestion) {
|
|
2896
3043
|
components.push(
|
|
2897
|
-
new UserMessageComponent(escapeTerminalControls2(pendingQuestion), getMarkdownTheme(), 1)
|
|
3044
|
+
new UserMessageComponent(escapeTerminalControls2(pendingQuestion), getMarkdownTheme(), 1, markdownTransformers)
|
|
2898
3045
|
);
|
|
2899
3046
|
}
|
|
2900
3047
|
return components;
|
|
@@ -2929,26 +3076,16 @@ function renderSteeringLines(questions, width, theme, maxLines) {
|
|
|
2929
3076
|
if (maxLines === 1 && questions.length > 1) {
|
|
2930
3077
|
return [
|
|
2931
3078
|
truncateToWidth3(
|
|
2932
|
-
theme.fg(
|
|
2933
|
-
"dim",
|
|
2934
|
-
`Steering (+${questions.length - 1} more): ${formatQuestion(questions[0] ?? "")}`
|
|
2935
|
-
),
|
|
3079
|
+
theme.fg("dim", `Steering (+${questions.length - 1} more): ${formatQuestion(questions[0] ?? "")}`),
|
|
2936
3080
|
width
|
|
2937
3081
|
)
|
|
2938
3082
|
];
|
|
2939
3083
|
}
|
|
2940
3084
|
const hasOverflow = questions.length > maxLines;
|
|
2941
3085
|
const questionLimit = hasOverflow ? Math.max(1, maxLines - 1) : maxLines;
|
|
2942
|
-
const lines = questions.slice(0, questionLimit).map(
|
|
2943
|
-
(question) => truncateToWidth3(theme.fg("dim", `Steering: ${formatQuestion(question)}`), width)
|
|
2944
|
-
);
|
|
3086
|
+
const lines = questions.slice(0, questionLimit).map((question) => truncateToWidth3(theme.fg("dim", `Steering: ${formatQuestion(question)}`), width));
|
|
2945
3087
|
if (hasOverflow) {
|
|
2946
|
-
lines.push(
|
|
2947
|
-
truncateToWidth3(
|
|
2948
|
-
theme.fg("dim", `Steering: \u2026 +${questions.length - questionLimit} more`),
|
|
2949
|
-
width
|
|
2950
|
-
)
|
|
2951
|
-
);
|
|
3088
|
+
lines.push(truncateToWidth3(theme.fg("dim", `Steering: \u2026 +${questions.length - questionLimit} more`), width));
|
|
2952
3089
|
}
|
|
2953
3090
|
return lines;
|
|
2954
3091
|
}
|
|
@@ -3005,13 +3142,9 @@ async function resolveBtwModel({
|
|
|
3005
3142
|
};
|
|
3006
3143
|
}
|
|
3007
3144
|
const reason = auth.ok ? "has no request credentials" : auth.error;
|
|
3008
|
-
reportWarning(
|
|
3009
|
-
`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`
|
|
3010
|
-
);
|
|
3145
|
+
reportWarning(`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`);
|
|
3011
3146
|
} catch (error) {
|
|
3012
|
-
reportWarning(
|
|
3013
|
-
`pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`
|
|
3014
|
-
);
|
|
3147
|
+
reportWarning(`pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`);
|
|
3015
3148
|
}
|
|
3016
3149
|
if (sameAsCurrent) return void 0;
|
|
3017
3150
|
}
|
|
@@ -3061,9 +3194,7 @@ function btw(pi, dependencies = {}) {
|
|
|
3061
3194
|
const runFullscreen = dependencies.runFullscreen ?? runBtwFullscreen;
|
|
3062
3195
|
const resumableThreads = /* @__PURE__ */ new Map();
|
|
3063
3196
|
let nextThreadNumber = 1;
|
|
3064
|
-
const listResumeThreads = () => [...resumableThreads.values()].reverse().filter((state) => state.thread.turns.length > 0 && state.title).sort(
|
|
3065
|
-
(first, second) => second.updatedAt - first.updatedAt || second.createdAt - first.createdAt
|
|
3066
|
-
).map((state) => ({
|
|
3197
|
+
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) => ({
|
|
3067
3198
|
id: state.id,
|
|
3068
3199
|
title: state.title ?? "Untitled side thread",
|
|
3069
3200
|
questionCount: state.thread.turns.length
|
|
@@ -3093,11 +3224,7 @@ function btw(pi, dependencies = {}) {
|
|
|
3093
3224
|
}
|
|
3094
3225
|
const branch = ctx.sessionManager.getBranch(treeResult.entryId);
|
|
3095
3226
|
if (branch.at(-1)?.id !== treeResult.entryId) {
|
|
3096
|
-
notifySafely3(
|
|
3097
|
-
ctx,
|
|
3098
|
-
"The selected main-thread branch is no longer available",
|
|
3099
|
-
"warning"
|
|
3100
|
-
);
|
|
3227
|
+
notifySafely3(ctx, "The selected main-thread branch is no longer available", "warning");
|
|
3101
3228
|
continue;
|
|
3102
3229
|
}
|
|
3103
3230
|
selectedConversationContext = buildConversationContext(branch);
|
|
@@ -3168,19 +3295,8 @@ function btw(pi, dependencies = {}) {
|
|
|
3168
3295
|
});
|
|
3169
3296
|
}
|
|
3170
3297
|
async function showCommandMenuForBtw(pi, ctx, resumeThreads) {
|
|
3171
|
-
const currentModel = ctx.model;
|
|
3172
|
-
const availableModels = ctx.modelRegistry.getAll();
|
|
3173
|
-
const currentThinkingLevel = pi.getThinkingLevel();
|
|
3174
|
-
const loaded = await readBtwSettings();
|
|
3175
|
-
const settings = loaded.kind === "loaded" ? loaded.settings : {};
|
|
3176
|
-
const configured = settings.model ? parseBtwModelReference(settings.model) : void 0;
|
|
3177
|
-
const configuredModel = configured ? availableModels.find(
|
|
3178
|
-
(model2) => model2.provider === configured.provider && model2.id === configured.modelId
|
|
3179
|
-
) : void 0;
|
|
3180
|
-
const model = configuredModel ?? currentModel;
|
|
3181
3298
|
return showBtwCommandMenu(ctx, {
|
|
3182
|
-
currentThinkingLevel,
|
|
3183
|
-
availableThinkingLevels: model ? getSupportedThinkingLevels(model) : BTW_THINKING_LEVELS,
|
|
3299
|
+
currentThinkingLevel: pi.getThinkingLevel(),
|
|
3184
3300
|
resumeThreads
|
|
3185
3301
|
});
|
|
3186
3302
|
}
|
|
@@ -3194,7 +3310,7 @@ async function loadSettingsForCommand(ctx) {
|
|
|
3194
3310
|
}
|
|
3195
3311
|
async function resolveBtwModelWithLoader(settings, ctx) {
|
|
3196
3312
|
return ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
3197
|
-
const loader = new
|
|
3313
|
+
const loader = new BorderedLoader2(tui, theme, "Resolving /btw model credentials...");
|
|
3198
3314
|
let settled = false;
|
|
3199
3315
|
loader.onAbort = () => {
|
|
3200
3316
|
if (settled) return;
|
|
@@ -3237,13 +3353,10 @@ async function runBtwThread({
|
|
|
3237
3353
|
const persistThinkingLevel = dependencies.persistThinkingLevel ?? ((level) => updateBtwSettings({ thinkingLevel: level }, { settingsPath }));
|
|
3238
3354
|
const now = dependencies.now ?? Date.now;
|
|
3239
3355
|
const thread = state?.thread ?? createSideThread(buildConversationContext(ctx.sessionManager.getBranch()));
|
|
3240
|
-
const thinkingLevels =
|
|
3356
|
+
const thinkingLevels = getSupportedThinkingLevels2(selected.model);
|
|
3241
3357
|
const pendingWrites = /* @__PURE__ */ new Set();
|
|
3242
3358
|
const steeringQuestions = [];
|
|
3243
|
-
let activeThinkingLevel = clampThinkingLevel(
|
|
3244
|
-
selected.model,
|
|
3245
|
-
state?.thinkingLevel ?? thinkingLevel
|
|
3246
|
-
);
|
|
3359
|
+
let activeThinkingLevel = clampThinkingLevel(selected.model, state?.thinkingLevel ?? thinkingLevel);
|
|
3247
3360
|
if (state) state.thinkingLevel = activeThinkingLevel;
|
|
3248
3361
|
let pendingQuestion = initialQuestion;
|
|
3249
3362
|
let composerDraft;
|
|
@@ -3269,13 +3382,7 @@ async function runBtwThread({
|
|
|
3269
3382
|
try {
|
|
3270
3383
|
while (true) {
|
|
3271
3384
|
if (!pendingQuestion) {
|
|
3272
|
-
const action = await interact(
|
|
3273
|
-
thread,
|
|
3274
|
-
thread.turns.length > 0,
|
|
3275
|
-
ctx,
|
|
3276
|
-
composerDraft,
|
|
3277
|
-
createThinkingControl()
|
|
3278
|
-
);
|
|
3385
|
+
const action = await interact(thread, thread.turns.length > 0, ctx, composerDraft, createThinkingControl());
|
|
3279
3386
|
if (action.kind === "close") return { kind: "closed" };
|
|
3280
3387
|
if (action.kind === "bringToMain") {
|
|
3281
3388
|
const choice = await chooseBringToMainAction(thread, ctx);
|
|
@@ -3361,20 +3468,13 @@ async function chooseBringToMain(thread, ctx, dependencies = {}) {
|
|
|
3361
3468
|
);
|
|
3362
3469
|
let selectedQuestion;
|
|
3363
3470
|
while (true) {
|
|
3364
|
-
const questionResult = await showMenu(
|
|
3365
|
-
ctx,
|
|
3366
|
-
"Start from which question?",
|
|
3367
|
-
questions,
|
|
3368
|
-
selectedQuestion
|
|
3369
|
-
);
|
|
3471
|
+
const questionResult = await showMenu(ctx, "Start from which question?", questions, selectedQuestion);
|
|
3370
3472
|
if (questionResult.kind === "close") return { kind: "closed" };
|
|
3371
3473
|
if (questionResult.kind === "back") break;
|
|
3372
3474
|
const answeredTurnIndex = questions.indexOf(questionResult.value);
|
|
3373
3475
|
if (answeredTurnIndex < 0) continue;
|
|
3374
3476
|
selectedQuestion = questionResult.value;
|
|
3375
|
-
const choice = makeChoice(
|
|
3376
|
-
buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex })
|
|
3377
|
-
);
|
|
3477
|
+
const choice = makeChoice(buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex }));
|
|
3378
3478
|
const preview = await showPreview(ctx, choice.draft, choice.summary);
|
|
3379
3479
|
if (preview.kind === "close") return { kind: "closed" };
|
|
3380
3480
|
if (preview.kind === "back") continue;
|
|
@@ -3497,10 +3597,7 @@ async function loadBringToMainDraft(draft, ctx, summary) {
|
|
|
3497
3597
|
const existing = ctx.ui.getEditorText();
|
|
3498
3598
|
if (!existing.trim()) {
|
|
3499
3599
|
ctx.ui.setEditorText(draft);
|
|
3500
|
-
ctx.ui.notify(
|
|
3501
|
-
`Brought ${describeContent()} to the main editor. Review and submit when ready.`,
|
|
3502
|
-
"info"
|
|
3503
|
-
);
|
|
3600
|
+
ctx.ui.notify(`Brought ${describeContent()} to the main editor. Review and submit when ready.`, "info");
|
|
3504
3601
|
return "loaded";
|
|
3505
3602
|
}
|
|
3506
3603
|
const appendOption = "Append after current draft Recommended";
|
|
@@ -3527,11 +3624,10 @@ ${draft}`);
|
|
|
3527
3624
|
if (action.value !== replaceOption) continue;
|
|
3528
3625
|
const current = ctx.ui.getEditorText();
|
|
3529
3626
|
const characters = [...current].length;
|
|
3530
|
-
const confirmed = await showBtwMenu(
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
);
|
|
3627
|
+
const confirmed = await showBtwMenu(ctx, `Replace the current ${characters}-character editor draft?`, [
|
|
3628
|
+
"Back Keep current editor text",
|
|
3629
|
+
"\u26A0 Replace current draft Cannot be undone"
|
|
3630
|
+
]);
|
|
3535
3631
|
if (confirmed.kind === "close") return "closed";
|
|
3536
3632
|
if (confirmed.kind === "back" || confirmed.value === "Back Keep current editor text") continue;
|
|
3537
3633
|
if (confirmed.value !== "\u26A0 Replace current draft Cannot be undone") continue;
|
|
@@ -3543,63 +3639,72 @@ ${draft}`);
|
|
|
3543
3639
|
continue;
|
|
3544
3640
|
}
|
|
3545
3641
|
ctx.ui.setEditorText(draft);
|
|
3546
|
-
ctx.ui.notify(
|
|
3547
|
-
`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`,
|
|
3548
|
-
"info"
|
|
3549
|
-
);
|
|
3642
|
+
ctx.ui.notify(`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`, "info");
|
|
3550
3643
|
return "loaded";
|
|
3551
3644
|
}
|
|
3552
3645
|
}
|
|
3553
3646
|
function truncatePreview(text) {
|
|
3554
3647
|
return text.length <= 72 ? text : `${text.slice(0, 69)}\u2026`;
|
|
3555
3648
|
}
|
|
3649
|
+
async function prepareCurrentTranscriptMarkdown(thread, pendingQuestion, ctx) {
|
|
3650
|
+
while (true) {
|
|
3651
|
+
const turnCount = thread.turns.length;
|
|
3652
|
+
const createMarkdownTransformers = ctx.signal ? await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion, ctx.signal) : await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion);
|
|
3653
|
+
if (!createMarkdownTransformers || ctx.signal?.aborted) return void 0;
|
|
3654
|
+
if (thread.turns.length === turnCount) return createMarkdownTransformers;
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3556
3657
|
async function askThreadQuestion(thread, question, selected, thinkingLevel, ctx, steering) {
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
settled = true;
|
|
3568
|
-
done({ kind: "aborted" });
|
|
3569
|
-
},
|
|
3570
|
-
thinkingLevel,
|
|
3571
|
-
{
|
|
3572
|
-
steering: {
|
|
3573
|
-
questions: steering.questions,
|
|
3574
|
-
onSubmit: steering.submit,
|
|
3575
|
-
thinking: { ...steering.thinking, keybindings }
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
);
|
|
3579
|
-
completeSideThreadTurn({
|
|
3580
|
-
thread,
|
|
3581
|
-
question,
|
|
3582
|
-
model: selected.model,
|
|
3583
|
-
thinkingLevel,
|
|
3584
|
-
auth: selected.auth,
|
|
3585
|
-
signal: view.signal,
|
|
3586
|
-
completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
|
|
3587
|
-
sessionId: readBtwSessionId(ctx)
|
|
3588
|
-
}).then((result) => {
|
|
3658
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, question, ctx);
|
|
3659
|
+
if (!createMarkdownTransformers) return { kind: "aborted" };
|
|
3660
|
+
return ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
3661
|
+
let settled = false;
|
|
3662
|
+
const view = new BtwAnsweringView(
|
|
3663
|
+
tui,
|
|
3664
|
+
theme,
|
|
3665
|
+
thread.turns,
|
|
3666
|
+
question,
|
|
3667
|
+
() => {
|
|
3589
3668
|
if (settled) return;
|
|
3590
3669
|
settled = true;
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3670
|
+
done({ kind: "aborted" });
|
|
3671
|
+
},
|
|
3672
|
+
thinkingLevel,
|
|
3673
|
+
{
|
|
3674
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
3675
|
+
steering: {
|
|
3676
|
+
questions: steering.questions,
|
|
3677
|
+
onSubmit: steering.submit,
|
|
3678
|
+
thinking: { ...steering.thinking, keybindings }
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
3682
|
+
completeSideThreadTurn({
|
|
3683
|
+
thread,
|
|
3684
|
+
question,
|
|
3685
|
+
model: selected.model,
|
|
3686
|
+
thinkingLevel,
|
|
3687
|
+
auth: selected.auth,
|
|
3688
|
+
signal: view.signal,
|
|
3689
|
+
completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
|
|
3690
|
+
sessionId: readBtwSessionId(ctx)
|
|
3691
|
+
}).then((result) => {
|
|
3692
|
+
if (settled) return;
|
|
3693
|
+
settled = true;
|
|
3694
|
+
view.finish();
|
|
3695
|
+
done(result);
|
|
3696
|
+
});
|
|
3697
|
+
return view;
|
|
3698
|
+
});
|
|
3597
3699
|
}
|
|
3598
3700
|
async function showThreadComposer(thread, startAtBottom, ctx, initialQuestion, thinking) {
|
|
3701
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, initialQuestion, ctx);
|
|
3702
|
+
if (!createMarkdownTransformers) return { kind: "close" };
|
|
3599
3703
|
return ctx.ui.custom(
|
|
3600
3704
|
(tui, theme, keybindings, done) => new BtwTranscriptPager(tui, theme, thread.turns, done, {
|
|
3601
3705
|
startAtBottom,
|
|
3602
3706
|
initialQuestion,
|
|
3707
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
3603
3708
|
thinking: { ...thinking, keybindings }
|
|
3604
3709
|
})
|
|
3605
3710
|
);
|