@yusukeshib/pi-babysit 0.6.15 → 0.6.16

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,11 @@ 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. This is the time babysit received the first byte of the line, not an
104
+ application event time. Narrow views place the date above the text. Old logs and
105
+ lines without capture metadata remain readable without invented timestamps.
101
106
  Only the selected log is loaded, bounded to the latest 1 MiB / 2,000 display
102
107
  lines with an omission notice; the full log file is unchanged. Empty logs show
103
108
  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), !isAgent), isAgent };
3679
+ },
3631
3680
  });
3632
3681
  return sessionWidget;
3633
3682
  }, { placement: "belowEditor" });
package/log-viewer.ts CHANGED
@@ -118,11 +118,18 @@ function agentBlocks(raw: string): Block[] {
118
118
  .map((b) => ({ ...b, text: sanitize(b.text) }));
119
119
  }
120
120
 
121
+ function dateLabel(timestamp: number): string {
122
+ const date = new Date(timestamp);
123
+ const pad = (n: number) => String(n).padStart(2, "0");
124
+ return `[${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}]`;
125
+ }
126
+
121
127
  export function createLogViewer(options: LogViewerOptions) {
122
128
  let header = "",
123
129
  raw = "",
124
130
  description = "",
125
131
  isAgent = false;
132
+ let lineTimestamps: (number | undefined)[] | undefined;
126
133
  let cache: string[] | undefined,
127
134
  cacheWidth = -1,
128
135
  cacheTheme: Theme | undefined;
@@ -144,6 +151,22 @@ export function createLogViewer(options: LogViewerOptions) {
144
151
  ]
145
152
  : [];
146
153
  for (const b of blocks) {
154
+ if (!isAgent && lineTimestamps?.some((time) => time !== undefined)) {
155
+ for (const [index, text] of raw.split(/\r\n|\r|\n/).entries()) {
156
+ const time = lineTimestamps[index];
157
+ const label = time === undefined ? "" : dateLabel(time);
158
+ const indent = label ? visibleWidth(label) + 1 : 0;
159
+ if (indent && width <= indent + 4) {
160
+ lines.push(...new Text(theme.fg("muted", label), 0, 0).render(width));
161
+ lines.push(...new Text(sanitize(text, true), 0, 0).render(width));
162
+ } else {
163
+ const wrapped = new Text(sanitize(text, true), 0, 0).render(width - indent);
164
+ lines.push(...wrapped.map((line, row) =>
165
+ (row === 0 && label ? theme.fg("muted", label) + " " : " ".repeat(indent)) + line));
166
+ }
167
+ }
168
+ continue;
169
+ }
147
170
  if (isAgent)
148
171
  lines.push(
149
172
  theme.fg(
@@ -321,9 +344,11 @@ export function createLogViewer(options: LogViewerOptions) {
321
344
  invalidate() {
322
345
  body.invalidate();
323
346
  },
324
- update(nextHeader: string, logText: string, agent: boolean, context = "") {
347
+ update(nextHeader: string, logText: string, agent: boolean, context = "", timestamps?: (number | undefined)[]) {
325
348
  header = nextHeader;
326
- if (raw !== logText || isAgent !== agent || description !== context) {
349
+ if (raw !== logText || isAgent !== agent || description !== context ||
350
+ JSON.stringify(lineTimestamps) !== JSON.stringify(timestamps)) {
351
+ lineTimestamps = timestamps;
327
352
  raw = logText;
328
353
  isAgent = agent;
329
354
  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.16",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",