@misha_misha/agentwatch 0.0.1 → 0.0.2

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  detectAgents
4
- } from "./chunk-LF76VUCL.js";
4
+ } from "./chunk-5QNDC2VP.js";
5
5
  import {
6
6
  claudeProjectsDir,
7
7
  detectWorkspaceRoot
@@ -12,12 +12,16 @@ import { render } from "ink";
12
12
 
13
13
  // src/ui/App.tsx
14
14
  import { useEffect, useReducer, useState } from "react";
15
- import { Box as Box5, Text as Text5, useApp, useInput } from "ink";
15
+ import { Box as Box10, Text as Text10, useApp, useInput, useStdout } from "ink";
16
16
 
17
17
  // src/ui/Timeline.tsx
18
18
  import { Box, Text } from "ink";
19
19
  import { jsx, jsxs } from "react/jsx-runtime";
20
- function Timeline({ events }) {
20
+ function Timeline({
21
+ events,
22
+ selectedIdx,
23
+ childCountByAgentId
24
+ }) {
21
25
  const header = /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { bold: true, dimColor: true, children: [
22
26
  "TIME ",
23
27
  pad("AGENT", 10),
@@ -32,16 +36,32 @@ function Timeline({ events }) {
32
36
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "waiting for activity\u2026 use Claude Code or edit a file in your workspace" }) })
33
37
  ] });
34
38
  }
35
- const visible = events.slice(0, 40);
39
+ const windowStart = selectedIdx != null && selectedIdx > 30 ? Math.max(0, selectedIdx - 15) : 0;
40
+ const visible = events.slice(windowStart, windowStart + 40);
36
41
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
37
42
  header,
38
- visible.map((e) => /* @__PURE__ */ jsx(EventRow, { event: e }, e.id))
43
+ visible.map((e, i) => /* @__PURE__ */ jsx(
44
+ EventRow,
45
+ {
46
+ event: e,
47
+ selected: windowStart + i === selectedIdx,
48
+ childCount: e.details?.subAgentId ? childCountByAgentId?.get(e.details.subAgentId) ?? 0 : 0
49
+ },
50
+ e.id
51
+ ))
39
52
  ] });
40
53
  }
41
- function EventRow({ event }) {
54
+ function EventRow({
55
+ event,
56
+ selected,
57
+ childCount
58
+ }) {
42
59
  const time = event.ts.slice(11, 19);
43
- const line = event.summary ?? event.path ?? event.cmd ?? event.tool ?? event.type;
44
- return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
60
+ const baseLine = event.summary ?? event.path ?? event.cmd ?? event.tool ?? event.type;
61
+ const duration = event.details?.durationMs != null ? ` \xB7 ${formatMs(event.details.durationMs)}` : "";
62
+ const err = event.details?.toolError ? " \xB7 ERR" : "";
63
+ const marker = childCount > 0 ? ` \u25B8 ${childCount} child events` : "";
64
+ return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { wrap: "truncate", inverse: selected, children: [
45
65
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
46
66
  time,
47
67
  " "
@@ -54,9 +74,17 @@ function EventRow({ event }) {
54
74
  pad(event.type, 13),
55
75
  " "
56
76
  ] }),
57
- /* @__PURE__ */ jsx(Text, { children: line })
77
+ /* @__PURE__ */ jsx(Text, { children: baseLine }),
78
+ duration && /* @__PURE__ */ jsx(Text, { dimColor: true, children: duration }),
79
+ err && /* @__PURE__ */ jsx(Text, { color: "red", children: err }),
80
+ childCount > 0 && /* @__PURE__ */ jsx(Text, { color: "yellow", children: marker })
58
81
  ] }) });
59
82
  }
