@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 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): { text: string; clipped: boolean } {
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 text = buffer.subarray(0, bytes).toString("utf8");
2584
- if (start > 0) text = text.slice(text.indexOf("\n") + 1);
2585
- return { text, clipped: start > 0 };
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) => ({ ...readViewerLog(logPath(id)), isAgent: kindOf(id) === "subagent" }),
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
- (current ??=
50
- (blocks.push({ kind: "assistant", text: "" }),
51
- blocks[blocks.length - 1]));
52
- for (const line of raw.split("\n")) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",