@yusukeshib/pi-babysit 0.6.15 → 0.6.17
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/README.md +8 -0
- package/index.ts +55 -6
- package/log-viewer.ts +65 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,14 @@ Pi's transcript shortcuts. At the bottom it follows new output; scrolling up
|
|
|
98
98
|
pauses following until you return to the bottom. The viewer resizes with the
|
|
99
99
|
terminal, leaving room for the editor/status/footer. Agent logs separate
|
|
100
100
|
assistant text, tool calls, and results; process logs show terminal output.
|
|
101
|
+
When babysit provides `output.timestamps.jsonl`, process lines begin with a muted
|
|
102
|
+
local `[YYYY-MM-DD HH:mm:ss]` capture time, with wrapped text aligned beneath the
|
|
103
|
+
message. Agent lines show the capture time of their block's first available event
|
|
104
|
+
in the same left column, with the assistant/tool/result/error label beside the
|
|
105
|
+
first content line; streamed updates and duplicate results keep that time. This
|
|
106
|
+
is the time babysit received the first byte of the event or line, not an
|
|
107
|
+
application event time. Narrow views place the date above the text. Old logs and
|
|
108
|
+
lines without capture metadata remain readable without invented timestamps.
|
|
101
109
|
Only the selected log is loaded, bounded to the latest 1 MiB / 2,000 display
|
|
102
110
|
lines with an omission notice; the full log file is unchanged. Empty logs show
|
|
103
111
|
a placeholder. Works in the released Pi 1.0 widget host. Mouse interaction
|
package/index.ts
CHANGED
|
@@ -2521,7 +2521,7 @@ export function createSessionWidget(options: {
|
|
|
2521
2521
|
getHeight: () => number;
|
|
2522
2522
|
requestRender: () => void;
|
|
2523
2523
|
getTheme: () => Theme;
|
|
2524
|
-
readLog: (id: string) => { text: string; isAgent: boolean; clipped?: boolean };
|
|
2524
|
+
readLog: (id: string) => { text: string; isAgent: boolean; clipped?: boolean; lineTimestamps?: (number | undefined)[] };
|
|
2525
2525
|
}) {
|
|
2526
2526
|
const listState: WidgetSessionListState = { mode: "running", page: 0 };
|
|
2527
2527
|
let selectedId: string | undefined;
|
|
@@ -2539,6 +2539,7 @@ export function createSessionWidget(options: {
|
|
|
2539
2539
|
log.text,
|
|
2540
2540
|
log.isAgent,
|
|
2541
2541
|
detail.startsWith("task: ") ? detail.slice(6) : detail,
|
|
2542
|
+
log.lineTimestamps,
|
|
2542
2543
|
);
|
|
2543
2544
|
}
|
|
2544
2545
|
function close() {
|
|
@@ -2571,8 +2572,44 @@ export function createSessionWidget(options: {
|
|
|
2571
2572
|
};
|
|
2572
2573
|
}
|
|
2573
2574
|
|
|
2575
|
+
/** Capture times are optional: old logs and incomplete sidecars remain readable. */
|
|
2576
|
+
function readLineTimestamps(file: string, data: Buffer, offset: number): (number | undefined)[] {
|
|
2577
|
+
const metadata = readViewerLog(path.join(path.dirname(file), "output.timestamps.jsonl")).text;
|
|
2578
|
+
const ranges: { start: number; end: number; time: number }[] = [];
|
|
2579
|
+
for (const line of metadata.split("\n")) {
|
|
2580
|
+
try {
|
|
2581
|
+
const record = JSON.parse(line);
|
|
2582
|
+
if (!Array.isArray(record) || record.length !== 3) continue;
|
|
2583
|
+
const [start, length, time] = record;
|
|
2584
|
+
if (!Number.isSafeInteger(start) || start < 0 ||
|
|
2585
|
+
!Number.isSafeInteger(length) || length <= 0 ||
|
|
2586
|
+
!Number.isSafeInteger(start + length) ||
|
|
2587
|
+
!Number.isSafeInteger(time) || time < 0 || time > 8640000000000000 ||
|
|
2588
|
+
start < (ranges.at(-1)?.end ?? 0)) continue;
|
|
2589
|
+
ranges.push({ start, end: start + length, time });
|
|
2590
|
+
} catch { /* A partial or malformed record has no trustworthy time. */ }
|
|
2591
|
+
}
|
|
2592
|
+
let range = 0;
|
|
2593
|
+
const timestamps: (number | undefined)[] = [];
|
|
2594
|
+
const addLine = (position: number) => {
|
|
2595
|
+
const absolute = offset + position;
|
|
2596
|
+
while (range < ranges.length && ranges[range].end <= absolute) range++;
|
|
2597
|
+
const entry = ranges[range];
|
|
2598
|
+
timestamps.push(position < data.length && entry && entry.start <= absolute ? entry.time : undefined);
|
|
2599
|
+
};
|
|
2600
|
+
addLine(0);
|
|
2601
|
+
for (let i = 0; i < data.length; i++) {
|
|
2602
|
+
if (data[i] !== 10 && data[i] !== 13) continue;
|
|
2603
|
+
if (data[i] === 13 && data[i + 1] === 10) i++;
|
|
2604
|
+
addLine(i + 1);
|
|
2605
|
+
}
|
|
2606
|
+
return timestamps;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2574
2609
|
/** Only the selected session is read; bounded snapshots never affect worker logs. */
|
|
2575
|
-
export function readViewerLog(file: string
|
|
2610
|
+
export function readViewerLog(file: string, withTimestamps = false): {
|
|
2611
|
+
text: string; clipped: boolean; lineTimestamps?: (number | undefined)[];
|
|
2612
|
+
} {
|
|
2576
2613
|
try {
|
|
2577
2614
|
const fd = fs.openSync(file, "r");
|
|
2578
2615
|
try {
|
|
@@ -2580,9 +2617,18 @@ export function readViewerLog(file: string): { text: string; clipped: boolean }
|
|
|
2580
2617
|
const start = Math.max(0, size - 1024 * 1024);
|
|
2581
2618
|
const buffer = Buffer.alloc(size - start);
|
|
2582
2619
|
const bytes = fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
2583
|
-
let
|
|
2584
|
-
|
|
2585
|
-
|
|
2620
|
+
let data = buffer.subarray(0, bytes);
|
|
2621
|
+
let offset = start;
|
|
2622
|
+
if (start > 0) {
|
|
2623
|
+
const skip = data.indexOf(10) + 1;
|
|
2624
|
+
data = data.subarray(skip);
|
|
2625
|
+
offset += skip;
|
|
2626
|
+
}
|
|
2627
|
+
const text = data.toString("utf8");
|
|
2628
|
+
return {
|
|
2629
|
+
text, clipped: start > 0,
|
|
2630
|
+
...(withTimestamps ? { lineTimestamps: readLineTimestamps(file, data, offset) } : {}),
|
|
2631
|
+
};
|
|
2586
2632
|
} finally { fs.closeSync(fd); }
|
|
2587
2633
|
} catch {
|
|
2588
2634
|
return { text: "", clipped: false };
|
|
@@ -3627,7 +3673,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3627
3673
|
getHeight: () => Math.max(3, tui.terminal.rows - 8),
|
|
3628
3674
|
requestRender: () => tui.requestRender(),
|
|
3629
3675
|
getTheme: () => ctx.ui.theme,
|
|
3630
|
-
readLog: (id) =>
|
|
3676
|
+
readLog: (id) => {
|
|
3677
|
+
const isAgent = kindOf(id) === "subagent";
|
|
3678
|
+
return { ...readViewerLog(logPath(id), true), isAgent };
|
|
3679
|
+
},
|
|
3631
3680
|
});
|
|
3632
3681
|
return sessionWidget;
|
|
3633
3682
|
}, { placement: "belowEditor" });
|
package/log-viewer.ts
CHANGED
|
@@ -31,7 +31,7 @@ function sanitize(text: string, sgr = false): string {
|
|
|
31
31
|
(s) => (sgr && /^\x1b\[[\d;]*m$/.test(s) ? s : ""),
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
|
-
type Block = { kind: "assistant" | "tool" | "result" | "error"; text: string };
|
|
34
|
+
type Block = { kind: "assistant" | "tool" | "result" | "error"; text: string; timestamp?: number };
|
|
35
35
|
function contentText(content: unknown): string {
|
|
36
36
|
if (typeof content === "string") return content;
|
|
37
37
|
return Array.isArray(content)
|
|
@@ -41,15 +41,17 @@ function contentText(content: unknown): string {
|
|
|
41
41
|
.join("")
|
|
42
42
|
: "";
|
|
43
43
|
}
|
|
44
|
-
function agentBlocks(raw: string): Block[] {
|
|
44
|
+
function agentBlocks(raw: string, timestamps?: (number | undefined)[]): Block[] {
|
|
45
45
|
const blocks: Block[] = [];
|
|
46
46
|
let current: Block | undefined;
|
|
47
47
|
const toolResults = new Map<string, Block>();
|
|
48
|
-
const assistant = () =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
const assistant = (timestamp: number | undefined) => {
|
|
49
|
+
current ??= (blocks.push({ kind: "assistant", text: "", timestamp }), blocks[blocks.length - 1]);
|
|
50
|
+
current.timestamp ??= timestamp;
|
|
51
|
+
return current;
|
|
52
|
+
};
|
|
53
|
+
for (const [index, line] of raw.split(/\r\n|\r|\n/).entries()) {
|
|
54
|
+
const timestamp = timestamps?.[index];
|
|
53
55
|
let e: any;
|
|
54
56
|
try {
|
|
55
57
|
e = JSON.parse(line);
|
|
@@ -62,28 +64,30 @@ function agentBlocks(raw: string): Block[] {
|
|
|
62
64
|
if (["message_start", "message_update", "message_end"].includes(e.type)) {
|
|
63
65
|
if (e.message?.role === "assistant") {
|
|
64
66
|
const text = contentText(e.message.content);
|
|
65
|
-
const b = assistant();
|
|
67
|
+
const b = assistant(timestamp);
|
|
66
68
|
if (text) b.text = text; // cumulative/final snapshots replace streamed deltas
|
|
67
69
|
if (e.message.errorMessage)
|
|
68
|
-
blocks.push({ kind: "error", text: String(e.message.errorMessage) });
|
|
70
|
+
blocks.push({ kind: "error", text: String(e.message.errorMessage), timestamp });
|
|
69
71
|
if (e.type === "message_end") current = undefined;
|
|
70
72
|
} else if (
|
|
71
73
|
!e.message &&
|
|
72
74
|
e.assistantMessageEvent?.type === "text_delta" &&
|
|
73
75
|
typeof e.assistantMessageEvent.delta === "string"
|
|
74
76
|
) {
|
|
75
|
-
assistant().text += e.assistantMessageEvent.delta;
|
|
77
|
+
assistant(timestamp).text += e.assistantMessageEvent.delta;
|
|
76
78
|
} else if (e.type === "message_end" && e.message?.role === "toolResult") {
|
|
77
79
|
const id = e.message.toolCallId;
|
|
78
80
|
const text = contentText(e.message.content);
|
|
79
81
|
if (id && toolResults.has(id)) {
|
|
80
82
|
const b = toolResults.get(id)!;
|
|
81
83
|
b.text = text;
|
|
84
|
+
b.timestamp ??= timestamp;
|
|
82
85
|
b.kind = e.message.isError ? "error" : b.kind;
|
|
83
86
|
} else {
|
|
84
87
|
const b: Block = {
|
|
85
88
|
kind: e.message.isError ? "error" : "result",
|
|
86
89
|
text,
|
|
90
|
+
timestamp,
|
|
87
91
|
};
|
|
88
92
|
blocks.push(b);
|
|
89
93
|
if (id) toolResults.set(id, b);
|
|
@@ -93,10 +97,12 @@ function agentBlocks(raw: string): Block[] {
|
|
|
93
97
|
blocks.push({
|
|
94
98
|
kind: "tool",
|
|
95
99
|
text: `${e.toolName ?? "tool"} ${JSON.stringify(e.args ?? {})}`,
|
|
100
|
+
timestamp,
|
|
96
101
|
});
|
|
97
102
|
} else if (e.type === "tool_execution_end") {
|
|
98
103
|
const b: Block = {
|
|
99
104
|
kind: e.isError ? "error" : "result",
|
|
105
|
+
timestamp,
|
|
100
106
|
text:
|
|
101
107
|
contentText(e.result?.content) ||
|
|
102
108
|
String(e.error?.message ?? e.error ?? ""),
|
|
@@ -108,6 +114,7 @@ function agentBlocks(raw: string): Block[] {
|
|
|
108
114
|
} else if (e.type === "error")
|
|
109
115
|
blocks.push({
|
|
110
116
|
kind: "error",
|
|
117
|
+
timestamp,
|
|
111
118
|
text: String(
|
|
112
119
|
e.message ?? e.error?.message ?? e.error ?? "Unknown error",
|
|
113
120
|
),
|
|
@@ -118,11 +125,18 @@ function agentBlocks(raw: string): Block[] {
|
|
|
118
125
|
.map((b) => ({ ...b, text: sanitize(b.text) }));
|
|
119
126
|
}
|
|
120
127
|
|
|
128
|
+
function dateLabel(timestamp: number): string {
|
|
129
|
+
const date = new Date(timestamp);
|
|
130
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
131
|
+
return `[${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}]`;
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
export function createLogViewer(options: LogViewerOptions) {
|
|
122
135
|
let header = "",
|
|
123
136
|
raw = "",
|
|
124
137
|
description = "",
|
|
125
138
|
isAgent = false;
|
|
139
|
+
let lineTimestamps: (number | undefined)[] | undefined;
|
|
126
140
|
let cache: string[] | undefined,
|
|
127
141
|
cacheWidth = -1,
|
|
128
142
|
cacheTheme: Theme | undefined;
|
|
@@ -134,7 +148,7 @@ export function createLogViewer(options: LogViewerOptions) {
|
|
|
134
148
|
const theme = options.getTheme();
|
|
135
149
|
if (cache && cacheWidth === width && cacheTheme === theme) return cache;
|
|
136
150
|
const blocks = isAgent
|
|
137
|
-
? agentBlocks(raw)
|
|
151
|
+
? agentBlocks(raw, lineTimestamps)
|
|
138
152
|
: [{ kind: "result" as const, text: sanitize(raw, true) }];
|
|
139
153
|
const lines: string[] = description
|
|
140
154
|
? [
|
|
@@ -144,7 +158,41 @@ export function createLogViewer(options: LogViewerOptions) {
|
|
|
144
158
|
]
|
|
145
159
|
: [];
|
|
146
160
|
for (const b of blocks) {
|
|
147
|
-
if (isAgent)
|
|
161
|
+
if (!isAgent && lineTimestamps?.some((time) => time !== undefined)) {
|
|
162
|
+
for (const [index, text] of raw.split(/\r\n|\r|\n/).entries()) {
|
|
163
|
+
const time = lineTimestamps[index];
|
|
164
|
+
const label = time === undefined ? "" : dateLabel(time);
|
|
165
|
+
const indent = label ? visibleWidth(label) + 1 : 0;
|
|
166
|
+
if (indent && width <= indent + 4) {
|
|
167
|
+
lines.push(...new Text(theme.fg("muted", label), 0, 0).render(width));
|
|
168
|
+
lines.push(...new Text(sanitize(text, true), 0, 0).render(width));
|
|
169
|
+
} else {
|
|
170
|
+
const wrapped = new Text(sanitize(text, true), 0, 0).render(width - indent);
|
|
171
|
+
lines.push(...wrapped.map((line, row) =>
|
|
172
|
+
(row === 0 && label ? theme.fg("muted", label) + " " : " ".repeat(indent)) + line));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (isAgent && b.timestamp !== undefined) {
|
|
178
|
+
const date = theme.fg("muted", dateLabel(b.timestamp));
|
|
179
|
+
const kind = theme.fg(b.kind === "error" ? "error" : b.kind === "assistant" ? "accent" : "toolTitle", `[${b.kind}]`);
|
|
180
|
+
const indent = visibleWidth(date) + 1;
|
|
181
|
+
const kindIndent = visibleWidth(kind) + 1;
|
|
182
|
+
if (width <= indent + kindIndent + 4) {
|
|
183
|
+
lines.push(...new Text(date, 0, 0).render(width));
|
|
184
|
+
lines.push(...new Text(`${kind} ${b.text}`, 0, 0).render(width));
|
|
185
|
+
} else {
|
|
186
|
+
const contentWidth = width - indent - kindIndent;
|
|
187
|
+
const rendered = b.kind === "assistant"
|
|
188
|
+
? new Markdown(b.text, 0, 0, getMarkdownTheme()).render(contentWidth)
|
|
189
|
+
: new Text(b.text, 0, 0).render(contentWidth);
|
|
190
|
+
lines.push(...rendered.map((line, row) =>
|
|
191
|
+
`${date} ${row === 0 ? kind + " " : " ".repeat(kindIndent)}${line}`));
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (isAgent) {
|
|
148
196
|
lines.push(
|
|
149
197
|
theme.fg(
|
|
150
198
|
b.kind === "error"
|
|
@@ -155,6 +203,7 @@ export function createLogViewer(options: LogViewerOptions) {
|
|
|
155
203
|
`[${b.kind}]`,
|
|
156
204
|
),
|
|
157
205
|
);
|
|
206
|
+
}
|
|
158
207
|
lines.push(
|
|
159
208
|
...(b.kind === "assistant"
|
|
160
209
|
? new Markdown(b.text, 0, 0, getMarkdownTheme()).render(width)
|
|
@@ -321,9 +370,11 @@ export function createLogViewer(options: LogViewerOptions) {
|
|
|
321
370
|
invalidate() {
|
|
322
371
|
body.invalidate();
|
|
323
372
|
},
|
|
324
|
-
update(nextHeader: string, logText: string, agent: boolean, context = "") {
|
|
373
|
+
update(nextHeader: string, logText: string, agent: boolean, context = "", timestamps?: (number | undefined)[]) {
|
|
325
374
|
header = nextHeader;
|
|
326
|
-
if (raw !== logText || isAgent !== agent || description !== context
|
|
375
|
+
if (raw !== logText || isAgent !== agent || description !== context ||
|
|
376
|
+
JSON.stringify(lineTimestamps) !== JSON.stringify(timestamps)) {
|
|
377
|
+
lineTimestamps = timestamps;
|
|
327
378
|
raw = logText;
|
|
328
379
|
isAgent = agent;
|
|
329
380
|
description = context;
|