83
+ function formatMs(ms) {
84
+ if (ms < 1e3) return `${ms}ms`;
85
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
86
+ return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s`;
87
+ }
60
88
  function agentColor(a) {
61
89
  switch (a) {
62
90
  case "claude-code":
@@ -91,19 +119,22 @@ function AgentPanel({ agents, events }) {
91
119
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
92
120
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: "Agents" }),
93
121
  agents.map((a) => {
94
- const count = events.filter((e) => e.agent === a.name).length;
95
- const last = events.find((e) => e.agent === a.name);
122
+ const forAgent = events.filter((e) => e.agent === a.name);
123
+ const count = forAgent.length;
124
+ const last = forAgent[0];
125
+ const dotColor = !a.present ? "gray" : a.instrumented ? "green" : "yellow";
126
+ const statusLabel = !a.present ? "not detected" : a.instrumented ? "installed" : "detected (events TBD)";
96
127
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
97
- /* @__PURE__ */ jsxs2(Text2, { color: a.present ? "green" : "gray", children: [
128
+ /* @__PURE__ */ jsxs2(Text2, { color: dotColor, children: [
98
129
  a.present ? "\u25CF" : "\u25CB",
99
130
  " ",
100
131
  a.label
101
132
  ] }),
102
133
  /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
103
134
  " ",
104
- a.present ? "installed" : "not detected"
135
+ statusLabel
105
136
  ] }),
106
- a.present && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
137
+ a.present && a.instrumented && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
107
138
  " ",
108
139
  "events: ",
109
140
  count,
@@ -143,78 +174,978 @@ function Header({ workspace, eventCount, filter, paused }) {
143
174
  // src/ui/PermissionView.tsx
144
175
  import { Box as Box4, Text as Text4 } from "ink";
145
176
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
146
- function PermissionView({ permissions }) {
147
- if (permissions.length === 0) {
148
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "double", paddingX: 1, children: [
149
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Permissions \u2014 Claude Code" }),
150
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No settings.json found at ~/.claude/ or project .claude/" }),
151
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press p to close." })
152
- ] });
153
- }
177
+ function PermissionView({
178
+ claude,
179
+ cursor,
180
+ openclaw,
181
+ viewportRows,
182
+ scrollOffset
183
+ }) {
184
+ const rows = buildRows(claude, cursor, openclaw);
185
+ const height = Math.max(3, viewportRows);
186
+ const maxScroll = Math.max(0, rows.length - height);
187
+ const offset = Math.min(scrollOffset, maxScroll);
188
+ const visible = rows.slice(offset, offset + height);
154
189
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "double", paddingX: 1, children: [
155
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Permissions \u2014 Claude Code" }),
156
- permissions.map((p) => /* @__PURE__ */ jsx4(Block, { perms: p }, p.source)),
157
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press p to close. These are the permissions Claude Code uses on your machine." }) })
190
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Permissions / Configuration across installed agents" }),
191
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: visible.map((row, i) => /* @__PURE__ */ jsx4(RowView, { row }, i)) }),
192
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
193
+ rows.length > height ? `${offset + 1}\u2013${offset + visible.length} of ${rows.length} [\u2191\u2193] scroll ` : "",
194
+ "[p] close [q] quit"
195
+ ] }) })
158
196
  ] });
159
197
  }
160
- function Block({ perms }) {
161
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
162
- /* @__PURE__ */ jsxs4(Text4, { children: [
163
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "source: " }),
164
- /* @__PURE__ */ jsx4(Text4, { children: perms.source })
198
+ function RowView({ row }) {
199
+ switch (row.kind) {
200
+ case "h1":
201
+ return /* @__PURE__ */ jsxs4(Text4, { bold: true, color: row.color ?? "cyan", children: [
202
+ "\u2501 ",
203
+ row.text,
204
+ " \u2501"
205
+ ] });
206
+ case "h2":
207
+ return /* @__PURE__ */ jsx4(Text4, { bold: true, color: row.color ?? "white", children: row.text });
208
+ case "kv":
209
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
210
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
211
+ row.label,
212
+ ": "
213
+ ] }),
214
+ /* @__PURE__ */ jsx4(Text4, { color: row.valueColor, children: row.value })
215
+ ] });
216
+ case "item":
217
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
218
+ " ",
219
+ /* @__PURE__ */ jsx4(Text4, { color: row.markColor, children: row.mark }),
220
+ /* @__PURE__ */ jsxs4(Text4, { children: [
221
+ " ",
222
+ row.text
223
+ ] })
224
+ ] });
225
+ case "text":
226
+ return /* @__PURE__ */ jsx4(Text4, { color: row.color, dimColor: row.dim, children: row.text || " " });
227
+ case "blank":
228
+ return /* @__PURE__ */ jsx4(Text4, { children: " " });
229
+ }
230
+ }
231
+ function buildRows(claude, cursor, openclaw) {
232
+ const rows = [];
233
+ rows.push({ kind: "h1", text: "Claude Code", color: "cyan" });
234
+ if (claude.length === 0) {
235
+ rows.push({ kind: "text", text: " No settings.json found.", dim: true });
236
+ } else {
237
+ for (const perms of claude) {
238
+ rows.push({ kind: "blank" });
239
+ rows.push({ kind: "kv", label: "source", value: perms.source });
240
+ rows.push({
241
+ kind: "kv",
242
+ label: "defaultMode",
243
+ value: perms.defaultMode,
244
+ valueColor: modeColor(perms.defaultMode)
245
+ });
246
+ rows.push({ kind: "blank" });
247
+ rows.push({
248
+ kind: "h2",
249
+ text: `CAN (${perms.allow.length})`,
250
+ color: "green"
251
+ });
252
+ if (perms.allow.length === 0) {
253
+ rows.push({
254
+ kind: "text",
255
+ text: " (none \u2014 defaultMode applies)",
256
+ dim: true
257
+ });
258
+ } else {
259
+ for (const a of perms.allow)
260
+ rows.push({ kind: "item", mark: "\u2713", markColor: "green", text: a });
261
+ }
262
+ rows.push({ kind: "blank" });
263
+ rows.push({
264
+ kind: "h2",
265
+ text: `CANNOT (${perms.deny.length})`,
266
+ color: "red"
267
+ });
268
+ if (perms.deny.length === 0) {
269
+ rows.push({
270
+ kind: "text",
271
+ text: " (none \u2014 no explicit denies)",
272
+ dim: true
273
+ });
274
+ } else {
275
+ for (const d of perms.deny)
276
+ rows.push({ kind: "item", mark: "\u2717", markColor: "red", text: d });
277
+ }
278
+ if (perms.flags.length > 0) {
279
+ rows.push({ kind: "blank" });
280
+ rows.push({ kind: "h2", text: "\u26A0 Flags", color: "yellow" });
281
+ for (const f of perms.flags) {
282
+ rows.push({
283
+ kind: "item",
284
+ mark: f.level === "risk" ? "\u2717" : "!",
285
+ markColor: f.level === "risk" ? "red" : "yellow",
286
+ text: f.message
287
+ });
288
+ }
289
+ }
290
+ }
291
+ }
292
+ rows.push({ kind: "blank" });
293
+ rows.push({ kind: "h1", text: "Cursor", color: "magenta" });
294
+ if (!cursor?.installed) {
295
+ rows.push({ kind: "text", text: " not detected", dim: true });
296
+ } else {
297
+ rows.push({ kind: "blank" });
298
+ if (cursor.permissions) {
299
+ rows.push({
300
+ kind: "kv",
301
+ label: "approvalMode",
302
+ value: cursor.permissions.approvalMode,
303
+ valueColor: modeColor(cursor.permissions.approvalMode)
304
+ });
305
+ rows.push({
306
+ kind: "kv",
307
+ label: "sandbox",
308
+ value: cursor.permissions.sandboxMode,
309
+ valueColor: cursor.permissions.sandboxMode === "disabled" ? "red" : "green"
310
+ });
311
+ rows.push({
312
+ kind: "text",
313
+ text: ` allow: ${cursor.permissions.allowCount} deny: ${cursor.permissions.denyCount}`
314
+ });
315
+ }
316
+ rows.push({
317
+ kind: "kv",
318
+ label: "MCP servers",
319
+ value: cursor.mcpServers.length === 0 ? "none" : `${cursor.mcpServers.length} (${cursor.mcpServers.join(", ")})`
320
+ });
321
+ rows.push({
322
+ kind: "kv",
323
+ label: ".cursorrules discovered",
324
+ value: String(cursor.cursorRulesFiles.length)
325
+ });
326
+ for (const f of cursor.cursorRulesFiles.slice(0, 10))
327
+ rows.push({ kind: "text", text: ` \u2022 ${f}`, dim: true });
328
+ }
329
+ rows.push({ kind: "blank" });
330
+ rows.push({ kind: "h1", text: "OpenClaw", color: "yellow" });
331
+ if (!openclaw) {
332
+ rows.push({ kind: "text", text: " not detected", dim: true });
333
+ } else {
334
+ rows.push({ kind: "blank" });
335
+ rows.push({ kind: "kv", label: "source", value: openclaw.source });
336
+ if (openclaw.defaultWorkspace) {
337
+ rows.push({
338
+ kind: "kv",
339
+ label: "default workspace",
340
+ value: openclaw.defaultWorkspace
341
+ });
342
+ }
343
+ rows.push({
344
+ kind: "text",
345
+ text: " OpenClaw runs with broad shell + file access per agent. No allow/deny list \u2014 scope is the workspace path.",
346
+ dim: true
347
+ });
348
+ rows.push({ kind: "blank" });
349
+ rows.push({
350
+ kind: "h2",
351
+ text: `Sub-agents (${openclaw.agents.length})`
352
+ });
353
+ if (openclaw.agents.length === 0) {
354
+ rows.push({ kind: "text", text: " (none configured)", dim: true });
355
+ } else {
356
+ for (const a of openclaw.agents) {
357
+ rows.push({ kind: "blank" });
358
+ rows.push({
359
+ kind: "text",
360
+ text: `${a.emoji ?? "\u2022"} ${a.name ?? a.id} (id: ${a.id}${a.default ? ", default" : ""})`
361
+ });
362
+ if (a.model) rows.push({ kind: "text", text: ` model: ${a.model}`, dim: true });
363
+ if (a.workspace)
364
+ rows.push({ kind: "text", text: ` workspace: ${a.workspace}`, dim: true });
365
+ }
366
+ }
367
+ }
368
+ rows.push({ kind: "blank" });
369
+ rows.push({
370
+ kind: "text",
371
+ text: "Gemini CLI exposes no permission model beyond auth, so it is omitted.",
372
+ dim: true
373
+ });
374
+ return rows;
375
+ }
376
+ function modeColor(mode) {
377
+ if (mode === "auto" || mode === "bypassPermissions") return "red";
378
+ if (mode === "ask" || mode === "allowlist") return "green";
379
+ return "yellow";
380
+ }
381
+ function permissionRowCount(claude, cursor, openclaw) {
382
+ return buildRows(claude, cursor, openclaw).length;
383
+ }
384
+
385
+ // src/ui/EventDetail.tsx
386
+ import { Box as Box5, Text as Text5 } from "ink";
387
+
388
+ // src/util/cost.ts
389
+ var RATES = {
390
+ "claude-opus-4-6": {
391
+ input: 15,
392
+ cacheCreate: 18.75,
393
+ cacheRead: 1.5,
394
+ output: 75
395
+ },
396
+ "claude-sonnet-4-6": {
397
+ input: 3,
398
+ cacheCreate: 3.75,
399
+ cacheRead: 0.3,
400
+ output: 15
401
+ },
402
+ "claude-haiku-4-5": {
403
+ input: 1,
404
+ cacheCreate: 1.25,
405
+ cacheRead: 0.1,
406
+ output: 5
407
+ },
408
+ // Fallback for unknown / synthetic models
409
+ default: {
410
+ input: 3,
411
+ cacheCreate: 3.75,
412
+ cacheRead: 0.3,
413
+ output: 15
414
+ }
415
+ };
416
+ function costOf(model, u) {
417
+ const rate = RATES[normalizeModel(model)] ?? RATES.default;
418
+ return (u.input * rate.input + u.cacheCreate * rate.cacheCreate + u.cacheRead * rate.cacheRead + u.output * rate.output) / 1e6;
419
+ }
420
+ function formatUSD(n) {
421
+ if (n === 0) return "$0";
422
+ if (n < 0.01) return `$${n.toFixed(4)}`;
423
+ if (n < 1) return `$${n.toFixed(3)}`;
424
+ return `$${n.toFixed(2)}`;
425
+ }
426
+ function normalizeModel(model) {
427
+ return model.replace(/\[.*?\]$/, "").toLowerCase();
428
+ }
429
+ function parseUsage(obj) {
430
+ if (!obj || typeof obj !== "object") return null;
431
+ const o = obj;
432
+ const input = typeof o.input_tokens === "number" ? o.input_tokens : 0;
433
+ const cacheCreate = typeof o.cache_creation_input_tokens === "number" ? o.cache_creation_input_tokens : 0;
434
+ const cacheRead = typeof o.cache_read_input_tokens === "number" ? o.cache_read_input_tokens : 0;
435
+ const output = typeof o.output_tokens === "number" ? o.output_tokens : 0;
436
+ if (input + cacheCreate + cacheRead + output === 0) return null;
437
+ return { input, cacheCreate, cacheRead, output };
438
+ }
439
+
440
+ // src/ui/EventDetail.tsx
441
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
442
+ function EventDetail({ event, width, height, scrollOffset }) {
443
+ const rows = buildRows2(event, width);
444
+ const visible = rows.slice(scrollOffset, scrollOffset + height - 4);
445
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "double", paddingX: 1, children: [
446
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: colorFor(event), children: [
447
+ event.ts.slice(11, 19),
448
+ " \u2014 ",
449
+ event.agent,
450
+ " \u2014 ",
451
+ event.type,
452
+ event.tool ? ` (${event.tool})` : ""
453
+ ] }),
454
+ event.path && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
455
+ "path: ",
456
+ event.path
165
457
  ] }),
166
- /* @__PURE__ */ jsxs4(Text4, { children: [
167
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "defaultMode: " }),
168
- /* @__PURE__ */ jsx4(Text4, { color: modeColor(perms.defaultMode), children: perms.defaultMode })
458
+ event.cmd && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
459
+ "cmd: ",
460
+ truncateLine(event.cmd, width - 6)
461
+ ] }),
462
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: visible.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(no additional content captured for this event)" }) : visible.map((r, i) => /* @__PURE__ */ jsx5(Row, { row: r }, i)) }),
463
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
464
+ rows.length > height - 4 ? `${scrollOffset + 1}\u2013${Math.min(scrollOffset + height - 4, rows.length)} of ${rows.length} \u2191\u2193 scroll ` : "",
465
+ "[esc] close"
466
+ ] }) })
467
+ ] });
468
+ }
469
+ function buildRows2(event, width) {
470
+ const d = event.details;
471
+ const rows = [];
472
+ const max = Math.max(40, width - 4);
473
+ if (d?.usage || d?.cost != null || d?.durationMs != null) {
474
+ rows.push({ kind: "heading", text: "tokens / cost / duration" });
475
+ const u = d?.usage;
476
+ if (u) {
477
+ rows.push({
478
+ kind: "text",
479
+ text: `in=${u.input} cache_create=${u.cacheCreate} cache_read=${u.cacheRead} out=${u.output}`,
480
+ dim: true
481
+ });
482
+ }
483
+ if (d?.cost != null) {
484
+ rows.push({
485
+ kind: "text",
486
+ text: `cost: ${formatUSD(d.cost)}${d.model ? ` (${d.model})` : ""}`,
487
+ dim: true
488
+ });
489
+ }
490
+ if (d?.durationMs != null) {
491
+ rows.push({
492
+ kind: "text",
493
+ text: `duration: ${formatDuration(d.durationMs)}${d.toolError ? " \u2014 ERROR" : ""}`,
494
+ dim: true
495
+ });
496
+ }
497
+ }
498
+ if (d?.toolResult) {
499
+ rows.push({
500
+ kind: "heading",
501
+ text: d.toolError ? "tool result (error)" : "tool result"
502
+ });
503
+ for (const l of wrap(d.toolResult, max)) rows.push({ kind: "text", text: l });
504
+ }
505
+ if (d?.fullText) {
506
+ rows.push({ kind: "heading", text: "text" });
507
+ for (const l of wrap(d.fullText, max)) rows.push({ kind: "text", text: l });
508
+ }
509
+ if (d?.thinking) {
510
+ rows.push({ kind: "heading", text: "extended thinking" });
511
+ for (const l of wrap(d.thinking, max))
512
+ rows.push({ kind: "text", text: l, dim: true });
513
+ }
514
+ if (d?.toolInput) {
515
+ rows.push({ kind: "heading", text: "tool input" });
516
+ const pretty = JSON.stringify(d.toolInput, null, 2);
517
+ for (const l of pretty.split("\n"))
518
+ for (const w of wrap(l, max))
519
+ rows.push({ kind: "text", text: w });
520
+ }
521
+ if (d?.toolUseId) {
522
+ rows.push({ kind: "text", text: "", dim: true });
523
+ rows.push({ kind: "text", text: `tool_use_id: ${d.toolUseId}`, dim: true });
524
+ }
525
+ return rows;
526
+ }
527
+ function Row({ row }) {
528
+ if (row.kind === "heading") {
529
+ return /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "cyan", children: [
530
+ "\u2014 ",
531
+ row.text,
532
+ " \u2014"
533
+ ] }) });
534
+ }
535
+ return /* @__PURE__ */ jsx5(Text5, { dimColor: row.dim, children: row.text || " " });
536
+ }
537
+ function wrap(text, width) {
538
+ const out = [];
539
+ for (const line of text.split("\n")) {
540
+ if (line.length <= width) {
541
+ out.push(line);
542
+ continue;
543
+ }
544
+ let rest = line;
545
+ while (rest.length > width) {
546
+ out.push(rest.slice(0, width));
547
+ rest = rest.slice(width);
548
+ }
549
+ if (rest) out.push(rest);
550
+ }
551
+ return out;
552
+ }
553
+ function truncateLine(s, n) {
554
+ return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
555
+ }
556
+ function formatDuration(ms) {
557
+ if (ms < 1e3) return `${ms}ms`;
558
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
559
+ return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s`;
560
+ }
561
+ function colorFor(e) {
562
+ switch (e.agent) {
563
+ case "claude-code":
564
+ return "cyan";
565
+ case "openclaw":
566
+ return "yellow";
567
+ case "cursor":
568
+ return "magenta";
569
+ case "codex":
570
+ return "green";
571
+ case "gemini":
572
+ return "blue";
573
+ default:
574
+ return "white";
575
+ }
576
+ }
577
+ function totalDetailRows(event, width) {
578
+ return buildRows2(event, width).length;
579
+ }
580
+
581
+ // src/ui/ProjectsView.tsx
582
+ import { Box as Box6, Text as Text6 } from "ink";
583
+
584
+ // src/util/project-index.ts
585
+ function buildProjectIndex(events) {
586
+ const byName = /* @__PURE__ */ new Map();
587
+ for (const e of events) {
588
+ const name = extractProjectName(e);
589
+ if (!name) continue;
590
+ let row = byName.get(name);
591
+ if (!row) {
592
+ row = {
593
+ name,
594
+ events: 0,
595
+ byAgent: /* @__PURE__ */ new Map(),
596
+ sessions: /* @__PURE__ */ new Set(),
597
+ cost: 0,
598
+ lastTs: e.ts
599
+ };
600
+ byName.set(name, row);
601
+ }
602
+ row.events += 1;
603
+ row.byAgent.set(e.agent, (row.byAgent.get(e.agent) ?? 0) + 1);
604
+ if (e.sessionId) row.sessions.add(e.sessionId);
605
+ if (e.details?.cost) row.cost += e.details.cost;
606
+ if (e.ts > row.lastTs) row.lastTs = e.ts;
607
+ }
608
+ const rows = Array.from(byName.values());
609
+ rows.sort((a, b) => a.lastTs < b.lastTs ? 1 : -1);
610
+ return rows;
611
+ }
612
+ function extractProjectName(e) {
613
+ const s = e.summary ?? "";
614
+ const m = s.match(/^\[([^\]/ ]+)/);
615
+ if (m) return m[1] ?? null;
616
+ return null;
617
+ }
618
+ function agoFromNow(iso) {
619
+ const then = new Date(iso).getTime();
620
+ const diff = Date.now() - then;
621
+ if (diff < 6e4) return "just now";
622
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
623
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
624
+ return `${Math.floor(diff / 864e5)}d ago`;
625
+ }
626
+ function buildSessionRows(events, project) {
627
+ const byId = /* @__PURE__ */ new Map();
628
+ for (const e of events) {
629
+ const p = (e.summary ?? "").match(/^\[([^\]/ ]+)/)?.[1];
630
+ if (p !== project) continue;
631
+ const sid = e.sessionId;
632
+ if (!sid) continue;
633
+ let row = byId.get(sid);
634
+ if (!row) {
635
+ row = {
636
+ sessionId: sid,
637
+ agent: e.agent,
638
+ subAgent: extractSubAgent(e),
639
+ project,
640
+ firstPrompt: "",
641
+ events: 0,
642
+ firstTs: e.ts,
643
+ lastTs: e.ts,
644
+ cost: 0,
645
+ hasError: false
646
+ };
647
+ byId.set(sid, row);
648
+ }
649
+ row.events += 1;
650
+ if (e.ts < row.firstTs) row.firstTs = e.ts;
651
+ if (e.ts > row.lastTs) row.lastTs = e.ts;
652
+ if (e.details?.cost) row.cost += e.details.cost;
653
+ if (e.details?.toolError) row.hasError = true;
654
+ if (!row.firstPrompt && e.type === "prompt" && e.details?.fullText) {
655
+ row.firstPrompt = e.details.fullText.trim().slice(0, 200);
656
+ }
657
+ }
658
+ const rows = Array.from(byId.values());
659
+ rows.sort((a, b) => a.lastTs < b.lastTs ? 1 : -1);
660
+ return rows;
661
+ }
662
+ function dateBucket(iso) {
663
+ const then = new Date(iso);
664
+ const now = /* @__PURE__ */ new Date();
665
+ const diffMs = now.getTime() - then.getTime();
666
+ const sameDay = then.getFullYear() === now.getFullYear() && then.getMonth() === now.getMonth() && then.getDate() === now.getDate();
667
+ if (sameDay) return "today";
668
+ if (diffMs < 48 * 36e5) return "yesterday";
669
+ if (diffMs < 7 * 864e5) return "7d";
670
+ return "older";
671
+ }
672
+ function extractSubAgent(e) {
673
+ const tool = e.tool ?? "";
674
+ const m = tool.match(/^openclaw:([^:]+)/);
675
+ if (m) return m[1];
676
+ return void 0;
677
+ }
678
+
679
+ // src/ui/ProjectsView.tsx
680
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
681
+ function ProjectsView({ projects, selectedIdx, searchQuery }) {
682
+ const filtered = searchQuery ? projects.filter(
683
+ (p) => p.name.toLowerCase().includes(searchQuery.toLowerCase())
684
+ ) : projects;
685
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "double", paddingX: 1, children: [
686
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
687
+ "Projects \u2014 ",
688
+ filtered.length,
689
+ " workspace",
690
+ filtered.length === 1 ? "" : "s"
169
691
  ] }),
170
- /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
171
- /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "green", children: [
172
- "CAN (",
173
- perms.allow.length,
174
- ")"
692
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "sorted by last activity \xB7 enter to filter timeline \xB7 esc to close" }),
693
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: filtered.length === 0 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No projects yet. Use Claude Code / OpenClaw / Cursor and they'll show up here as events stream in." }) : filtered.map((p, i) => /* @__PURE__ */ jsx6(
694
+ ProjectRowView,
695
+ {
696
+ row: p,
697
+ selected: i === selectedIdx
698
+ },
699
+ p.name
700
+ )) })
701
+ ] });
702
+ }
703
+ function ProjectRowView({
704
+ row,
705
+ selected
706
+ }) {
707
+ const agentCounts = Array.from(row.byAgent.entries()).sort(([, a], [, b]) => b - a).map(([name, n]) => `${shortName(name)}:${n}`).join(" ");
708
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
709
+ /* @__PURE__ */ jsxs6(Text6, { inverse: selected, children: [
710
+ /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: selected ? "\u25B6 " : " " }),
711
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: row.name.padEnd(26) }),
712
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
713
+ " ",
714
+ agoFromNow(row.lastTs).padStart(10)
175
715
  ] }),
