@narumitw/pi-btw 0.58.1 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.ts CHANGED
@@ -4,25 +4,16 @@
4
4
  // src/btw.ts
5
5
  import {
6
6
  clampThinkingLevel,
7
- getSupportedThinkingLevels
7
+ getSupportedThinkingLevels as getSupportedThinkingLevels2
8
8
  } from "@earendil-works/pi-ai";
9
- import {
10
- BorderedLoader
11
- } from "@earendil-works/pi-coding-agent";
9
+ import { BorderedLoader as BorderedLoader2 } from "@earendil-works/pi-coding-agent";
12
10
 
13
11
  // src/bring-to-main.ts
14
- import {
15
- Key,
16
- matchesKey,
17
- truncateToWidth,
18
- visibleWidth
19
- } from "@earendil-works/pi-tui";
12
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
20
13
  var RESERVED_APP_ROWS = 3;
21
14
  var GRAPHEME_SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
22
15
  function getAnsweredTurns(turns) {
23
- return turns.filter(
24
- (turn) => turn.kind === "answered"
25
- );
16
+ return turns.filter((turn) => turn.kind === "answered");
26
17
  }
27
18
  function buildQuickBringToMainSegments(turns, scope) {
28
19
  const answered = getAnsweredTurns(turns);
@@ -82,9 +73,7 @@ function segmentsFromTextRange(lines, anchor, cursor) {
82
73
  return segments;
83
74
  }
84
75
  function estimateBringToMainTokens(segments) {
85
- return Math.ceil(
86
- Buffer.byteLength(segments.map((segment) => segment.text).join("\n"), "utf8") / 4
87
- );
76
+ return Math.ceil(Buffer.byteLength(segments.map((segment) => segment.text).join("\n"), "utf8") / 4);
88
77
  }
89
78
  function summarizeBringToMain(segments) {
90
79
  return {
@@ -94,10 +83,8 @@ function summarizeBringToMain(segments) {
94
83
  };
95
84
  }
96
85
  function formatBtwBringToMain(segments) {
97
- const body = segments.map(
98
- (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) {
@@ -1335,6 +1302,12 @@ import {
1335
1302
  } from "@earendil-works/pi-coding-agent";
1336
1303
  import { Key as Key3, matchesKey as matchesKey3 } from "@earendil-works/pi-tui";
1337
1304
 
1305
+ // src/menu.ts
1306
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
1307
+ import {
1308
+ BorderedLoader
1309
+ } from "@earendil-works/pi-coding-agent";
1310
+
1338
1311
  // src/settings.ts
1339
1312
  import { randomUUID } from "node:crypto";
1340
1313
  import { constants } from "node:fs";
@@ -1343,15 +1316,7 @@ import { basename, dirname, join } from "node:path";
1343
1316
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
1344
1317
 
1345
1318
  // src/side-thread.ts
1346
- var BTW_THINKING_LEVELS = [
1347
- "off",
1348
- "minimal",
1349
- "low",
1350
- "medium",
1351
- "high",
1352
- "xhigh",
1353
- "max"
1354
- ];
1319
+ var BTW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
1355
1320
  function createSideThread(conversationContext) {
1356
1321
  return { conversationContext, turns: [] };
1357
1322
  }
@@ -1365,10 +1330,7 @@ function buildSideThreadMessages(thread, question) {
1365
1330
  return messages;
1366
1331
  }
1367
1332
  const [first, ...rest] = answeredTurns;
1368
- messages.push(
1369
- createUserMessage(buildUserPrompt(first.question, thread.conversationContext)),
1370
- first.response
1371
- );
1333
+ messages.push(createUserMessage(buildUserPrompt(first.question, thread.conversationContext)), first.response);
1372
1334
  for (const turn of rest) {
1373
1335
  messages.push(createUserMessage(buildFollowUpPrompt(turn.question)), turn.response);
1374
1336
  }
@@ -1434,13 +1396,7 @@ function buildUserPrompt(question, conversationContext) {
1434
1396
  ].join("\n");
1435
1397
  }
1436
1398
  function buildFollowUpPrompt(question) {
1437
- return [
1438
- "Continue the same side conversation.",
1439
- "",
1440
- "<side_question>",
1441
- question,
1442
- "</side_question>"
1443
- ].join("\n");
1399
+ return ["Continue the same side conversation.", "", "<side_question>", question, "</side_question>"].join("\n");
1444
1400
  }
1445
1401
  function createUserMessage(text) {
1446
1402
  return {
@@ -1632,9 +1588,7 @@ async function readSettingsContents(settingsPath) {
1632
1588
  throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
1633
1589
  }
1634
1590
  try {
1635
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
1636
- buffer.subarray(0, offset)
1637
- );
1591
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, offset));
1638
1592
  } catch {
1639
1593
  throw new Error("settings file is not valid UTF-8");
1640
1594
  }
@@ -1652,10 +1606,7 @@ async function publishSettings(settingsPath, document, signal, beforeRename) {
1652
1606
  const directory = dirname(settingsPath);
1653
1607
  await mkdir(directory, { recursive: true });
1654
1608
  signal?.throwIfAborted();
1655
- const temporaryPath = join(
1656
- directory,
1657
- `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`
1658
- );
1609
+ const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);
1659
1610
  try {
1660
1611
  await writeFile(temporaryPath, contents, {
1661
1612
  encoding: "utf8",
@@ -1683,6 +1634,10 @@ function applyBtwSettingsPatch(current, patch) {
1683
1634
  if (Object.keys(keys).length) updated.keybindings = keys;
1684
1635
  else delete updated.keybindings;
1685
1636
  }
1637
+ if (Object.hasOwn(patch, "model")) {
1638
+ if (patch.model === void 0) delete updated.model;
1639
+ else updated.model = patch.model;
1640
+ }
1686
1641
  if (Object.hasOwn(patch, "thinkingLevel")) {
1687
1642
  if (patch.thinkingLevel === void 0) delete updated.thinkingLevel;
1688
1643
  else updated.thinkingLevel = patch.thinkingLevel;
@@ -1716,12 +1671,30 @@ function formatError2(error) {
1716
1671
  var SAME_AS_MAIN_THREAD = "Same as main thread";
1717
1672
  async function showBtwCommandMenu(ctx, options) {
1718
1673
  if (ctx.mode !== "tui") return "closed";
1719
- const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
1674
+ const { defineMenu, runMenu, sanitizeTerminalText } = await import("@narumitw/pi-tui-kit");
1720
1675
  if (ctx.signal?.aborted) return "closed";
1721
1676
  const settingsPath = options.settingsPath ?? btwSettingsPath();
1722
1677
  const readSettings = options.readSettings ?? readBtwSettings;
1723
1678
  const updateSettings = options.updateSettings ?? updateBtwSettings;
1724
- const levels = options.availableThinkingLevels.length > 0 ? [...options.availableThinkingLevels] : ["off"];
1679
+ const getAvailable = ctx.modelRegistry.getAvailable;
1680
+ const allAvailableModels = deduplicateModels(
1681
+ options.availableModels ?? (typeof getAvailable === "function" ? getAvailable.call(ctx.modelRegistry) : ctx.modelRegistry.getAll())
1682
+ );
1683
+ const currentModel = options.currentModel ?? ctx.model;
1684
+ const scopedModels = options.scopedModels ?? ctx.scopedModels ?? [];
1685
+ const selectableModels = availableModelsInScope(allAvailableModels, scopedModels);
1686
+ const modelItemIds = new Map(selectableModels.map((model, index) => [model, `btw-settings-model:${index}`]));
1687
+ const modelsByItemId = new Map(selectableModels.map((model) => [modelItemIds.get(model), model]));
1688
+ const safeModelMetadata = (value, fallback) => {
1689
+ const safe = sanitizeTerminalText(value).trim() || fallback;
1690
+ return [...safe].slice(0, 512).join("");
1691
+ };
1692
+ const displayModelReference = (model) => `${safeModelMetadata(model.id, "unknown model")} [${safeModelMetadata(model.provider, "unknown provider")}]`;
1693
+ const rawModelReference = (model) => {
1694
+ const reference = `${model.provider}/${model.id}`;
1695
+ const parsed = parseBtwModelReference(reference);
1696
+ return parsed?.provider === model.provider && parsed.modelId === model.id ? reference : void 0;
1697
+ };
1725
1698
  const displaySettingsPath = sanitizeSingleLine(settingsPath);
1726
1699
  const resumeThreads = options.resumeThreads ?? [];
1727
1700
  let startSelected = false;
@@ -1736,11 +1709,7 @@ async function showBtwCommandMenu(ctx, options) {
1736
1709
  };
1737
1710
  const shortcutValue = (settings, action) => {
1738
1711
  if (!keybindings) return "Default";
1739
- const effective = resolveBtwShortcuts(
1740
- settings.keybindings,
1741
- keybindings,
1742
- effectiveFullscreenCopyOnSelect(settings)
1743
- );
1712
+ const effective = resolveBtwShortcuts(settings.keybindings, keybindings, effectiveFullscreenCopyOnSelect(settings));
1744
1713
  const configured = settings.keybindings?.[action];
1745
1714
  if (configured !== void 0 && !effective.keys[action].includes(configured)) {
1746
1715
  return `Fallback (${effective.label(action)}; saved ${formatKeyLabel2(configured)})`;
@@ -1749,8 +1718,7 @@ async function showBtwCommandMenu(ctx, options) {
1749
1718
  return `${source} (${effective.label(action)})`;
1750
1719
  };
1751
1720
  const saveShortcut = async (state, value, signal) => {
1752
- if (!keybindings || state.kind !== "valid" || signal.aborted)
1753
- return { kind: "rejected" };
1721
+ if (!keybindings || state.kind !== "valid" || signal.aborted) return { kind: "rejected" };
1754
1722
  const action = shortcut;
1755
1723
  const manager = keybindings;
1756
1724
  const validate = (settings) => validateBtwShortcutEdit(
@@ -1785,6 +1753,48 @@ async function showBtwCommandMenu(ctx, options) {
1785
1753
  return { kind: "rejected" };
1786
1754
  }
1787
1755
  };
1756
+ const saveModel = async (model, signal) => {
1757
+ const modelReference = model ? rawModelReference(model) : void 0;
1758
+ if (model && !modelReference) return { kind: "cancelled" };
1759
+ const result2 = await ctx.ui.custom(
1760
+ (tui, theme, _keybindings, done) => {
1761
+ const loader = new BorderedLoader(tui, theme, "Saving Pi BTW model...");
1762
+ const ownerController = new AbortController();
1763
+ const saveSignal = AbortSignal.any([signal, loader.signal, ownerController.signal]);
1764
+ let settled = false;
1765
+ const finish = (value) => {
1766
+ if (settled) return;
1767
+ settled = true;
1768
+ done(value);
1769
+ };
1770
+ const cancel = () => finish({ kind: "cancelled" });
1771
+ loader.onAbort = cancel;
1772
+ saveSignal.addEventListener("abort", cancel, { once: true });
1773
+ queueMicrotask(() => {
1774
+ void (async () => {
1775
+ await Promise.resolve();
1776
+ if (saveSignal.aborted) return;
1777
+ try {
1778
+ await updateSettings({ model: modelReference }, { settingsPath, signal: saveSignal });
1779
+ finish({ kind: "saved" });
1780
+ } catch (error) {
1781
+ finish(saveSignal.aborted ? { kind: "cancelled" } : { kind: "failed", error });
1782
+ }
1783
+ })();
1784
+ });
1785
+ return {
1786
+ render: (width) => loader.render(width),
1787
+ invalidate: () => loader.invalidate(),
1788
+ handleInput: (data) => loader.handleInput(data),
1789
+ dispose() {
1790
+ ownerController.abort(new DOMException("Pi BTW model save disposed", "AbortError"));
1791
+ loader.dispose();
1792
+ }
1793
+ };
1794
+ }
1795
+ );
1796
+ return result2 ?? { kind: "cancelled" };
1797
+ };
1788
1798
  const loadState = async () => {
1789
1799
  const loaded = await readSettings(settingsPath);
1790
1800
  if (loaded.kind === "invalid") {
@@ -1792,16 +1802,79 @@ async function showBtwCommandMenu(ctx, options) {
1792
1802
  }
1793
1803
  return { kind: "valid", settings: loaded.kind === "loaded" ? loaded.settings : {} };
1794
1804
  };
1795
- const currentMainThinkingLevel = clampToAvailableThinkingLevel(
1796
- options.currentThinkingLevel,
1797
- levels
1798
- );
1799
- const displayThinkingLevel = (settings) => settings.thinkingLevel === void 0 ? SAME_AS_MAIN_THREAD : clampToAvailableThinkingLevel(settings.thinkingLevel, levels);
1800
- const displayThinkingSummary = (settings) => settings.thinkingLevel === void 0 ? `${SAME_AS_MAIN_THREAD} (currently ${currentMainThinkingLevel})` : displayThinkingLevel(settings);
1805
+ const configuredModel = (settings) => {
1806
+ if (!settings.model) return void 0;
1807
+ const reference = parseBtwModelReference(settings.model);
1808
+ return reference ? allAvailableModels.find((model) => model.provider === reference.provider && model.id === reference.modelId) : void 0;
1809
+ };
1810
+ const selectableConfiguredModel = (settings) => {
1811
+ const configured = configuredModel(settings);
1812
+ return configured ? selectableModels.find((model) => sameModel(model, configured)) : void 0;
1813
+ };
1814
+ const thinkingLevels = (settings) => {
1815
+ const overridden = options.availableThinkingLevels;
1816
+ const effectiveModel = configuredModel(settings) ?? currentModel;
1817
+ const available = overridden && overridden.length > 0 ? overridden : effectiveModel ? getSupportedThinkingLevels(effectiveModel) : BTW_THINKING_LEVELS;
1818
+ return available.length > 0 ? [...available] : ["off"];
1819
+ };
1820
+ const currentMainThinkingLevel = (settings) => clampToAvailableThinkingLevel(options.currentThinkingLevel, thinkingLevels(settings));
1821
+ const displayThinkingLevel = (settings) => settings.thinkingLevel === void 0 ? SAME_AS_MAIN_THREAD : clampToAvailableThinkingLevel(settings.thinkingLevel, thinkingLevels(settings));
1822
+ const displayThinkingSummary = (settings) => settings.thinkingLevel === void 0 ? `${SAME_AS_MAIN_THREAD} (currently ${currentMainThinkingLevel(settings)})` : displayThinkingLevel(settings);
1801
1823
  const displayRememberSummary = (settings) => {
1802
1824
  const value = effectiveRememberThinkingLevelChanges(settings) ? "On" : "Off";
1803
1825
  return settings.thinkingLevel === void 0 ? `${value} (fixed levels only)` : value;
1804
1826
  };
1827
+ const displayModelValue = (settings) => {
1828
+ if (!settings.model) {
1829
+ return currentModel ? `${SAME_AS_MAIN_THREAD} (${displayModelReference(currentModel)})` : SAME_AS_MAIN_THREAD;
1830
+ }
1831
+ const available = configuredModel(settings);
1832
+ if (!available) return `${SAME_AS_MAIN_THREAD} \xB7 ${safeModelMetadata(settings.model, "unknown model")} unavailable`;
1833
+ const reference = displayModelReference(available);
1834
+ return selectableConfiguredModel(settings) ? reference : `${reference} \xB7 outside current scope`;
1835
+ };
1836
+ const modelItems = (settings) => {
1837
+ const configured = configuredModel(settings);
1838
+ const selectable = selectableConfiguredModel(settings);
1839
+ const retained = settings.model && !selectable;
1840
+ return [
1841
+ {
1842
+ id: "same-as-main",
1843
+ label: SAME_AS_MAIN_THREAD,
1844
+ description: currentModel ? `Currently ${displayModelReference(currentModel)}` : "Use the main thread model when /btw starts."
1845
+ },
1846
+ ...selectableModels.map((model) => {
1847
+ const reference = displayModelReference(model);
1848
+ const name = safeModelMetadata(model.name ?? "", "");
1849
+ const validReference = rawModelReference(model);
1850
+ return {
1851
+ id: modelItemIds.get(model),
1852
+ label: reference,
1853
+ ...name ? { details: [`Model Name: ${name}`] } : {},
1854
+ searchText: [reference, name].filter(Boolean).join(" "),
1855
+ ...!validReference ? {
1856
+ disabled: true,
1857
+ disabledReason: "This model identity cannot be stored in pi-btw.json."
1858
+ } : {}
1859
+ };
1860
+ }),
1861
+ ...retained ? [
1862
+ {
1863
+ id: "configured-model",
1864
+ label: configured ? displayModelReference(configured) : safeModelMetadata(settings.model, "unknown model"),
1865
+ ...configured ? { description: "Configured outside the current model scope; retained until changed." } : {
1866
+ disabled: true,
1867
+ disabledReason: "Configured model is unavailable; /btw falls back to the main model."
1868
+ }
1869
+ }
1870
+ ] : []
1871
+ ];
1872
+ };
1873
+ const selectedModelItemId = (settings) => {
1874
+ const selected = selectableConfiguredModel(settings);
1875
+ return selected ? modelItemIds.get(selected) : settings.model ? "configured-model" : "same-as-main";
1876
+ };
1877
+ const currentModelItemId = (settings) => settings.model && configuredModel(settings) ? selectedModelItemId(settings) : "same-as-main";
1805
1878
  const menu = defineMenu({
1806
1879
  start: "main",
1807
1880
  screens: {
@@ -1809,6 +1882,7 @@ async function showBtwCommandMenu(ctx, options) {
1809
1882
  kind: "actions",
1810
1883
  title: "Pi BTW",
1811
1884
  lines: [
1885
+ `Model: ${displayModelValue(state.settings)}`,
1812
1886
  `Thinking: ${displayThinkingSummary(state.settings)} \xB7 Remember changes: ${displayRememberSummary(state.settings)}`,
1813
1887
  `Copy on select: ${effectiveFullscreenCopyOnSelect(state.settings) ? "On" : "Off"}`
1814
1888
  ],
@@ -1836,7 +1910,7 @@ async function showBtwCommandMenu(ctx, options) {
1836
1910
  {
1837
1911
  id: "settings",
1838
1912
  label: "Settings",
1839
- description: "Choose thinking, keybindings, and selection copying",
1913
+ description: "Choose model, thinking, keybindings, and selection copying",
1840
1914
  to: state.kind === "invalid" ? "invalid" : "settings"
1841
1915
  }
1842
1916
  ],
@@ -1860,12 +1934,19 @@ async function showBtwCommandMenu(ctx, options) {
1860
1934
  title: "Pi BTW Settings",
1861
1935
  lines: [`User settings \xB7 ${displaySettingsPath}`],
1862
1936
  items: [
1937
+ {
1938
+ id: "model",
1939
+ label: "Model",
1940
+ description: "Choose the model for future pi-btw side threads without changing the main session.",
1941
+ currentValue: displayModelValue(state.settings),
1942
+ action: "open-model"
1943
+ },
1863
1944
  {
1864
1945
  id: "thinkingLevel",
1865
1946
  label: "Thinking level",
1866
- description: `Set the starting level for future pi-btw side threads. Currently ${currentMainThinkingLevel}.`,
1947
+ description: `Set the starting level for future pi-btw side threads. Currently ${currentMainThinkingLevel(state.settings)}.`,
1867
1948
  currentValue: displayThinkingLevel(state.settings),
1868
- values: [SAME_AS_MAIN_THREAD, ...levels],
1949
+ values: [SAME_AS_MAIN_THREAD, ...thinkingLevels(state.settings)],
1869
1950
  action: "set-thinking"
1870
1951
  },
1871
1952
  {
@@ -1893,6 +1974,21 @@ async function showBtwCommandMenu(ctx, options) {
1893
1974
  }))
1894
1975
  ]
1895
1976
  }),
1977
+ model: ({ state }) => ({
1978
+ kind: "choice",
1979
+ title: "Pi BTW Model",
1980
+ lines: [
1981
+ "Same as main thread is the default and fallback when a configured model is unavailable.",
1982
+ ...state.settings.model && !selectableConfiguredModel(state.settings) ? [`Configured: ${displayModelValue(state.settings)}`] : []
1983
+ ],
1984
+ items: modelItems(state.settings),
1985
+ action: "set-model",
1986
+ currentItemId: currentModelItemId(state.settings),
1987
+ initialItemId: selectedModelItemId(state.settings),
1988
+ enableSearch: true,
1989
+ viewportSize: 10,
1990
+ hint: "back"
1991
+ }),
1896
1992
  shortcut: ({ state }) => ({
1897
1993
  kind: "actions",
1898
1994
  title: shortcutLabels[shortcut],
@@ -1923,13 +2019,32 @@ async function showBtwCommandMenu(ctx, options) {
1923
2019
  },
1924
2020
  actions: {
1925
2021
  "edit-shortcut": ({ itemId }) => {
1926
- if (!BTW_SHORTCUT_ACTIONS.includes(itemId))
1927
- return { kind: "rejected" };
2022
+ if (!BTW_SHORTCUT_ACTIONS.includes(itemId)) return { kind: "rejected" };
1928
2023
  shortcut = itemId;
1929
2024
  return { kind: "to", screen: "shortcut" };
1930
2025
  },
1931
2026
  "save-shortcut": ({ state, value, signal }) => saveShortcut(state, value?.trim() ?? "", signal),
1932
2027
  "reset-shortcut": ({ state, signal }) => saveShortcut(state, void 0, signal),
2028
+ "open-model": async () => ({ kind: "to", screen: "model" }),
2029
+ "set-model": async ({ state, itemId, signal }) => {
2030
+ if (itemId === "configured-model" && configuredModel(state.settings)) {
2031
+ return { kind: "back" };
2032
+ }
2033
+ const model = itemId ? modelsByItemId.get(itemId) : void 0;
2034
+ if (itemId !== "same-as-main" && (!model || !rawModelReference(model))) return { kind: "rejected" };
2035
+ const result2 = await saveModel(model, signal);
2036
+ if (result2.kind === "failed") {
2037
+ notifySaveFailure(ctx, result2.error);
2038
+ return { kind: "rejected" };
2039
+ }
2040
+ if (result2.kind === "cancelled" || signal.aborted) return { kind: "close" };
2041
+ notifySafely(
2042
+ ctx,
2043
+ model ? `Pi BTW model: ${displayModelReference(model)}.` : `Pi BTW model: ${SAME_AS_MAIN_THREAD}.`,
2044
+ "info"
2045
+ );
2046
+ return { kind: "back" };
2047
+ },
1933
2048
  start: async () => {
1934
2049
  startSelected = true;
1935
2050
  return { kind: "close" };
@@ -1945,8 +2060,9 @@ async function showBtwCommandMenu(ctx, options) {
1945
2060
  resumedThreadId = itemId;
1946
2061
  return { kind: "close" };
1947
2062
  },
1948
- "set-thinking": async ({ value, signal }) => {
2063
+ "set-thinking": async ({ state, value, signal }) => {
1949
2064
  if (!value) return { kind: "rejected" };
2065
+ const levels = thinkingLevels(state.settings);
1950
2066
  const patch = value === SAME_AS_MAIN_THREAD ? { thinkingLevel: void 0 } : levels.includes(value) ? { thinkingLevel: value } : void 0;
1951
2067
  if (!patch) return { kind: "rejected" };
1952
2068
  try {
@@ -1962,10 +2078,7 @@ async function showBtwCommandMenu(ctx, options) {
1962
2078
  "set-remember": async ({ value, signal }) => {
1963
2079
  if (value !== "On" && value !== "Off") return { kind: "rejected" };
1964
2080
  try {
1965
- await updateSettings(
1966
- { rememberThinkingLevelChanges: value === "On" },
1967
- { settingsPath, signal }
1968
- );
2081
+ await updateSettings({ rememberThinkingLevelChanges: value === "On" }, { settingsPath, signal });
1969
2082
  if (signal.aborted) return { kind: "rejected" };
1970
2083
  notifySafely(ctx, `Remember thinking level changes: ${value}.`, "info");
1971
2084
  return { kind: "stay" };
@@ -1977,10 +2090,7 @@ async function showBtwCommandMenu(ctx, options) {
1977
2090
  "set-fullscreen-copy": async ({ value, signal }) => {
1978
2091
  if (value !== "On" && value !== "Off") return { kind: "rejected" };
1979
2092
  try {
1980
- await updateSettings(
1981
- { fullscreenCopyOnSelect: value === "On" },
1982
- { settingsPath, signal }
1983
- );
2093
+ await updateSettings({ fullscreenCopyOnSelect: value === "On" }, { settingsPath, signal });
1984
2094
  if (signal.aborted) return { kind: "rejected" };
1985
2095
  notifySafely(ctx, `Copy selection automatically: ${value}.`, "info");
1986
2096
  return { kind: "stay" };
@@ -2055,6 +2165,21 @@ async function runBtwMenuPreservingEditor(ctx, run, onKeybindings) {
2055
2165
  }
2056
2166
  return result;
2057
2167
  }
2168
+ function deduplicateModels(models) {
2169
+ return models.filter((model, index) => models.findIndex((candidate) => sameModel(candidate, model)) === index);
2170
+ }
2171
+ function availableModelsInScope(availableModels, scopedModels) {
2172
+ if (scopedModels.length === 0) return [...availableModels];
2173
+ return deduplicateModels(
2174
+ scopedModels.flatMap((entry) => {
2175
+ const available = availableModels.find((model) => sameModel(model, entry.model));
2176
+ return available ? [available] : [];
2177
+ })
2178
+ );
2179
+ }
2180
+ function sameModel(left, right) {
2181
+ return left.provider === right.provider && left.id === right.id;
2182
+ }
2058
2183
  function clampToAvailableThinkingLevel(requested, available) {
2059
2184
  if (available.includes(requested)) return requested;
2060
2185
  const requestedIndex = BTW_THINKING_LEVELS.indexOf(requested);
@@ -2145,80 +2270,77 @@ async function pickMainEntry(pi, ctx, dependencies = {}) {
2145
2270
  controller.abort(new Error("The main-thread tree picker closed"));
2146
2271
  }
2147
2272
  };
2148
- const result = await showBtwCustomPreservingEditor(
2149
- 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);
2273
+ const result = await showBtwCustomPreservingEditor(ctx, (tui, _theme, _keybindings, done) => {
2274
+ let settled = false;
2275
+ let selector;
2276
+ const finish = (value) => {
2277
+ if (settled) return;
2278
+ settled = true;
2279
+ abortCopies();
2280
+ done(value);
2281
+ };
2282
+ const onCopy = (entryId, displayText) => {
2283
+ if (settled) return;
2284
+ const text = entryId ? rawCopyText.get(entryId) : displayText;
2285
+ if (!text) {
2286
+ notifySafely2(ctx, "Selected entry has no text to copy", "warning");
2287
+ return;
2288
+ }
2289
+ const controller = new AbortController();
2290
+ copyControllers.add(controller);
2291
+ let operation;
2292
+ try {
2293
+ operation = copy(text, controller.signal);
2294
+ } catch (error) {
2295
+ operation = Promise.reject(error);
2296
+ }
2297
+ let task;
2298
+ task = operation.then(() => {
2299
+ if (!settled) notifySafely2(ctx, "Copied selected message", "info");
2300
+ }).catch((error) => {
2301
+ if (!settled && !controller.signal.aborted) {
2302
+ notifySafely2(ctx, `Could not copy selected message: ${formatError4(error)}`, "error");
2173
2303
  }
2174
- 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) {
2304
+ }).finally(() => {
2305
+ copyControllers.delete(controller);
2306
+ copyTasks.delete(task);
2307
+ });
2308
+ copyTasks.add(task);
2309
+ };
2310
+ const restoreLabel = (entryId) => {
2311
+ const previous = savedLabels.get(entryId);
2312
+ selector?.setViewLabel?.(entryId, previous?.label, previous?.labelTimestamp);
2313
+ tui.requestRender();
2314
+ };
2315
+ const onLabelChange = (entryId, label) => {
2316
+ if (settled) return;
2317
+ try {
2318
+ if (!ctx.sessionManager.getEntry(entryId)) {
2206
2319
  restoreLabel(entryId);
2207
- notifySafely2(ctx, `Could not update tree label: ${formatError4(error)}`, "error");
2320
+ notifySafely2(ctx, "The selected main-thread entry is no longer available", "warning");
2321
+ return;
2208
2322
  }
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
- );
2323
+ const persistedLabel = label === void 0 ? void 0 : sanitizeSingleLine(label);
2324
+ pi.setLabel(entryId, persistedLabel);
2325
+ savedLabels.set(entryId, { label: persistedLabel });
2326
+ selector?.setViewLabel?.(entryId, persistedLabel);
2327
+ tui.requestRender();
2328
+ } catch (error) {
2329
+ restoreLabel(entryId);
2330
+ notifySafely2(ctx, `Could not update tree label: ${formatError4(error)}`, "error");
2331
+ }
2332
+ };
2333
+ selector = createSelector({
2334
+ tree,
2335
+ currentLeafId,
2336
+ terminalRows: tui.terminal.rows,
2337
+ onSelect: (entryId) => finish({ kind: "selected", entryId }),
2338
+ onCancel: () => finish({ kind: "back" }),
2339
+ onCopy,
2340
+ onLabelChange
2341
+ });
2342
+ return new MainThreadTreePickerComponent(selector, () => finish({ kind: "closed" }));
2343
+ });
2222
2344
  abortCopies();
2223
2345
  await Promise.allSettled([...copyTasks]);
2224
2346
  return result ?? { kind: "closed" };
@@ -2254,8 +2376,7 @@ function sanitizeEntryForDisplay(entry) {
2254
2376
  switch (entry.type) {
2255
2377
  case "message": {
2256
2378
  const message = { ...entry.message };
2257
- if ("content" in entry.message)
2258
- message.content = sanitizeDisplayContent(entry.message.content);
2379
+ if ("content" in entry.message) message.content = sanitizeDisplayContent(entry.message.content);
2259
2380
  for (const key of ["role", "errorMessage", "command", "toolName"]) {
2260
2381
  const value = message[key];
2261
2382
  if (typeof value === "string") message[key] = sanitizeSingleLine(value);
@@ -2389,6 +2510,49 @@ function formatError4(error) {
2389
2510
  return error instanceof Error ? error.message : String(error);
2390
2511
  }
2391
2512
 
2513
+ // src/transcript-markdown.ts
2514
+ var MERMAID_MARKDOWN_MODULE = "@narumitw/pi-tui-kit/markdown";
2515
+ var noMarkdownTransformers = () => [];
2516
+ async function prepareBtwTranscriptMarkdown(turns, pendingQuestion, signal) {
2517
+ const documents = turns.flatMap(
2518
+ (turn) => turn.kind === "answered" ? [turn.question, turn.answer] : [turn.question]
2519
+ );
2520
+ if (pendingQuestion) documents.push(pendingQuestion);
2521
+ if (!documents.some((document) => /mermaid/iu.test(document))) return noMarkdownTransformers;
2522
+ if (signal?.aborted) return void 0;
2523
+ const markdownModule = await settleUnlessAborted(
2524
+ import(MERMAID_MARKDOWN_MODULE),
2525
+ signal
2526
+ );
2527
+ if (!markdownModule || signal?.aborted) return void 0;
2528
+ const { createMermaidMarkdownTransformer, prepareMermaidMarkdownRenderer } = markdownModule;
2529
+ const preparations = /* @__PURE__ */ new Set();
2530
+ for (const document of documents) {
2531
+ const preparation = prepareMermaidMarkdownRenderer(document);
2532
+ if (preparation) preparations.add(preparation);
2533
+ }
2534
+ if (preparations.size > 0 && !await settleUnlessAborted(Promise.all(preparations), signal)) return void 0;
2535
+ if (signal?.aborted) return void 0;
2536
+ return (theme) => {
2537
+ const transformer = createMermaidMarkdownTransformer(theme);
2538
+ return transformer ? [transformer] : [];
2539
+ };
2540
+ }
2541
+ async function settleUnlessAborted(operation, signal) {
2542
+ if (!signal) return operation;
2543
+ let onAbort;
2544
+ const aborted = new Promise((resolve) => {
2545
+ onAbort = () => resolve(void 0);
2546
+ signal.addEventListener("abort", onAbort, { once: true });
2547
+ if (signal.aborted) onAbort();
2548
+ });
2549
+ try {
2550
+ return await Promise.race([operation, aborted]);
2551
+ } finally {
2552
+ if (onAbort) signal.removeEventListener("abort", onAbort);
2553
+ }
2554
+ }
2555
+
2392
2556
  // src/transcript-pager.ts
2393
2557
  import {
2394
2558
  AssistantMessageComponent,
@@ -2427,7 +2591,7 @@ var BtwTranscriptPager = class {
2427
2591
  this.onAction = onAction;
2428
2592
  this.options = options;
2429
2593
  this.shortcuts = getBtwShortcuts(tui, options.thinking?.keybindings);
2430
- this.transcriptComponents = buildTranscriptComponents(turns, this.theme);
2594
+ this.transcriptComponents = buildTranscriptComponents(turns, this.theme, void 0, options.markdownTransformers);
2431
2595
  this.canBringToMain = turns.some((turn) => turn.kind === "answered");
2432
2596
  this.thinkingLevel = options.thinking?.level;
2433
2597
  const editorTheme = {
@@ -2497,17 +2661,10 @@ var BtwTranscriptPager = class {
2497
2661
  const safeWidth = Math.max(1, width);
2498
2662
  const editorLines = this.editor.render(safeWidth);
2499
2663
  const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_LINES);
2500
- const viewportHeight = Math.max(
2501
- 0,
2502
- availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES
2503
- );
2664
+ const viewportHeight = Math.max(0, availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES);
2504
2665
  const contentLines = renderTranscriptLines(this.transcriptComponents, safeWidth);
2505
2666
  this.lastContentLineCount = contentLines.length;
2506
- this.scrollView.updateLayout(
2507
- contentLines.length,
2508
- viewportHeight,
2509
- () => this.tui.requestRender()
2510
- );
2667
+ this.scrollView.updateLayout(contentLines.length, viewportHeight, () => this.tui.requestRender());
2511
2668
  return fitComposerLayout(
2512
2669
  renderSideThreadHeader(safeWidth, this.theme, this.thinkingLevel),
2513
2670
  contentLines.slice(this.scrollView.scrollTop, this.scrollView.scrollTop + viewportHeight),
@@ -2638,7 +2795,12 @@ var BtwAnsweringView = class {
2638
2795
  this.onCancel = onCancel;
2639
2796
  this.options = options;
2640
2797
  this.shortcuts = getBtwShortcuts(tui, options.steering?.thinking?.keybindings);
2641
- this.transcriptComponents = buildTranscriptComponents(turns, this.theme, pendingQuestion);
2798
+ this.transcriptComponents = buildTranscriptComponents(
2799
+ turns,
2800
+ this.theme,
2801
+ pendingQuestion,
2802
+ options.markdownTransformers
2803
+ );
2642
2804
  this.thinkingLevel = options.steering?.thinking?.level ?? thinkingLevel;
2643
2805
  this.loader = new Loader(
2644
2806
  this.tui,
@@ -2722,10 +2884,7 @@ var BtwAnsweringView = class {
2722
2884
  const safeWidth = Math.max(1, width);
2723
2885
  const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_LINES);
2724
2886
  const editorLines = this.editor?.render(safeWidth) ?? [];
2725
- const steeringCapacity = Math.max(
2726
- 0,
2727
- availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES
2728
- );
2887
+ const steeringCapacity = Math.max(0, availableRows - editorLines.length - TRANSCRIPT_CHROME_LINES);
2729
2888
  const steeringLines = renderSteeringLines(
2730
2889
  this.options.steering?.questions ?? [],
2731
2890
  safeWidth,
@@ -2738,11 +2897,7 @@ var BtwAnsweringView = class {
2738
2897
  );
2739
2898
  const contentLines = renderTranscriptLines(this.transcriptComponents, safeWidth);
2740
2899
  this.lastContentLineCount = contentLines.length;
2741
- this.scrollView.updateLayout(
2742
- contentLines.length,
2743
- viewportHeight,
2744
- () => this.tui.requestRender()
2745
- );
2900
+ this.scrollView.updateLayout(contentLines.length, viewportHeight, () => this.tui.requestRender());
2746
2901
  return fitComposerLayout(
2747
2902
  renderSideThreadHeader(safeWidth, this.theme, this.thinkingLevel),
2748
2903
  contentLines.slice(this.scrollView.scrollTop, this.scrollView.scrollTop + viewportHeight),
@@ -2847,12 +3002,7 @@ var BtwAnsweringView = class {
2847
3002
  }
2848
3003
  createSteeringComponent() {
2849
3004
  return {
2850
- render: (width) => renderSteeringLines(
2851
- this.options.steering?.questions ?? [],
2852
- width,
2853
- this.theme,
2854
- MAX_STEERING_DISPLAY_LINES
2855
- ),
3005
+ render: (width) => renderSteeringLines(this.options.steering?.questions ?? [], width, this.theme, MAX_STEERING_DISPLAY_LINES),
2856
3006
  invalidate() {
2857
3007
  }
2858
3008
  };
@@ -2867,21 +3017,18 @@ var BtwAnsweringView = class {
2867
3017
  return Math.max(0, this.lastContentLineCount - this.scrollView.viewportHeight);
2868
3018
  }
2869
3019
  };
2870
- function buildTranscriptComponents(turns, theme, pendingQuestion) {
3020
+ function buildTranscriptComponents(turns, theme, pendingQuestion, markdownTransformers = []) {
2871
3021
  const components = turns.flatMap((turn) => {
2872
3022
  const question = new UserMessageComponent(
2873
3023
  escapeTerminalControls2(turn.question),
2874
3024
  getMarkdownTheme(),
2875
- 1
3025
+ 1,
3026
+ markdownTransformers
2876
3027
  );
2877
3028
  if (turn.kind === "error") {
2878
- const error = new Markdown(
2879
- `Error: ${escapeTerminalControls2(turn.answer)}`,
2880
- 1,
2881
- 1,
2882
- getMarkdownTheme(),
2883
- { color: (text) => theme.fg("error", text) }
2884
- );
3029
+ const error = new Markdown(`Error: ${escapeTerminalControls2(turn.answer)}`, 1, 1, getMarkdownTheme(), {
3030
+ color: (text) => theme.fg("error", text)
3031
+ });
2885
3032
  return [question, error];
2886
3033
  }
2887
3034
  const response = {
@@ -2890,11 +3037,11 @@ function buildTranscriptComponents(turns, theme, pendingQuestion) {
2890
3037
  stopReason: "stop",
2891
3038
  errorMessage: void 0
2892
3039
  };
2893
- return [question, new AssistantMessageComponent(response, true, getMarkdownTheme(), "", 1)];
3040
+ return [question, new AssistantMessageComponent(response, true, getMarkdownTheme(), "", 1, markdownTransformers)];
2894
3041
  });
2895
3042
  if (pendingQuestion) {
2896
3043
  components.push(
2897
- new UserMessageComponent(escapeTerminalControls2(pendingQuestion), getMarkdownTheme(), 1)
3044
+ new UserMessageComponent(escapeTerminalControls2(pendingQuestion), getMarkdownTheme(), 1, markdownTransformers)
2898
3045
  );
2899
3046
  }
2900
3047
  return components;
@@ -2929,26 +3076,16 @@ function renderSteeringLines(questions, width, theme, maxLines) {
2929
3076
  if (maxLines === 1 && questions.length > 1) {
2930
3077
  return [
2931
3078
  truncateToWidth3(
2932
- theme.fg(
2933
- "dim",
2934
- `Steering (+${questions.length - 1} more): ${formatQuestion(questions[0] ?? "")}`
2935
- ),
3079
+ theme.fg("dim", `Steering (+${questions.length - 1} more): ${formatQuestion(questions[0] ?? "")}`),
2936
3080
  width
2937
3081
  )
2938
3082
  ];
2939
3083
  }
2940
3084
  const hasOverflow = questions.length > maxLines;
2941
3085
  const questionLimit = hasOverflow ? Math.max(1, maxLines - 1) : maxLines;
2942
- const lines = questions.slice(0, questionLimit).map(
2943
- (question) => truncateToWidth3(theme.fg("dim", `Steering: ${formatQuestion(question)}`), width)
2944
- );
3086
+ const lines = questions.slice(0, questionLimit).map((question) => truncateToWidth3(theme.fg("dim", `Steering: ${formatQuestion(question)}`), width));
2945
3087
  if (hasOverflow) {
2946
- lines.push(
2947
- truncateToWidth3(
2948
- theme.fg("dim", `Steering: \u2026 +${questions.length - questionLimit} more`),
2949
- width
2950
- )
2951
- );
3088
+ lines.push(truncateToWidth3(theme.fg("dim", `Steering: \u2026 +${questions.length - questionLimit} more`), width));
2952
3089
  }
2953
3090
  return lines;
2954
3091
  }
@@ -3005,13 +3142,9 @@ async function resolveBtwModel({
3005
3142
  };
3006
3143
  }
3007
3144
  const reason = auth.ok ? "has no request credentials" : auth.error;
3008
- reportWarning(
3009
- `pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`
3010
- );
3145
+ reportWarning(`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`);
3011
3146
  } catch (error) {
3012
- reportWarning(
3013
- `pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`
3014
- );
3147
+ reportWarning(`pi-btw model ${settings.model} credentials failed (${formatError5(error)}); ${fallbackAction}.`);
3015
3148
  }
3016
3149
  if (sameAsCurrent) return void 0;
3017
3150
  }
@@ -3061,9 +3194,7 @@ function btw(pi, dependencies = {}) {
3061
3194
  const runFullscreen = dependencies.runFullscreen ?? runBtwFullscreen;
3062
3195
  const resumableThreads = /* @__PURE__ */ new Map();
3063
3196
  let nextThreadNumber = 1;
3064
- const listResumeThreads = () => [...resumableThreads.values()].reverse().filter((state) => state.thread.turns.length > 0 && state.title).sort(
3065
- (first, second) => second.updatedAt - first.updatedAt || second.createdAt - first.createdAt
3066
- ).map((state) => ({
3197
+ const listResumeThreads = () => [...resumableThreads.values()].reverse().filter((state) => state.thread.turns.length > 0 && state.title).sort((first, second) => second.updatedAt - first.updatedAt || second.createdAt - first.createdAt).map((state) => ({
3067
3198
  id: state.id,
3068
3199
  title: state.title ?? "Untitled side thread",
3069
3200
  questionCount: state.thread.turns.length
@@ -3093,11 +3224,7 @@ function btw(pi, dependencies = {}) {
3093
3224
  }
3094
3225
  const branch = ctx.sessionManager.getBranch(treeResult.entryId);
3095
3226
  if (branch.at(-1)?.id !== treeResult.entryId) {
3096
- notifySafely3(
3097
- ctx,
3098
- "The selected main-thread branch is no longer available",
3099
- "warning"
3100
- );
3227
+ notifySafely3(ctx, "The selected main-thread branch is no longer available", "warning");
3101
3228
  continue;
3102
3229
  }
3103
3230
  selectedConversationContext = buildConversationContext(branch);
@@ -3168,19 +3295,8 @@ function btw(pi, dependencies = {}) {
3168
3295
  });
3169
3296
  }
3170
3297
  async function showCommandMenuForBtw(pi, ctx, resumeThreads) {
3171
- const currentModel = ctx.model;
3172
- const availableModels = ctx.modelRegistry.getAll();
3173
- const currentThinkingLevel = pi.getThinkingLevel();
3174
- const loaded = await readBtwSettings();
3175
- const settings = loaded.kind === "loaded" ? loaded.settings : {};
3176
- const configured = settings.model ? parseBtwModelReference(settings.model) : void 0;
3177
- const configuredModel = configured ? availableModels.find(
3178
- (model2) => model2.provider === configured.provider && model2.id === configured.modelId
3179
- ) : void 0;
3180
- const model = configuredModel ?? currentModel;
3181
3298
  return showBtwCommandMenu(ctx, {
3182
- currentThinkingLevel,
3183
- availableThinkingLevels: model ? getSupportedThinkingLevels(model) : BTW_THINKING_LEVELS,
3299
+ currentThinkingLevel: pi.getThinkingLevel(),
3184
3300
  resumeThreads
3185
3301
  });
3186
3302
  }
@@ -3194,7 +3310,7 @@ async function loadSettingsForCommand(ctx) {
3194
3310
  }
3195
3311
  async function resolveBtwModelWithLoader(settings, ctx) {
3196
3312
  return ctx.ui.custom((tui, theme, _keybindings, done) => {
3197
- const loader = new BorderedLoader(tui, theme, "Resolving /btw model credentials...");
3313
+ const loader = new BorderedLoader2(tui, theme, "Resolving /btw model credentials...");
3198
3314
  let settled = false;
3199
3315
  loader.onAbort = () => {
3200
3316
  if (settled) return;
@@ -3237,13 +3353,10 @@ async function runBtwThread({
3237
3353
  const persistThinkingLevel = dependencies.persistThinkingLevel ?? ((level) => updateBtwSettings({ thinkingLevel: level }, { settingsPath }));
3238
3354
  const now = dependencies.now ?? Date.now;
3239
3355
  const thread = state?.thread ?? createSideThread(buildConversationContext(ctx.sessionManager.getBranch()));
3240
- const thinkingLevels = getSupportedThinkingLevels(selected.model);
3356
+ const thinkingLevels = getSupportedThinkingLevels2(selected.model);
3241
3357
  const pendingWrites = /* @__PURE__ */ new Set();
3242
3358
  const steeringQuestions = [];
3243
- let activeThinkingLevel = clampThinkingLevel(
3244
- selected.model,
3245
- state?.thinkingLevel ?? thinkingLevel
3246
- );
3359
+ let activeThinkingLevel = clampThinkingLevel(selected.model, state?.thinkingLevel ?? thinkingLevel);
3247
3360
  if (state) state.thinkingLevel = activeThinkingLevel;
3248
3361
  let pendingQuestion = initialQuestion;
3249
3362
  let composerDraft;
@@ -3269,13 +3382,7 @@ async function runBtwThread({
3269
3382
  try {
3270
3383
  while (true) {
3271
3384
  if (!pendingQuestion) {
3272
- const action = await interact(
3273
- thread,
3274
- thread.turns.length > 0,
3275
- ctx,
3276
- composerDraft,
3277
- createThinkingControl()
3278
- );
3385
+ const action = await interact(thread, thread.turns.length > 0, ctx, composerDraft, createThinkingControl());
3279
3386
  if (action.kind === "close") return { kind: "closed" };
3280
3387
  if (action.kind === "bringToMain") {
3281
3388
  const choice = await chooseBringToMainAction(thread, ctx);
@@ -3361,20 +3468,13 @@ async function chooseBringToMain(thread, ctx, dependencies = {}) {
3361
3468
  );
3362
3469
  let selectedQuestion;
3363
3470
  while (true) {
3364
- const questionResult = await showMenu(
3365
- ctx,
3366
- "Start from which question?",
3367
- questions,
3368
- selectedQuestion
3369
- );
3471
+ const questionResult = await showMenu(ctx, "Start from which question?", questions, selectedQuestion);
3370
3472
  if (questionResult.kind === "close") return { kind: "closed" };
3371
3473
  if (questionResult.kind === "back") break;
3372
3474
  const answeredTurnIndex = questions.indexOf(questionResult.value);
3373
3475
  if (answeredTurnIndex < 0) continue;
3374
3476
  selectedQuestion = questionResult.value;
3375
- const choice = makeChoice(
3376
- buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex })
3377
- );
3477
+ const choice = makeChoice(buildQuickBringToMainSegments(thread.turns, { kind: "from", answeredTurnIndex }));
3378
3478
  const preview = await showPreview(ctx, choice.draft, choice.summary);
3379
3479
  if (preview.kind === "close") return { kind: "closed" };
3380
3480
  if (preview.kind === "back") continue;
@@ -3497,10 +3597,7 @@ async function loadBringToMainDraft(draft, ctx, summary) {
3497
3597
  const existing = ctx.ui.getEditorText();
3498
3598
  if (!existing.trim()) {
3499
3599
  ctx.ui.setEditorText(draft);
3500
- ctx.ui.notify(
3501
- `Brought ${describeContent()} to the main editor. Review and submit when ready.`,
3502
- "info"
3503
- );
3600
+ ctx.ui.notify(`Brought ${describeContent()} to the main editor. Review and submit when ready.`, "info");
3504
3601
  return "loaded";
3505
3602
  }
3506
3603
  const appendOption = "Append after current draft Recommended";
@@ -3527,11 +3624,10 @@ ${draft}`);
3527
3624
  if (action.value !== replaceOption) continue;
3528
3625
  const current = ctx.ui.getEditorText();
3529
3626
  const characters = [...current].length;
3530
- const confirmed = await showBtwMenu(
3531
- ctx,
3532
- `Replace the current ${characters}-character editor draft?`,
3533
- ["Back Keep current editor text", "\u26A0 Replace current draft Cannot be undone"]
3534
- );
3627
+ const confirmed = await showBtwMenu(ctx, `Replace the current ${characters}-character editor draft?`, [
3628
+ "Back Keep current editor text",
3629
+ "\u26A0 Replace current draft Cannot be undone"
3630
+ ]);
3535
3631
  if (confirmed.kind === "close") return "closed";
3536
3632
  if (confirmed.kind === "back" || confirmed.value === "Back Keep current editor text") continue;
3537
3633
  if (confirmed.value !== "\u26A0 Replace current draft Cannot be undone") continue;
@@ -3543,63 +3639,72 @@ ${draft}`);
3543
3639
  continue;
3544
3640
  }
3545
3641
  ctx.ui.setEditorText(draft);
3546
- ctx.ui.notify(
3547
- `Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`,
3548
- "info"
3549
- );
3642
+ ctx.ui.notify(`Replaced the main-editor draft with ${describeContent()}. Review and submit when ready.`, "info");
3550
3643
  return "loaded";
3551
3644
  }
3552
3645
  }
3553
3646
  function truncatePreview(text) {
3554
3647
  return text.length <= 72 ? text : `${text.slice(0, 69)}\u2026`;
3555
3648
  }
3649
+ async function prepareCurrentTranscriptMarkdown(thread, pendingQuestion, ctx) {
3650
+ while (true) {
3651
+ const turnCount = thread.turns.length;
3652
+ const createMarkdownTransformers = ctx.signal ? await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion, ctx.signal) : await prepareBtwTranscriptMarkdown(thread.turns, pendingQuestion);
3653
+ if (!createMarkdownTransformers || ctx.signal?.aborted) return void 0;
3654
+ if (thread.turns.length === turnCount) return createMarkdownTransformers;
3655
+ }
3656
+ }
3556
3657
  async function askThreadQuestion(thread, question, selected, thinkingLevel, ctx, steering) {
3557
- return ctx.ui.custom(
3558
- (tui, theme, keybindings, done) => {
3559
- let settled = false;
3560
- const view = new BtwAnsweringView(
3561
- tui,
3562
- theme,
3563
- thread.turns,
3564
- question,
3565
- () => {
3566
- if (settled) return;
3567
- settled = true;
3568
- done({ kind: "aborted" });
3569
- },
3570
- thinkingLevel,
3571
- {
3572
- steering: {
3573
- questions: steering.questions,
3574
- onSubmit: steering.submit,
3575
- thinking: { ...steering.thinking, keybindings }
3576
- }
3577
- }
3578
- );
3579
- completeSideThreadTurn({
3580
- thread,
3581
- question,
3582
- model: selected.model,
3583
- thinkingLevel,
3584
- auth: selected.auth,
3585
- signal: view.signal,
3586
- completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
3587
- sessionId: readBtwSessionId(ctx)
3588
- }).then((result) => {
3658
+ const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, question, ctx);
3659
+ if (!createMarkdownTransformers) return { kind: "aborted" };
3660
+ return ctx.ui.custom((tui, theme, keybindings, done) => {
3661
+ let settled = false;
3662
+ const view = new BtwAnsweringView(
3663
+ tui,
3664
+ theme,
3665
+ thread.turns,
3666
+ question,
3667
+ () => {
3589
3668
  if (settled) return;
3590
3669
  settled = true;
3591
- view.finish();
3592
- done(result);
3593
- });
3594
- return view;
3595
- }
3596
- );
3670
+ done({ kind: "aborted" });
3671
+ },
3672
+ thinkingLevel,
3673
+ {
3674
+ markdownTransformers: createMarkdownTransformers(theme),
3675
+ steering: {
3676
+ questions: steering.questions,
3677
+ onSubmit: steering.submit,
3678
+ thinking: { ...steering.thinking, keybindings }
3679
+ }
3680
+ }
3681
+ );
3682
+ completeSideThreadTurn({
3683
+ thread,
3684
+ question,
3685
+ model: selected.model,
3686
+ thinkingLevel,
3687
+ auth: selected.auth,
3688
+ signal: view.signal,
3689
+ completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
3690
+ sessionId: readBtwSessionId(ctx)
3691
+ }).then((result) => {
3692
+ if (settled) return;
3693
+ settled = true;
3694
+ view.finish();
3695
+ done(result);
3696
+ });
3697
+ return view;
3698
+ });
3597
3699
  }
3598
3700
  async function showThreadComposer(thread, startAtBottom, ctx, initialQuestion, thinking) {
3701
+ const createMarkdownTransformers = await prepareCurrentTranscriptMarkdown(thread, initialQuestion, ctx);
3702
+ if (!createMarkdownTransformers) return { kind: "close" };
3599
3703
  return ctx.ui.custom(
3600
3704
  (tui, theme, keybindings, done) => new BtwTranscriptPager(tui, theme, thread.turns, done, {
3601
3705
  startAtBottom,
3602
3706
  initialQuestion,
3707
+ markdownTransformers: createMarkdownTransformers(theme),
3603
3708
  thinking: { ...thinking, keybindings }
3604
3709
  })
3605
3710
  );