pi-better-subagents 0.1.30 → 0.1.31

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/shared-navigator.ts +27 -58
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth as piTruncateToWidth, visibleWidth as piVisibleWidth } from "@earendil-works/pi-tui";
2
3
  import type { Component } from "@earendil-works/pi-tui";
3
4
  import { createRenderScheduler, type RenderScheduler } from "./shared-render-scheduler.ts";
4
5
 
@@ -280,10 +281,10 @@ function refreshMainListWidget(): void {
280
281
  }) : undefined;
281
282
  const changed = nextSignature !== s.lastMainListSignature;
282
283
  s.lastMainListSignature = nextSignature;
283
- s.lastMainListLines = rows.length ? buildMainListLines(rows, MAIN_LIST_FALLBACK_WIDTH, deps.truncate, themeFg(ctx), {
284
+ s.lastMainListLines = rows.length ? ["", ...buildMainListLines(rows, MAIN_LIST_FALLBACK_WIDTH, deps.truncate, themeFg(ctx), {
284
285
  selectedId: s.mainListFocused ? s.mainListSelectedId : undefined,
285
286
  focused: s.mainListFocused === true,
286
- }) : undefined;
287
+ })] : undefined;
287
288
  if (!rows.length) {
288
289
  if (!s.mainListWidgetInstalled) return;
289
290
  try { (ctx.ui as any).setWidget?.(MAIN_LIST_WIDGET_KEY, undefined); } catch { /* ignore */ }
@@ -323,10 +324,10 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
323
324
  state().lastMainListLines = undefined;
324
325
  return [];
325
326
  }
326
- const lines = buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
327
+ const lines = ["", ...buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
327
328
  selectedId: state().mainListFocused ? state().mainListSelectedId : undefined,
328
329
  focused: state().mainListFocused === true,
329
- });
330
+ })];
330
331
  state().lastMainListLines = lines;
331
332
  return lines;
332
333
  },
@@ -1269,7 +1270,8 @@ export function wrapLogText(text: string, width: number): string[] {
1269
1270
  continue;
1270
1271
  }
1271
1272
  const sourceIndent = source.match(/^[ \t]*/)?.[0] ?? "";
1272
- const indent = sourceIndent.replace(/\t/g, " ");
1273
+ // Leave room for a full-width grapheme even on deeply indented log lines.
1274
+ const indent = sourceIndent.replace(/\t/g, " ").slice(0, max - 2);
1273
1275
  let remaining = source.slice(sourceIndent.length);
1274
1276
  const contentWidth = Math.max(1, max - visibleWidth(indent));
1275
1277
  if (!remaining) {
@@ -1302,33 +1304,25 @@ function lastDelimiterBreak(value: string): number {
1302
1304
  return last;
1303
1305
  }
1304
1306
 
1307
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
1308
+
1305
1309
  function splitVisiblePrefix(value: string, width: number): [string, string] {
1306
1310
  const max = Math.max(0, Math.floor(width));
1307
1311
  let visible = 0;
1308
- let index = 0;
1309
- while (index < value.length && visible < max) {
1310
- if (value[index] === "\u001b" || value[index] === "\u009b") {
1311
- const match = value.slice(index).match(ANSI_RE);
1312
- if (match?.index === 0) {
1313
- index += match[0].length;
1314
- continue;
1315
- }
1316
- }
1317
- if (value[index] === "<") {
1318
- const close = value.indexOf(">", index);
1319
- if (close !== -1) {
1320
- const tag = value.slice(index, close + 1);
1321
- if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
1322
- index = close + 1;
1323
- continue;
1324
- }
1325
- }
1312
+ let end = 0;
1313
+ for (const { segment, index } of GRAPHEME_SEGMENTER.segment(value)) {
1314
+ if (index < end) continue;
1315
+ const formatting = value.slice(index).match(FORMATTING_PREFIX_RE);
1316
+ if (formatting) {
1317
+ end = index + formatting[0].length;
1318
+ continue;
1326
1319
  }
1327
- const codePoint = value.codePointAt(index)!;
1328
- index += codePoint > 0xFFFF ? 2 : 1;
1329
- visible += 1;
1320
+ const cells = piVisibleWidth(segment);
1321
+ if (visible + cells > max) break;
1322
+ visible += cells;
1323
+ end = index + segment.length;
1330
1324
  }
1331
- return [value.slice(0, index), value.slice(index)];
1325
+ return [value.slice(0, end), value.slice(end)];
1332
1326
  }
1333
1327
 
1334
1328
  function cycleLogTailRows(current: number): number {
@@ -1387,44 +1381,19 @@ function safeTruncate(line: string, width: number, truncate: (s: string, width:
1387
1381
 
1388
1382
  const ANSI_RE = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
1389
1383
 
1384
+ const FORMATTING_PREFIX_RE = new RegExp(`^(?:${ANSI_RE.source}|<\\/?[a-zA-Z][\\w-]*>|</>)`);
1385
+
1390
1386
  function visibleWidth(value: string): number {
1391
- return String(value ?? "")
1387
+ const cleaned = String(value ?? "")
1392
1388
  .replace(ANSI_RE, "")
1393
1389
  .replace(/<\/?[a-zA-Z][\w-]*>/g, "")
1394
- .replace(/<\/>/g, "")
1395
- .length;
1390
+ .replace(/<\/>/g, "");
1391
+ return piVisibleWidth(cleaned);
1396
1392
  }
1397
1393
 
1398
1394
  function truncateVisible(value: string, width: number): string {
1399
1395
  const str = String(value ?? "");
1400
1396
  const max = Math.max(0, Math.floor(width || 0));
1401
1397
  if (visibleWidth(str) <= max) return str;
1402
- let out = "";
1403
- let vis = 0;
1404
- let i = 0;
1405
- while (i < str.length && vis < max) {
1406
- if (str[i] === "\u001b" || str[i] === "\u009b") {
1407
- const match = str.slice(i).match(ANSI_RE);
1408
- if (match && match.index === 0) {
1409
- out += match[0];
1410
- i += match[0].length;
1411
- continue;
1412
- }
1413
- }
1414
- if (str[i] === "<") {
1415
- const close = str.indexOf(">", i);
1416
- if (close !== -1) {
1417
- const tag = str.slice(i, close + 1);
1418
- if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
1419
- out += tag;
1420
- i = close + 1;
1421
- continue;
1422
- }
1423
- }
1424
- }
1425
- out += str[i];
1426
- vis += 1;
1427
- i += 1;
1428
- }
1429
- return out;
1398
+ return piTruncateToWidth(str, max);
1430
1399
  }