@narumitw/pi-btw 0.58.1 → 0.59.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 +2 -0
- package/dist/index.ts +284 -365
- package/dist/index.ts.map +3 -3
- package/package.json +52 -53
- package/src/bring-to-main.ts +494 -547
- package/src/btw.ts +814 -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 +416 -454
- package/src/settings.ts +203 -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
|
@@ -6,23 +6,14 @@ import {
|
|
|
6
6
|
clampThinkingLevel,
|
|
7
7
|
getSupportedThinkingLevels
|
|
8
8
|
} from "@earendil-works/pi-ai";
|
|
9
|
-
import {
|
|
10
|
-
BorderedLoader
|
|
11
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { BorderedLoader } 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
|
-
}
|
|
966
|
-
}
|
|
967
|
-
if (!copyOnSelect) {
|
|
968
|
-
for (const copyKey of keybindings.getKeys("app.message.copy")) {
|
|
969
|
-
unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
|
|
970
|
-
}
|
|
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)));
|
|
971
908
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
)
|
|
975
|
-
|
|
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) {
|
|
@@ -1343,15 +1310,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
1343
1310
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
1344
1311
|
|
|
1345
1312
|
// src/side-thread.ts
|
|
1346
|
-
var BTW_THINKING_LEVELS = [
|
|
1347
|
-
"off",
|
|
1348
|
-
"minimal",
|
|
1349
|
-
"low",
|
|
1350
|
-
"medium",
|
|
1351
|
-
"high",
|
|
1352
|
-
"xhigh",
|
|
1353
|
-
"max"
|
|
1354
|
-
];
|
|
1313
|
+
var BTW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1355
1314
|
function createSideThread(conversationContext) {
|
|
1356
1315
|
return { conversationContext, turns: [] };
|
|
1357
1316
|
}
|
|
@@ -1365,10 +1324,7 @@ function buildSideThreadMessages(thread, question) {
|
|
|
1365
1324
|
return messages;
|
|
1366
1325
|
}
|
|
1367
1326
|
const [first, ...rest] = answeredTurns;
|
|
1368
|
-
messages.push(
|
|
1369
|
-
createUserMessage(buildUserPrompt(first.question, thread.conversationContext)),
|
|
1370
|
-
first.response
|
|
1371
|
-
);
|
|
1327
|
+
messages.push(createUserMessage(buildUserPrompt(first.question, thread.conversationContext)), first.response);
|
|
1372
1328
|
for (const turn of rest) {
|
|
1373
1329
|
messages.push(createUserMessage(buildFollowUpPrompt(turn.question)), turn.response);
|
|
1374
1330
|
}
|
|
@@ -1434,13 +1390,7 @@ function buildUserPrompt(question, conversationContext) {
|
|
|
1434
1390
|
].join("\n");
|
|
1435
1391
|
}
|
|
1436
1392
|
function buildFollowUpPrompt(question) {
|
|
1437
|
-
return [
|
|
1438
|
-
"Continue the same side conversation.",
|
|
1439
|
-
"",
|
|
1440
|
-
"<side_question>",
|
|
1441
|
-
question,
|
|
1442
|
-
"</side_question>"
|
|
1443
|
-
].join("\n");
|
|
1393
|
+
return ["Continue the same side conversation.", "", "<side_question>", question, "</side_question>"].join("\n");
|
|
1444
1394
|
}
|
|
1445
1395
|
function createUserMessage(text) {
|
|
1446
1396
|
return {
|
|
@@ -1632,9 +1582,7 @@ async function readSettingsContents(settingsPath) {
|
|
|
1632
1582
|
throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
1633
1583
|
}
|
|
1634
1584
|
try {
|
|
1635
|
-
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
1636
|
-
buffer.subarray(0, offset)
|
|
1637
|
-
);
|
|
1585
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, offset));
|
|
1638
1586
|
} catch {
|
|
1639
1587
|
throw new Error("settings file is not valid UTF-8");
|
|
1640
1588
|
}
|
|
@@ -1652,10 +1600,7 @@ async function publishSettings(settingsPath, document, signal, beforeRename) {
|
|
|
1652
1600
|
const directory = dirname(settingsPath);
|
|
1653
1601
|
await mkdir(directory, { recursive: true });
|
|
1654
1602
|
signal?.throwIfAborted();
|
|
1655
|
-
const temporaryPath = join(
|
|
1656
|
-
directory,
|
|
1657
|
-
`.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`
|
|
1658
|
-
);
|
|
1603
|
+
const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
1659
1604
|
try {
|
|
1660
1605
|
await writeFile(temporaryPath, contents, {
|
|
1661
1606
|
encoding: "utf8",
|
|
@@ -1736,11 +1681,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1736
1681
|
};
|
|
1737
1682
|
const shortcutValue = (settings, action) => {
|
|
1738
1683
|
if (!keybindings) return "Default";
|
|
1739
|
-
const effective = resolveBtwShortcuts(
|
|
1740
|
-
settings.keybindings,
|
|
1741
|
-
keybindings,
|
|
1742
|
-
effectiveFullscreenCopyOnSelect(settings)
|
|
1743
|
-
);
|
|
1684
|
+
const effective = resolveBtwShortcuts(settings.keybindings, keybindings, effectiveFullscreenCopyOnSelect(settings));
|
|
1744
1685
|
const configured = settings.keybindings?.[action];
|
|
1745
1686
|
if (configured !== void 0 && !effective.keys[action].includes(configured)) {
|
|
1746
1687
|
return `Fallback (${effective.label(action)}; saved ${formatKeyLabel2(configured)})`;
|
|
@@ -1749,8 +1690,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1749
1690
|
return `${source} (${effective.label(action)})`;
|
|
1750
1691
|
};
|
|
1751
1692
|
const saveShortcut = async (state, value, signal) => {
|
|
1752
|
-
if (!keybindings || state.kind !== "valid" || signal.aborted)
|
|
1753
|
-
return { kind: "rejected" };
|
|
1693
|
+
if (!keybindings || state.kind !== "valid" || signal.aborted) return { kind: "rejected" };
|
|
1754
1694
|
const action = shortcut;
|
|
1755
1695
|
const manager = keybindings;
|
|
1756
1696
|
const validate = (settings) => validateBtwShortcutEdit(
|
|
@@ -1792,10 +1732,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1792
1732
|
}
|
|
1793
1733
|
return { kind: "valid", settings: loaded.kind === "loaded" ? loaded.settings : {} };
|
|
1794
1734
|
};
|
|
1795
|
-
const currentMainThinkingLevel = clampToAvailableThinkingLevel(
|
|
1796
|
-
options.currentThinkingLevel,
|
|
1797
|
-
levels
|
|
1798
|
-
);
|
|
1735
|
+
const currentMainThinkingLevel = clampToAvailableThinkingLevel(options.currentThinkingLevel, levels);
|
|
1799
1736
|
const displayThinkingLevel = (settings) => settings.thinkingLevel === void 0 ? SAME_AS_MAIN_THREAD : clampToAvailableThinkingLevel(settings.thinkingLevel, levels);
|
|
1800
1737
|
const displayThinkingSummary = (settings) => settings.thinkingLevel === void 0 ? `${SAME_AS_MAIN_THREAD} (currently ${currentMainThinkingLevel})` : displayThinkingLevel(settings);
|
|
1801
1738
|
const displayRememberSummary = (settings) => {
|
|
@@ -1923,8 +1860,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1923
1860
|
},
|
|
1924
1861
|
actions: {
|
|
1925
1862
|
"edit-shortcut": ({ itemId }) => {
|
|
1926
|
-
if (!BTW_SHORTCUT_ACTIONS.includes(itemId))
|
|
1927
|
-
return { kind: "rejected" };
|
|
1863
|
+
if (!BTW_SHORTCUT_ACTIONS.includes(itemId)) return { kind: "rejected" };
|
|
1928
1864
|
shortcut = itemId;
|
|
1929
1865
|
return { kind: "to", screen: "shortcut" };
|
|
1930
1866
|
},
|
|
@@ -1962,10 +1898,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1962
1898
|
"set-remember": async ({ value, signal }) => {
|
|
1963
1899
|
if (value !== "On" && value !== "Off") return { kind: "rejected" };
|
|
1964
1900
|
try {
|
|
1965
|
-
await updateSettings(
|
|
1966
|
-
{ rememberThinkingLevelChanges: value === "On" },
|
|
1967
|
-
{ settingsPath, signal }
|
|
1968
|
-
);
|
|
1901
|
+
await updateSettings({ rememberThinkingLevelChanges: value === "On" }, { settingsPath, signal });
|
|
1969
1902
|
if (signal.aborted) return { kind: "rejected" };
|
|
1970
1903
|
notifySafely(ctx, `Remember thinking level changes: ${value}.`, "info");
|
|
1971
1904
|
return { kind: "stay" };
|
|
@@ -1977,10 +1910,7 @@ async function showBtwCommandMenu(ctx, options) {
|
|
|
1977
1910
|
"set-fullscreen-copy": async ({ value, signal }) => {
|
|
1978
1911
|
if (value !== "On" && value !== "Off") return { kind: "rejected" };
|
|
1979
1912
|
try {
|
|
1980
|
-
await updateSettings(
|
|
1981
|
-
{ fullscreenCopyOnSelect: value === "On" },
|
|
1982
|
-
{ settingsPath, signal }
|
|
1983
|
-
);
|
|
1913
|
+
await updateSettings({ fullscreenCopyOnSelect: value === "On" }, { settingsPath, signal });
|
|
1984
1914
|
if (signal.aborted) return { kind: "rejected" };
|
|
1985
1915
|
notifySafely(ctx, `Copy selection automatically: ${value}.`, "info");
|
|
1986
1916
|
return { kind: "stay" };
|
|
@@ -2145,80 +2075,77 @@ async function pickMainEntry(pi, ctx, dependencies = {}) {
|
|
|
2145
2075
|
controller.abort(new Error("The main-thread tree picker closed"));
|
|
2146
2076
|
}
|
|
2147
2077
|
};
|
|
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
|
-
|
|
2078
|
+
const result = await showBtwCustomPreservingEditor(ctx, (tui, _theme, _keybindings, done) => {
|
|
2079
|
+
let settled = false;
|
|
2080
|
+
let selector;
|
|
2081
|
+
const finish = (value) => {
|
|
2082
|
+
if (settled) return;
|
|
2083
|
+
settled = true;
|
|
2084
|
+
abortCopies();
|
|
2085
|
+
done(value);
|
|
2086
|
+
};
|
|
2087
|
+
const onCopy = (entryId, displayText) => {
|
|
2088
|
+
if (settled) return;
|
|
2089
|
+
const text = entryId ? rawCopyText.get(entryId) : displayText;
|
|
2090
|
+
if (!text) {
|
|
2091
|
+
notifySafely2(ctx, "Selected entry has no text to copy", "warning");
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const controller = new AbortController();
|
|
2095
|
+
copyControllers.add(controller);
|
|
2096
|
+
let operation;
|
|
2097
|
+
try {
|
|
2098
|
+
operation = copy(text, controller.signal);
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
operation = Promise.reject(error);
|
|
2101
|
+
}
|
|
2102
|
+
let task;
|
|
2103
|
+
task = operation.then(() => {
|
|
2104
|
+
if (!settled) notifySafely2(ctx, "Copied selected message", "info");
|
|
2105
|
+
}).catch((error) => {
|
|
2106
|
+
if (!settled && !controller.signal.aborted) {
|
|
2107
|
+
notifySafely2(ctx, `Could not copy selected message: ${formatError4(error)}`, "error");
|
|
2173
2108
|
}
|
|
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) {
|
|
2109
|
+
}).finally(() => {
|
|
2110
|
+
copyControllers.delete(controller);
|
|
2111
|
+
copyTasks.delete(task);
|
|
2112
|
+
});
|
|
2113
|
+
copyTasks.add(task);
|
|
2114
|
+
};
|
|
2115
|
+
const restoreLabel = (entryId) => {
|
|
2116
|
+
const previous = savedLabels.get(entryId);
|
|
2117
|
+
selector?.setViewLabel?.(entryId, previous?.label, previous?.labelTimestamp);
|
|
2118
|
+
tui.requestRender();
|
|
2119
|
+
};
|
|
2120
|
+
const onLabelChange = (entryId, label) => {
|
|
2121
|
+
if (settled) return;
|
|
2122
|
+
try {
|
|
2123
|
+
if (!ctx.sessionManager.getEntry(entryId)) {
|
|
2206
2124
|
restoreLabel(entryId);
|
|
2207
|
-
notifySafely2(ctx,
|
|
2125
|
+
notifySafely2(ctx, "The selected main-thread entry is no longer available", "warning");
|
|
2126
|
+
return;
|
|
2208
2127
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2128
|
+
const persistedLabel = label === void 0 ? void 0 : sanitizeSingleLine(label);
|
|
2129
|
+
pi.setLabel(entryId, persistedLabel);
|
|
2130
|
+
savedLabels.set(entryId, { label: persistedLabel });
|
|
2131
|
+
selector?.setViewLabel?.(entryId, persistedLabel);
|
|
2132
|
+
tui.requestRender();
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
restoreLabel(entryId);
|
|
2135
|
+
notifySafely2(ctx, `Could not update tree label: ${formatError4(error)}`, "error");
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
selector = createSelector({
|
|
2139
|
+
tree,
|
|
2140
|
+
currentLeafId,
|
|
2141
|
+
terminalRows: tui.terminal.rows,
|
|
2142
|
+
onSelect: (entryId) => finish({ kind: "selected", entryId }),
|
|
2143
|
+
onCancel: () => finish({ kind: "back" }),
|
|
2144
|
+
onCopy,
|
|
2145
|
+
onLabelChange
|
|
2146
|
+
});
|
|
2147
|
+
return new MainThreadTreePickerComponent(selector, () => finish({ kind: "closed" }));
|
|
2148
|
+
});
|
|
2222
2149
|
abortCopies();
|
|
2223
2150
|
await Promise.allSettled([...copyTasks]);
|
|
2224
2151
|
return result ?? { kind: "closed" };
|
|
@@ -2254,8 +2181,7 @@ function sanitizeEntryForDisplay(entry) {
|
|
|
2254
2181
|
switch (entry.type) {
|
|
2255
2182
|
case "message": {
|
|
2256
2183
|
const message = { ...entry.message };
|
|
2257
|
-
if ("content" in entry.message)
|
|
2258
|
-
message.content = sanitizeDisplayContent(entry.message.content);
|
|
2184
|
+
if ("content" in entry.message) message.content = sanitizeDisplayContent(entry.message.content);
|
|
2259
2185
|
for (const key of ["role", "errorMessage", "command", "toolName"]) {
|
|
2260
2186
|
const value = message[key];
|
|
2261
2187
|
if (typeof value === "string") message[key] = sanitizeSingleLine(value);
|
|
@@ -2389,6 +2315,49 @@ function formatError4(error) {
|
|
|
2389
2315
|
return error instanceof Error ? error.message : String(error);
|
|
2390
2316
|
}
|
|
2391
2317
|
|
|
2318
|
+
// src/transcript-markdown.ts
|
|
2319
|
+
var MERMAID_MARKDOWN_MODULE = "@narumitw/pi-tui-kit/markdown";
|
|
2320
|
+
var noMarkdownTransformers = () => [];
|
|
2321
|
+
async function prepareBtwTranscriptMarkdown(turns, pendingQuestion, signal) {
|
|
2322
|
+
const documents = turns.flatMap(
|
|
2323
|
+
(turn) => turn.kind === "answered" ? [turn.question, turn.answer] : [turn.question]
|
|
2324
|
+
);
|
|
2325
|
+
if (pendingQuestion) documents.push(pendingQuestion);
|
|
2326
|
+
if (!documents.some((document) => /mermaid/iu.test(document))) return noMarkdownTransformers;
|
|
2327
|
+
if (signal?.aborted) return void 0;
|
|
2328
|
+
const markdownModule = await settleUnlessAborted(
|
|
2329
|
+
import(MERMAID_MARKDOWN_MODULE),
|
|
2330
|
+
signal
|
|
2331
|
+
);
|
|
2332
|
+
if (!markdownModule || signal?.aborted) return void 0;
|
|
2333
|
+
const { createMermaidMarkdownTransformer, prepareMermaidMarkdownRenderer } = markdownModule;
|
|
2334
|
+
const preparations = /* @__PURE__ */ new Set();
|
|
2335
|
+
for (const document of documents) {
|
|
2336
|
+
const preparation = prepareMermaidMarkdownRenderer(document);
|
|
2337
|
+
if (preparation) preparations.add(preparation);
|
|
2338
|
+
}
|
|
2339
|
+
if (preparations.size > 0 && !await settleUnlessAborted(Promise.all(preparations), signal)) return void 0;
|
|
2340
|
+
if (signal?.aborted) return void 0;
|
|
2341
|
+
return (theme) => {
|
|
2342
|
+
const transformer = createMermaidMarkdownTransformer(theme);
|
|
2343
|
+
return transformer ? [transformer] : [];
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
async function settleUnlessAborted(operation, signal) {
|
|
2347
|
+
if (!signal) return operation;
|
|
2348
|
+
let onAbort;
|
|
2349
|
+
const aborted = new Promise((resolve) => {
|
|
2350
|
+
onAbort = () => resolve(void 0);
|
|
2351
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2352
|
+
if (signal.aborted) onAbort();
|
|
2353
|
+
});
|
|
2354
|
+
try {
|
|
2355
|
+
return await Promise.race([operation, aborted]);
|
|
2356
|
+
} finally {
|
|
2357
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2392
2361
|
// src/transcript-pager.ts
|
|
2393
2362
|
import {
|
|
2394
2363
|
AssistantMessageComponent,
|
|
@@ -2427,7 +2396,7 @@ var BtwTranscriptPager = class {
|
|
|
2427
2396
|
this.onAction = onAction;
|
|
2428
2397
|
this.options = options;
|
|
2429
2398
|
this.shortcuts = getBtwShortcuts(tui, options.thinking?.keybindings);
|
|
2430
|
-
this.transcriptComponents = buildTranscriptComponents(turns, this.theme);
|
|
2399
|
+
this.transcriptComponents = buildTranscriptComponents(turns, this.theme, void 0, options.markdownTransformers);
|
|
2431
2400
|
this.canBringToMain = turns.some((turn) => turn.kind === "answered");
|
|
2432
2401
|
this.thinkingLevel = options.thinking?.level;
|
|
2433
2402
|
const editorTheme = {
|
|
@@ -2497,17 +2466,10 @@ var BtwTranscriptPager = class {
|
|
|
2497
2466
|
const safeWidth = Math.max(1, width);
|
|
2498
2467
|
const editorLines = this.editor.render(safeWidth);
|
|
2499
2468
|
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
|
-
);
|
|
2469
|
+
const viewportHeight = Math.max(0, availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES);
|
|
2504
2470
|
const contentLines = renderTranscriptLines(this.transcriptComponents, safeWidth);
|
|
2505
2471
|
this.lastContentLineCount = contentLines.length;
|
|
2506
|
-
this.scrollView.updateLayout(
|
|
2507
|
-
contentLines.length,
|
|
2508
|
-
viewportHeight,
|
|
2509
|
-
() => this.tui.requestRender()
|
|
2510
|
-
);
|
|
2472
|
+
this.scrollView.updateLayout(contentLines.length, viewportHeight, () => this.tui.requestRender());
|
|
2511
2473
|
return fitComposerLayout(
|
|
2512
2474
|
renderSideThreadHeader(safeWidth, this.theme, this.thinkingLevel),
|
|
2513
2475
|
contentLines.slice(this.scrollView.scrollTop, this.scrollView.scrollTop + viewportHeight),
|
|
@@ -2638,7 +2600,12 @@ var BtwAnsweringView = class {
|
|
|
2638
2600
|
this.onCancel = onCancel;
|
|
2639
2601
|
this.options = options;
|
|
2640
2602
|
this.shortcuts = getBtwShortcuts(tui, options.steering?.thinking?.keybindings);
|
|
2641
|
-
this.transcriptComponents = buildTranscriptComponents(
|
|
2603
|
+
this.transcriptComponents = buildTranscriptComponents(
|
|
2604
|
+
turns,
|
|
2605
|
+
this.theme,
|
|
2606
|
+
pendingQuestion,
|
|
2607
|
+
options.markdownTransformers
|
|
2608
|
+
);
|
|
2642
2609
|
this.thinkingLevel = options.steering?.thinking?.level ?? thinkingLevel;
|
|
2643
2610
|
this.loader = new Loader(
|
|
2644
2611
|
this.tui,
|
|
@@ -2722,10 +2689,7 @@ var BtwAnsweringView = class {
|
|
|
2722
2689
|
const safeWidth = Math.max(1, width);
|
|
2723
2690
|
const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_LINES);
|
|
2724
2691
|
const editorLines = this.editor?.render(safeWidth) ?? [];
|
|
2725
|
-
const steeringCapacity = Math.max(
|
|
2726
|
-
0,
|
|
2727
|
-
availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES
|
|
2728
|
-
);
|
|
2692
|
+
const steeringCapacity = Math.max(0, availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES);
|
|
2729
2693
|
const steeringLines = renderSteeringLines(
|
|
2730
2694
|
this.options.steering?.questions ?? [],
|
|
2731
2695
|
safeWidth,
|
|
@@ -2738,11 +2702,7 @@ var BtwAnsweringView = class {
|
|
|
2738
2702
|
);
|
|
2739
2703
|
const contentLines = renderTranscriptLines(this.transcriptComponents, safeWidth);
|
|
2740
2704
|
this.lastContentLineCount = contentLines.length;
|
|
2741
|
-
this.scrollView.updateLayout(
|
|
2742
|
-
contentLines.length,
|
|
2743
|
-
viewportHeight,
|
|
2744
|
-
() => this.tui.requestRender()
|
|
2745
|
-
);
|
|
2705
|
+
this.scrollView.updateLayout(contentLines.length, viewportHeight, () => this.tui.requestRender());
|
|
2746
2706
|
return fitComposerLayout(
|
|
2747
2707
|
renderSideThreadHeader(safeWidth, this.theme, this.thinkingLevel),
|
|
2748
2708
|
contentLines.slice(this.scrollView.scrollTop, this.scrollView.scrollTop + viewportHeight),
|
|
@@ -2847,12 +2807,7 @@ var BtwAnsweringView = class {
|
|
|
2847
2807
|
}
|
|
2848
2808
|
createSteeringComponent() {
|
|
2849
2809
|
return {
|
|
2850
|
-
render: (width) => renderSteeringLines(
|
|
2851
|
-
this.options.steering?.questions ?? [],
|
|
2852
|
-
width,
|
|
2853
|
-
this.theme,
|
|
2854
|
-
MAX_STEERING_DISPLAY_LINES
|
|
2855
|
-
),
|
|
2810
|
+
render: (width) => renderSteeringLines(this.options.steering?.questions ?? [], width, this.theme, MAX_STEERING_DISPLAY_LINES),
|
|
2856
2811
|
invalidate() {
|
|
2857
2812
|
}
|
|
2858
2813
|
};
|
|
@@ -2867,21 +2822,18 @@ var BtwAnsweringView = class {
|
|
|
2867
2822
|
return Math.max(0, this.lastContentLineCount - this.scrollView.viewportHeight);
|
|
2868
2823
|
}
|
|
2869
2824
|
};
|
|
2870
|
-
function buildTranscriptComponents(turns, theme, pendingQuestion) {
|
|
2825
|
+
function buildTranscriptComponents(turns, theme, pendingQuestion, markdownTransformers = []) {
|
|
2871
2826
|
const components = turns.flatMap((turn) => {
|
|
2872
2827
|
const question = new UserMessageComponent(
|
|
2873
2828
|
escapeTerminalControls2(turn.question),
|
|
2874
2829
|
getMarkdownTheme(),
|
|
2875
|
-
1
|
|
2830
|
+
1,
|
|
2831
|
+
markdownTransformers
|
|
2876
2832
|
);
|
|
2877
2833
|
if (turn.kind === "error") {
|
|
2878
|
-
const error = new Markdown(
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
1,
|
|
2882
|
-
getMarkdownTheme(),
|
|
2883
|
-
{ color: (text) => theme.fg("error", text) }
|
|
2884
|
-
);
|
|
2834
|
+
const error = new Markdown(`Error: ${escapeTerminalControls2(turn.answer)}`, 1, 1, getMarkdownTheme(), {
|
|
2835
|
+
color: (text) => theme.fg("error", text)
|
|
2836
|
+
});
|
|
2885
2837
|
return [question, error];
|
|
2886
2838
|
}
|
|
2887
2839
|
const response = {
|
|
@@ -2890,11 +2842,11 @@ function buildTranscriptComponents(turns, theme, pendingQuestion) {
|
|
|
2890
2842
|
stopReason: "stop",
|
|
2891
2843
|
errorMessage: void 0
|
|
2892
2844
|
};
|
|
2893
|
-
return [question, new AssistantMessageComponent(response, true, getMarkdownTheme(), "", 1)];
|
|
2845
|
+
return [question, new AssistantMessageComponent(response, true, getMarkdownTheme(), "", 1, markdownTransformers)];
|
|
2894
2846
|
});
|
|
2895
2847
|
if (pendingQuestion) {
|
|
2896
2848
|
components.push(
|
|
2897
|
-
new UserMessageComponent(escapeTerminalControls2(pendingQuestion), getMarkdownTheme(), 1)
|
|
2849
|
+
new UserMessageComponent(escapeTerminalControls2(pendingQuestion), getMarkdownTheme(), 1, markdownTransformers)
|
|
2898
2850
|
);
|
|
2899
2851
|
}
|
|
2900
2852
|
return components;
|
|
@@ -2929,26 +2881,16 @@ function renderSteeringLines(questions, width, theme, maxLines) {
|
|
|
2929
2881
|
if (maxLines === 1 && questions.length > 1) {
|
|
2930
2882
|
return [
|
|
2931
2883
|
truncateToWidth3(
|
|
2932
|
-
theme.fg(
|
|
2933
|
-
"dim",
|
|
2934
|
-
`Steering (+${questions.length - 1} more): ${formatQuestion(questions[0] ?? "")}`
|
|
2935
|
-
),
|
|
2884
|
+
theme.fg("dim", `Steering (+${questions.length - 1} more): ${formatQuestion(questions[0] ?? "")}`),
|
|
2936
2885
|
width
|
|
2937
2886
|
)
|
|
2938
2887
|
];
|
|
2939
2888
|
}
|
|
2940
2889
|
const hasOverflow = questions.length > maxLines;
|
|
2941
2890
|
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
|
-
);
|
|
2891
|
+
const lines = questions.slice(0, questionLimit).map((question) => truncateToWidth3(theme.fg("dim", `Steering: ${formatQuestion(question)}`), width));
|
|
2945
2892
|
if (hasOverflow) {
|
|
2946
|
-
lines.push(
|
|
2947
|
-
truncateToWidth3(
|
|
2948
|
-
theme.fg("dim", `Steering: \u2026 +${questions.length - questionLimit} more`),
|
|
2949
|
-
width
|
|
2950
|
-
)
|
|
2951
|
-
);
|
|
2893
|
+
lines.push(truncateToWidth3(theme.fg("dim", `Steering: \u2026 +${questions.length - questionLimit} more`), width));
|
|
2952
2894
|
}
|
|
2953
2895
|
return lines;
|
|
2954
2896
|
}
|
|
@@ -3005,13 +2947,9 @@ async function resolveBtwModel({
|
|
|
3005
2947
|
};
|
|
3006
2948
|
}
|
|
3007
2949
|
const reason = auth.ok ? "has no request credentials" : auth.error;
|
|
3008
|
-
reportWarning(
|
|
3009
|
-
`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`
|
|
3010
|
-
);
|
|
2950
|
+
reportWarning(`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`);
|
|
3011
2951
|
} catch (error) {
|
|
3012
|
-
reportWarning(
|
|
3013
|
-
`pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`
|
|
3014
|
-
);
|
|
2952
|
+
reportWarning(`pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`);
|
|
3015
2953
|
}
|
|
3016
2954
|
if (sameAsCurrent) return void 0;
|
|
3017
2955
|
}
|
|
@@ -3061,9 +2999,7 @@ function btw(pi, dependencies = {}) {
|
|
|
3061
2999
|
const runFullscreen = dependencies.runFullscreen ?? runBtwFullscreen;
|
|
3062
3000
|
const resumableThreads = /* @__PURE__ */ new Map();
|
|
3063
3001
|
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) => ({
|
|
3002
|
+
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
3003
|
id: state.id,
|
|
3068
3004
|
title: state.title ?? "Untitled side thread",
|
|
3069
3005
|
questionCount: state.thread.turns.length
|
|
@@ -3093,11 +3029,7 @@ function btw(pi, dependencies = {}) {
|
|
|
3093
3029
|
}
|
|
3094
3030
|
const branch = ctx.sessionManager.getBranch(treeResult.entryId);
|
|
3095
3031
|
if (branch.at(-1)?.id !== treeResult.entryId) {
|
|
3096
|
-
notifySafely3(
|
|
3097
|
-
ctx,
|
|
3098
|
-
"The selected main-thread branch is no longer available",
|
|
3099
|
-
"warning"
|
|
3100
|
-
);
|
|
3032
|
+
notifySafely3(ctx, "The selected main-thread branch is no longer available", "warning");
|
|
3101
3033
|
continue;
|
|
3102
3034
|
}
|
|
3103
3035
|
selectedConversationContext = buildConversationContext(branch);
|
|
@@ -3174,9 +3106,7 @@ async function showCommandMenuForBtw(pi, ctx, resumeThreads) {
|
|
|
3174
3106
|
const loaded = await readBtwSettings();
|
|
3175
3107
|
const settings = loaded.kind === "loaded" ? loaded.settings : {};
|
|
3176
3108
|
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;
|
|
3109
|
+
const configuredModel = configured ? availableModels.find((model2) => model2.provider === configured.provider && model2.id === configured.modelId) : void 0;
|
|
3180
3110
|
const model = configuredModel ?? currentModel;
|
|
3181
3111
|
return showBtwCommandMenu(ctx, {
|
|
3182
3112
|
currentThinkingLevel,
|
|
@@ -3240,10 +3170,7 @@ async function runBtwThread({
|
|
|
3240
3170
|
const thinkingLevels = getSupportedThinkingLevels(selected.model);
|
|
3241
3171
|
const pendingWrites = /* @__PURE__ */ new Set();
|
|
3242
3172
|
const steeringQuestions = [];
|
|
3243
|
-
let activeThinkingLevel = clampThinkingLevel(
|
|
3244
|
-
selected.model,
|
|
3245
|
-
state?.thinkingLevel ?? thinkingLevel
|
|
3246
|
-
);
|
|
3173
|
+
let activeThinkingLevel = clampThinkingLevel(selected.model, state?.thinkingLevel ?? thinkingLevel);
|
|
3247
3174
|
if (state) state.thinkingLevel = activeThinkingLevel;
|
|
3248
3175
|
let pendingQuestion = initialQuestion;
|
|
3249
3176
|
let composerDraft;
|
|
@@ -3269,13 +3196,7 @@ async function runBtwThread({
|
|
|
3269
3196
|
try {
|
|
3270
3197
|
while (true) {
|
|
3271
3198
|
if (!pendingQuestion) {
|
|
3272
|
-
const action = await interact(
|
|
3273
|
-
thread,
|
|
3274
|
-
thread.turns.length > 0,
|
|
3275
|
-
ctx,
|
|
3276
|
-
composerDraft,
|
|
3277
|
-
createThinkingControl()
|
|
3278
|
-
);
|
|
3199
|
+
const action = await interact(thread, thread.turns.length > 0, ctx, composerDraft, createThinkingControl());
|
|
3279
3200
|
if (action.kind === "close") return { kind: "closed" };
|
|
3280
3201
|
if (action.kind === "bringToMain") {
|
|
3281
3202
|
const choice = await chooseBringToMainAction(thread, ctx);
|
|
@@ -3361,20 +3282,13 @@ async function chooseBringToMain(thread, ctx, dependencies = {}) {
|
|
|
3361
3282
|
);
|
|
3362
3283
|
let selectedQuestion;
|
|
3363
3284
|
while (true) {
|
|
3364
|
-
const questionResult = await showMenu(
|
|
3365
|
-
ctx,
|
|
3366
|
-
"Start from which question?",
|
|
3367
|
-
questions,
|
|
3368
|
-
selectedQuestion
|
|
3369
|
-
);
|
|
3285
|
+
const questionResult = await showMenu(ctx, "Start from which question?", questions, selectedQuestion);
|
|
3370
3286
|
if (questionResult.kind === "close") return { kind: "closed" };
|
|
3371
3287
|
if (questionResult.kind === "back") break;
|
|
3372
3288
|
const answeredTurnIndex = questions.indexOf(questionResult.value);
|
|
3373
3289
|
if (answeredTurnIndex < 0) continue;
|
|
3374
3290
|
selectedQuestion = questionResult.value;
|
|
3375
|
-
const choice = makeChoice(
|
|
3376
|
-
buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex })
|
|
3377
|
-
);
|
|
3291
|
+
const choice = makeChoice(buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex }));
|
|
3378
3292
|
const preview = await showPreview(ctx, choice.draft, choice.summary);
|
|
3379
3293
|
if (preview.kind === "close") return { kind: "closed" };
|
|
3380
3294
|
if (preview.kind === "back") continue;
|
|
@@ -3497,10 +3411,7 @@ async function loadBringToMainDraft(draft, ctx, summary) {
|
|
|
3497
3411
|
const existing = ctx.ui.getEditorText();
|
|
3498
3412
|
if (!existing.trim()) {
|
|
3499
3413
|
ctx.ui.setEditorText(draft);
|
|
3500
|
-
ctx.ui.notify(
|
|
3501
|
-
`Brought ${describeContent()} to the main editor. Review and submit when ready.`,
|
|
3502
|
-
"info"
|
|
3503
|
-
);
|
|
3414
|
+
ctx.ui.notify(`Brought ${describeContent()} to the main editor. Review and submit when ready.`, "info");
|
|
3504
3415
|
return "loaded";
|
|
3505
3416
|
}
|
|
3506
3417
|
const appendOption = "Append after current draft Recommended";
|
|
@@ -3527,11 +3438,10 @@ ${draft}`);
|
|
|
3527
3438
|
if (action.value !== replaceOption) continue;
|
|
3528
3439
|
const current = ctx.ui.getEditorText();
|
|
3529
3440
|
const characters = [...current].length;
|
|
3530
|
-
const confirmed = await showBtwMenu(
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
);
|
|
3441
|
+
const confirmed = await showBtwMenu(ctx, `Replace the current ${characters}-character editor draft?`, [
|
|
3442
|
+
"Back Keep current editor text",
|
|
3443
|
+
"\u26A0 Replace current draft Cannot be undone"
|
|
3444
|
+
]);
|
|
3535
3445
|
if (confirmed.kind === "close") return "closed";
|
|
3536
3446
|
if (confirmed.kind === "back" || confirmed.value === "Back Keep current editor text") continue;
|
|
3537
3447
|
if (confirmed.value !== "\u26A0 Replace current draft Cannot be undone") continue;
|
|
@@ -3543,63 +3453,72 @@ ${draft}`);
|
|
|
3543
3453
|
continue;
|
|
3544
3454
|
}
|
|
3545
3455
|
ctx.ui.setEditorText(draft);
|
|
3546
|
-
ctx.ui.notify(
|
|
3547
|
-
`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`,
|
|
3548
|
-
"info"
|
|
3549
|
-
);
|
|
3456
|
+
ctx.ui.notify(`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`, "info");
|
|
3550
3457
|
return "loaded";
|
|
3551
3458
|
}
|
|
3552
3459
|
}
|
|
3553
3460
|
function truncatePreview(text) {
|
|
3554
3461
|
return text.length <= 72 ? text : `${text.slice(0, 69)}\u2026`;
|
|
3555
3462
|
}
|
|
3463
|
+
async function prepareCurrentTranscriptMarkdown(thread, pendingQuestion, ctx) {
|
|
3464
|
+
while (true) {
|
|
3465
|
+
const turnCount = thread.turns.length;
|
|
3466
|
+
const createMarkdownTransformers = ctx.signal ? await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion, ctx.signal) : await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion);
|
|
3467
|
+
if (!createMarkdownTransformers || ctx.signal?.aborted) return void 0;
|
|
3468
|
+
if (thread.turns.length === turnCount) return createMarkdownTransformers;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3556
3471
|
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) => {
|
|
3472
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, question, ctx);
|
|
3473
|
+
if (!createMarkdownTransformers) return { kind: "aborted" };
|
|
3474
|
+
return ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
3475
|
+
let settled = false;
|
|
3476
|
+
const view = new BtwAnsweringView(
|
|
3477
|
+
tui,
|
|
3478
|
+
theme,
|
|
3479
|
+
thread.turns,
|
|
3480
|
+
question,
|
|
3481
|
+
() => {
|
|
3589
3482
|
if (settled) return;
|
|
3590
3483
|
settled = true;
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3484
|
+
done({ kind: "aborted" });
|
|
3485
|
+
},
|
|
3486
|
+
thinkingLevel,
|
|
3487
|
+
{
|
|
3488
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
3489
|
+
steering: {
|
|
3490
|
+
questions: steering.questions,
|
|
3491
|
+
onSubmit: steering.submit,
|
|
3492
|
+
thinking: { ...steering.thinking, keybindings }
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
);
|
|
3496
|
+
completeSideThreadTurn({
|
|
3497
|
+
thread,
|
|
3498
|
+
question,
|
|
3499
|
+
model: selected.model,
|
|
3500
|
+
thinkingLevel,
|
|
3501
|
+
auth: selected.auth,
|
|
3502
|
+
signal: view.signal,
|
|
3503
|
+
completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
|
|
3504
|
+
sessionId: readBtwSessionId(ctx)
|
|
3505
|
+
}).then((result) => {
|
|
3506
|
+
if (settled) return;
|
|
3507
|
+
settled = true;
|
|
3508
|
+
view.finish();
|
|
3509
|
+
done(result);
|
|
3510
|
+
});
|
|
3511
|
+
return view;
|
|
3512
|
+
});
|
|
3597
3513
|
}
|
|
3598
3514
|
async function showThreadComposer(thread, startAtBottom, ctx, initialQuestion, thinking) {
|
|
3515
|
+
const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, initialQuestion, ctx);
|
|
3516
|
+
if (!createMarkdownTransformers) return { kind: "close" };
|
|
3599
3517
|
return ctx.ui.custom(
|
|
3600
3518
|
(tui, theme, keybindings, done) => new BtwTranscriptPager(tui, theme, thread.turns, done, {
|
|
3601
3519
|
startAtBottom,
|
|
3602
3520
|
initialQuestion,
|
|
3521
|
+
markdownTransformers: createMarkdownTransformers(theme),
|
|
3603
3522
|
thinking: { ...thinking, keybindings }
|
|
3604
3523
|
})
|
|
3605
3524
|
);
|