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 +3 -1
- package/parse.ts +2 -58
- package/shared-log-utils.ts +104 -0
- package/shared-navigator.ts +11 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-subagents",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
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
|
+
}
|
package/shared-navigator.ts
CHANGED
|
@@ -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
|
|
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"]), "
|
|
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];
|