176
- perms.allow.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (none \u2014 defaultMode applies)" }) : perms.allow.map((a, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
716
+ row.cost > 0 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
177
717
  " ",
178
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713" }),
179
- " ",
180
- a
181
- ] }, i))
718
+ /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: formatUSD(row.cost) })
719
+ ] })
182
720
  ] }),
183
- /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
184
- /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "red", children: [
185
- "CANNOT (",
186
- perms.deny.length,
187
- ")"
721
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
722
+ " ",
723
+ row.events,
724
+ " events \xB7 ",
725
+ row.sessions.size,
726
+ " session",
727
+ row.sessions.size === 1 ? "" : "s",
728
+ " \xB7 ",
729
+ agentCounts
730
+ ] })
731
+ ] });
732
+ }
733
+ function shortName(a) {
734
+ switch (a) {
735
+ case "claude-code":
736
+ return "claude";
737
+ case "openclaw":
738
+ return "openclaw";
739
+ case "cursor":
740
+ return "cursor";
741
+ case "codex":
742
+ return "codex";
743
+ case "gemini":
744
+ return "gemini";
745
+ default:
746
+ return a;
747
+ }
748
+ }
749
+
750
+ // src/ui/SessionsView.tsx
751
+ import { Box as Box7, Text as Text7 } from "ink";
752
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
753
+ function SessionsView({
754
+ project,
755
+ sessions,
756
+ selectedIdx,
757
+ viewportRows,
758
+ scrollOffset
759
+ }) {
760
+ const lines = buildLines(sessions);
761
+ const height = Math.max(3, viewportRows);
762
+ const maxScroll = Math.max(0, lines.length - height);
763
+ const offset = Math.min(scrollOffset, maxScroll);
764
+ const visible = lines.slice(offset, offset + height);
765
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "double", paddingX: 1, children: [
766
+ /* @__PURE__ */ jsxs7(Text7, { children: [
767
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "cyan", children: [
768
+ "Sessions \u2014",
769
+ " "
188
770
  ] }),
189
- perms.deny.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (none \u2014 no explicit denies)" }) : perms.deny.map((d, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
190
- " ",
191
- /* @__PURE__ */ jsx4(Text4, { color: "red", children: "\u2717" }),
192
- " ",
193
- d
194
- ] }, i))
771
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: project }),
772
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
773
+ " ",
774
+ sessions.length,
775
+ " session",
776
+ sessions.length === 1 ? "" : "s"
777
+ ] })
195
778
  ] }),
196
- perms.additionalDirectories.length > 0 && /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
197
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Additional writable directories" }),
198
- perms.additionalDirectories.map((d, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
199
- " \u2022 ",
200
- d
201
- ] }, i))
779
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191\u2193] select [enter] open session [esc] back to projects [q] quit" }),
780
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", marginTop: 1, children: visible.length === 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(no sessions found for this project)" }) : visible.map((l, i) => /* @__PURE__ */ jsx7(LineView, { line: l, selectedIdx }, i)) }),
781
+ lines.length > height && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
782
+ offset + 1,
783
+ "\u2013",
784
+ offset + visible.length,
785
+ " of ",
786
+ lines.length
787
+ ] }) })
788
+ ] });
789
+ }
790
+ function LineView({ line, selectedIdx }) {
791
+ if (line.kind === "bucket") {
792
+ return /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { bold: true, dimColor: true, children: bucketLabel(line.label) }) });
793
+ }
794
+ const r = line.row;
795
+ const selected = line.absIdx === selectedIdx;
796
+ const agentTag = tagFor(r.agent, r.subAgent);
797
+ return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", inverse: selected, children: [
798
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: selected ? "\u25B6 " : " " }),
799
+ /* @__PURE__ */ jsx7(Text7, { color: colorForAgent(r.agent), children: pad2(agentTag, 22) }),
800
+ /* @__PURE__ */ jsxs7(Text7, { children: [
801
+ " ",
802
+ truncate(r.firstPrompt || "(no user prompt yet)", 56)
202
803
  ] }),
