pi-better-subagents 0.1.29 → 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 +31 -59
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.1.29",
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
 
@@ -179,7 +180,10 @@ export function ensureBackgroundWorkNavigator(ctx: ExtensionContext, deps: HostD
179
180
  if (!isNavigatorUiAvailable(ctx)) return;
180
181
  const s = state();
181
182
  s.uiCtx = ctx;
182
- s.deps = deps;
183
+ s.deps = {
184
+ ...deps,
185
+ createTranscriptComponent: deps.createTranscriptComponent ?? s.deps?.createTranscriptComponent,
186
+ };
183
187
  try { (ctx.ui as any).setWidget?.(MAIN_LIST_WIDGET_KEY, undefined); } catch { /* ignore */ }
184
188
  s.mainListWidgetInstalled = false;
185
189
  s.mainListRequestRender = undefined;
@@ -277,10 +281,10 @@ function refreshMainListWidget(): void {
277
281
  }) : undefined;
278
282
  const changed = nextSignature !== s.lastMainListSignature;
279
283
  s.lastMainListSignature = nextSignature;
280
- 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), {
281
285
  selectedId: s.mainListFocused ? s.mainListSelectedId : undefined,
282
286
  focused: s.mainListFocused === true,
283
- }) : undefined;
287
+ })] : undefined;
284
288
  if (!rows.length) {
285
289
  if (!s.mainListWidgetInstalled) return;
286
290
  try { (ctx.ui as any).setWidget?.(MAIN_LIST_WIDGET_KEY, undefined); } catch { /* ignore */ }
@@ -320,10 +324,10 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
320
324
  state().lastMainListLines = undefined;
321
325
  return [];
322
326
  }
323
- const lines = buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
327
+ const lines = ["", ...buildMainListLines(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
324
328
  selectedId: state().mainListFocused ? state().mainListSelectedId : undefined,
325
329
  focused: state().mainListFocused === true,
326
- });
330
+ })];
327
331
  state().lastMainListLines = lines;
328
332
  return lines;
329
333
  },
@@ -1266,7 +1270,8 @@ export function wrapLogText(text: string, width: number): string[] {
1266
1270
  continue;
1267
1271
  }
1268
1272
  const sourceIndent = source.match(/^[ \t]*/)?.[0] ?? "";
1269
- 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);
1270
1275
  let remaining = source.slice(sourceIndent.length);
1271
1276
  const contentWidth = Math.max(1, max - visibleWidth(indent));
1272
1277
  if (!remaining) {
@@ -1299,33 +1304,25 @@ function lastDelimiterBreak(value: string): number {
1299
1304
  return last;
1300
1305
  }
1301
1306
 
1307
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
1308
+
1302
1309
  function splitVisiblePrefix(value: string, width: number): [string, string] {
1303
1310
  const max = Math.max(0, Math.floor(width));
1304
1311
  let visible = 0;
1305
- let index = 0;
1306
- while (index < value.length && visible < max) {
1307
- if (value[index] === "\u001b" || value[index] === "\u009b") {
1308
- const match = value.slice(index).match(ANSI_RE);
1309
- if (match?.index === 0) {
1310
- index += match[0].length;
1311
- continue;
1312
- }
1313
- }
1314
- if (value[index] === "<") {
1315
- const close = value.indexOf(">", index);
1316
- if (close !== -1) {
1317
- const tag = value.slice(index, close + 1);
1318
- if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
1319
- index = close + 1;
1320
- continue;
1321
- }
1322
- }
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;
1323
1319
  }
1324
- const codePoint = value.codePointAt(index)!;
1325
- index += codePoint > 0xFFFF ? 2 : 1;
1326
- visible += 1;
1320
+ const cells = piVisibleWidth(segment);
1321
+ if (visible + cells > max) break;
1322
+ visible += cells;
1323
+ end = index + segment.length;
1327
1324
  }
1328
- return [value.slice(0, index), value.slice(index)];
1325
+ return [value.slice(0, end), value.slice(end)];
1329
1326
  }
1330
1327
 
1331
1328
  function cycleLogTailRows(current: number): number {
@@ -1384,44 +1381,19 @@ function safeTruncate(line: string, width: number, truncate: (s: string, width:
1384
1381
 
1385
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");
1386
1383
 
1384
+ const FORMATTING_PREFIX_RE = new RegExp(`^(?:${ANSI_RE.source}|<\\/?[a-zA-Z][\\w-]*>|</>)`);
1385
+
1387
1386
  function visibleWidth(value: string): number {
1388
- return String(value ?? "")
1387
+ const cleaned = String(value ?? "")
1389
1388
  .replace(ANSI_RE, "")
1390
1389
  .replace(/<\/?[a-zA-Z][\w-]*>/g, "")
1391
- .replace(/<\/>/g, "")
1392
- .length;
1390
+ .replace(/<\/>/g, "");
1391
+ return piVisibleWidth(cleaned);
1393
1392
  }
1394
1393
 
1395
1394
  function truncateVisible(value: string, width: number): string {
1396
1395
  const str = String(value ?? "");
1397
1396
  const max = Math.max(0, Math.floor(width || 0));
1398
1397
  if (visibleWidth(str) <= max) return str;
1399
- let out = "";
1400
- let vis = 0;
1401
- let i = 0;
1402
- while (i < str.length && vis < max) {
1403
- if (str[i] === "\u001b" || str[i] === "\u009b") {
1404
- const match = str.slice(i).match(ANSI_RE);
1405
- if (match && match.index === 0) {
1406
- out += match[0];
1407
- i += match[0].length;
1408
- continue;
1409
- }
1410
- }
1411
- if (str[i] === "<") {
1412
- const close = str.indexOf(">", i);
1413
- if (close !== -1) {
1414
- const tag = str.slice(i, close + 1);
1415
- if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
1416
- out += tag;
1417
- i = close + 1;
1418
- continue;
1419
- }
1420
- }
1421
- }
1422
- out += str[i];
1423
- vis += 1;
1424
- i += 1;
1425
- }
1426
- return out;
1398
+ return piTruncateToWidth(str, max);
1427
1399
  }