@narumitw/pi-btw 0.58.0 → 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/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
- (segment) => `${segment.role === "user" ? "User" : "Assistant"}:
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, "&lt;btw_context").replace(
464
- /<\/btw_context[ \t\r\n]*>/g,
465
- (terminator) => terminator.replaceAll("<", "&lt;").replaceAll(">", "&gt;")
466
- );
435
+ }).join("").replace(/<btw_context(?=[ \t\r\n>])/g, "&lt;btw_context").replace(/<\/btw_context[ \t\r\n]*>/g, (terminator) => terminator.replaceAll("<", "&lt;").replaceAll(">", "&gt;"));
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
- parent.terminal,
954
- parent.getShowHardwareCursor(),
955
- void 0,
956
- {
957
- mouse: true,
958
- copyOnSelect,
959
- searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
960
- scrollToEndIndicator: () => {
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
- if (!copyOnSelect) {
968
- for (const copyKey of keybindings.getKeys("app.message.copy")) {
969
- unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
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) => property === "ui" ? ui : Reflect.get(target, property, target)
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
- ctx,
2150
- (tui, _theme, _keybindings, done) => {
2151
- let settled = false;
2152
- let selector;
2153
- const finish = (value) => {
2154
- if (settled) return;
2155
- settled = true;
2156
- abortCopies();
2157
- done(value);
2158
- };
2159
- const onCopy = (entryId, displayText) => {
2160
- if (settled) return;
2161
- const text = entryId ? rawCopyText.get(entryId) : displayText;
2162
- if (!text) {
2163
- notifySafely2(ctx, "Selected entry has no text to copy", "warning");
2164
- return;
2165
- }
2166
- const controller = new AbortController();
2167
- copyControllers.add(controller);
2168
- let operation;
2169
- try {
2170
- operation = copy(text, controller.signal);
2171
- } catch (error) {
2172
- operation = Promise.reject(error);
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
- let task;
2175
- task = operation.then(() => {
2176
- if (!settled) notifySafely2(ctx, "Copied selected message", "info");
2177
- }).catch((error) => {
2178
- if (!settled && !controller.signal.aborted) {
2179
- notifySafely2(ctx, `Could not copy selected message: ${formatError4(error)}`, "error");
2180
- }
2181
- }).finally(() => {
2182
- copyControllers.delete(controller);
2183
- copyTasks.delete(task);
2184
- });
2185
- copyTasks.add(task);
2186
- };
2187
- const restoreLabel = (entryId) => {
2188
- const previous = savedLabels.get(entryId);
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, `Could not update tree label: ${formatError4(error)}`, "error");
2125
+ notifySafely2(ctx, "The selected main-thread entry is no longer available", "warning");
2126
+ return;
2208
2127
  }
2209
- };
2210
- selector = createSelector({
2211
- tree,
2212
- currentLeafId,
2213
- terminalRows: tui.terminal.rows,
2214
- onSelect: (entryId) => finish({ kind: "selected", entryId }),
2215
- onCancel: () => finish({ kind: "back" }),
2216
- onCopy,
2217
- onLabelChange
2218
- });
2219
- return new MainThreadTreePickerComponent(selector, () => finish({ kind: "closed" }));
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(turns, this.theme, pendingQuestion);
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
- `Error: ${escapeTerminalControls2(turn.answer)}`,
2880
- 1,
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
  }
@@ -2998,15 +2940,16 @@ async function resolveBtwModel({
2998
2940
  const fallbackAction = sameAsCurrent ? "no distinct current model is available" : `falling back to ${fallback}`;
2999
2941
  try {
3000
2942
  const auth = await modelRegistry.getApiKeyAndHeaders(configuredModel);
3001
- if (auth.ok && hasRequestAuth(auth)) return { model: configuredModel, auth };
2943
+ if (auth.ok && hasRequestAuth(auth)) {
2944
+ return {
2945
+ model: auth.baseUrl ? { ...configuredModel, baseUrl: auth.baseUrl } : configuredModel,
2946
+ auth
2947
+ };
2948
+ }
3002
2949
  const reason = auth.ok ? "has no request credentials" : auth.error;
3003
- reportWarning(
3004
- `pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`
3005
- );
2950
+ reportWarning(`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`);
3006
2951
  } catch (error) {
3007
- reportWarning(
3008
- `pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`
3009
- );
2952
+ reportWarning(`pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`);
3010
2953
  }
3011
2954
  if (sameAsCurrent) return void 0;
3012
2955
  }
@@ -3014,7 +2957,12 @@ async function resolveBtwModel({
3014
2957
  if (!currentModel) return void 0;
3015
2958
  try {
3016
2959
  const auth = await modelRegistry.getApiKeyAndHeaders(currentModel);
3017
- if (auth.ok && hasRequestAuth(auth)) return { model: currentModel, auth };
2960
+ if (auth.ok && hasRequestAuth(auth)) {
2961
+ return {
2962
+ model: auth.baseUrl ? { ...currentModel, baseUrl: auth.baseUrl } : currentModel,
2963
+ auth
2964
+ };
2965
+ }
3018
2966
  } catch {
3019
2967
  }
3020
2968
  return void 0;
@@ -3051,9 +2999,7 @@ function btw(pi, dependencies = {}) {
3051
2999
  const runFullscreen = dependencies.runFullscreen ?? runBtwFullscreen;
3052
3000
  const resumableThreads = /* @__PURE__ */ new Map();
3053
3001
  let nextThreadNumber = 1;
3054
- const listResumeThreads = () => [...resumableThreads.values()].reverse().filter((state) => state.thread.turns.length > 0 && state.title).sort(
3055
- (first, second) => second.updatedAt - first.updatedAt || second.createdAt - first.createdAt
3056
- ).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) => ({
3057
3003
  id: state.id,
3058
3004
  title: state.title ?? "Untitled side thread",
3059
3005
  questionCount: state.thread.turns.length
@@ -3083,11 +3029,7 @@ function btw(pi, dependencies = {}) {
3083
3029
  }
3084
3030
  const branch = ctx.sessionManager.getBranch(treeResult.entryId);
3085
3031
  if (branch.at(-1)?.id !== treeResult.entryId) {
3086
- notifySafely3(
3087
- ctx,
3088
- "The selected main-thread branch is no longer available",
3089
- "warning"
3090
- );
3032
+ notifySafely3(ctx, "The selected main-thread branch is no longer available", "warning");
3091
3033
  continue;
3092
3034
  }
3093
3035
  selectedConversationContext = buildConversationContext(branch);
@@ -3164,9 +3106,7 @@ async function showCommandMenuForBtw(pi, ctx, resumeThreads) {
3164
3106
  const loaded = await readBtwSettings();
3165
3107
  const settings = loaded.kind === "loaded" ? loaded.settings : {};
3166
3108
  const configured = settings.model ? parseBtwModelReference(settings.model) : void 0;
3167
- const configuredModel = configured ? availableModels.find(
3168
- (model2) => model2.provider === configured.provider && model2.id === configured.modelId
3169
- ) : void 0;
3109
+ const configuredModel = configured ? availableModels.find((model2) => model2.provider === configured.provider && model2.id === configured.modelId) : void 0;
3170
3110
  const model = configuredModel ?? currentModel;
3171
3111
  return showBtwCommandMenu(ctx, {
3172
3112
  currentThinkingLevel,
@@ -3230,10 +3170,7 @@ async function runBtwThread({
3230
3170
  const thinkingLevels = getSupportedThinkingLevels(selected.model);
3231
3171
  const pendingWrites = /* @__PURE__ */ new Set();
3232
3172
  const steeringQuestions = [];
3233
- let activeThinkingLevel = clampThinkingLevel(
3234
- selected.model,
3235
- state?.thinkingLevel ?? thinkingLevel
3236
- );
3173
+ let activeThinkingLevel = clampThinkingLevel(selected.model, state?.thinkingLevel ?? thinkingLevel);
3237
3174
  if (state) state.thinkingLevel = activeThinkingLevel;
3238
3175
  let pendingQuestion = initialQuestion;
3239
3176
  let composerDraft;
@@ -3259,13 +3196,7 @@ async function runBtwThread({
3259
3196
  try {
3260
3197
  while (true) {
3261
3198
  if (!pendingQuestion) {
3262
- const action = await interact(
3263
- thread,
3264
- thread.turns.length > 0,
3265
- ctx,
3266
- composerDraft,
3267
- createThinkingControl()
3268
- );
3199
+ const action = await interact(thread, thread.turns.length > 0, ctx, composerDraft, createThinkingControl());
3269
3200
  if (action.kind === "close") return { kind: "closed" };
3270
3201
  if (action.kind === "bringToMain") {
3271
3202
  const choice = await chooseBringToMainAction(thread, ctx);
@@ -3351,20 +3282,13 @@ async function chooseBringToMain(thread, ctx, dependencies = {}) {
3351
3282
  );
3352
3283
  let selectedQuestion;
3353
3284
  while (true) {
3354
- const questionResult = await showMenu(
3355
- ctx,
3356
- "Start from which question?",
3357
- questions,
3358
- selectedQuestion
3359
- );
3285
+ const questionResult = await showMenu(ctx, "Start from which question?", questions, selectedQuestion);
3360
3286
  if (questionResult.kind === "close") return { kind: "closed" };
3361
3287
  if (questionResult.kind === "back") break;
3362
3288
  const answeredTurnIndex = questions.indexOf(questionResult.value);
3363
3289
  if (answeredTurnIndex < 0) continue;
3364
3290
  selectedQuestion = questionResult.value;
3365
- const choice = makeChoice(
3366
- buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex })
3367
- );
3291
+ const choice = makeChoice(buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex }));
3368
3292
  const preview = await showPreview(ctx, choice.draft, choice.summary);
3369
3293
  if (preview.kind === "close") return { kind: "closed" };
3370
3294
  if (preview.kind === "back") continue;
@@ -3487,10 +3411,7 @@ async function loadBringToMainDraft(draft, ctx, summary) {
3487
3411
  const existing = ctx.ui.getEditorText();
3488
3412
  if (!existing.trim()) {
3489
3413
  ctx.ui.setEditorText(draft);
3490
- ctx.ui.notify(
3491
- `Brought ${describeContent()} to the main editor. Review and submit when ready.`,
3492
- "info"
3493
- );
3414
+ ctx.ui.notify(`Brought ${describeContent()} to the main editor. Review and submit when ready.`, "info");
3494
3415
  return "loaded";
3495
3416
  }
3496
3417
  const appendOption = "Append after current draft Recommended";
@@ -3517,11 +3438,10 @@ ${draft}`);
3517
3438
  if (action.value !== replaceOption) continue;
