@oh-my-pi/pi-tui 8.1.0 → 8.2.1

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/package.json CHANGED
@@ -1,12 +1,27 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "8.1.0",
3
+ "version": "8.2.1",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
- "main": "src/index.ts",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ },
13
+ "./components/*": {
14
+ "types": "./src/components/*.ts",
15
+ "import": "./src/components/*.ts"
16
+ },
17
+ "./*": {
18
+ "types": "./src/*.ts",
19
+ "import": "./src/*.ts"
20
+ }
21
+ },
7
22
  "scripts": {
8
- "test": "bun test test/*.test.ts",
9
- "prepublishOnly": "cp tsconfig.publish.json tsconfig.json"
23
+ "check": "tsgo -p tsconfig.json",
24
+ "test": "bun test test/*.test.ts"
10
25
  },
11
26
  "files": [
12
27
  "src/**/*",
@@ -31,16 +46,15 @@
31
46
  "engines": {
32
47
  "bun": ">=1.0.0"
33
48
  },
34
- "types": "./src/index.ts",
35
49
  "dependencies": {
36
- "@types/mime-types": "^2.1.4",
50
+ "@types/mime-types": "^3.0.1",
37
51
  "chalk": "^5.5.0",
38
52
  "get-east-asian-width": "^1.3.0",
39
- "marked": "^15.0.12",
53
+ "marked": "^17.0.1",
40
54
  "mime-types": "^3.0.1"
41
55
  },
42
56
  "devDependencies": {
43
- "@xterm/headless": "^5.5.0",
44
- "@xterm/xterm": "^5.5.0"
57
+ "@xterm/headless": "^6.0.0",
58
+ "@xterm/xterm": "^6.0.0"
45
59
  }
46
60
  }
