@narumitw/pi-btw 0.42.1 → 0.46.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/src/btw.ts CHANGED
@@ -1,11 +1,13 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import type { Api, Model } from "@earendil-works/pi-ai";
1
+ import {
2
+ type Api,
3
+ clampThinkingLevel,
4
+ getSupportedThinkingLevels,
5
+ type Model,
6
+ } from "@earendil-works/pi-ai";
4
7
  import {
5
8
  BorderedLoader,
6
9
  type ExtensionAPI,
7
10
  type ExtensionCommandContext,
8
- getAgentDir,
9
11
  type KeybindingsManager,
10
12
  type Theme,
11
13
  } from "@earendil-works/pi-coding-agent";
@@ -22,6 +24,18 @@ import {
22
24
  getAnsweredTurns,
23
25
  summarizeBringToMain,
24
26
  } from "./bring-to-main.js";
27
+ import {
28
+ type BtwCommandMenuResult,
29
+ runBtwMenuPreservingEditor,
30
+ showBtwCommandMenu,
31
+ } from "./menu.js";
32
+ import {
33
+ type BtwSettings,
34
+ effectiveRememberThinkingLevelChanges,
35
+ parseBtwModelReference,
36
+ readBtwSettings,
37
+ updateBtwSettings,
38
+ } from "./settings.js";
25
39
  import {
26
40
  BTW_THINKING_LEVELS,
27
41
  type BtwThinkingLevel,
@@ -30,12 +44,22 @@ import {
30
44
  type SideQuestionAuth,
31
45
  type SideThread,
32
46
  } from "./side-thread.js";
47
+ import { sanitizeSingleLine } from "./text.js";
33
48
  import {
34
49
  BtwAnsweringView,
50
+ type BtwThinkingControl,
35
51
  BtwTranscriptPager,
36
52
  type TranscriptPagerAction,
37
53
  } from "./transcript-pager.js";
38
54
 
55
+ export {
56
+ BTW_SETTINGS_FILE,
57
+ type BtwSettings,
58
+ type BtwSettingsLoadResult,
59
+ normalizeBtwSettings,
60
+ parseBtwModelReference,
61
+ readBtwSettings,
62
+ } from "./settings.js";
39
63
  export {
40
64
  BTW_THINKING_LEVELS,
41
65
  type BtwThinkingLevel,
@@ -43,19 +67,9 @@ export {
43
67
  completeSideQuestion,
44
68
  loadCompleteSimple,
45
69
  } from "./side-thread.js";
70
+ export { sanitizeSingleLine } from "./text.js";
46
71
 
47
72
  const MAX_CONTEXT_CHARS = 40_000;
48
- export const BTW_SETTINGS_FILE = "pi-btw.json";
49
-
50
- export interface BtwSettings {
51
- model?: string;
52
- thinkingLevel?: BtwThinkingLevel;
53
- }
54
-
55
- export type BtwSettingsLoadResult =
56
- | { kind: "missing" }
57
- | { kind: "invalid"; reason: string }
58
- | { kind: "loaded"; settings: BtwSettings };
59
73
 
60
74
  interface LoadBtwThinkingLevelOptions {
61
75
  settingsPath?: string;
@@ -84,50 +98,25 @@ export interface ResolvedBtwModel {
84
98
  auth: SideQuestionAuth;
85
99
  }
86
100
 
87
- export function normalizeBtwSettings(value: unknown): BtwSettings | undefined {
88
- if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
89
-
90
- const settings: BtwSettings = {};
91
- if (Object.hasOwn(value, "model")) {
92
- const model = Reflect.get(value, "model");
93
- if (typeof model !== "string" || !parseBtwModelReference(model)) return undefined;
94
- settings.model = model;
95
- }
96
- if (Object.hasOwn(value, "thinkingLevel")) {
97
- const thinkingLevel = Reflect.get(value, "thinkingLevel");
98
- if (!isBtwThinkingLevel(thinkingLevel)) return undefined;
99
- settings.thinkingLevel = thinkingLevel;
100
- }
101
- return settings;
102
- }
103
-
104
- export function parseBtwModelReference(
105
- reference: string,
106
- ): { provider: string; modelId: string } | undefined {
107
- if (/\s/.test(reference)) return undefined;
108
- const separator = reference.indexOf("/");
109
- if (separator <= 0 || separator === reference.length - 1) return undefined;
110
- return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
111
- }
112
-
113
101
  export async function resolveBtwModel({
114
102
  settings,
115
103
  currentModel,
116
104
  modelRegistry,
117
105
  warn,
118
106
  }: ResolveBtwModelOptions): Promise<ResolvedBtwModel | undefined> {
107
+ const reportWarning = (message: string) => warn?.(sanitizeSingleLine(message));
119
108
  if (settings.model) {
120
109
  const fallback = currentModel
121
110
  ? `${currentModel.provider}/${currentModel.id}`
122
111
  : "the current model";
123
112
  const reference = parseBtwModelReference(settings.model);
124
113
  if (!reference) {
125
- warn?.(`pi-btw model ${settings.model} is invalid; falling back to ${fallback}.`);
126
- return resolveBtwModel({ settings: {}, currentModel, modelRegistry, warn });
114
+ reportWarning(`pi-btw model ${settings.model} is invalid; falling back to ${fallback}.`);
115
+ return resolveBtwModel({ settings: {}, currentModel, modelRegistry, warn: reportWarning });
127
116
  }
128
117
  const configuredModel = modelRegistry.find(reference.provider, reference.modelId);
129
118
  if (!configuredModel) {
130
- warn?.(`pi-btw model ${settings.model} was not found; falling back to ${fallback}.`);
119
+ reportWarning(`pi-btw model ${settings.model} was not found; falling back to ${fallback}.`);
131
120
  } else {
132
121
  const sameAsCurrent =
133
122
  configuredModel === currentModel ||
@@ -140,9 +129,11 @@ export async function resolveBtwModel({
140
129
  const auth = await modelRegistry.getApiKeyAndHeaders(configuredModel);
141
130
  if (auth.ok && hasRequestAuth(auth)) return { model: configuredModel, auth };
142
131
  const reason = auth.ok ? "has no request credentials" : auth.error;
143
- warn?.(`pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`);
132
+ reportWarning(
133
+ `pi-btw model ${settings.model} is unavailable (${reason}); ${fallbackAction}.`,
134
+ );
144
135
  } catch (error: unknown) {
145
- warn?.(
136
+ reportWarning(
146
137
  `pi-btw model ${settings.model} credentials failed (${formatError(error)}); ${fallbackAction}.`,
147
138
  );
148
139
  }
@@ -168,26 +159,6 @@ function hasRequestAuth(auth: SideQuestionAuth): boolean {
168
159
  );
169
160
  }
170
161
 
171
- export async function readBtwSettings(
172
- settingsPath = join(getAgentDir(), BTW_SETTINGS_FILE),
173
- ): Promise<BtwSettingsLoadResult> {
174
- let contents: string;
175
- try {
176
- contents = await readFile(settingsPath, "utf8");
177
- } catch (error: unknown) {
178
- if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
179
- return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
180
- }
181
-
182
- try {
183
- const settings = normalizeBtwSettings(JSON.parse(contents) as unknown);
184
- if (settings) return { kind: "loaded", settings };
185
- return { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
186
- } catch (error: unknown) {
187
- return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
188
- }
189
- }
190
-
191
162
  export async function loadBtwThinkingLevel(
192
163
  currentThinkingLevel: BtwThinkingLevel,
193
164
  options: LoadBtwThinkingLevelOptions = {},
@@ -199,24 +170,44 @@ export async function loadBtwThinkingLevel(
199
170
  }
200
171
 
201
172
  options.warn?.(
202
- `pi-btw settings ignored: ${settings.reason}; expected optional model "provider/model-id" and thinkingLevel "${BTW_THINKING_LEVELS.join('" | "')}". Using current Pi thinking level.`,
173
+ sanitizeSingleLine(
174
+ `pi-btw settings ignored: ${settings.reason}; expected optional model "provider/model-id", thinkingLevel "${BTW_THINKING_LEVELS.join('" | "')}", and boolean rememberThinkingLevelChanges. Using current Pi thinking level.`,
175
+ ),
203
176
  );
204
177
  return currentThinkingLevel;
205
178
  }
206
179
 
207
- function isBtwThinkingLevel(value: unknown): value is BtwThinkingLevel {
208
- return BTW_THINKING_LEVELS.includes(value as BtwThinkingLevel);
180
+ function formatError(error: unknown): string {
181
+ return error instanceof Error ? error.message : String(error);
209
182
  }
210
183
 
211
- function isNodeError(error: unknown): error is NodeJS.ErrnoException {
212
- return error instanceof Error && "code" in error;
184
+ function notifySafely(
185
+ ctx: ExtensionCommandContext,
186
+ message: string,
187
+ level: Parameters<ExtensionCommandContext["ui"]["notify"]>[1],
188
+ ): void {
189
+ try {
190
+ ctx.ui.notify(sanitizeSingleLine(message), level);
191
+ } catch {
192
+ // Async command continuations may finish after their ExtensionContext is replaced.
193
+ }
213
194
  }
214
195
 
215
- function formatError(error: unknown): string {
216
- return error instanceof Error ? error.message : String(error);
196
+ export interface BtwExtensionDependencies {
197
+ showCommandMenu?: (
198
+ pi: ExtensionAPI,
199
+ ctx: ExtensionCommandContext,
200
+ ) => Promise<BtwCommandMenuResult>;
201
+ loadSettings?: typeof loadSettingsForCommand;
202
+ resolveModel?: typeof resolveBtwModelWithLoader;
203
+ runThread?: typeof runBtwThread;
217
204
  }
218
205
 
219
- export default function btw(pi: ExtensionAPI) {
206
+ export default function btw(pi: ExtensionAPI, dependencies: BtwExtensionDependencies = {}) {
207
+ const showCommandMenu = dependencies.showCommandMenu ?? showCommandMenuForBtw;
208
+ const loadSettings = dependencies.loadSettings ?? loadSettingsForCommand;
209
+ const resolveModel = dependencies.resolveModel ?? resolveBtwModelWithLoader;
210
+ const runThread = dependencies.runThread ?? runBtwThread;
220
211
  pi.registerCommand("btw", {
221
212
  description: "Ask a quick side question without adding it to the main conversation",
222
213
  handler: async (args, ctx) => {
@@ -225,33 +216,57 @@ export default function btw(pi: ExtensionAPI) {
225
216
  ctx.ui.notify("/btw requires interactive TUI mode", "error");
226
217
  return;
227
218
  }
219
+ if (!question && (await showCommandMenu(pi, ctx)) !== "start") return;
228
220
 
229
- const settings = await loadSettingsForCommand(ctx);
230
- const resolution = await resolveBtwModelWithLoader(settings, ctx);
221
+ const settings = await loadSettings(ctx);
222
+ const resolution = await resolveModel(settings, ctx);
231
223
  if (resolution.kind === "cancelled") {
232
- ctx.ui.notify("Cancelled", "info");
224
+ notifySafely(ctx, "Cancelled", "info");
233
225
  return;
234
226
  }
235
227
  if (resolution.kind === "unavailable") {
236
- ctx.ui.notify("No available model for /btw", "error");
228
+ notifySafely(ctx, "No available model for /btw", "error");
237
229
  return;
238
230
  }
239
231
 
240
- await runBtwThread({
232
+ await runThread({
241
233
  initialQuestion: question || undefined,
242
234
  selected: resolution.selected,
243
235
  thinkingLevel: settings.thinkingLevel ?? pi.getThinkingLevel(),
236
+ rememberThinkingLevelChanges: effectiveRememberThinkingLevelChanges(settings),
244
237
  ctx,
245
238
  });
246
239
  },
247
240
  });
248
241
  }
249
242
 
243
+ async function showCommandMenuForBtw(
244
+ pi: ExtensionAPI,
245
+ ctx: ExtensionCommandContext,
246
+ ): Promise<BtwCommandMenuResult> {
247
+ const currentModel = ctx.model;
248
+ const availableModels = ctx.modelRegistry.getAll();
249
+ const currentThinkingLevel = pi.getThinkingLevel();
250
+ const loaded = await readBtwSettings();
251
+ const settings = loaded.kind === "loaded" ? loaded.settings : {};
252
+ const configured = settings.model ? parseBtwModelReference(settings.model) : undefined;
253
+ const configuredModel = configured
254
+ ? availableModels.find(
255
+ (model) => model.provider === configured.provider && model.id === configured.modelId,
256
+ )
257
+ : undefined;
258
+ const model = configuredModel ?? currentModel;
259
+ return showBtwCommandMenu(ctx, {
260
+ currentThinkingLevel,
261
+ availableThinkingLevels: model ? getSupportedThinkingLevels(model) : BTW_THINKING_LEVELS,
262
+ });
263
+ }
264
+
250
265
  async function loadSettingsForCommand(ctx: ExtensionCommandContext): Promise<BtwSettings> {
251
266
  const settingsResult = await readBtwSettings();
252
267
  if (settingsResult.kind === "loaded") return settingsResult.settings;
253
268
  if (settingsResult.kind === "invalid") {
254
- ctx.ui.notify(`pi-btw settings ignored: ${settingsResult.reason}`, "warning");
269
+ notifySafely(ctx, `pi-btw settings ignored: ${settingsResult.reason}`, "warning");
255
270
  }
256
271
  return {};
257
272
  }
@@ -279,7 +294,7 @@ async function resolveBtwModelWithLoader(
279
294
  currentModel: ctx.model,
280
295
  modelRegistry: ctx.modelRegistry,
281
296
  warn: (message) => {
282
- if (!settled) ctx.ui.notify(message, "warning");
297
+ if (!settled) notifySafely(ctx, message, "warning");
283
298
  },
284
299
  })
285
300
  .then((selected) => {
@@ -302,10 +317,13 @@ interface RunBtwThreadDependencies {
302
317
  interact?: typeof showThreadComposer;
303
318
  chooseBringToMain?: typeof chooseBringToMain;
304
319
  deliverBringToMain?: typeof loadBringToMainDraft;
320
+ persistThinkingLevel?: (level: BtwThinkingLevel) => Promise<unknown>;
305
321
  }
306
322
 
307
323
  export type BtwThreadResult = { kind: "closed" };
308
324
 
325
+ type BtwThreadThinkingControl = Omit<BtwThinkingControl, "keybindings">;
326
+
309
327
  type BtwBringToMainChoice =
310
328
  | BtwThreadResult
311
329
  | {
@@ -322,6 +340,8 @@ interface RunBtwThreadOptions {
322
340
  initialQuestion?: string;
323
341
  selected: ResolvedBtwModel;
324
342
  thinkingLevel: BtwThinkingLevel;
343
+ rememberThinkingLevelChanges?: boolean;
344
+ settingsPath?: string;
325
345
  ctx: ExtensionCommandContext;
326
346
  dependencies?: RunBtwThreadDependencies;
327
347
  }
@@ -330,6 +350,8 @@ export async function runBtwThread({
330
350
  initialQuestion,
331
351
  selected,
332
352
  thinkingLevel,
353
+ rememberThinkingLevelChanges = false,
354
+ settingsPath,
333
355
  ctx,
334
356
  dependencies = {},
335
357
  }: RunBtwThreadOptions): Promise<BtwThreadResult> {
@@ -337,44 +359,82 @@ export async function runBtwThread({
337
359
  const interact = dependencies.interact ?? showThreadComposer;
338
360
  const chooseBringToMainAction = dependencies.chooseBringToMain ?? chooseBringToMain;
339
361
  const deliverBringToMainDraft = dependencies.deliverBringToMain ?? loadBringToMainDraft;
362
+ const persistThinkingLevel =
363
+ dependencies.persistThinkingLevel ??
364
+ ((level: BtwThinkingLevel) => updateBtwSettings({ thinkingLevel: level }, { settingsPath }));
340
365
  const thread = createSideThread(buildConversationContext(ctx.sessionManager.getBranch()));
366
+ const thinkingLevels = getSupportedThinkingLevels(selected.model);
367
+ const pendingWrites = new Set<Promise<void>>();
368
+ let activeThinkingLevel = clampThinkingLevel(selected.model, thinkingLevel);
341
369
  let pendingQuestion = initialQuestion;
342
370
  let composerDraft: string | undefined;
343
371
 
344
- while (true) {
345
- if (!pendingQuestion) {
346
- const action = await interact(thread, thread.turns.length > 0, ctx, composerDraft);
347
- if (action.kind === "close") return { kind: "closed" };
348
- if (action.kind === "bringToMain") {
349
- const choice = await chooseBringToMainAction(thread, ctx);
350
- if (choice.kind === "closed") return choice;
351
- if (choice.kind === "back") {
372
+ try {
373
+ while (true) {
374
+ if (!pendingQuestion) {
375
+ const thinking: BtwThreadThinkingControl = {
376
+ level: activeThinkingLevel,
377
+ levels: thinkingLevels,
378
+ onChange: (level) => {
379
+ if (!thinkingLevels.includes(level)) return;
380
+ activeThinkingLevel = level;
381
+ if (!rememberThinkingLevelChanges) return;
382
+ let write!: Promise<void>;
383
+ write = Promise.resolve()
384
+ .then(() => persistThinkingLevel(level))
385
+ .then(() => undefined)
386
+ .catch((error: unknown) => {
387
+ notifySafely(
388
+ ctx,
389
+ `Thinking level changed to ${level}, but could not be remembered in pi-btw.json: ${formatError(error)}`,
390
+ "warning",
391
+ );
392
+ })
393
+ .finally(() => pendingWrites.delete(write));
394
+ pendingWrites.add(write);
395
+ },
396
+ };
397
+ const action = await interact(
398
+ thread,
399
+ thread.turns.length > 0,
400
+ ctx,
401
+ composerDraft,
402
+ thinking,
403
+ );
404
+ if (action.kind === "close") return { kind: "closed" };
405
+ if (action.kind === "bringToMain") {
406
+ const choice = await chooseBringToMainAction(thread, ctx);
407
+ if (choice.kind === "closed") return choice;
408
+ if (choice.kind === "back") {
409
+ composerDraft = action.questionDraft;
410
+ continue;
411
+ }
412
+ const delivery = await deliverBringToMainDraft(choice.draft, ctx, choice.summary);
413
+ if (delivery === "loaded" || delivery === "closed") return { kind: "closed" };
352
414
  composerDraft = action.questionDraft;
353
415
  continue;
354
416
  }
355
- const delivery = await deliverBringToMainDraft(choice.draft, ctx, choice.summary);
356
- if (delivery === "loaded" || delivery === "closed") return { kind: "closed" };
357
- composerDraft = action.questionDraft;
358
- continue;
417
+ composerDraft = undefined;
418
+ pendingQuestion = action.question;
359
419
  }
360
- composerDraft = undefined;
361
- pendingQuestion = action.question;
362
- }
363
420
 
364
- const result = await ask(thread, pendingQuestion, selected, thinkingLevel, ctx);
365
- if (result.kind === "aborted") {
366
- ctx.ui.notify("Cancelled", "info");
367
- return { kind: "closed" };
368
- }
369
- if (result.kind === "error") {
370
- thread.turns.push({
371
- kind: "error",
372
- question: pendingQuestion,
373
- answer: result.message,
374
- });
375
- }
421
+ const result = await ask(thread, pendingQuestion, selected, activeThinkingLevel, ctx);
422
+ if (result.kind === "aborted") {
423
+ notifySafely(ctx, "Cancelled", "info");
424
+ return { kind: "closed" };
425
+ }
426
+ if (result.kind === "error") {
427
+ thread.turns.push({
428
+ kind: "error",
429
+ question: pendingQuestion,
430
+ answer: result.message,
431
+ });
432
+ }
376
433
 
377
- pendingQuestion = undefined;
434
+ pendingQuestion = undefined;
435
+ }
436
+ } finally {
437
+ await Promise.allSettled([...pendingWrites]);
378
438
  }
379
439
  }
380
440
 
@@ -393,13 +453,21 @@ async function showBtwCustomPreservingEditor<T>(
393
453
  let completed = false;
394
454
  const result = await ctx.ui.custom<T>((tui, theme, keybindings, done) =>
395
455
  factory(tui, theme, keybindings, (value) => {
396
- liveEditorText = ctx.ui.getEditorText();
456
+ try {
457
+ liveEditorText = ctx.ui.getEditorText();
458
+ } catch {
459
+ // Keep completion finite if session replacement invalidates the editor context.
460
+ }
397
461
  completed = true;
398
462
  done(value);
399
463
  }),
400
464
  );
401
- if (completed && ctx.ui.getEditorText() !== liveEditorText) {
402
- ctx.ui.setEditorText(liveEditorText);
465
+ if (completed) {
466
+ try {
467
+ if (ctx.ui.getEditorText() !== liveEditorText) ctx.ui.setEditorText(liveEditorText);
468
+ } catch {
469
+ // A replaced context owns a different editor and must not receive stale restoration.
470
+ }
403
471
  }
404
472
  return result;
405
473
  }
@@ -602,35 +670,6 @@ async function showBtwMenu(
602
670
  : terminalBtwMenuAction(result);
603
671
  }
604
672
 
605
- async function runBtwMenuPreservingEditor(
606
- ctx: ExtensionCommandContext,
607
- run: (menuContext: MenuContext) => Promise<RunMenuResult>,
608
- ): Promise<RunMenuResult> {
609
- let liveEditorText = ctx.ui.getEditorText();
610
- let completed = false;
611
- const ui = new Proxy(ctx.ui, {
612
- get(target, property) {
613
- if (property === "custom") {
614
- return <Value>(factory: BtwCustomFactory<Value>) =>
615
- target.custom<Value>((tui, theme, keybindings, done) =>
616
- factory(tui, theme, keybindings, (value) => {
617
- liveEditorText = target.getEditorText();
618
- completed = true;
619
- done(value);
620
- }),
621
- );
622
- }
623
- const value = Reflect.get(target, property, target) as unknown;
624
- return typeof value === "function" ? value.bind(target) : value;
625
- },
626
- });
627
- const result = await run({ mode: ctx.mode, hasUI: ctx.hasUI, ui });
628
- if (result.kind !== "stale" && completed && ctx.ui.getEditorText() !== liveEditorText) {
629
- ctx.ui.setEditorText(liveEditorText);
630
- }
631
- return result;
632
- }
633
-
634
673
  function terminalBtwMenuAction(result: RunMenuResult): { kind: "back" } | { kind: "close" } {
635
674
  if (result.kind === "closed") return { kind: result.reason };
636
675
  if (result.kind === "error") throw result.error;
@@ -715,11 +754,18 @@ async function askThreadQuestion(
715
754
  return ctx.ui.custom<Awaited<ReturnType<typeof completeSideThreadTurn>>>(
716
755
  (tui, theme, _keybindings, done) => {
717
756
  let settled = false;
718
- const view = new BtwAnsweringView(tui, theme, thread.turns, question, () => {
719
- if (settled) return;
720
- settled = true;
721
- done({ kind: "aborted" });
722
- });
757
+ const view = new BtwAnsweringView(
758
+ tui,
759
+ theme,
760
+ thread.turns,
761
+ question,
762
+ () => {
763
+ if (settled) return;
764
+ settled = true;
765
+ done({ kind: "aborted" });
766
+ },
767
+ thinkingLevel,
768
+ );
723
769
  completeSideThreadTurn({
724
770
  thread,
725
771
  question,
@@ -742,28 +788,19 @@ async function showThreadComposer(
742
788
  thread: SideThread,
743
789
  startAtBottom: boolean,
744
790
  ctx: ExtensionCommandContext,
745
- initialQuestion?: string,
791
+ initialQuestion: string | undefined,
792
+ thinking: BtwThreadThinkingControl,
746
793
  ): Promise<TranscriptPagerAction> {
747
794
  return ctx.ui.custom<TranscriptPagerAction>(
748
- (tui, theme, _keybindings, done) =>
795
+ (tui, theme, keybindings, done) =>
749
796
  new BtwTranscriptPager(tui, theme, thread.turns, done, {
750
797
  startAtBottom,
751
798
  initialQuestion,
799
+ thinking: { ...thinking, keybindings },
752
800
  }),
753
801
  );
754
802
  }
755
803
 
756
- export function sanitizeSingleLine(text: string) {
757
- return [...text.replace(/[\r\n\t]/g, " ")]
758
- .filter((character) => {
759
- const code = character.charCodeAt(0);
760
- return code > 31 && (code < 127 || code > 159);
761
- })
762
- .join("")
763
- .replace(/ +/g, " ")
764
- .trim();
765
- }
766
-
767
804
  type MessageContentBlock = {
768
805
  type?: string;
769
806
  text?: string;