203
- perms.flags.length > 0 && /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
204
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "yellow", children: "\u26A0 Flags" }),
205
- perms.flags.map((f, i) => /* @__PURE__ */ jsxs4(Text4, { color: f.level === "risk" ? "red" : "yellow", children: [
206
- " ",
207
- f.level === "risk" ? "\u2717" : "!",
208
- " ",
209
- f.message
210
- ] }, i))
211
- ] })
804
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
805
+ " \xB7 ",
806
+ r.events,
807
+ "ev \xB7 ",
808
+ agoFromNow(r.lastTs)
809
+ ] }),
810
+ r.cost > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
811
+ " \xB7 ",
812
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: formatUSD(r.cost) })
813
+ ] }),
814
+ r.hasError && /* @__PURE__ */ jsx7(Text7, { color: "red", children: " \xB7 ERR" })
815
+ ] }) });
816
+ }
817
+ function buildLines(sessions) {
818
+ const lines = [];
819
+ let currentBucket = "";
820
+ let idx = 0;
821
+ for (const row of sessions) {
822
+ const bucket = dateBucket(row.lastTs);
823
+ if (bucket !== currentBucket) {
824
+ currentBucket = bucket;
825
+ lines.push({ kind: "bucket", label: bucket });
826
+ }
827
+ lines.push({ kind: "session", row, absIdx: idx++ });
828
+ }
829
+ return lines;
830
+ }
831
+ function bucketLabel(b) {
832
+ if (b === "today") return "TODAY";
833
+ if (b === "yesterday") return "YESTERDAY";
834
+ if (b === "7d") return "LAST 7 DAYS";
835
+ return "OLDER";
836
+ }
837
+ function tagFor(agent, sub) {
838
+ if (sub) return `[${agent}:${sub}]`;
839
+ return `[${agent}]`;
840
+ }
841
+ function colorForAgent(a) {
842
+ switch (a) {
843
+ case "claude-code":
844
+ return "cyan";
845
+ case "openclaw":
846
+ return "yellow";
847
+ case "cursor":
848
+ return "magenta";
849
+ case "codex":
850
+ return "green";
851
+ case "gemini":
852
+ return "blue";
853
+ default:
854
+ return "white";
855
+ }
856
+ }
857
+ function pad2(s, n) {
858
+ return s.length >= n ? s.slice(0, n) : s + " ".repeat(n - s.length);
859
+ }
860
+ function truncate(s, n) {
861
+ return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
862
+ }
863
+ function sessionLineCount(sessions) {
864
+ return buildLines(sessions).length;
865
+ }
866
+
867
+ // src/util/clipboard.ts
868
+ import { spawnSync } from "child_process";
869
+ import { platform } from "os";
870
+ function copyToClipboard(text) {
871
+ const os = platform();
872
+ try {
873
+ if (os === "darwin") {
874
+ return run("pbcopy", [], text);
875
+ }
876
+ if (os === "linux") {
877
+ if (commandExists("wl-copy")) return run("wl-copy", [], text);
878
+ if (commandExists("xclip")) return run("xclip", ["-selection", "clipboard"], text);
879
+ if (commandExists("xsel")) return run("xsel", ["--clipboard", "--input"], text);
880
+ return {
881
+ ok: false,
882
+ reason: "install wl-copy / xclip / xsel for clipboard support"
883
+ };
884
+ }
885
+ if (os === "win32") {
886
+ return run("clip", [], text);
887
+ }
888
+ return { ok: false, reason: `clipboard not supported on ${os}` };
889
+ } catch (err) {
890
+ return { ok: false, reason: String(err) };
891
+ }
892
+ }
893
+ function run(cmd, args, input) {
894
+ const res = spawnSync(cmd, args, {
895
+ input,
896
+ stdio: ["pipe", "ignore", "ignore"]
897
+ });
898
+ if (res.error) return { ok: false, reason: String(res.error) };
899
+ if (res.status !== 0)
900
+ return { ok: false, reason: `${cmd} exited ${res.status}` };
901
+ return { ok: true };
902
+ }
903
+ function commandExists(cmd) {
904
+ const res = spawnSync("sh", ["-c", `command -v ${cmd}`], {
905
+ stdio: ["ignore", "ignore", "ignore"]
906
+ });
907
+ return res.status === 0;
908
+ }
909
+ function eventToYankText(summary, path, cmd, toolResult, fullText) {
910
+ if (toolResult && toolResult.trim()) return toolResult;
911
+ if (fullText && fullText.trim()) return fullText;
912
+ if (cmd) return cmd;
913
+ if (path) return path;
914
+ return summary ?? "";
915
+ }
916
+
917
+ // src/util/notifier.ts
918
+ import { spawnSync as spawnSync2 } from "child_process";
919
+ import { platform as platform2 } from "os";
920
+ var RATE_MS = 6e4;
921
+ var recent = /* @__PURE__ */ new Map();
922
+ var notifierDisabled = false;
923
+ function shouldNotify(event) {
924
+ if ((event.type === "file_read" || event.type === "file_write") && event.path && /(^|\/)\.env($|\.)/.test(event.path)) {
925
+ return gate(`env:${event.path}`, {
926
+ title: "\u26A0 agentwatch \u2014 .env access",
927
+ body: `${event.agent} ${event.type} ${event.path}`
928
+ });
929
+ }
930
+ if (event.path && /(^|\/)(\.ssh|\.aws|\.gnupg)($|\/)/.test(event.path)) {
931
+ return gate(`creds:${event.path}`, {
932
+ title: "\u26A0 agentwatch \u2014 credential path touched",
933
+ body: `${event.agent} ${event.type} ${event.path}`
934
+ });
935
+ }
936
+ if (event.type === "shell_exec" && event.cmd) {
937
+ const cmd = event.cmd;
938
+ if (/\brm\s+-rf\b/.test(cmd)) {
939
+ return gate(`rm-rf:${cmd.slice(0, 40)}`, {
940
+ title: "\u26A0 agentwatch \u2014 rm -rf",
941
+ body: `${event.agent}: ${cmd.slice(0, 160)}`
942
+ });
943
+ }
944
+ if (/\bsudo\b/.test(cmd)) {
945
+ return gate(`sudo:${cmd.slice(0, 40)}`, {
946
+ title: "\u26A0 agentwatch \u2014 sudo",
947
+ body: `${event.agent}: ${cmd.slice(0, 160)}`
948
+ });
949
+ }
950
+ if (/curl[^|]*\|\s*(sh|bash)/.test(cmd)) {
951
+ return gate(`curl-sh:${cmd.slice(0, 40)}`, {
952
+ title: "\u26A0 agentwatch \u2014 curl | sh",
953
+ body: `${event.agent}: ${cmd.slice(0, 160)}`
954
+ });
955
+ }
956
+ }
957
+ if (event.details?.toolError) {
958
+ const tool = event.tool ?? "tool";
959
+ return gate(`err:${tool}:${event.sessionId ?? ""}`, {
960
+ title: `\u26A0 agentwatch \u2014 ${tool} failed`,
961
+ body: `${event.agent} in ${projectOf(event) ?? "?"}: ${event.summary ?? ""}`.slice(0, 200)
962
+ });
963
+ }
964
+ return null;
965
+ }
966
+ function gate(key, payload) {
967
+ const now = Date.now();
968
+ const last = recent.get(key);
969
+ if (last && now - last < RATE_MS) return null;
970
+ recent.set(key, now);
971
+ return payload;
972
+ }
973
+ function projectOf(event) {
974
+ const m = (event.summary ?? "").match(/^\[([^\]/ ]+)/);
975
+ return m?.[1];
976
+ }
977
+ function notify(title, body) {
978
+ if (notifierDisabled) return;
979
+ const os = platform2();
980
+ const silentStdio = {
981
+ stdio: ["ignore", "ignore", "ignore"]
982
+ };
983
+ try {
984
+ if (os === "darwin") {
985
+ const escTitle = title.replace(/"/g, '\\"');
986
+ const escBody = body.replace(/"/g, '\\"');
987
+ spawnSync2(
988
+ "osascript",
989
+ ["-e", `display notification "${escBody}" with title "${escTitle}"`],
990
+ silentStdio
991
+ );
992
+ return;
993
+ }
994
+ if (os === "linux") {
995
+ spawnSync2("notify-send", [title, body], silentStdio);
996
+ return;
997
+ }
998
+ if (os === "win32") {
999
+ const msg = `[System.Windows.Forms.MessageBox]::Show('${body.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
1000
+ spawnSync2("powershell", ["-Command", msg], silentStdio);
1001
+ return;
1002
+ }
1003
+ } catch {
1004
+ notifierDisabled = true;
1005
+ }
1006
+ }
1007
+
1008
+ // src/ui/HelpView.tsx
1009
+ import { Box as Box8, Text as Text8 } from "ink";
1010
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1011
+ var GROUPS = [
1012
+ {
1013
+ title: "Navigate",
1014
+ rows: [
1015
+ ["\u2191 \u2193 / j k", "move selection in timeline"],
1016
+ ["enter", "open event detail pane"],
1017
+ ["esc", "close current view / clear selection"],
1018
+ ["P", "projects grid \u2014 every workspace on this machine"],
1019
+ ["enter on project", "sessions list for that project (by date)"],
1020
+ ["enter on session", "scoped timeline for that session"],
1021
+ ["q / ctrl-c", "quit agentwatch"]
1022
+ ]
1023
+ },
1024
+ {
1025
+ title: "Filter & scope",
1026
+ rows: [
1027
+ ["/", "full-text search (summary, path, cmd, tool, text)"],
1028
+ ["f", "cycle agent filter (claude / openclaw / cursor / \u2026)"],
1029
+ ["a", "toggle agent side panel"],
1030
+ ["x", "drill into selected Agent event's subagent run"],
1031
+ ["X", "unscope subagent"],
1032
+ ["A", "clear project filter"]
1033
+ ]
1034
+ },
1035
+ {
1036
+ title: "Actions",
1037
+ rows: [
1038
+ ["y", "yank selected event content to clipboard"],
1039
+ ["space", "pause / resume live event stream"],
1040
+ ["c", "clear event buffer"]
1041
+ ]
1042
+ },
1043
+ {
1044
+ title: "Info views",
1045
+ rows: [
1046
+ ["p", "permissions view (Claude + Cursor + OpenClaw)"],
1047
+ ["\u2191\u2193 / j k inside permissions", "scroll"]
1048
+ ]
1049
+ },
1050
+ {
1051
+ title: "Detail pane (open with enter)",
1052
+ rows: [
1053
+ ["\u2191 \u2193 / j k", "scroll detail content"],
1054
+ ["esc", "close detail"]
1055
+ ]
1056
+ },
1057
+ {
1058
+ title: "Help",
1059
+ rows: [
1060
+ ["?", "toggle this help"],
1061
+ ["esc", "close this help"]
1062
+ ]
1063
+ }
1064
+ ];
1065
+ function HelpView() {
1066
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "double", paddingX: 1, children: [
1067
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: "agentwatch \u2014 keybindings" }),
1068
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press ? or esc to close." }),
1069
+ GROUPS.map((g) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
1070
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "yellow", children: g.title }),
1071
+ g.rows.map(([k, d]) => /* @__PURE__ */ jsxs8(Text8, { children: [
1072
+ /* @__PURE__ */ jsx8(Text8, { color: "green", children: pad3(k, 34) }),
1073
+ /* @__PURE__ */ jsxs8(Text8, { children: [
1074
+ " ",
1075
+ d
1076
+ ] })
1077
+ ] }, k))
1078
+ ] }, g.title)),
1079
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "docs: github.com/mishanefedov/agentwatch \xB7 issues + feature requests welcome" }) })
212
1080
  ] });
213
1081
  }
214
- function modeColor(mode) {
215
- if (mode === "auto" || mode === "bypassPermissions") return "red";
216
- if (mode === "ask") return "green";
217
- return "yellow";
1082
+ function pad3(s, n) {
1083
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
1084
+ }
1085
+
1086
+ // src/ui/Breadcrumb.tsx
1087
+ import { Box as Box9, Text as Text9 } from "ink";
1088
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1089
+ function Breadcrumb(props) {
1090
+ const parts = [
1091
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "agentwatch" }, "root")
1092
+ ];
1093
+ const push = (el) => {
1094
+ parts.push(
1095
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " \xB7 " }, `sep-${parts.length}`)
1096
+ );
1097
+ parts.push(el);
1098
+ };
1099
+ if (props.view === "help") push(/* @__PURE__ */ jsx9(Text9, { children: "help" }, "v"));
1100
+ if (props.view === "projects") push(/* @__PURE__ */ jsx9(Text9, { children: "projects" }, "v"));
1101
+ if (props.view === "permissions") push(/* @__PURE__ */ jsx9(Text9, { children: "permissions" }, "v"));
1102
+ if (props.sessionsForProject)
1103
+ push(
1104
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1105
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: props.sessionsForProject }),
1106
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " (sessions)" })
1107
+ ] }, "proj-list")
1108
+ );
1109
+ if (props.projectFilter && !props.sessionsForProject)
1110
+ push(
1111
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1112
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "project " }),
1113
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: props.projectFilter })
1114
+ ] }, "proj")
1115
+ );
1116
+ if (props.sessionFilter)
1117
+ push(
1118
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1119
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "session " }),
1120
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: props.sessionFilter.slice(0, 16) })
1121
+ ] }, "sess")
1122
+ );
1123
+ if (props.subAgentScope)
1124
+ push(
1125
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1126
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "sub " }),
1127
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: props.subAgentScope.slice(0, 12) })
1128
+ ] }, "sub")
1129
+ );
1130
+ if (props.agentFilter)
1131
+ push(
1132
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1133
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "agent " }),
1134
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: props.agentFilter })
1135
+ ] }, "agent")
1136
+ );
1137
+ if (props.searchQuery)
1138
+ push(
1139
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1140
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "search " }),
1141
+ /* @__PURE__ */ jsxs9(Text9, { bold: true, color: "yellow", children: [
1142
+ '"',
1143
+ props.searchQuery,
1144
+ '"'
1145
+ ] })
1146
+ ] }, "q")
1147
+ );
1148
+ return /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsx9(Text9, { children: parts }) });
218
1149
  }
219
1150
 
220
1151
  // src/adapters/claude-code.ts
@@ -224,6 +1155,13 @@ import { createInterface } from "readline";
224
1155
  import { basename, sep } from "path";
225
1156
 
226
1157
  // src/schema.ts
1158
+ function clampTs(ts) {
1159
+ const t = new Date(ts).getTime();
1160
+ if (!Number.isFinite(t)) return (/* @__PURE__ */ new Date()).toISOString();
1161
+ const now = Date.now();
1162
+ if (t > now + 6e4) return new Date(now).toISOString();
1163
+ return ts;
1164
+ }
227
1165
  function riskOf(type, path, cmd) {
228
1166
  if (type === "shell_exec") {
229
1167
  if (cmd && /\b(rm|sudo|curl|wget|chmod|chown)\b/.test(cmd)) return 9;
@@ -249,23 +1187,61 @@ function nextId() {
249
1187
  return `${Date.now().toString(36)}-${counter.toString(36)}`;
250
1188
  }
251
1189
 
1190
+ // src/util/recent-writes.ts
1191
+ var DEDUPE_WINDOW_MS = 5e3;
1192
+ var EXPIRY_MS = 3e4;
1193
+ var recent2 = /* @__PURE__ */ new Map();
1194
+ var lastSweep = 0;
1195
+ function markAgentWrite(path, ts = Date.now()) {
1196
+ const t = typeof ts === "string" ? new Date(ts).getTime() : ts;
1197
+ if (!path || Number.isNaN(t)) return;
1198
+ recent2.set(path, t);
1199
+ sweepIfDue();
1200
+ }
1201
+ function wasRecentlyWrittenByAgent(path) {
1202
+ const t = recent2.get(path);
1203
+ if (t == null) return false;
1204
+ return Date.now() - t <= DEDUPE_WINDOW_MS;
1205
+ }
1206
+ function sweepIfDue() {
1207
+ const now = Date.now();
1208
+ if (now - lastSweep < EXPIRY_MS) return;
1209
+ lastSweep = now;
1210
+ for (const [p, t] of recent2) {
1211
+ if (now - t > EXPIRY_MS) recent2.delete(p);
1212
+ }
1213
+ }
1214
+
252
1215
  // src/adapters/claude-code.ts
1216
+ var MAX_PENDING_TOOL_USES = 5e3;
1217
+ var pendingToolUses = /* @__PURE__ */ new Map();
1218
+ var orphanResults = /* @__PURE__ */ new Map();
1219
+ function capMap(m, max) {
1220
+ while (m.size > max) {
1221
+ const first = m.keys().next().value;
1222
+ if (first === void 0) break;
1223
+ m.delete(first);
1224
+ }
1225
+ }
253
1226
  var BACKFILL_BYTES = 64 * 1024;
254
- function startClaudeAdapter(emit) {
1227
+ function startClaudeAdapter(sink) {
1228
+ const { emit, enrich } = normalizeSink(sink);
255
1229
  const dir = claudeProjectsDir();
256
1230
  if (!existsSync(dir)) {
257
1231
  return () => {
258
1232
  };
259
1233
  }
260
1234
  const cursors = /* @__PURE__ */ new Map();
261
- const sessionRe = /[\\/]projects[\\/][^\\/]+[\\/][^\\/]+\.jsonl$/;
1235
+ const mainRe = /[\\/]projects[\\/][^\\/]+[\\/][^\\/]+\.jsonl$/;
1236
+ const subRe = /[\\/]projects[\\/][^\\/]+[\\/][^\\/]+[\\/]subagents[\\/][^\\/]+\.jsonl$/;
262
1237
  const watcher = chokidar.watch(dir, {
263
1238
  persistent: true,
264
1239
  ignoreInitial: false,
265
- depth: 3
1240
+ depth: 5
266
1241
  });
267
1242
  const process2 = (file, isInitialAdd) => {
268
- if (!sessionRe.test(file)) return;
1243
+ const isSub = subRe.test(file);
1244
+ if (!isSub && !mainRe.test(file)) return;
269
1245
  const size = safeSize(file);
270
1246
  let cursor = cursors.get(file);
271
1247
  if (!cursor) {
@@ -282,6 +1258,7 @@ function startClaudeAdapter(emit) {
282
1258
  });
283
1259
  const sessionId = basename(file, ".jsonl");
284
1260
  const project = extractProject(file);
1261
+ const subAgentId = isSub ? extractSubAgentId(file) : void 0;
285
1262
  let consumed = 0;
286
1263
  let skippedFirst = false;
287
1264
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
@@ -294,8 +1271,33 @@ function startClaudeAdapter(emit) {
294
1271
  if (!line.trim()) return;
295
1272
  try {
296
1273
  const obj = JSON.parse(line);
297
- const event = translateClaudeLine(obj, sessionId, project);
298
- if (event) emit(event);
1274
+ handleToolResults(obj, enrich);
1275
+ const event = translateClaudeLine(obj, sessionId, project, subAgentId);
1276
+ if (event) {
1277
+ emit(event);
1278
+ if (event.path && (event.type === "file_write" || event.type === "file_read")) {
1279
+ markAgentWrite(event.path, event.ts);
1280
+ }
1281
+ const toolUseId = event.details?.toolUseId;
1282
+ if (toolUseId && orphanResults.has(toolUseId)) {
1283
+ const orphan = orphanResults.get(toolUseId);
1284
+ orphanResults.delete(toolUseId);
1285
+ enrich(event.id, {
1286
+ toolResult: orphan.content,
1287
+ toolError: orphan.isError,
1288
+ durationMs: Math.max(
1289
+ 0,
1290
+ new Date(orphan.ts).getTime() - new Date(event.ts).getTime()
1291
+ )
1292
+ });
1293
+ } else if (toolUseId) {
1294
+ pendingToolUses.set(toolUseId, {
1295
+ eventId: event.id,
1296
+ ts: event.ts
1297
+ });
1298
+ capMap(pendingToolUses, MAX_PENDING_TOOL_USES);
1299
+ }
1300
+ }
299
1301
  } catch {
300
1302
  }
301
1303
  });
@@ -326,6 +1328,81 @@ function extractProject(file) {
326
1328
  }
327
1329
  return "";
328
1330
  }
1331
+ function extractSubAgentId(file) {
1332
+ const base = basename(file, ".jsonl");
1333
+ return base.replace(/^agent-/, "");
1334
+ }
1335
+ function normalizeSink(sink) {
1336
+ if (typeof sink === "function") {
1337
+ return { emit: sink, enrich: () => {
1338
+ } };
1339
+ }
1340
+ return sink;
1341
+ }
1342
+ function handleToolResults(obj, enrich) {
1343
+ if (!obj || typeof obj !== "object") return;
1344
+ const o = obj;
1345
+ const role = o.role ?? o.message?.role;
1346
+ if (role !== "user") return;
1347
+ const content = o.message?.content;
1348
+ if (!Array.isArray(content)) return;
1349
+ const ts = typeof o.timestamp === "string" && o.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1350
+ for (const block of content) {
1351
+ if (typeof block !== "object" || block === null) continue;
1352
+ const b = block;
1353
+ if (b.type !== "tool_result") continue;
1354
+ const id = typeof b.tool_use_id === "string" ? b.tool_use_id : void 0;
1355
+ if (!id) continue;
1356
+ const isError = b.is_error === true;
1357
+ const resultText = flattenResultContent(b.content);
1358
+ const subAgentId = extractSubAgentIdFromResult(resultText);
1359
+ const pending = pendingToolUses.get(id);
1360
+ if (pending) {
1361
+ pendingToolUses.delete(id);
1362
+ enrich(pending.eventId, {
1363
+ toolResult: resultText,
1364
+ toolError: isError,
1365
+ durationMs: Math.max(
1366
+ 0,
1367
+ new Date(ts).getTime() - new Date(pending.ts).getTime()
1368
+ ),
1369
+ ...subAgentId ? { subAgentId } : {}
1370
+ });
1371
+ } else {
1372
+ orphanResults.set(id, { ts, content: resultText, isError });
1373
+ if (orphanResults.size > 1e3) {
1374
+ const first = orphanResults.keys().next().value;
1375
+ if (first) orphanResults.delete(first);
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ function extractSubAgentIdFromResult(text) {
1381
+ const m = text.match(/agentId[":\s]+([a-f0-9]{16,})/) || text.match(/agent-([a-f0-9]{16,})/);
1382
+ return m?.[1];
1383
+ }
1384
+ var MAX_TOOL_RESULT_BYTES = 256 * 1024;
1385
+ function flattenResultContent(content) {
1386
+ if (typeof content === "string") return capBytes(content);
1387
+ if (!Array.isArray(content)) return "";
1388
+ const parts = [];
1389
+ for (const c of content) {
1390
+ if (typeof c === "string") {
1391
+ parts.push(c);
1392
+ } else if (typeof c === "object" && c !== null) {
1393
+ const rec = c;
1394
+ if (typeof rec.text === "string") parts.push(rec.text);
1395
+ }
1396
+ }
1397
+ return capBytes(parts.join("\n"));
1398
+ }
1399
+ function capBytes(s, max = MAX_TOOL_RESULT_BYTES) {
1400
+ if (s.length <= max) return s;
1401
+ const truncated = s.length - max;
1402
+ return s.slice(0, max) + `
1403
+
1404
+ \u2026 [${truncated.toLocaleString()} bytes truncated]`;
1405
+ }
329
1406
  function safeSize(file) {
330
1407
  try {
331
1408
  return statSync(file).size;
@@ -333,17 +1410,26 @@ function safeSize(file) {
333
1410
  return 0;
334
1411
  }
335
1412
  }
336
- function translateClaudeLine(obj, sessionId, project = "") {
1413
+ function translateClaudeLine(obj, sessionId, project = "", subAgentId) {
337
1414
  if (!obj || typeof obj !== "object") return null;
338
1415
  const o = obj;
339
- const ts = typeof o.timestamp === "string" && o.timestamp || (/* @__PURE__ */ new Date()).toISOString();
340
- const prefix = project ? `[${project}] ` : "";
1416
+ const ts = clampTs(
1417
+ typeof o.timestamp === "string" && o.timestamp || (/* @__PURE__ */ new Date()).toISOString()
1418
+ );
1419
+ const tagParts = [];
1420
+ if (project) tagParts.push(project);
1421
+ if (subAgentId) tagParts.push(`sub:${subAgentId.slice(0, 8)}`);
1422
+ const prefix = tagParts.length > 0 ? `[${tagParts.join(" / ")}] ` : "";
341
1423
  const role = o.role ?? o.message?.role;
342
1424
  const type = o.type;
343
1425
  const content = o.message?.content;
344
1426
  if (type === "tool_result" || type === "summary") return null;
345
1427
  if (type === "worktree-state" || type === "compact") return null;
346
1428
  if (type === "assistant" || role === "assistant") {
1429
+ const msg = o.message;
1430
+ const model = typeof msg?.model === "string" ? msg.model : "default";
1431
+ const usage = parseUsage(msg?.usage) ?? void 0;
1432
+ const cost = usage ? costOf(model, usage) : void 0;
347
1433
  const toolUse = findToolUse(content);
348
1434
  if (toolUse) {
349
1435
  const evType = inferToolType(toolUse.name);
@@ -358,19 +1444,35 @@ function translateClaudeLine(obj, sessionId, project = "") {
358
1444
  tool: toolUse.name,
359
1445
  summary: prefix + summary,
360
1446
  sessionId,
361
- riskScore: riskOf(evType, toolUse.path, toolUse.cmd)
1447
+ riskScore: riskOf(evType, toolUse.path, toolUse.cmd),
1448
+ details: {
1449
+ toolInput: toolUse.input,
1450
+ toolUseId: toolUse.id,
1451
+ thinking: extractThinking(content),
1452
+ usage,
1453
+ cost,
1454
+ model
1455
+ }
362
1456
  };
363
1457
  }
364
1458
  const text = extractText(content);
365
- if (!text) return null;
1459
+ const thinking = extractThinking(content);
1460
+ if (!text && !thinking) return null;
366
1461
  return {
367
1462
  id: nextId(),
368
1463
  ts,
369
1464
  agent: "claude-code",
370
1465
  type: "response",
371
- summary: prefix + truncate(text),
1466
+ summary: prefix + truncate2(text || thinking || ""),
372
1467
  sessionId,
373
- riskScore: riskOf("response")
1468
+ riskScore: riskOf("response"),
1469
+ details: {
1470
+ fullText: text || void 0,
1471
+ thinking: thinking || void 0,
1472
+ usage,
1473
+ cost,
1474
+ model
1475
+ }
374
1476
  };
375
1477
  }
376
1478
  if (type === "user" || role === "user") {
@@ -381,9 +1483,10 @@ function translateClaudeLine(obj, sessionId, project = "") {
381
1483
  ts,
382
1484
  agent: "claude-code",
383
1485
  type: "prompt",
384
- summary: prefix + truncate(text),
1486
+ summary: prefix + truncate2(text),
385
1487
  sessionId,
386
- riskScore: riskOf("prompt")
1488
+ riskScore: riskOf("prompt"),
1489
+ details: { fullText: text }
387
1490
  };
388
1491
  }
389
1492
  return null;
@@ -395,25 +1498,38 @@ function findToolUse(content) {
395
1498
  const rec = c;
396
1499
  if (rec.type !== "tool_use") continue;
397
1500
  const name = typeof rec.name === "string" ? rec.name : "unknown";
1501
+ const id = typeof rec.id === "string" ? rec.id : void 0;
398
1502
  const input = rec.input ?? {};
399
1503
  const path = typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : void 0;
400
1504
  const cmd = typeof input.command === "string" ? input.command : void 0;
401
- return { name, path, cmd, input };
1505
+ return { name, path, cmd, input, id };
402
1506
  }
403
1507
  return null;
404
1508
  }
1509
+ function extractThinking(content) {
1510
+ if (!Array.isArray(content)) return "";
1511
+ const parts = [];
1512
+ for (const c of content) {
1513
+ if (typeof c !== "object" || c === null) continue;
1514
+ const rec = c;
1515
+ if (rec.type === "thinking" && typeof rec.thinking === "string") {
1516
+ parts.push(rec.thinking);
1517
+ }
1518
+ }
1519
+ return parts.join("\n").trim();
1520
+ }
405
1521
  function buildToolSummary(t) {
406
- if (/^Bash/i.test(t.name) && t.cmd) return `Bash: ${truncate(t.cmd, 100)}`;
1522
+ if (/^Bash/i.test(t.name) && t.cmd) return `Bash: ${truncate2(t.cmd, 100)}`;
407
1523
  if (/^(Write|Edit|MultiEdit|Read)/i.test(t.name) && t.path) {
408
1524
  return `${t.name}: ${t.path}`;
409
1525
  }
410
1526
  if (/^(Grep|Glob)/i.test(t.name)) {
411
1527
  const pat = typeof t.input.pattern === "string" ? t.input.pattern : typeof t.input.glob === "string" ? t.input.glob : "";
412
- return `${t.name}: ${truncate(pat, 100)}`;
1528
+ return `${t.name}: ${truncate2(pat, 100)}`;
413
1529
  }
414
1530
  if (/^Task/i.test(t.name)) {
415
1531
  const desc = typeof t.input.description === "string" ? t.input.description : "";
416
- return `Task: ${truncate(desc, 100)}`;
1532
+ return `Task: ${truncate2(desc, 100)}`;
417
1533
  }
418
1534
  if (/^WebFetch/i.test(t.name)) {
419
1535
  const url = typeof t.input.url === "string" ? t.input.url : "";
@@ -422,7 +1538,7 @@ function buildToolSummary(t) {
422
1538
  const firstVal = Object.values(t.input).find(
423
1539
  (v) => typeof v === "string"
424
1540
  );
425
- return firstVal ? `${t.name}: ${truncate(firstVal, 100)}` : t.name;
1541
+ return firstVal ? `${t.name}: ${truncate2(firstVal, 100)}` : t.name;
426
1542
  }
427
1543
  function extractText(content) {
428
1544
  if (typeof content === "string") return content;
@@ -458,7 +1574,7 @@ function inferToolType(name) {
458
1574
  if (/^(Write|Edit|MultiEdit)/i.test(name)) return "file_write";
459
1575
  return "tool_call";
460
1576
  }
461
- function truncate(s, max = 140) {
1577
+ function truncate2(s, max = 140) {
462
1578
  const clean = s.replace(/\s+/g, " ").trim();
463
1579
  if (!clean) return "";
464
1580
  return clean.length <= max ? clean : clean.slice(0, max - 1) + "\u2026";
@@ -471,7 +1587,8 @@ import { createInterface as createInterface2 } from "readline";
471
1587
  import { basename as basename2, join, sep as sep2 } from "path";
472
1588
  import { homedir } from "os";
473
1589
  var sessionCwd = /* @__PURE__ */ new Map();
474
- function startOpenClawAdapter(emit) {
1590
+ function startOpenClawAdapter(sink) {
1591
+ const emit = typeof sink === "function" ? sink : sink.emit;
475
1592
  const root = join(homedir(), ".openclaw");
476
1593
  if (!existsSync2(root)) return () => {
477
1594
  };
@@ -511,7 +1628,7 @@ function startOpenClawAdapter(emit) {
511
1628
  };
512
1629
  }
513
1630
  function processSession(file, startFromEnd, cursors, emit) {
514
- const subAgent = extractSubAgent(file);
1631
+ const subAgent = extractSubAgent2(file);
515
1632
  const sessionId = basename2(file, ".jsonl");
516
1633
  streamLines(file, startFromEnd, cursors, (line) => {
517
1634
  let obj;
@@ -580,7 +1697,7 @@ function safeSize2(file) {
580
1697
  return 0;
581
1698
  }
582
1699
  }
583
- function extractSubAgent(file) {
1700
+ function extractSubAgent2(file) {
584
1701
  const parts = file.split(sep2);
585
1702
  const agentsIdx = parts.lastIndexOf("agents");
586
1703
  if (agentsIdx >= 0 && parts[agentsIdx + 1]) return parts[agentsIdx + 1];
@@ -589,7 +1706,9 @@ function extractSubAgent(file) {
589
1706
  function translateSession(obj, subAgent, sessionId) {
590
1707
  if (!obj || typeof obj !== "object") return null;
591
1708
  const o = obj;
592
- const ts = typeof o.timestamp === "string" && o.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1709
+ const ts = clampTs(
1710
+ typeof o.timestamp === "string" && o.timestamp || (/* @__PURE__ */ new Date()).toISOString()
1711
+ );
593
1712
  const t = o.type;
594
1713
  const projectLabel = () => {
595
1714
  const cwd = sessionCwd.get(sessionId);
@@ -634,7 +1753,10 @@ function translateSession(obj, subAgent, sessionId) {
634
1753
  const content = msg?.content;
635
1754
  const text = extractText2(content);
636
1755
  if (role === "user") {
637
- return base("prompt", { summary: truncate2(text) });
1756
+ return base("prompt", {
1757
+ summary: truncate3(text),
1758
+ details: { fullText: text }
1759
+ });
638
1760
  }
639
1761
  if (role === "assistant") {
640
1762
  const toolUse = extractToolUse(content);
@@ -644,11 +1766,15 @@ function translateSession(obj, subAgent, sessionId) {
644
1766
  tool: `openclaw:${subAgent}:${toolUse.name}`,
645
1767
  path: toolUse.path,
646
1768
  cmd: toolUse.cmd,
647
- summary: truncate2(toolUse.summary)
1769
+ summary: truncate3(toolUse.summary),
1770
+ details: { toolInput: toolUse.input }
648
1771
  });
649
1772
  }
650
1773
  if (!text) return null;
651
- return base("response", { summary: truncate2(text) });
1774
+ return base("response", {
1775
+ summary: truncate3(text),
1776
+ details: { fullText: text }
1777
+ });
652
1778
  }
653
1779
  }
654
1780
  return null;
@@ -656,7 +1782,9 @@ function translateSession(obj, subAgent, sessionId) {
656
1782
  function translateAudit(obj) {
657
1783
  if (!obj || typeof obj !== "object") return null;
658
1784
  const o = obj;
659
- const ts = typeof o.ts === "string" && o.ts || (/* @__PURE__ */ new Date()).toISOString();
1785
+ const ts = clampTs(
1786
+ typeof o.ts === "string" && o.ts || (/* @__PURE__ */ new Date()).toISOString()
1787
+ );
660
1788
  const event = typeof o.event === "string" ? o.event : "config.event";
661
1789
  const configPath = typeof o.configPath === "string" ? o.configPath : void 0;
662
1790
  const cwd = typeof o.cwd === "string" ? o.cwd : void 0;
@@ -692,7 +1820,7 @@ function extractToolUse(content) {
692
1820
  const path = typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : void 0;
693
1821
  const cmd = typeof input.command === "string" ? input.command : void 0;
694
1822
  const summary = cmd ?? path ?? name;
695
- return { name, path, cmd, summary };
1823
+ return { name, path, cmd, summary, input };
696
1824
  }
697
1825
  }
698
1826
  return null;
@@ -703,7 +1831,7 @@ function inferToolType2(name) {
703
1831
  if (/^(Write|Edit|MultiEdit|Create)/i.test(name)) return "file_write";
704
1832
  return "tool_call";
705
1833
  }
706
- function truncate2(s, max = 140) {
1834
+ function truncate3(s, max = 140) {
707
1835
  const clean = s.replace(/\s+/g, " ").trim();
708
1836
  if (!clean) return "";
709
1837
  return clean.length <= max ? clean : clean.slice(0, max - 1) + "\u2026";
@@ -719,7 +1847,8 @@ import {
719
1847
  } from "fs";
720
1848
  import { homedir as homedir2 } from "os";
721
1849
  import { join as join2 } from "path";
722
- function startCursorAdapter(workspace, emit) {
1850
+ function startCursorAdapter(workspace, sink) {
1851
+ const emit = typeof sink === "function" ? sink : sink.emit;
723
1852
  const cursorDir = join2(homedir2(), ".cursor");
724
1853
  const installed = existsSync3(cursorDir);
725
1854
  const status = {
@@ -792,8 +1921,8 @@ function startCursorAdapter(workspace, emit) {
792
1921
  ignoreInitial: true
793
1922
  });
794
1923
  w.on("change", () => {
795
- const recent = readRecentFiles(stateFile);
796
- for (const path of recent) {
1924
+ const recent3 = readRecentFiles(stateFile);
1925
+ for (const path of recent3) {
797
1926
  if (lastRecentFiles.has(path)) continue;
798
1927
  lastRecentFiles.add(path);
799
1928
  const project = extractProject2(path);
@@ -912,8 +2041,139 @@ function extractProject2(path) {
912
2041
  return segs[segs.length - 2] ?? "";
913
2042
  }
914
2043
 
915
- // src/adapters/fs-watcher.ts
2044
+ // src/adapters/gemini.ts
916
2045
  import chokidar4 from "chokidar";
2046
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
2047
+ import { homedir as homedir3 } from "os";
2048
+ import { basename as basename3, join as join3, sep as sep3 } from "path";
2049
+ function startGeminiAdapter(sink) {
2050
+ const { emit } = normalizeSink2(sink);
2051
+ const root = join3(homedir3(), ".gemini", "tmp");
2052
+ if (!existsSync4(root)) return () => {
2053
+ };
2054
+ const emittedIds = /* @__PURE__ */ new Map();
2055
+ const watcher = chokidar4.watch(root, {
2056
+ persistent: true,
2057
+ ignoreInitial: false,
2058
+ depth: 4
2059
+ });
2060
+ const sessionRe = /[\\/]chats[\\/]session-[^\\/]+\.json$/;
2061
+ const process2 = (file, _isInitial) => {
2062
+ if (!sessionRe.test(file)) return;
2063
+ let doc;
2064
+ try {
2065
+ doc = JSON.parse(readFileSync2(file, "utf8"));
2066
+ } catch {
2067
+ return;
2068
+ }
2069
+ if (!doc || typeof doc !== "object") return;
2070
+ const d = doc;
2071
+ const sessionId = typeof d.sessionId === "string" && d.sessionId || basename3(file, ".json");
2072
+ const kind = typeof d.kind === "string" ? d.kind : "main";
2073
+ const project = extractProject3(file);
2074
+ const messages = Array.isArray(d.messages) ? d.messages : [];
2075
+ let seen = emittedIds.get(file);
2076
+ if (!seen) {
2077
+ seen = /* @__PURE__ */ new Set();
2078
+ emittedIds.set(file, seen);
2079
+ }
2080
+ for (const m of messages) {
2081
+ if (!m || typeof m !== "object") continue;
2082
+ const msg = m;
2083
+ const id = typeof msg.id === "string" ? msg.id : void 0;
2084
+ if (!id || seen.has(id)) continue;
2085
+ seen.add(id);
2086
+ const ev = translate(msg, sessionId, kind, project);
2087
+ if (ev) emit(ev);
2088
+ }
2089
+ };
2090
+ watcher.on("add", (f) => process2(f, true));
2091
+ watcher.on("change", (f) => process2(f, false));
2092
+ watcher.on("error", swallow3);
2093
+ return () => {
2094
+ void watcher.close();
2095
+ };
2096
+ }
2097
+ function translate(msg, sessionId, kind, project) {
2098
+ const ts = clampTs(
2099
+ typeof msg.timestamp === "string" && msg.timestamp || (/* @__PURE__ */ new Date()).toISOString()
2100
+ );
2101
+ const type = typeof msg.type === "string" ? msg.type : "";
2102
+ const text = extractText3(msg.content);
2103
+ const subAgentSuffix = kind === "subagent" ? " / sub:gemini" : "";
2104
+ const prefix = project ? `[${project}${subAgentSuffix}] ` : "";
2105
+ let eventType;
2106
+ if (type === "user") {
2107
+ if (!text) return null;
2108
+ eventType = "prompt";
2109
+ } else if (type === "gemini") {
2110
+ if (!text) return null;
2111
+ eventType = "response";
2112
+ } else if (type === "error") {
2113
+ if (!text) return null;
2114
+ eventType = "response";
2115
+ } else {
2116
+ return null;
2117
+ }
2118
+ return {
2119
+ id: nextId(),
2120
+ ts,
2121
+ agent: "gemini",
2122
+ type: eventType,
2123
+ sessionId,
2124
+ summary: prefix + truncate4(text),
2125
+ riskScore: type === "error" ? 6 : riskOf(eventType),
2126
+ tool: kind === "subagent" ? "gemini:subagent" : "gemini",
2127
+ details: { fullText: text }
2128
+ };
2129
+ }
2130
+ function extractText3(content) {
2131
+ if (typeof content === "string") return content.trim();
2132
+ if (!Array.isArray(content)) return "";
2133
+ const parts = [];
2134
+ for (const item of content) {
2135
+ if (typeof item === "string") {
2136
+ parts.push(item);
2137
+ } else if (item && typeof item === "object") {
2138
+ const rec = item;
2139
+ if (typeof rec.text === "string") parts.push(rec.text);
2140
+ }
2141
+ }
2142
+ return parts.join("\n").trim();
2143
+ }
2144
+ function extractProject3(file) {
2145
+ const parts = file.split(sep3);
2146
+ const tmpIdx = parts.lastIndexOf("tmp");
2147
+ if (tmpIdx >= 0) {
2148
+ const candidate = parts[tmpIdx + 1];
2149
+ if (candidate && candidate !== "chats") return candidate;
2150
+ }
2151
+ const chatsIdx = parts.lastIndexOf("chats");
2152
+ if (chatsIdx > 0) {
2153
+ const cand = parts[chatsIdx - 1];
2154
+ if (cand && cand !== "tmp") return cand;
2155
+ }
2156
+ return "";
2157
+ }
2158
+ function truncate4(s, max = 140) {
2159
+ const clean = s.replace(/\s+/g, " ").trim();
2160
+ if (!clean) return "";
2161
+ return clean.length <= max ? clean : clean.slice(0, max - 1) + "\u2026";
2162
+ }
2163
+ function normalizeSink2(sink) {
2164
+ if (typeof sink === "function") return { emit: sink, enrich: () => {
2165
+ } };
2166
+ return sink;
2167
+ }
2168
+ function swallow3(err) {
2169
+ if (typeof err !== "object" || err === null) return;
2170
+ const code = err.code;
2171
+ if (code === "EMFILE" || code === "ENOSPC" || code === "EACCES") return;
2172
+ console.error("[agentwatch/gemini]", String(err));
2173
+ }
2174
+
2175
+ // src/adapters/fs-watcher.ts
2176
+ import chokidar5 from "chokidar";
917
2177
  var DEFAULT_IGNORES = [
918
2178
  /(^|[/\\])node_modules([/\\]|$)/,
919
2179
  /(^|[/\\])\.git([/\\]|$)/,
@@ -938,8 +2198,9 @@ var DEFAULT_IGNORES = [
938
2198
  /yarn\.lock$/,
939
2199
  /bun\.lockb$/
940
2200
  ];
941
- function startFsAdapter(root, emit) {
942
- const watcher = chokidar4.watch(root, {
2201
+ function startFsAdapter(root, sink) {
2202
+ const emit = typeof sink === "function" ? sink : sink.emit;
2203
+ const watcher = chokidar5.watch(root, {
943
2204
  persistent: true,
944
2205
  ignoreInitial: true,
945
2206
  ignored: (p) => DEFAULT_IGNORES.some((r) => r.test(p)),
@@ -950,6 +2211,7 @@ function startFsAdapter(root, emit) {
950
2211
  console.error("[agentwatch/fs]", String(err));
951
2212
  });
952
2213
  const emitFs = (path) => {
2214
+ if (wasRecentlyWrittenByAgent(path)) return;
953
2215
  const event = {
954
2216
  id: nextId(),
955
2217
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -973,15 +2235,71 @@ function isSuppressible(err) {
973
2235
  return code === "EMFILE" || code === "ENOSPC" || code === "EACCES";
974
2236
  }
975
2237
 
2238
+ // src/adapters/registry.ts
2239
+ function startAllAdapters(sink, workspace) {
2240
+ const started = [];
2241
+ started.push({
2242
+ name: "claude-code",
2243
+ stop: wrap2(() => startClaudeAdapter(sink), "claude-code")
2244
+ });
2245
+ started.push({
2246
+ name: "openclaw",
2247
+ stop: wrap2(() => startOpenClawAdapter(sink), "openclaw")
2248
+ });
2249
+ const cursor = safeStart(() => startCursorAdapter(workspace, sink), "cursor");
2250
+ if (cursor) {
2251
+ started.push({
2252
+ name: "cursor",
2253
+ stop: cursor.stop,
2254
+ status: cursor.status
2255
+ });
2256
+ }
2257
+ started.push({
2258
+ name: "gemini",
2259
+ stop: wrap2(() => startGeminiAdapter(sink), "gemini")
2260
+ });
2261
+ started.push({
2262
+ name: "fs-watcher",
2263
+ stop: wrap2(() => startFsAdapter(workspace, sink), "fs-watcher")
2264
+ });
2265
+ return started;
2266
+ }
2267
+ function stopAllAdapters(adapters) {
2268
+ for (const a of adapters) {
2269
+ try {
2270
+ a.stop();
2271
+ } catch (err) {
2272
+ console.error(`[agentwatch] adapter ${a.name} stop failed:`, err);
2273
+ }
2274
+ }
2275
+ }
2276
+ function wrap2(start, name) {
2277
+ try {
2278
+ return start();
2279
+ } catch (err) {
2280
+ console.error(`[agentwatch] adapter ${name} failed to start:`, err);
2281
+ return () => {
2282
+ };
2283
+ }
2284
+ }
2285
+ function safeStart(start, name) {
2286
+ try {
2287
+ return start();
2288
+ } catch (err) {
2289
+ console.error(`[agentwatch] adapter ${name} failed to start:`, err);
2290
+ return null;
2291
+ }
2292
+ }
2293
+
976
2294
  // src/util/claude-permissions.ts
977
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
978
- import { homedir as homedir3 } from "os";
979
- import { join as join3 } from "path";
2295
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
2296
+ import { homedir as homedir4 } from "os";
2297
+ import { join as join4 } from "path";
980
2298
  function readClaudePermissions(workspace) {
981
- const sources = [join3(homedir3(), ".claude", "settings.json")];
2299
+ const sources = [join4(homedir4(), ".claude", "settings.json")];
982
2300
  if (workspace) {
983
- sources.push(join3(workspace, ".claude", "settings.json"));
984
- sources.push(join3(workspace, ".claude", "settings.local.json"));
2301
+ sources.push(join4(workspace, ".claude", "settings.json"));
2302
+ sources.push(join4(workspace, ".claude", "settings.local.json"));
985
2303
  }
986
2304
  const out = [];
987
2305
  for (const path of sources) {
@@ -991,9 +2309,9 @@ function readClaudePermissions(workspace) {
991
2309
  return out;
992
2310
  }
993
2311
  function readOne(path) {
994
- if (!existsSync4(path)) return null;
2312
+ if (!existsSync5(path)) return null;
995
2313
  try {
996
- const raw = readFileSync2(path, "utf8");
2314
+ const raw = readFileSync3(path, "utf8");
997
2315
  const obj = JSON.parse(raw);
998
2316
  const perms = obj.permissions ?? {};
999
2317
  const allow = toStringArray(perms.allow);
@@ -1051,9 +2369,58 @@ function assessRisk({
1051
2369
  return flags;
1052
2370
  }
1053
2371
 
2372
+ // src/util/openclaw-config.ts
2373
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2374
+ import { homedir as homedir5 } from "os";
2375
+ import { join as join5 } from "path";
2376
+ function readOpenClawConfig() {
2377
+ const path = join5(homedir5(), ".openclaw", "openclaw.json");
2378
+ if (!existsSync6(path)) return null;
2379
+ try {
2380
+ const raw = readFileSync4(path, "utf8");
2381
+ const obj = JSON.parse(raw);
2382
+ const agentsObj = obj.agents ?? {};
2383
+ const defaults = agentsObj.defaults ?? {};
2384
+ const list = Array.isArray(agentsObj.list) ? agentsObj.list : [];
2385
+ return {
2386
+ source: path,
2387
+ defaultWorkspace: typeof defaults.workspace === "string" ? defaults.workspace : void 0,
2388
+ agents: list.filter(
2389
+ (a) => typeof a === "object" && a !== null
2390
+ ).map((a) => {
2391
+ const identity = a.identity ?? {};
2392
+ return {
2393
+ id: typeof a.id === "string" ? a.id : "unknown",
2394
+ default: a.default === true,
2395
+ workspace: typeof a.workspace === "string" ? a.workspace : void 0,
2396
+ model: typeof a.model === "string" ? a.model : void 0,
2397
+ name: typeof identity.name === "string" ? identity.name : void 0,
2398
+ emoji: typeof identity.emoji === "string" ? identity.emoji : void 0
2399
+ };
2400
+ })
2401
+ };
2402
+ } catch {
2403
+ return null;
2404
+ }
2405
+ }
2406
+
1054
2407
  // src/ui/App.tsx
1055
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2408
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1056
2409
  var MAX_EVENTS = 500;
2410
+ function matchesQuery(e, q) {
2411
+ const needle = q.toLowerCase();
2412
+ if ((e.summary ?? "").toLowerCase().includes(needle)) return true;
2413
+ if ((e.path ?? "").toLowerCase().includes(needle)) return true;
2414
+ if ((e.cmd ?? "").toLowerCase().includes(needle)) return true;
2415
+ if ((e.tool ?? "").toLowerCase().includes(needle)) return true;
2416
+ if ((e.agent ?? "").toLowerCase().includes(needle)) return true;
2417
+ const d = e.details;
2418
+ if (d) {
2419
+ if ((d.fullText ?? "").toLowerCase().includes(needle)) return true;
2420
+ if ((d.thinking ?? "").toLowerCase().includes(needle)) return true;
2421
+ }
2422
+ return false;
2423
+ }
1057
2424
  function findInsertIdx(events, ts) {
1058
2425
  let lo = 0;
1059
2426
  let hi = events.length;
@@ -1072,54 +2439,420 @@ function reducer(state, action) {
1072
2439
  const idx = findInsertIdx(next, action.event.ts);
1073
2440
  next.splice(idx, 0, action.event);
1074
2441
  if (next.length > MAX_EVENTS) next.length = MAX_EVENTS;
1075
- return { ...state, events: next };
2442
+ let sel = state.selectedIdx;
2443
+ if (sel !== null && idx <= sel) sel = sel + 1;
2444
+ return { ...state, events: next, selectedIdx: sel };
2445
+ }
2446
+ case "enrich": {
2447
+ const next = state.events.slice();
2448
+ for (let i = 0; i < next.length; i++) {
2449
+ if (next[i].id !== action.eventId) continue;
2450
+ const e = next[i];
2451
+ next[i] = {
2452
+ ...e,
2453
+ details: { ...e.details ?? {}, ...action.patch }
2454
+ };
2455
+ return { ...state, events: next };
2456
+ }
2457
+ return state;
1076
2458
  }
1077
2459
  case "toggle-agents":
1078
2460
  return { ...state, showAgents: !state.showAgents };
1079
2461
  case "toggle-permissions":
1080
- return { ...state, showPermissions: !state.showPermissions };
2462
+ return {
2463
+ ...state,
2464
+ showPermissions: !state.showPermissions,
2465
+ permissionsScroll: 0
2466
+ };
1081
2467
  case "cycle-filter": {
1082
2468
  const idx = state.filterAgent ? action.agents.indexOf(state.filterAgent) : -1;
1083
2469
  const next = idx + 1 >= action.agents.length ? null : action.agents[idx + 1];
1084
- return { ...state, filterAgent: next ?? null };
2470
+ return { ...state, filterAgent: next ?? null, selectedIdx: null };
1085
2471
  }
1086
2472
  case "toggle-pause":
1087
2473
  return { ...state, paused: !state.paused };
1088
2474
  case "clear":
1089
- return { ...state, events: [] };
2475
+ return { ...state, events: [], selectedIdx: null };
2476
+ case "move": {
2477
+ if (action.max <= 0) return state;
2478
+ const cur = state.selectedIdx ?? -1;
2479
+ const next = Math.max(0, Math.min(action.max - 1, cur + action.delta));
2480
+ return { ...state, selectedIdx: next };
2481
+ }
2482
+ case "open-detail":
2483
+ if (state.selectedIdx === null) return state;
2484
+ return { ...state, detailOpen: true, detailScroll: 0 };
2485
+ case "close-detail":
2486
+ return { ...state, detailOpen: false, detailScroll: 0 };
2487
+ case "scroll-detail": {
2488
+ const next = Math.max(0, Math.min(action.max, state.detailScroll + action.delta));
2489
+ return { ...state, detailScroll: next };
2490
+ }
2491
+ case "open-search":
2492
+ return { ...state, searchOpen: true, selectedIdx: null };
2493
+ case "close-search":
2494
+ return { ...state, searchOpen: false, searchQuery: "" };
2495
+ case "confirm-search":
2496
+ return { ...state, searchOpen: false };
2497
+ case "search-input":
2498
+ return {
2499
+ ...state,
2500
+ searchQuery: state.searchQuery + action.char,
2501
+ selectedIdx: null
2502
+ };
2503
+ case "search-backspace":
2504
+ return {
2505
+ ...state,
2506
+ searchQuery: state.searchQuery.slice(0, -1),
2507
+ selectedIdx: null
2508
+ };
2509
+ case "scope-subagent":
2510
+ return {
2511
+ ...state,
2512
+ subAgentScope: action.subAgentId,
2513
+ selectedIdx: null,
2514
+ detailOpen: false
2515
+ };
2516
+ case "unscope-subagent":
2517
+ return { ...state, subAgentScope: null, selectedIdx: null };
2518
+ case "toggle-projects":
2519
+ return {
2520
+ ...state,
2521
+ projectsOpen: !state.projectsOpen,
2522
+ projectsSelectedIdx: 0,
2523
+ detailOpen: false,
2524
+ showPermissions: false
2525
+ };
2526
+ case "projects-move": {
2527
+ if (action.max <= 0) return state;
2528
+ const next = Math.max(
2529
+ 0,
2530
+ Math.min(action.max - 1, state.projectsSelectedIdx + action.delta)
2531
+ );
2532
+ return { ...state, projectsSelectedIdx: next };
2533
+ }
2534
+ case "projects-select":
2535
+ return {
2536
+ ...state,
2537
+ sessionsForProject: action.name,
2538
+ sessionsSelectedIdx: 0,
2539
+ sessionsScroll: 0,
2540
+ projectsOpen: false
2541
+ };
2542
+ case "set-project-filter":
2543
+ return { ...state, projectFilter: action.project, selectedIdx: null };
2544
+ case "scroll-permissions": {
2545
+ const next = Math.max(0, Math.min(action.max, state.permissionsScroll + action.delta));
2546
+ return { ...state, permissionsScroll: next };
2547
+ }
2548
+ case "open-sessions":
2549
+ return {
2550
+ ...state,
2551
+ sessionsForProject: action.project,
2552
+ sessionsSelectedIdx: 0,
2553
+ sessionsScroll: 0
2554
+ };
2555
+ case "close-sessions":
2556
+ return { ...state, sessionsForProject: null };
2557
+ case "sessions-move": {
2558
+ if (action.max <= 0) return state;
2559
+ const next = Math.max(
2560
+ 0,
2561
+ Math.min(action.max - 1, state.sessionsSelectedIdx + action.delta)
2562
+ );
2563
+ return { ...state, sessionsSelectedIdx: next };
2564
+ }
2565
+ case "sessions-scroll": {
2566
+ const next = Math.max(0, Math.min(action.max, state.sessionsScroll + action.delta));
2567
+ return { ...state, sessionsScroll: next };
2568
+ }
2569
+ case "sessions-open-selected":
2570
+ return {
2571
+ ...state,
2572
+ sessionFilter: action.sessionId,
2573
+ sessionsForProject: null,
2574
+ selectedIdx: null
2575
+ };
2576
+ case "flash":
2577
+ return { ...state, flashMessage: action.text };
2578
+ case "flash-clear":
2579
+ return { ...state, flashMessage: null };
2580
+ case "toggle-help":
2581
+ return { ...state, showHelp: !state.showHelp };
2582
+ case "home":
2583
+ return {
2584
+ ...state,
2585
+ showHelp: false,
2586
+ showPermissions: false,
2587
+ detailOpen: false,
2588
+ projectsOpen: false,
2589
+ sessionsForProject: null,
2590
+ projectFilter: null,
2591
+ sessionFilter: null,
2592
+ subAgentScope: null,
2593
+ filterAgent: null,
2594
+ searchQuery: "",
2595
+ searchOpen: false,
2596
+ selectedIdx: null,
2597
+ detailScroll: 0,
2598
+ permissionsScroll: 0,
2599
+ sessionsScroll: 0
2600
+ };
2601
+ case "clear-filters":
2602
+ return {
2603
+ ...state,
2604
+ projectFilter: null,
2605
+ sessionFilter: null,
2606
+ subAgentScope: null,
2607
+ filterAgent: null,
2608
+ searchQuery: "",
2609
+ selectedIdx: null
2610
+ };
2611
+ case "back": {
2612
+ if (state.showHelp) return { ...state, showHelp: false };
2613
+ if (state.detailOpen) return { ...state, detailOpen: false, detailScroll: 0 };
2614
+ if (state.showPermissions)
2615
+ return { ...state, showPermissions: false, permissionsScroll: 0 };
2616
+ if (state.sessionsForProject)
2617
+ return { ...state, sessionsForProject: null, projectsOpen: true };
2618
+ if (state.projectsOpen) return { ...state, projectsOpen: false };
2619
+ if (state.subAgentScope)
2620
+ return { ...state, subAgentScope: null, selectedIdx: null };
2621
+ if (state.sessionFilter)
2622
+ return { ...state, sessionFilter: null, selectedIdx: null };
2623
+ if (state.projectFilter)
2624
+ return { ...state, projectFilter: null, selectedIdx: null };
2625
+ if (state.filterAgent)
2626
+ return { ...state, filterAgent: null, selectedIdx: null };
2627
+ if (state.searchQuery)
2628
+ return { ...state, searchQuery: "", selectedIdx: null };
2629
+ if (state.selectedIdx !== null) return { ...state, selectedIdx: null };
2630
+ return state;
2631
+ }
1090
2632
  }
1091
2633
  }
1092
2634
  function App() {
1093
2635
  const { exit } = useApp();
1094
2636
  const [workspace] = useState(detectWorkspaceRoot());
1095
2637
  const [agents] = useState(detectAgents());
1096
- const [permissions] = useState(() => readClaudePermissions(workspace));
2638
+ const [claudePerms] = useState(() => readClaudePermissions(workspace));
2639
+ const [openclawCfg] = useState(() => readOpenClawConfig());
2640
+ const [cursorStatus, setCursorStatus] = useState(
2641
+ void 0
2642
+ );
2643
+ const { stdout } = useStdout();
1097
2644
  const [state, dispatch] = useReducer(reducer, {
1098
2645
  events: [],
1099
2646
  filterAgent: null,
1100
2647
  showAgents: true,
1101
2648
  showPermissions: false,
1102
- paused: false
2649
+ paused: false,
2650
+ selectedIdx: null,
2651
+ detailOpen: false,
2652
+ detailScroll: 0,
2653
+ searchOpen: false,
2654
+ searchQuery: "",
2655
+ subAgentScope: null,
2656
+ projectsOpen: false,
2657
+ projectsSelectedIdx: 0,
2658
+ projectFilter: null,
2659
+ permissionsScroll: 0,
2660
+ sessionsForProject: null,
2661
+ sessionsSelectedIdx: 0,
2662
+ sessionsScroll: 0,
2663
+ sessionFilter: null,
2664
+ flashMessage: null,
2665
+ showHelp: false
1103
2666
  });
1104
2667
  useEffect(() => {
1105
- const onEvent = (e) => dispatch({ type: "event", event: e });
1106
- const stopClaude = startClaudeAdapter(onEvent);
1107
- const stopOpenClaw = startOpenClawAdapter(onEvent);
1108
- const cursor = startCursorAdapter(workspace, onEvent);
1109
- const stopFs = startFsAdapter(workspace, onEvent);
1110
- return () => {
1111
- stopClaude();
1112
- stopOpenClaw();
1113
- cursor.stop();
1114
- stopFs();
2668
+ const launchedAt = Date.now();
2669
+ const sink = {
2670
+ emit: (e) => {
2671
+ dispatch({ type: "event", event: e });
2672
+ const eventMs = new Date(e.ts).getTime();
2673
+ if (eventMs < launchedAt) return;
2674
+ const alert = shouldNotify(e);
2675
+ if (alert) notify(alert.title, alert.body);
2676
+ },
2677
+ enrich: (eventId, patch) => {
2678
+ dispatch({ type: "enrich", eventId, patch });
2679
+ }
1115
2680
  };
2681
+ const adapters = startAllAdapters(sink, workspace);
2682
+ const cursorAdapter = adapters.find((a) => a.name === "cursor");
2683
+ if (cursorAdapter?.status) setCursorStatus(cursorAdapter.status);
2684
+ return () => stopAllAdapters(adapters);
1116
2685
  }, [workspace]);
2686
+ const agentFiltered = state.filterAgent ? state.events.filter((e) => e.agent === state.filterAgent) : state.events;
2687
+ const scoped = state.subAgentScope ? agentFiltered.filter(
2688
+ (e) => e.sessionId === `agent-${state.subAgentScope}` || e.sessionId === state.subAgentScope || e.details?.subAgentId === state.subAgentScope
2689
+ ) : agentFiltered;
2690
+ const projectScoped = state.projectFilter ? scoped.filter(
2691
+ (e) => (e.summary ?? "").startsWith(`[${state.projectFilter}`)
2692
+ ) : scoped;
2693
+ const sessionScoped = state.sessionFilter ? projectScoped.filter((e) => e.sessionId === state.sessionFilter) : projectScoped;
2694
+ const filtered = state.searchQuery ? sessionScoped.filter((e) => matchesQuery(e, state.searchQuery)) : sessionScoped;
2695
+ const projects = buildProjectIndex(state.events);
2696
+ const sessionsForOpen = state.sessionsForProject ? buildSessionRows(state.events, state.sessionsForProject) : [];
2697
+ const childCountByAgentId = /* @__PURE__ */ new Map();
2698
+ for (const e of state.events) {
2699
+ if (e.sessionId?.startsWith("agent-")) {
2700
+ const aid = e.sessionId.slice("agent-".length);
2701
+ childCountByAgentId.set(aid, (childCountByAgentId.get(aid) ?? 0) + 1);
2702
+ }
2703
+ }
2704
+ const cols = stdout.columns || 120;
2705
+ const rows = stdout.rows || 30;
2706
+ const tooNarrow = cols < 60;
2707
+ const tooShort = rows < 12;
2708
+ const selectedEvent = state.selectedIdx !== null ? filtered[state.selectedIdx] : void 0;
2709
+ const detailRowCount = selectedEvent ? totalDetailRows(selectedEvent, cols - 6) : 0;
1117
2710
  useInput((input, key) => {
1118
- if (input === "q" || key.ctrl && input === "c") {
2711
+ if (key.ctrl && input === "c") {
2712
+ exit();
2713
+ setImmediate(() => process.exit(0));
2714
+ return;
2715
+ }
2716
+ if (state.searchOpen) {
2717
+ if (key.escape) {
2718
+ dispatch({ type: "close-search" });
2719
+ return;
2720
+ }
2721
+ if (key.return) {
2722
+ dispatch({ type: "confirm-search" });
2723
+ return;
2724
+ }
2725
+ if (key.backspace || key.delete) {
2726
+ dispatch({ type: "search-backspace" });
2727
+ return;
2728
+ }
2729
+ if (input && !key.ctrl && !key.meta) {
2730
+ dispatch({ type: "search-input", char: input });
2731
+ return;
2732
+ }
2733
+ return;
2734
+ }
2735
+ if (input === "q") {
1119
2736
  exit();
1120
2737
  setImmediate(() => process.exit(0));
1121
2738
  return;
1122
2739
  }
2740
+ if (state.projectsOpen) {
2741
+ if (key.escape) {
2742
+ dispatch({ type: "back" });
2743
+ return;
2744
+ }
2745
+ if (key.downArrow || input === "j") {
2746
+ dispatch({ type: "projects-move", delta: 1, max: projects.length });
2747
+ return;
2748
+ }
2749
+ if (key.upArrow || input === "k") {
2750
+ dispatch({ type: "projects-move", delta: -1, max: projects.length });
2751
+ return;
2752
+ }
2753
+ if (key.return) {
2754
+ const p = projects[state.projectsSelectedIdx];
2755
+ if (p) dispatch({ type: "projects-select", name: p.name });
2756
+ return;
2757
+ }
2758
+ return;
2759
+ }
2760
+ if (state.sessionsForProject) {
2761
+ const lineCount = sessionLineCount(sessionsForOpen);
2762
+ const viewport = Math.max(3, rows - 8);
2763
+ const maxScroll = Math.max(0, lineCount - viewport);
2764
+ if (key.escape) {
2765
+ dispatch({ type: "back" });
2766
+ return;
2767
+ }
2768
+ if (key.downArrow || input === "j") {
2769
+ dispatch({
2770
+ type: "sessions-move",
2771
+ delta: 1,
2772
+ max: sessionsForOpen.length
2773
+ });
2774
+ dispatch({ type: "sessions-scroll", delta: 1, max: maxScroll });
2775
+ return;
2776
+ }
2777
+ if (key.upArrow || input === "k") {
2778
+ dispatch({
2779
+ type: "sessions-move",
2780
+ delta: -1,
2781
+ max: sessionsForOpen.length
2782
+ });
2783
+ dispatch({ type: "sessions-scroll", delta: -1, max: maxScroll });
2784
+ return;
2785
+ }
2786
+ if (key.return) {
2787
+ const s = sessionsForOpen[state.sessionsSelectedIdx];
2788
+ if (s)
2789
+ dispatch({ type: "sessions-open-selected", sessionId: s.sessionId });
2790
+ return;
2791
+ }
2792
+ return;
2793
+ }
2794
+ if (state.showPermissions) {
2795
+ const total = permissionRowCount(claudePerms, cursorStatus, openclawCfg);
2796
+ const viewport = Math.max(3, rows - 8);
2797
+ const maxScroll = Math.max(0, total - viewport);
2798
+ if (key.escape || input === "p") {
2799
+ dispatch({ type: "back" });
2800
+ return;
2801
+ }
2802
+ if (key.downArrow || input === "j") {
2803
+ dispatch({ type: "scroll-permissions", delta: 1, max: maxScroll });
2804
+ return;
2805
+ }
2806
+ if (key.upArrow || input === "k") {
2807
+ dispatch({ type: "scroll-permissions", delta: -1, max: maxScroll });
2808
+ return;
2809
+ }
2810
+ return;
2811
+ }
2812
+ if (state.detailOpen) {
2813
+ if (key.escape) {
2814
+ dispatch({ type: "back" });
2815
+ return;
2816
+ }
2817
+ if (key.downArrow || input === "j") {
2818
+ dispatch({ type: "scroll-detail", delta: 1, max: Math.max(0, detailRowCount - (rows - 10)) });
2819
+ return;
2820
+ }
2821
+ if (key.upArrow || input === "k") {
2822
+ dispatch({ type: "scroll-detail", delta: -1, max: Math.max(0, detailRowCount - (rows - 10)) });
2823
+ return;
2824
+ }
2825
+ return;
2826
+ }
2827
+ if (input === "/") dispatch({ type: "open-search" });
2828
+ if (input === "x" && state.selectedIdx !== null) {
2829
+ const ev = filtered[state.selectedIdx];
2830
+ const sid = ev?.details?.subAgentId;
2831
+ if (sid) dispatch({ type: "scope-subagent", subAgentId: sid });
2832
+ }
2833
+ if (input === "X") dispatch({ type: "unscope-subagent" });
2834
+ if (input === "y" && state.selectedIdx !== null) {
2835
+ const ev = filtered[state.selectedIdx];
2836
+ if (ev) {
2837
+ const text = eventToYankText(
2838
+ ev.summary,
2839
+ ev.path,
2840
+ ev.cmd,
2841
+ ev.details?.toolResult,
2842
+ ev.details?.fullText
2843
+ );
2844
+ if (text) {
2845
+ const res = copyToClipboard(text);
2846
+ const message = res.ok ? `\u2713 copied ${text.length} chars to clipboard` : `\u2717 ${res.reason}`;
2847
+ dispatch({ type: "flash", text: message });
2848
+ setTimeout(() => dispatch({ type: "flash-clear" }), 2e3);
2849
+ }
2850
+ }
2851
+ }
2852
+ if (input === "P") dispatch({ type: "toggle-projects" });
2853
+ if (input === "A") {
2854
+ dispatch({ type: "set-project-filter", project: null });
2855
+ }
1123
2856
  if (input === "a") dispatch({ type: "toggle-agents" });
1124
2857
  if (input === "f") {
1125
2858
  const presentAgents = agents.filter((a) => a.present).map((a) => a.name);
@@ -1129,10 +2862,31 @@ function App() {
1129
2862
  if (input === " ") dispatch({ type: "toggle-pause" });
1130
2863
  if (input === "p") dispatch({ type: "toggle-permissions" });
1131
2864
  if (input === "c") dispatch({ type: "clear" });
2865
+ if (key.downArrow || input === "j")
2866
+ dispatch({ type: "move", delta: 1, max: filtered.length });
2867
+ if (key.upArrow || input === "k")
2868
+ dispatch({ type: "move", delta: -1, max: filtered.length });
2869
+ if (key.return || input === "l") dispatch({ type: "open-detail" });
2870
+ if (key.escape) dispatch({ type: "back" });
1132
2871
  });
1133
- const filtered = state.filterAgent ? state.events.filter((e) => e.agent === state.filterAgent) : state.events;
1134
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1135
- /* @__PURE__ */ jsx5(
2872
+ if (tooNarrow || tooShort) {
2873
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", padding: 1, children: [
2874
+ /* @__PURE__ */ jsx10(Text10, { color: "yellow", bold: true, children: "Terminal too small for the agentwatch TUI" }),
2875
+ /* @__PURE__ */ jsxs10(Text10, { children: [
2876
+ "Detected: ",
2877
+ cols,
2878
+ " cols \xD7 ",
2879
+ rows,
2880
+ " rows"
2881
+ ] }),
2882
+ /* @__PURE__ */ jsx10(Text10, { children: "Minimum: 60 cols \xD7 12 rows" }),
2883
+ /* @__PURE__ */ jsx10(Text10, { children: " " }),
2884
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Resize the window and restart, or run `agentwatch doctor` for a compact view." }),
2885
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Press q to quit." })
2886
+ ] });
2887
+ }
2888
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
2889
+ /* @__PURE__ */ jsx10(
1136
2890
  Header,
1137
2891
  {
1138
2892
  workspace,
@@ -1141,20 +2895,80 @@ function App() {
1141
2895
  paused: state.paused
1142
2896
  }
1143
2897
  ),
1144
- state.showPermissions ? /* @__PURE__ */ jsx5(PermissionView, { permissions }) : /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", children: [
1145
- /* @__PURE__ */ jsx5(Box5, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx5(Timeline, { events: filtered }) }),
1146
- state.showAgents && /* @__PURE__ */ jsx5(Box5, { width: 32, marginLeft: 1, children: /* @__PURE__ */ jsx5(AgentPanel, { agents, events: state.events }) })
2898
+ /* @__PURE__ */ jsx10(
2899
+ Breadcrumb,
2900
+ {
2901
+ projectFilter: state.projectFilter,
2902
+ sessionFilter: state.sessionFilter,
2903
+ sessionsForProject: state.sessionsForProject,
2904
+ subAgentScope: state.subAgentScope,
2905
+ agentFilter: state.filterAgent,
2906
+ searchQuery: state.searchQuery,
2907
+ view: state.showHelp ? "help" : state.detailOpen ? "detail" : state.showPermissions ? "permissions" : state.sessionsForProject ? "sessions" : state.projectsOpen ? "projects" : "timeline"
2908
+ }
2909
+ ),
2910
+ state.showHelp ? /* @__PURE__ */ jsx10(HelpView, {}) : state.sessionsForProject ? /* @__PURE__ */ jsx10(
2911
+ SessionsView,
2912
+ {
2913
+ project: state.sessionsForProject,
2914
+ sessions: sessionsForOpen,
2915
+ selectedIdx: state.sessionsSelectedIdx,
2916
+ viewportRows: Math.max(3, rows - 8),
2917
+ scrollOffset: state.sessionsScroll
2918
+ }
2919
+ ) : state.projectsOpen ? /* @__PURE__ */ jsx10(
2920
+ ProjectsView,
2921
+ {
2922
+ projects,
2923
+ selectedIdx: state.projectsSelectedIdx,
2924
+ searchQuery: state.searchQuery
2925
+ }
2926
+ ) : state.detailOpen && selectedEvent ? /* @__PURE__ */ jsx10(
2927
+ EventDetail,
2928
+ {
2929
+ event: selectedEvent,
2930
+ width: cols,
2931
+ height: rows - 4,
2932
+ scrollOffset: state.detailScroll
2933
+ }
2934
+ ) : state.showPermissions ? /* @__PURE__ */ jsx10(
2935
+ PermissionView,
2936
+ {
2937
+ claude: claudePerms,
2938
+ cursor: cursorStatus,
2939
+ openclaw: openclawCfg,
2940
+ viewportRows: Math.max(3, rows - 8),
2941
+ scrollOffset: state.permissionsScroll
2942
+ }
2943
+ ) : /* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", children: [
2944
+ /* @__PURE__ */ jsx10(Box10, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(
2945
+ Timeline,
2946
+ {
2947
+ events: filtered,
2948
+ selectedIdx: state.selectedIdx,
2949
+ childCountByAgentId
2950
+ }
2951
+ ) }),
2952
+ state.showAgents && /* @__PURE__ */ jsx10(Box10, { width: 32, marginLeft: 1, children: /* @__PURE__ */ jsx10(AgentPanel, { agents, events: state.events }) })
1147
2953
  ] }),
1148
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1149
- "[q] quit [a] agents [f] filter [p] permissions [space] ",
1150
- state.paused ? "resume" : "pause",
1151
- " [c] clear"
1152
- ] }) })
2954
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexDirection: "column", children: [
2955
+ state.flashMessage && /* @__PURE__ */ jsx10(Text10, { color: state.flashMessage.startsWith("\u2713") ? "green" : "red", children: state.flashMessage }),
2956
+ (state.searchOpen || state.searchQuery) && /* @__PURE__ */ jsxs10(Text10, { children: [
2957
+ /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "/ " }),
2958
+ /* @__PURE__ */ jsx10(Text10, { children: state.searchQuery }),
2959
+ state.searchOpen && /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "\u258C" }),
2960
+ state.searchQuery && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2961
+ " matches: ",
2962
+ filtered.length
2963
+ ] })
2964
+ ] }),
2965
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: state.searchOpen ? "[type to search] [enter] confirm [esc] clear" : state.sessionsForProject ? "[\u2191\u2193] select session [enter] open [esc] back to projects" : state.projectsOpen ? "[\u2191\u2193] select project [enter] sessions [esc] close" : state.detailOpen ? "[esc] close [\u2191\u2193] scroll" : `[?] help [q] quit [0] home [esc] back [\u2191\u2193] select [enter] detail [/] search [P] projects [p] permissions [Z] clear filters` })
2966
+ ] })
1153
2967
  ] });
1154
2968
  }
1155
2969
 
1156
2970
  // src/index.tsx
1157
- import { jsx as jsx6 } from "react/jsx-runtime";
2971
+ import { jsx as jsx11 } from "react/jsx-runtime";
1158
2972
  var arg = process.argv[2];
1159
2973
  var ENTER_ALT_SCREEN = "\x1B[?1049h\x1B[2J\x1B[H";
1160
2974
  var LEAVE_ALT_SCREEN = "\x1B[?1049l";
@@ -1185,7 +2999,7 @@ Environment:
1185
2999
  process.exit(0);
1186
3000
  }
1187
3001
  if (arg === "doctor") {
1188
- const { detectAgents: detectAgents2 } = await import("./detect-57ZQXQNN.js");
3002
+ const { detectAgents: detectAgents2 } = await import("./detect-JH6COHZ5.js");
1189
3003
  const { detectWorkspaceRoot: detectWorkspaceRoot2 } = await import("./workspace-N6FQVHKD.js");
1190
3004
  const agents = detectAgents2();
1191
3005
  console.log(`workspace: ${detectWorkspaceRoot2()}
@@ -1193,10 +3007,23 @@ if (arg === "doctor") {
1193
3007
  console.log("agents:");
1194
3008
  for (const a of agents) {
1195
3009
  const mark = a.present ? "\u25CF" : "\u25CB";
1196
- const status = a.present ? "installed" : "not detected";
1197
- console.log(` ${mark} ${a.label.padEnd(14)} ${status}`);
3010
+ const status = !a.present ? "not detected" : a.instrumented ? "installed (events captured)" : "detected (events not yet captured \u2014 help us ship this)";
3011
+ console.log(` ${mark} ${a.label.padEnd(18)} ${status}`);
1198
3012
  if (a.configPath) console.log(` config: ${a.configPath}`);
1199
3013
  }
3014
+ const notInstrumented = agents.filter((a) => a.present && !a.instrumented);
3015
+ if (notInstrumented.length > 0) {
3016
+ console.log("");
3017
+ console.log("Agents detected but not yet instrumented:");
3018
+ for (const a of notInstrumented) {
3019
+ console.log(` - ${a.label}`);
3020
+ }
3021
+ console.log("");
3022
+ console.log(
3023
+ "If you want events captured for these, open an issue with a redacted session file:"
3024
+ );
3025
+ console.log(" https://github.com/mishanefedov/agentwatch/issues/new");
3026
+ }
1200
3027
  process.exit(0);
1201
3028
  }
1202
3029
  enterAltScreen();
@@ -1206,5 +3033,5 @@ for (const sig of ["exit", "SIGINT", "SIGTERM", "SIGHUP"]) {
1206
3033
  if (sig !== "exit") process.exit(0);
1207
3034
  });
1208
3035
  }
1209
- var { waitUntilExit } = render(/* @__PURE__ */ jsx6(App, {}));
3036
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx11(App, {}));
1210
3037
  waitUntilExit().finally(() => leaveAltScreen());