3518
3439
  const current = ctx.ui.getEditorText();
3519
3440
  const characters = [...current].length;
3520
- const confirmed = await showBtwMenu(
3521
- ctx,
3522
- `Replace the current ${characters}-character editor draft?`,
3523
- ["Back Keep current editor text", "\u26A0 Replace current draft Cannot be undone"]
3524
- );
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
+ ]);
3525
3445
  if (confirmed.kind === "close") return "closed";
3526
3446
  if (confirmed.kind === "back" || confirmed.value === "Back Keep current editor text") continue;
3527
3447
  if (confirmed.value !== "\u26A0 Replace current draft Cannot be undone") continue;
@@ -3533,63 +3453,72 @@ ${draft}`);
3533
3453
  continue;
3534
3454
  }
3535
3455
  ctx.ui.setEditorText(draft);
3536
- ctx.ui.notify(
3537
- `Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`,
3538
- "info"
3539
- );
3456
+ ctx.ui.notify(`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`, "info");
3540
3457
  return "loaded";
3541
3458
  }
3542
3459
  }
3543
3460
  function truncatePreview(text) {
3544
3461
  return text.length <= 72 ? text : `${text.slice(0, 69)}\u2026`;
3545
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
+ }
3546
3471
  async function askThreadQuestion(thread, question, selected, thinkingLevel, ctx, steering) {
3547
- return ctx.ui.custom(
3548
- (tui, theme, keybindings, done) => {
3549
- let settled = false;
3550
- const view = new BtwAnsweringView(
3551
- tui,
3552
- theme,
3553
- thread.turns,
3554
- question,
3555
- () => {
3556
- if (settled) return;
3557
- settled = true;
3558
- done({ kind: "aborted" });
3559
- },
3560
- thinkingLevel,
3561
- {
3562
- steering: {
3563
- questions: steering.questions,
3564
- onSubmit: steering.submit,
3565
- thinking: { ...steering.thinking, keybindings }
3566
- }
3567
- }
3568
- );
3569
- completeSideThreadTurn({
3570
- thread,
3571
- question,
3572
- model: selected.model,
3573
- thinkingLevel,
3574
- auth: selected.auth,
3575
- signal: view.signal,
3576
- completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
3577
- sessionId: readBtwSessionId(ctx)
3578
- }).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
+ () => {
3579
3482
  if (settled) return;
3580
3483
  settled = true;
3581
- view.finish();
3582
- done(result);
3583
- });
3584
- return view;
3585
- }
3586
- );
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
+ });
3587
3513
  }
3588
3514
  async function showThreadComposer(thread, startAtBottom, ctx, initialQuestion, thinking) {
3515
+ const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, initialQuestion, ctx);
3516
+ if (!createMarkdownTransformers) return { kind: "close" };
3589
3517
  return ctx.ui.custom(
3590
3518
  (tui, theme, keybindings, done) => new BtwTranscriptPager(tui, theme, thread.turns, done, {
3591
3519
  startAtBottom,
3592
3520
  initialQuestion,
3521
+ markdownTransformers: createMarkdownTransformers(theme),
3593
3522
  thinking: { ...thinking, keybindings }
3594
3523
  })
3595
3524
  );