pi-better-subagents 0.1.6 → 0.1.7

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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,6 +30,8 @@
30
30
  "access": "public"
31
31
  },
32
32
  "scripts": {
33
+ "pretest": "node ../../scripts/sync-shared-log-utils.mjs",
34
+ "prepack": "node ../../scripts/sync-shared-log-utils.mjs",
33
35
  "typecheck": "node -e \"console.log('pi-better-subagents: typecheck skipped for legacy mixed TS/MJS package')\"",
34
36
  "test": "node --import tsx --test tests/*.test.mjs",
35
37
  "test:cross-session": "node --import tsx --test --test-name-pattern \"callback session isolation\" tests/extension_health_lifecycle.test.mjs",
package/parse.ts CHANGED
@@ -23,10 +23,10 @@
23
23
  import {
24
24
  closeSync,
25
25
  openSync,
26
- readFileSync,
27
26
  readSync,
28
27
  statSync,
29
28
  } from "node:fs";
29
+ import { readBoundedTail } from "./shared-log-utils.ts";
30
30
  import { readAppendedLines, type LogCursor } from "./log-cursor.ts";
31
31
  import { logPathFor } from "./registry.ts";
32
32
 
@@ -87,63 +87,7 @@ interface TailRead {
87
87
  * Read at most `maxBytes` from the end of `path`. Avoids `readFileSync` so logs
88
88
  * larger than Node's max string length can still be tailed for live output.
89
89
  */
90
- function readTail(path: string, maxBytes: number): TailRead {
91
- let totalBytes = 0;
92
- try {
93
- totalBytes = statSync(path).size;
94
- } catch {
95
- return { text: "", truncated: false, totalBytes: 0, error: "log not found" };
96
- }
97
-
98
- if (totalBytes === 0) {
99
- return { text: "", truncated: false, totalBytes: 0 };
100
- }
101
-
102
- if (totalBytes <= maxBytes) {
103
- try {
104
- return { text: readFileSync(path, "utf-8"), truncated: false, totalBytes };
105
- } catch (e) {
106
- return {
107
- text: "",
108
- truncated: false,
109
- totalBytes,
110
- error: `read failed: ${(e as Error).message}`,
111
- };
112
- }
113
- }
114
-
115
- let fd: number;
116
- try {
117
- fd = openSync(path, "r");
118
- } catch (e) {
119
- return {
120
- text: "",
121
- truncated: true,
122
- totalBytes,
123
- error: `open failed: ${(e as Error).message}`,
124
- };
125
- }
126
-
127
- const buf = Buffer.alloc(maxBytes);
128
- const offset = totalBytes - maxBytes;
129
- let read = 0;
130
- try {
131
- read = readSync(fd, buf, 0, maxBytes, offset);
132
- } catch (e) {
133
- closeSync(fd);
134
- return {
135
- text: "",
136
- truncated: true,
137
- totalBytes,
138
- error: `tail read failed: ${(e as Error).message}`,
139
- };
140
- }
141
- closeSync(fd);
142
-
143
- const text = buf.toString("utf-8", 0, read);
144
-
145
- return { text, truncated: true, totalBytes };
146
- }
90
+ const readTail: (path: string, maxBytes: number) => TailRead = readBoundedTail;
147
91
 
148
92
  /** Last `n` lines of a run's log, or a placeholder if empty/unreadable. */
149
93
  export function tailLog(id: string, n: number, maxBytes = maxRawTailBytes()): string {
@@ -0,0 +1,104 @@
1
+ // Generated from packages/log-utils/index.ts. Do not edit directly.
2
+ import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
3
+
4
+ export interface TailRead {
5
+ text: string;
6
+ truncated: boolean;
7
+ totalBytes: number;
8
+ error?: string;
9
+ }
10
+
11
+ function errorText(error: unknown): string {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+
15
+ /**
16
+ * Read no more than `maxBytes` from a file's end. This avoids whole-file
17
+ * allocation for live logs that can grow past Node's string-size limit.
18
+ */
19
+ export function readBoundedTail(path: string, maxBytes: number): TailRead {
20
+ const budget = Math.max(1, Math.floor(maxBytes));
21
+ let totalBytes: number;
22
+ try {
23
+ totalBytes = statSync(path).size;
24
+ } catch (error) {
25
+ return { text: "", truncated: false, totalBytes: 0, error: errorText(error) };
26
+ }
27
+ if (totalBytes === 0) return { text: "", truncated: false, totalBytes };
28
+ if (totalBytes <= budget) {
29
+ try {
30
+ return { text: readFileSync(path, "utf8"), truncated: false, totalBytes };
31
+ } catch (error) {
32
+ return { text: "", truncated: false, totalBytes, error: errorText(error) };
33
+ }
34
+ }
35
+
36
+ let fd: number | undefined;
37
+ try {
38
+ fd = openSync(path, "r");
39
+ const buffer = Buffer.allocUnsafe(budget);
40
+ const start = totalBytes - budget;
41
+ let offset = 0;
42
+ while (offset < budget) {
43
+ const read = readSync(fd, buffer, offset, budget - offset, start + offset);
44
+ if (read <= 0) break;
45
+ offset += read;
46
+ }
47
+ return { text: buffer.toString("utf8", 0, offset), truncated: true, totalBytes };
48
+ } catch (error) {
49
+ return { text: "", truncated: true, totalBytes, error: errorText(error) };
50
+ } finally {
51
+ if (fd !== undefined) {
52
+ try { closeSync(fd); } catch { /* best effort */ }
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Convert terminal-like output into display rows. A bare carriage return is a
59
+ * cursor reset, so repeated progress redraws collapse to their latest state;
60
+ * CRLF remains a normal newline. Individual rows are capped defensively.
61
+ */
62
+ export function terminalDisplayRows(text: string, maxRowChars = 8 * 1024): string[] {
63
+ const rowLimit = Math.max(64, Math.floor(maxRowChars));
64
+ const rows: string[] = [];
65
+ let current = "";
66
+ let progress = "";
67
+
68
+ const append = (value: string) => {
69
+ current += value;
70
+ if (current.length > rowLimit) current = `...${current.slice(-(rowLimit - 3))}`;
71
+ };
72
+ const emit = () => {
73
+ const value = current || progress;
74
+ if (value) rows.push(value);
75
+ current = "";
76
+ progress = "";
77
+ };
78
+
79
+ for (let i = 0; i < text.length; i += 1) {
80
+ const char = text[i]!;
81
+ if (char === "\r") {
82
+ if (text[i + 1] === "\n") {
83
+ emit();
84
+ i += 1;
85
+ } else {
86
+ progress = current || progress;
87
+ current = "";
88
+ }
89
+ } else if (char === "\n") {
90
+ emit();
91
+ } else {
92
+ append(char);
93
+ }
94
+ }
95
+ const final = current || progress;
96
+ if (final) rows.push(final);
97
+ return rows;
98
+ }
99
+
100
+ export function tailTerminalDisplay(text: string, rows: number, maxRowChars?: number): string {
101
+ const rendered = terminalDisplayRows(text, maxRowChars);
102
+ const count = Math.max(1, Math.floor(rows));
103
+ return rendered.slice(-count).join("\n");
104
+ }
@@ -35,7 +35,7 @@ export type BackgroundWorkDetail = {
35
35
  statusTone: BackgroundWorkStatusTone;
36
36
  subtitle?: string;
37
37
  metadata: Array<{ label: string; value: string }>;
38
- foldedSections?: Array<{ id: string; label: string; text: string; collapsedText?: string }>;
38
+ foldedSections?: Array<{ id: string; label: string; text: string; collapsedText?: string; expandedByDefault?: boolean }>;
39
39
  evidence: { label: string; text: string };
40
40
  footerActions?: string[];
41
41
  };
@@ -94,7 +94,7 @@ export const MAIN_LIST_WIDGET_KEY = "background-work-list";
94
94
  export const DETAIL_TICK_MS = 1000;
95
95
  export const CLOSE_ARM_MS = 3000;
96
96
  export const DEFAULT_LOG_TAIL_ROWS = 10;
97
- export const LOG_TAIL_ROW_CHOICES = [10, 25, 50, 100] as const;
97
+ export const LOG_TAIL_ROW_CHOICES = [10, 25] as const;
98
98
  // Row rebuild cadence. Each tick asks every provider for rows, which stats and
99
99
  // parses run logs, so this is a per-provider I/O cadence and not a paint rate:
100
100
  // the widget repaints from render() whenever the TUI asks. 1 Hz matches
@@ -671,6 +671,7 @@ function createOverlayComponent(
671
671
  const expandedSections = new Set<string>();
672
672
  let detailId: string | null = initialDetailId ?? null;
673
673
  let detail: BackgroundWorkDetail | null = detailId ? (detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? null) : null;
674
+ applyDefaultExpandedSections(detail, expandedSections);
674
675
  if (detailId) {
675
676
  const idx = overlayState.rows.findIndex((row) => row.navigatorId === detailId);
676
677
  if (idx >= 0) overlayState.selected = idx;
@@ -729,6 +730,7 @@ function createOverlayComponent(
729
730
  detailId = row.navigatorId;
730
731
  expandedSections.clear();
731
732
  detail = detailFor(row.navigatorId, Date.now(), { logTailLines: logTailRows }) ?? fallbackDetail(row);
733
+ applyDefaultExpandedSections(detail, expandedSections);
732
734
  mode = "detail";
733
735
  startDetailTimer();
734
736
  requestRender();
@@ -815,16 +817,6 @@ function createOverlayComponent(
815
817
  requestRender();
816
818
  }
817
819
  }
818
- else if (data === "[") {
819
- logTailRows = previousLogTailRows(logTailRows);
820
- if (detailId) detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
821
- requestRender();
822
- }
823
- else if (data === "]") {
824
- logTailRows = nextLogTailRows(logTailRows);
825
- if (detailId) detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
826
- requestRender();
827
- }
828
820
  else if (data === "l" || data === "L") {
829
821
  logTailRows = cycleLogTailRows(logTailRows);
830
822
  if (detailId) detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
@@ -939,7 +931,7 @@ function buildDetailLines(
939
931
  const foldedAction = toggleableSectionId
940
932
  ? (options.expandedSections?.has(toggleableSectionId) ? "Enter collapse" : "Enter expand")
941
933
  : null;
942
- const actions = [foldedAction, ...(detail.footerActions?.length ? detail.footerActions : ["x close"]), "[ fewer", "] more", "l cycle", "Esc close"].filter(Boolean).join(" · ");
934
+ const actions = [foldedAction, ...(detail.footerActions?.length ? detail.footerActions : ["x close"]), "l 10/25", "Esc close"].filter(Boolean).join(" · ");
943
935
  lines.push(dim(` ← back · ${actions}`, fg));
944
936
  lines.push("");
945
937
  lines.push(` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`);
@@ -1004,6 +996,12 @@ function firstToggleableSectionId(detail: BackgroundWorkDetail | null | undefine
1004
996
  return detail && isFoldableEvidence(detail) ? EVIDENCE_SECTION_ID : undefined;
1005
997
  }
1006
998
 
999
+ function applyDefaultExpandedSections(detail: BackgroundWorkDetail | null, expandedSections: Set<string>): void {
1000
+ for (const section of detail?.foldedSections ?? []) {
1001
+ if (section.expandedByDefault) expandedSections.add(section.id);
1002
+ }
1003
+ }
1004
+
1007
1005
  function isFoldableEvidence(detail: BackgroundWorkDetail): boolean {
1008
1006
  return !/log/i.test(detail.evidence.label);
1009
1007
  }
@@ -1043,19 +1041,6 @@ function wrapEvidenceText(text: string, width: number): string[] {
1043
1041
  return rows.length ? rows : ["(no output yet)"];
1044
1042
  }
1045
1043
 
1046
- function nextLogTailRows(current: number): number {
1047
- for (const value of LOG_TAIL_ROW_CHOICES) if (value > current) return value;
1048
- return LOG_TAIL_ROW_CHOICES[LOG_TAIL_ROW_CHOICES.length - 1];
1049
- }
1050
-
1051
- function previousLogTailRows(current: number): number {
1052
- for (let i = LOG_TAIL_ROW_CHOICES.length - 1; i >= 0; i -= 1) {
1053
- const value = LOG_TAIL_ROW_CHOICES[i]!;
1054
- if (value < current) return value;
1055
- }
1056
- return LOG_TAIL_ROW_CHOICES[0];
1057
- }
1058
-
1059
1044
  function cycleLogTailRows(current: number): number {
1060
1045
  const idx = LOG_TAIL_ROW_CHOICES.findIndex((value) => value === current);
1061
1046
  return LOG_TAIL_ROW_CHOICES[(idx + 1) % LOG_TAIL_ROW_CHOICES.length];