@@ -1,36 +1,34 @@
1
- import { readdirSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { basename, dirname, join } from "node:path";
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
4
 
5
- // Use fd to walk directory tree (fast, respects .gitignore)
6
- function walkDirectoryWithFd(
5
+ async function walkDirectoryWithFd(
7
6
  baseDir: string,
8
7
  fdPath: string,
9
8
  query: string,
10
9
  maxResults: number,
11
- ): Array<{ path: string; isDirectory: boolean }> {
10
+ ): Promise<Array<{ path: string; isDirectory: boolean }>> {
12
11
  const args = ["--base-directory", baseDir, "--max-results", String(maxResults), "--type", "f", "--type", "d"];
13
12
 
14
- // Add query as pattern if provided
15
13
  if (query) {
16
14
  args.push(query);
17
15
  }
18
16
 
19
- const result = Bun.spawnSync([fdPath, ...args], {
17
+ const proc = Bun.spawn([fdPath, ...args], {
20
18
  stdout: "pipe",
21
19
  stderr: "pipe",
22
20
  });
23
21
 
24
- if (!result.success || !result.stdout) {
22
+ const exitCode = await proc.exited;
23
+ if (exitCode !== 0 || !proc.stdout) {
25
24
  return [];
26
25
  }
27
26
 
28
- const stdout = new TextDecoder().decode(result.stdout);
27
+ const stdout = await new Response(proc.stdout).text();
29
28
  const lines = stdout.trim().split("\n").filter(Boolean);
30
29
  const results: Array<{ path: string; isDirectory: boolean }> = [];
31
30
 
32
31
  for (const line of lines) {
33
- // fd outputs directories with trailing /
34
32
  const isDirectory = line.endsWith("/");
35
33
  results.push({
36
34
  path: line,
@@ -62,10 +60,10 @@ export interface AutocompleteProvider {
62
60
  lines: string[],
63
61
  cursorLine: number,
64
62
  cursorCol: number,
65
- ): {
63
+ ): Promise<{
66
64
  items: AutocompleteItem[];
67
65
  prefix: string; // What we're matching against (e.g., "/" or "src/")
68
- } | null;
66
+ } | null>;
69
67
 
70
68
  // Apply the selected item
71
69
  // Returns the new text and cursor position
@@ -87,6 +85,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
87
85
  private commands: (SlashCommand | AutocompleteItem)[];
88
86
  private basePath: string;
89
87
  private fdPath: string | null;
88
+ private dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
89
+ private readonly DIR_CACHE_TTL = 2000; // 2 seconds
90
90
 
91
91
  constructor(
92
92
  commands: (SlashCommand | AutocompleteItem)[] = [],
@@ -98,11 +98,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
98
98
  this.fdPath = fdPath ?? Bun.which("fd") ?? Bun.which("fdfind");
99
99
  }
100
100
 
101
- getSuggestions(
101
+ async getSuggestions(
102
102
  lines: string[],
103
103
  cursorLine: number,
104
104
  cursorCol: number,
105
- ): { items: AutocompleteItem[]; prefix: string } | null {
105
+ ): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
106
106
  const currentLine = lines[cursorLine] || "";
107
107
  const textBeforeCursor = currentLine.slice(0, cursorCol);
108
108
 
@@ -111,9 +111,10 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
111
111
  if (atMatch) {
112
112
  const prefix = atMatch[1] ?? "@"; // The @... part
113
113
  const query = prefix.slice(1); // Remove the @
114
- const suggestions = query.length > 0 ? this.getFuzzyFileSuggestions(query) : this.getFileSuggestions("@");
114
+ const suggestions =
115
+ query.length > 0 ? await this.getFuzzyFileSuggestions(query) : await this.getFileSuggestions("@");
115
116
  if (suggestions.length === 0 && query.length > 0) {
116
- const fallback = this.getFileSuggestions(prefix);
117
+ const fallback = await this.getFileSuggestions(prefix);
117
118
  if (fallback.length === 0) return null;
118
119
  return { items: fallback, prefix };
119
120
  }
@@ -133,11 +134,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
133
134
  // No space yet - complete command names
134
135
  const prefix = textBeforeCursor.slice(1); // Remove the "/"
135
136
  const filtered = this.commands
136
- .filter((cmd) => {
137
+ .filter(cmd => {
137
138
  const name = "name" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem
138
139
  return name?.toLowerCase().startsWith(prefix.toLowerCase());
139
140
  })
140
- .map((cmd) => ({
141
+ .map(cmd => ({
141
142
  value: "name" in cmd ? cmd.name : cmd.value,
142
143
  label: "name" in cmd ? cmd.name : cmd.label,
143
144
  ...(cmd.description && { description: cmd.description }),
@@ -154,7 +155,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
154
155
  const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
155
156
  const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
156
157
 
157
- const command = this.commands.find((cmd) => {
158
+ const command = this.commands.find(cmd => {
158
159
  const name = "name" in cmd ? cmd.name : cmd.value;
159
160
  return name === commandName;
160
161
  });
@@ -178,7 +179,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
178
179
  const pathMatch = this.extractPathPrefix(textBeforeCursor, false);
179
180
 
180
181
  if (pathMatch !== null) {
181
- const suggestions = this.getFileSuggestions(pathMatch);
182
+ const suggestions = await this.getFileSuggestions(pathMatch);
182
183
  if (suggestions.length === 0) return null;
183
184
 
184
185
  // Check if we have an exact match that is a directory
@@ -311,19 +312,51 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
311
312
  }
312
313
 
313
314
  // Expand home directory (~/) to actual home path
314
- private expandHomePath(path: string): string {
315
- if (path.startsWith("~/")) {
316
- const expandedPath = join(homedir(), path.slice(2));
315
+ private expandHomePath(filePath: string): string {
316
+ if (filePath.startsWith("~/")) {
317
+ const expandedPath = path.join(os.homedir(), filePath.slice(2));
317
318
  // Preserve trailing slash if original path had one
318
- return path.endsWith("/") && !expandedPath.endsWith("/") ? `${expandedPath}/` : expandedPath;
319
- } else if (path === "~") {
320
- return homedir();
319
+ return filePath.endsWith("/") && !expandedPath.endsWith("/") ? `${expandedPath}/` : expandedPath;
320
+ } else if (filePath === "~") {
321
+ return os.homedir();
322
+ }
323
+ return filePath;
324
+ }
325
+
326
+ private async getCachedDirEntries(searchDir: string): Promise<fs.Dirent[]> {
327
+ const now = Date.now();
328
+ const cached = this.dirCache.get(searchDir);
329
+
330
+ if (cached && now - cached.timestamp < this.DIR_CACHE_TTL) {
331
+ return cached.entries;
332
+ }
333
+
334
+ const entries = await fs.promises.readdir(searchDir, { withFileTypes: true });
335
+ this.dirCache.set(searchDir, { entries, timestamp: now });
336
+
337
+ if (this.dirCache.size > 100) {
338
+ const sortedKeys = [...this.dirCache.entries()]
339
+ .sort((a, b) => a[1].timestamp - b[1].timestamp)
340
+ .slice(0, 50)
341
+ .map(([key]) => key);
342
+ for (const key of sortedKeys) {
343
+ this.dirCache.delete(key);
344
+ }
345
+ }
346
+
347
+ return entries;
348
+ }
349
+
350
+ public invalidateDirCache(dir?: string): void {
351
+ if (dir) {
352
+ this.dirCache.delete(dir);
353
+ } else {
354
+ this.dirCache.clear();
321
355
  }
322
- return path;
323
356
  }
324
357
 
325
358
  // Get file/directory suggestions for a given path prefix
326
- private getFileSuggestions(prefix: string): AutocompleteItem[] {
359
+ private async getFileSuggestions(prefix: string): Promise<AutocompleteItem[]> {
327
360
  try {
328
361
  let searchDir: string;
329
362
  let searchPrefix: string;
@@ -354,7 +387,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
354
387
  if (prefix.startsWith("~") || expandedPrefix === "/") {
355
388
  searchDir = expandedPrefix;
356
389
  } else {
357
- searchDir = join(this.basePath, expandedPrefix);
390
+ searchDir = path.join(this.basePath, expandedPrefix);
358
391
  }
359
392
  searchPrefix = "";
360
393
  } else if (expandedPrefix.endsWith("/")) {
@@ -362,22 +395,22 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
362
395
  if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) {
363
396
  searchDir = expandedPrefix;
364
397
  } else {
365
- searchDir = join(this.basePath, expandedPrefix);
398
+ searchDir = path.join(this.basePath, expandedPrefix);
366
399
  }
367
400
  searchPrefix = "";
368
401
  } else {
369
402
  // Split into directory and file prefix
370
- const dir = dirname(expandedPrefix);
371
- const file = basename(expandedPrefix);
403
+ const dir = path.dirname(expandedPrefix);
404
+ const file = path.basename(expandedPrefix);
372
405
  if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) {
373
406
  searchDir = dir;
374
407
  } else {
375
- searchDir = join(this.basePath, dir);
408
+ searchDir = path.join(this.basePath, dir);
376
409
  }
377
410
  searchPrefix = file;
378
411
  }
379
412
 
380
- const entries = readdirSync(searchDir, { withFileTypes: true });
413
+ const entries = await this.getCachedDirEntries(searchDir);
381
414
  const suggestions: AutocompleteItem[] = [];
382
415
 
383
416
  for (const entry of entries) {
@@ -389,8 +422,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
389
422
  let isDirectory = entry.isDirectory();
390
423
  if (!isDirectory && entry.isSymbolicLink()) {
391
424
  try {
392
- const fullPath = join(searchDir, entry.name);
393
- isDirectory = statSync(fullPath).isDirectory();
425
+ const fullPath = path.join(searchDir, entry.name);
426
+ isDirectory = (await fs.promises.stat(fullPath)).isDirectory();
394
427
  } catch {
395
428
  // Broken symlink, file deleted between readdir and stat, or permission error
396
429
  continue;
@@ -408,10 +441,10 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
408
441
  } else if (pathWithoutAt.includes("/")) {
409
442
  if (pathWithoutAt.startsWith("~/")) {
410
443
  const homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/
411
- const dir = dirname(homeRelativeDir);
412
- relativePath = `@~/${dir === "." ? name : join(dir, name)}`;
444
+ const dir = path.dirname(homeRelativeDir);
445
+ relativePath = `@~/${dir === "." ? name : path.join(dir, name)}`;
413
446
  } else {
414
- relativePath = `@${join(dirname(pathWithoutAt), name)}`;
447
+ relativePath = `@${path.join(path.dirname(pathWithoutAt), name)}`;
415
448
  }
416
449
  } else {
417
450
  if (pathWithoutAt.startsWith("~")) {
@@ -427,18 +460,18 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
427
460
  // Preserve ~/ format for home directory paths
428
461
  if (prefix.startsWith("~/")) {
429
462
  const homeRelativeDir = prefix.slice(2); // Remove ~/
430
- const dir = dirname(homeRelativeDir);
431
- relativePath = `~/${dir === "." ? name : join(dir, name)}`;
463
+ const dir = path.dirname(homeRelativeDir);
464
+ relativePath = `~/${dir === "." ? name : path.join(dir, name)}`;
432
465
  } else if (prefix.startsWith("/")) {
433
466
  // Absolute path - construct properly
434
- const dir = dirname(prefix);
467
+ const dir = path.dirname(prefix);
435
468
  if (dir === "/") {
436
469
  relativePath = `/${name}`;
437
470
  } else {
438
471
  relativePath = `${dir}/${name}`;
439
472
  }
440
473
  } else {
441
- relativePath = join(dirname(prefix), name);
474
+ relativePath = path.join(path.dirname(prefix), name);
442
475
  }
443
476
  } else {
444
477
  // For standalone entries, preserve ~/ if original prefix was ~/
@@ -474,7 +507,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
474
507
  // Score an entry against the query (higher = better match)
475
508
  // isDirectory adds bonus to prioritize folders
476
509
  private scoreEntry(filePath: string, query: string, isDirectory: boolean): number {
477
- const fileName = basename(filePath);
510
+ const fileName = path.basename(filePath);
478
511
  const lowerFileName = fileName.toLowerCase();
479
512
  const lowerQuery = query.toLowerCase();
480
513
 
@@ -495,34 +528,28 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
495
528
  return score;
496
529
  }
497
530
 
498
- // Fuzzy file search using fd (fast, respects .gitignore)
499
- private getFuzzyFileSuggestions(query: string): AutocompleteItem[] {
531
+ private async getFuzzyFileSuggestions(query: string): Promise<AutocompleteItem[]> {
500
532
  if (!this.fdPath) {
501
- // fd not available, return empty results
502
533
  return [];
503
534
  }
504
535
 
505
536
  try {
506
- const entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);
537
+ const entries = await walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);
507
538
 
508
- // Score entries
509
539
  const scoredEntries = entries
510
- .map((entry) => ({
540
+ .map(entry => ({
511
541
  ...entry,
512
542
  score: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,
513
543
  }))
514
- .filter((entry) => entry.score > 0);
544
+ .filter(entry => entry.score > 0);
515
545
 
516
- // Sort by score (descending) and take top 20
517
546
  scoredEntries.sort((a, b) => b.score - a.score);
518
547
  const topEntries = scoredEntries.slice(0, 20);
519
548
 
520
- // Build suggestions
521
549
  const suggestions: AutocompleteItem[] = [];
522
550
  for (const { path: entryPath, isDirectory } of topEntries) {
523
- // fd already includes trailing / for directories
524
551
  const pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;
525
- const entryName = basename(pathWithoutSlash);
552
+ const entryName = path.basename(pathWithoutSlash);
526
553
 
527
554
  suggestions.push({
528
555
  value: `@${entryPath}`,
@@ -533,17 +560,16 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
533
560
 
534
561
  return suggestions;
535
562
  } catch {
536
- // Directory doesn't exist or not accessible
537
563
  return [];
538
564
  }
539
565
  }
540
566
 
541
567
  // Force file completion (called on Tab key) - always returns suggestions
542
- getForceFileSuggestions(
568
+ async getForceFileSuggestions(
543
569
  lines: string[],
544
570
  cursorLine: number,
545
571
  cursorCol: number,
546
- ): { items: AutocompleteItem[]; prefix: string } | null {
572
+ ): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
547
573
  const currentLine = lines[cursorLine] || "";
548
574
  const textBeforeCursor = currentLine.slice(0, cursorCol);
549
575
 
@@ -555,7 +581,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
555
581
  // Force extract path prefix - this will always return something
556
582
  const pathMatch = this.extractPathPrefix(textBeforeCursor, true);
557
583
  if (pathMatch !== null) {
558
- const suggestions = this.getFileSuggestions(pathMatch);
584
+ const suggestions = await this.getFileSuggestions(pathMatch);
559
585
  if (suggestions.length === 0) return null;
560
586
 
561
587
  return {
@@ -1,5 +1,5 @@
1
- import type { Component } from "@oh-my-pi/pi-tui/tui";
2
- import { applyBackgroundToLine, visibleWidth } from "@oh-my-pi/pi-tui/utils";
1
+ import type { Component } from "../tui";
2
+ import { applyBackgroundToLine, visibleWidth } from "../utils";
3
3
 
4
4
  /**
5
5
  * Box component - a container that applies padding and background to all children
@@ -1,4 +1,4 @@
1
- import { matchesKey } from "@oh-my-pi/pi-tui/keys";
1
+ import { matchesKey } from "../keys";
2
2
  import { Loader } from "./loader";
3
3
 
4
4
  /**
@@ -1,14 +1,8 @@
1
- import type { AutocompleteProvider, CombinedAutocompleteProvider } from "@oh-my-pi/pi-tui/autocomplete";
2
- import { matchesKey } from "@oh-my-pi/pi-tui/keys";
3
- import type { SymbolTheme } from "@oh-my-pi/pi-tui/symbols";
4
- import { type Component, CURSOR_MARKER, type Focusable } from "@oh-my-pi/pi-tui/tui";
5
- import {
6
- getSegmenter,
7
- isPunctuationChar,
8
- isWhitespaceChar,
9
- truncateToWidth,
10
- visibleWidth,
11
- } from "@oh-my-pi/pi-tui/utils";
1
+ import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete";
2
+ import { matchesKey } from "../keys";
3
+ import type { SymbolTheme } from "../symbols";
4
+ import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
5
+ import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils";
12
6
  import { SelectList, type SelectListTheme } from "./select-list";
13
7
 
14
8
  const segmenter = getSegmenter();
@@ -296,6 +290,7 @@ export class Editor implements Component, Focusable {
296
290
  private autocompleteList?: SelectList;
297
291
  private isAutocompleting: boolean = false;
298
292
  private autocompletePrefix: string = "";
293
+ private autocompleteRequestId: number = 0;
299
294
  public onAutocompleteUpdate?: () => void;
300
295
 
301
296
  // Paste tracking for large pastes
@@ -316,6 +311,9 @@ export class Editor implements Component, Focusable {
316
311
  private undoStack: EditorState[] = [];
317
312
  private suspendUndo = false;
318
313
 
314
+ // Debounce timer for autocomplete updates
315
+ private autocompleteTimeout: ReturnType<typeof setTimeout> | null = null;
316
+
319
317
  public onSubmit?: (text: string) => void;
320
318
  public onAltEnter?: (text: string) => void;
321
319
  public onChange?: (text: string) => void;
@@ -357,7 +355,7 @@ export class Editor implements Component, Focusable {
357
355
  setHistoryStorage(storage: HistoryStorage): void {
358
356
  this.historyStorage = storage;
359
357
  const recent = storage.getRecent(100);
360
- this.history = recent.map((entry) => entry.prompt);
358
+ this.history = recent.map(entry => entry.prompt);
361
359
  this.historyIndex = -1;
362
360
  }
363
361
 
@@ -549,7 +547,7 @@ export class Editor implements Component, Focusable {
549
547
  // Rebuild 'before' without the last grapheme
550
548
  const beforeWithoutLast = beforeGraphemes
551
549
  .slice(0, -1)
552
- .map((g) => g.segment)
550
+ .map(g => g.segment)
553
551
  .join("");
554
552
  displayText = beforeWithoutLast + marker + cursor;
555
553
  displayWidth -= 1; // Back to original width (reverse video replaces, doesn't add)
@@ -1125,8 +1123,8 @@ export class Editor implements Component, Focusable {
1125
1123
  this.tryTriggerAutocomplete();
1126
1124
  }
1127
1125
  }
1128
- // Also auto-trigger when typing letters in a slash command context
1129
- else if (/[a-zA-Z0-9.\-_]/.test(char)) {
1126
+ // Also auto-trigger when typing letters/path chars in a slash command context
1127
+ else if (/[a-zA-Z0-9.\-_/]/.test(char)) {
1130
1128
  const currentLine = this.state.lines[this.state.cursorLine] || "";
1131
1129
  const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
1132
1130
  // Check if we're in a slash command (with or without space for arguments)
@@ -1139,7 +1137,7 @@ export class Editor implements Component, Focusable {
1139
1137
  }
1140
1138
  }
1141
1139
  } else {
1142
- this.updateAutocomplete();
1140
+ this.debouncedUpdateAutocomplete();
1143
1141
  }
1144
1142
  }
1145
1143
 
@@ -1158,7 +1156,7 @@ export class Editor implements Component, Focusable {
1158
1156
  // Filter out non-printable characters except newlines
1159
1157
  let filteredText = tabExpandedText
1160
1158
  .split("")
1161
- .filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
1159
+ .filter(char => char === "\n" || char.charCodeAt(0) >= 32)
1162
1160
  .join("");
1163
1161
 
1164
1162
  // If pasting a file path (starts with /, ~, or .) and the character before
@@ -1302,7 +1300,7 @@ export class Editor implements Component, Focusable {
1302
1300
 
1303
1301
  // Update or re-trigger autocomplete after backspace
1304
1302
  if (this.isAutocompleting) {
1305
- this.updateAutocomplete();
1303
+ this.debouncedUpdateAutocomplete();
1306
1304
  } else {
1307
1305
  // If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
1308
1306
  const currentLine = this.state.lines[this.state.cursorLine] || "";
@@ -1388,7 +1386,7 @@ export class Editor implements Component, Focusable {
1388
1386
  }
1389
1387
 
1390
1388
  if (this.isAutocompleting) {
1391
- this.updateAutocomplete();
1389
+ this.debouncedUpdateAutocomplete();
1392
1390
  } else {
1393
1391
  const currentLine = this.state.lines[this.state.cursorLine] || "";
1394
1392
  const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
@@ -1613,7 +1611,7 @@ export class Editor implements Component, Focusable {
1613
1611
 
1614
1612
  // Update or re-trigger autocomplete after forward delete
1615
1613
  if (this.isAutocompleting) {
1616
- this.updateAutocomplete();
1614
+ this.debouncedUpdateAutocomplete();
1617
1615
  } else {
1618
1616
  const currentLine = this.state.lines[this.state.cursorLine] || "";
1619
1617
  const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
@@ -1841,9 +1839,8 @@ export class Editor implements Component, Focusable {
1841
1839
  }
1842
1840
 
1843
1841
  // Autocomplete methods
1844
- private tryTriggerAutocomplete(explicitTab: boolean = false): void {
1842
+ private async tryTriggerAutocomplete(explicitTab: boolean = false): Promise<void> {
1845
1843
  if (!this.autocompleteProvider) return;
1846
-
1847
1844
  // Check if we should trigger file completion on Tab
1848
1845
  if (explicitTab) {
1849
1846
  const provider = this.autocompleteProvider as CombinedAutocompleteProvider;
@@ -1855,18 +1852,23 @@ export class Editor implements Component, Focusable {
1855
1852
  }
1856
1853
  }
1857
1854
 
1858
- const suggestions = this.autocompleteProvider.getSuggestions(
1855
+ const requestId = ++this.autocompleteRequestId;
1856
+
1857
+ const suggestions = await this.autocompleteProvider.getSuggestions(
1859
1858
  this.state.lines,
1860
1859
  this.state.cursorLine,
1861
1860
  this.state.cursorCol,
1862
1861
  );
1862
+ if (requestId !== this.autocompleteRequestId) return;
1863
1863
 
1864
1864
  if (suggestions && suggestions.items.length > 0) {
1865
1865
  this.autocompletePrefix = suggestions.prefix;
1866
1866
  this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
1867
1867
  this.isAutocompleting = true;
1868
+ this.onAutocompleteUpdate?.();
1868
1869
  } else {
1869
1870
  this.cancelAutocomplete();
1871
+ this.onAutocompleteUpdate?.();
1870
1872
  }
1871
1873
  }
1872
1874
 
@@ -1893,7 +1895,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
1893
1895
  17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
1894
1896
  536643416/job/55932288317 havea look at .gi
1895
1897
  */
1896
- private forceFileAutocomplete(): void {
1898
+ private async forceFileAutocomplete(): Promise<void> {
1897
1899
  if (!this.autocompleteProvider) return;
1898
1900
 
1899
1901
  // Check if provider supports force file suggestions via runtime check
@@ -1901,27 +1903,33 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
1901
1903
  getForceFileSuggestions?: CombinedAutocompleteProvider["getForceFileSuggestions"];
1902
1904
  };
1903
1905
  if (typeof provider.getForceFileSuggestions !== "function") {
1904
- this.tryTriggerAutocomplete(true);
1906
+ await this.tryTriggerAutocomplete(true);
1905
1907
  return;
1906
1908
  }
1907
1909
 
1908
- const suggestions = provider.getForceFileSuggestions(
1910
+ const requestId = ++this.autocompleteRequestId;
1911
+ const suggestions = await provider.getForceFileSuggestions(
1909
1912
  this.state.lines,
1910
1913
  this.state.cursorLine,
1911
1914
  this.state.cursorCol,
1912
1915
  );
1916
+ if (requestId !== this.autocompleteRequestId) return;
1913
1917
 
1914
1918
  if (suggestions && suggestions.items.length > 0) {
1915
1919
  this.autocompletePrefix = suggestions.prefix;
1916
1920
  this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
1917
1921
  this.isAutocompleting = true;
1922
+ this.onAutocompleteUpdate?.();
1918
1923
  } else {
1919
1924
  this.cancelAutocomplete();
1925
+ this.onAutocompleteUpdate?.();
1920
1926
  }
1921
1927
  }
1922
1928
 
1923
1929
  private cancelAutocomplete(notifyCancel: boolean = false): void {
1924
1930
  const wasAutocompleting = this.isAutocompleting;
1931
+ this.clearAutocompleteTimeout();
1932
+ this.autocompleteRequestId += 1;
1925
1933
  this.isAutocompleting = false;
1926
1934
  this.autocompleteList = undefined;
1927
1935
  this.autocompletePrefix = "";
@@ -1934,21 +1942,42 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
1934
1942
  return this.isAutocompleting;
1935
1943
  }
1936
1944
 
1937
- private updateAutocomplete(): void {
1945
+ private async updateAutocomplete(): Promise<void> {
1938
1946
  if (!this.isAutocompleting || !this.autocompleteProvider) return;
1947
+ const requestId = ++this.autocompleteRequestId;
1939
1948
 
1940
- const suggestions = this.autocompleteProvider.getSuggestions(
1949
+ const suggestions = await this.autocompleteProvider.getSuggestions(
1941
1950
  this.state.lines,
1942
1951
  this.state.cursorLine,
1943
1952
  this.state.cursorCol,
1944
1953
  );
1954
+ if (requestId !== this.autocompleteRequestId) return;
1945
1955
 
1946
1956
  if (suggestions && suggestions.items.length > 0) {
1947
1957
  this.autocompletePrefix = suggestions.prefix;
1948
1958
  // Always create new SelectList to ensure update
1949
1959
  this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
1960
+ this.onAutocompleteUpdate?.();
1950
1961
  } else {
1951
1962
  this.cancelAutocomplete();
1963
+ this.onAutocompleteUpdate?.();
1964
+ }
1965
+ }
1966
+
1967
+ private debouncedUpdateAutocomplete(): void {
1968
+ if (this.autocompleteTimeout) {
1969
+ clearTimeout(this.autocompleteTimeout);
1970
+ }
1971
+ this.autocompleteTimeout = setTimeout(() => {
1972
+ this.updateAutocomplete();
1973
+ this.autocompleteTimeout = null;
1974
+ }, 100);
1975
+ }
1976
+
1977
+ private clearAutocompleteTimeout(): void {
1978
+ if (this.autocompleteTimeout) {
1979
+ clearTimeout(this.autocompleteTimeout);
1980
+ this.autocompleteTimeout = null;
1952
1981
  }
1953
1982
  }
1954
1983
  }
@@ -4,8 +4,8 @@ import {
4
4
  type ImageDimensions,
5
5
  imageFallback,
6
6
  renderImage,
7
- } from "@oh-my-pi/pi-tui/terminal-image";
8
- import type { Component } from "@oh-my-pi/pi-tui/tui";
7
+ } from "../terminal-image";
8
+ import type { Component } from "../tui";
9
9
 
10
10
  export interface ImageTheme {
11
11
  fallbackColor: (str: string) => string;
@@ -1,6 +1,6 @@
1
- import { getEditorKeybindings } from "@oh-my-pi/pi-tui/keybindings";
2
- import { type Component, CURSOR_MARKER, type Focusable } from "@oh-my-pi/pi-tui/tui";
3
- import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "@oh-my-pi/pi-tui/utils";
1
+ import { getEditorKeybindings } from "../keybindings";
2
+ import { type Component, CURSOR_MARKER, type Focusable } from "../tui";
3
+ import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils";
4
4
 
5
5
  const segmenter = getSegmenter();
6
6
 
@@ -186,7 +186,7 @@ export class Input implements Component, Focusable {
186
186
 
187
187
  // Regular character input - accept printable characters including Unicode,
188
188
  // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F)
189
- const hasControlChars = [...data].some((ch) => {
189
+ const hasControlChars = [...data].some(ch => {
190
190
  const code = ch.charCodeAt(0);
191
191
  return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
192
192
  });
@@ -1,4 +1,4 @@
1
- import type { TUI } from "@oh-my-pi/pi-tui/tui";
1
+ import type { TUI } from "../tui";
2
2
  import { Text } from "./text";
3
3
 
4
4
  /**
@@ -1,9 +1,9 @@
1
- import type { MermaidImage } from "@oh-my-pi/pi-tui/mermaid";
2
- import type { SymbolTheme } from "@oh-my-pi/pi-tui/symbols";
3
- import { encodeITerm2, encodeKitty, getCapabilities, getCellDimensions } from "@oh-my-pi/pi-tui/terminal-image";
4
- import type { Component } from "@oh-my-pi/pi-tui/tui";
5
- import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui/utils";
6
1
  import { marked, type Token } from "marked";
2
+ import type { MermaidImage } from "../mermaid";
3
+ import type { SymbolTheme } from "../symbols";
4
+ import { encodeITerm2, encodeKitty, getCapabilities, getCellDimensions } from "../terminal-image";
5
+ import type { Component } from "../tui";
6
+ import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils";
7
7
 
8
8
  /**
9
9
  * Default text styling for markdown content.
@@ -628,7 +628,7 @@ export class Markdown implements Component {
628
628
  } else {
629
629
  // Distribute space proportionally based on natural widths
630
630
  const totalNatural = naturalWidths.reduce((a, b) => a + b, 0);
631
- columnWidths = naturalWidths.map((w) => {
631
+ columnWidths = naturalWidths.map(w => {
632
632
  const proportion = w / totalNatural;
633
633
  return Math.max(1, Math.floor(proportion * availableForCells));
634
634
  });
@@ -648,7 +648,7 @@ export class Markdown implements Component {
648
648
  const v = t.vertical;
649
649
 
650
650
  // Render top border
651
- const topBorderCells = columnWidths.map((w) => h.repeat(w));
651
+ const topBorderCells = columnWidths.map(w => h.repeat(w));
652
652
  lines.push(`${t.topLeft}${h}${topBorderCells.join(`${h}${t.teeDown}${h}`)}${h}${t.topRight}`);
653
653
 
654
654
  // Render header with wrapping
@@ -656,7 +656,7 @@ export class Markdown implements Component {
656
656
  const text = this.renderInlineTokens(cell.tokens || []);
657
657
  return this.wrapCellText(text, columnWidths[i]);
658
658
  });
659
- const headerLineCount = Math.max(...headerCellLines.map((c) => c.length));
659
+ const headerLineCount = Math.max(...headerCellLines.map(c => c.length));
660
660
 
661
661
  for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
662
662
  const rowParts = headerCellLines.map((cellLines, colIdx) => {
@@ -668,7 +668,7 @@ export class Markdown implements Component {
668
668
  }
669
669
 
670
670
  // Render separator
671
- const separatorCells = columnWidths.map((w) => h.repeat(w));
671
+ const separatorCells = columnWidths.map(w => h.repeat(w));
672
672
  lines.push(`${t.teeRight}${h}${separatorCells.join(`${h}${t.cross}${h}`)}${h}${t.teeLeft}`);
673
673
 
674
674
  // Render rows with wrapping
@@ -677,7 +677,7 @@ export class Markdown implements Component {
677
677
  const text = this.renderInlineTokens(cell.tokens || []);
678
678
  return this.wrapCellText(text, columnWidths[i]);
679
679
  });
680
- const rowLineCount = Math.max(...rowCellLines.map((c) => c.length));
680
+ const rowLineCount = Math.max(...rowCellLines.map(c => c.length));
681
681
 
682
682
  for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
683
683
  const rowParts = rowCellLines.map((cellLines, colIdx) => {
@@ -689,7 +689,7 @@ export class Markdown implements Component {
689
689
  }
690
690
 
691
691
  // Render bottom border
692
- const bottomBorderCells = columnWidths.map((w) => h.repeat(w));
692
+ const bottomBorderCells = columnWidths.map(w => h.repeat(w));
693
693
  lines.push(`${t.bottomLeft}${h}${bottomBorderCells.join(`${h}${t.teeUp}${h}`)}${h}${t.bottomRight}`);
694
694
 
695
695
  lines.push(""); // Add spacing after table
@@ -1,7 +1,7 @@
1
- import { matchesKey } from "@oh-my-pi/pi-tui/keys";
2
- import type { SymbolTheme } from "@oh-my-pi/pi-tui/symbols";
3
- import type { Component } from "@oh-my-pi/pi-tui/tui";
4
- import { truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui/utils";
1
+ import { matchesKey } from "../keys";
2
+ import type { SymbolTheme } from "../symbols";
3
+ import type { Component } from "../tui";
4
+ import { truncateToWidth, visibleWidth } from "../utils";
5
5
 
6
6
  export interface SelectItem {
7
7
  value: string;
@@ -37,7 +37,7 @@ export class SelectList implements Component {
37
37
  }
38
38
 
39
39
  setFilter(filter: string): void {
40
- this.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));
40
+ this.filteredItems = this.items.filter(item => item.value.toLowerCase().startsWith(filter.toLowerCase()));
41
41
  // Reset selection when filter changes
42
42
  this.selectedIndex = 0;
43
43
  }
@@ -1,6 +1,6 @@
1
- import { matchesKey } from "@oh-my-pi/pi-tui/keys";
2
- import type { Component } from "@oh-my-pi/pi-tui/tui";
3
- import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui/utils";
1
+ import { matchesKey } from "../keys";
2
+ import type { Component } from "../tui";
3
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
4
4
 
5
5
  export interface SettingItem {
6
6
  /** Unique identifier for this setting */
@@ -53,7 +53,7 @@ export class SettingsList implements Component {
53
53
 
54
54
  /** Update an item's currentValue */
55
55
  updateValue(id: string, newValue: string): void {
56
- const item = this.items.find((i) => i.id === id);
56
+ const item = this.items.find(i => i.id === id);
57
57
  if (item) {
58
58
  item.currentValue = newValue;
59
59
  }
@@ -88,7 +88,7 @@ export class SettingsList implements Component {
88
88
  const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
89
89
 
90
90
  // Calculate max label width for alignment
91
- const maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));
91
+ const maxLabelWidth = Math.min(30, Math.max(...this.items.map(item => visibleWidth(item.label))));
92
92
 
93
93
  // Render visible items
94
94
  for (let i = startIndex; i < endIndex; i++) {
@@ -1,4 +1,4 @@
1
- import type { Component } from "@oh-my-pi/pi-tui/tui";
1
+ import type { Component } from "../tui";
2
2
 
3
3
  /**
4
4
  * Spacer component that renders empty lines
@@ -8,10 +8,9 @@
8
8
  * - Tab / Arrow Right: Next tab (wraps around)
9
9
  * - Shift+Tab / Arrow Left: Previous tab (wraps around)
10
10
  */
11
-
12
- import { matchesKey } from "@oh-my-pi/pi-tui/keys";
13
- import type { Component } from "@oh-my-pi/pi-tui/tui";
14
- import { wrapTextWithAnsi } from "@oh-my-pi/pi-tui/utils";
11
+ import { matchesKey } from "../keys";
12
+ import type { Component } from "../tui";
13
+ import { wrapTextWithAnsi } from "../utils";
15
14
 
16
15
  /** Tab definition */
17
16
  export interface Tab {
@@ -1,5 +1,5 @@
1
- import type { Component } from "@oh-my-pi/pi-tui/tui";
2
- import { applyBackgroundToLine, wrapTextWithAnsi } from "@oh-my-pi/pi-tui/utils";
1
+ import type { Component } from "../tui";
2
+ import { applyBackgroundToLine, wrapTextWithAnsi } from "../utils";
3
3
 
4
4
  /**
5
5
  * Text component - displays multi-line text with word wrapping
@@ -1,5 +1,5 @@
1
- import type { Component } from "@oh-my-pi/pi-tui/tui";
2
- import { truncateToWidth } from "@oh-my-pi/pi-tui/utils";
1
+ import type { Component } from "../tui";
2
+ import { truncateToWidth } from "../utils";
3
3
 
4
4
  /**
5
5
  * Text component that truncates to fit viewport width
package/src/fuzzy.ts CHANGED
@@ -110,7 +110,7 @@ export function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) =>
110
110
  const tokens = query
111
111
  .trim()
112
112
  .split(/\s+/)
113
- .filter((t) => t.length > 0);
113
+ .filter(t => t.length > 0);
114
114
 
115
115
  if (tokens.length === 0) {
116
116
  return items;
@@ -139,5 +139,5 @@ export function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) =>
139
139
  }
140
140
 
141
141
  results.sort((a, b) => a.totalScore - b.totalScore);
142
- return results.map((r) => r.item);
142
+ return results.map(r => r.item);
143
143
  }
@@ -123,7 +123,7 @@ export class EditorKeybindingsManager {
123
123
  const keyArray = Array.isArray(keys) ? keys : [keys];
124
124
  this.actionToKeys.set(
125
125
  action as EditorAction,
126
- keyArray.map((key) => normalizeKeyId(key as KeyId)),
126
+ keyArray.map(key => normalizeKeyId(key as KeyId)),
127
127
  );
128
128
  }
129
129
 
@@ -133,7 +133,7 @@ export class EditorKeybindingsManager {
133
133
  const keyArray = Array.isArray(keys) ? keys : [keys];
134
134
  this.actionToKeys.set(
135
135
  action as EditorAction,
136
- keyArray.map((key) => normalizeKeyId(key as KeyId)),
136
+ keyArray.map(key => normalizeKeyId(key as KeyId)),
137
137
  );
138
138
  }
139
139
  }
package/src/mermaid.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { mkdir, rm } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
4
  import { $ } from "bun";
5
5
 
6
6
  export interface MermaidImage {
@@ -31,12 +31,11 @@ export async function renderMermaidToPng(
31
31
  return null;
32
32
  }
33
33
 
34
- const tmpDir = join(tmpdir(), `mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`);
35
- const inputPath = join(tmpDir, "input.mmd");
36
- const outputPath = join(tmpDir, "output.png");
34
+ const tmpDir = path.join(os.tmpdir(), `mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`);
35
+ const inputPath = path.join(tmpDir, "input.mmd");
36
+ const outputPath = path.join(tmpDir, "output.png");
37
37
 
38
38
  try {
39
- await mkdir(tmpDir, { recursive: true });
40
39
  await Bun.write(inputPath, source);
41
40
 
42
41
  const args: string[] = ["-i", inputPath, "-o", outputPath, "-q"];
@@ -64,7 +63,7 @@ export async function renderMermaidToPng(
64
63
  return null;
65
64
  }
66
65
 
67
- const buffer = Buffer.from(await outputFile.arrayBuffer());
66
+ const buffer = Buffer.from(await outputFile.bytes());
68
67
  const base64 = buffer.toString("base64");
69
68
 
70
69
  const dims = parsePngDimensions(buffer);
@@ -80,7 +79,7 @@ export async function renderMermaidToPng(
80
79
  } catch {
81
80
  return null;
82
81
  } finally {
83
- await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
82
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
84
83
  }
85
84
  }
86
85
 
@@ -16,7 +16,6 @@
16
16
  * Based on code from OpenTUI (https://github.com/anomalyco/opentui)
17
17
  * MIT License - Copyright (c) 2025 opentui
18
18
  */
19
-
20
19
  import { EventEmitter } from "events";
21
20
 
22
21
  const ESC = "\x1b";
@@ -111,7 +110,7 @@ function isCompleteCsiSequence(data: string): "complete" | "incomplete" {
111
110
  if (lastChar === "M" || lastChar === "m") {
112
111
  // Check if we have the right structure
113
112
  const parts = payload.slice(1, -1).split(";");
114
- if (parts.length === 3 && parts.every((p) => /^\d+$/.test(p))) {
113
+ if (parts.length === 3 && parts.every(p => /^\d+$/.test(p))) {
115
114
  return "complete";
116
115
  }
117
116
  }
package/src/tui.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Minimal TUI implementation with differential rendering
3
3
  */
4
-
5
4
  import * as fs from "node:fs";
6
5
  import * as os from "node:os";
7
6
  import * as path from "node:path";
@@ -337,7 +336,7 @@ export class TUI extends Container {
337
336
 
338
337
  /** Check if there are any visible overlays */
339
338
  hasOverlay(): boolean {
340
- return this.overlayStack.some((o) => this.isOverlayVisible(o));
339
+ return this.overlayStack.some(o => this.isOverlayVisible(o));
341
340
  }
342
341
 
343
342
  /** Check if an overlay entry is currently visible */
@@ -366,7 +365,7 @@ export class TUI extends Container {
366
365
 
367
366
  start(): void {
368
367
  this.terminal.start(
369
- (data) => this.handleInput(data),
368
+ data => this.handleInput(data),
370
369
  () => this.requestRender(),
371
370
  );
372
371
  this.terminal.hideCursor();
@@ -423,7 +422,7 @@ export class TUI extends Container {
423
422
 
424
423
  async waitForRender(): Promise<void> {
425
424
  if (!this.renderRequested && !this.rendering) return;
426
- await new Promise<void>((resolve) => {
425
+ await new Promise<void>(resolve => {
427
426
  const check = () => {
428
427
  if (!this.renderRequested && !this.rendering) {
429
428
  resolve();
@@ -467,7 +466,7 @@ export class TUI extends Container {
467
466
 
468
467
  // If focused component is an overlay, verify it's still visible
469
468
  // (visibility can change due to terminal resize or visible() callback)
470
- const focusedOverlay = this.overlayStack.find((o) => o.component === this.focusedComponent);
469
+ const focusedOverlay = this.overlayStack.find(o => o.component === this.focusedComponent);
471
470
  if (focusedOverlay && !this.isOverlayVisible(focusedOverlay)) {
472
471
  // Focused overlay is no longer visible, redirect to topmost visible overlay
473
472
  const topVisible = this.getTopmostVisibleOverlay();
@@ -752,7 +751,7 @@ export class TUI extends Container {
752
751
 
753
752
  private applyLineResets(lines: string[]): string[] {
754
753
  const reset = TUI.SEGMENT_RESET;
755
- return lines.map((line) => (this.containsImage(line) ? line : line + reset));
754
+ return lines.map(line => (this.containsImage(line) ? line : line + reset));
756
755
  }
757
756
 
758
757
  /**
package/src/utils.ts CHANGED
@@ -555,7 +555,7 @@ function wrapSingleLine(line: string, width: number): string[] {
555
555
  }
556
556
 
557
557
  // Trailing whitespace can cause lines to exceed the requested width
558
- return wrapped.length > 0 ? wrapped.map((line) => line.trimEnd()) : [""];
558
+ return wrapped.length > 0 ? wrapped.map(line => line.trimEnd()) : [""];
559
559
  }
560
560
 
561
561
  const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;