agenttop 0.8.2 → 0.9.0

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
@@ -17,7 +17,7 @@ import {
17
17
  setNickname,
18
18
  startMcpServer,
19
19
  unarchiveSession
20
- } from "./chunk-MQFICHZ4.js";
20
+ } from "./chunk-2GBTSKHW.js";
21
21
 
22
22
  // src/index.tsx
23
23
  import { readFileSync as readFileSync4 } from "fs";
@@ -27,7 +27,7 @@ import React16 from "react";
27
27
  import { render } from "ink";
28
28
 
29
29
  // src/ui/App.tsx
30
- import { useState as useState13, useEffect as useEffect9, useCallback as useCallback4 } from "react";
30
+ import { useState as useState15, useEffect as useEffect9, useCallback as useCallback6 } from "react";
31
31
  import { Box as Box15, Text as Text14, useApp, useStdout as useStdout3 } from "ink";
32
32
 
33
33
  // src/config/themes.ts
@@ -347,132 +347,16 @@ var deriveSeverityColors = (c) => ({
347
347
  critical: c.critical
348
348
  });
349
349
 
350
- // src/hooks/installer.ts
351
- import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, chmodSync } from "fs";
352
- import { join, dirname } from "path";
353
- import { homedir } from "os";
354
- import { fileURLToPath } from "url";
355
- var HOOK_FILENAME = "agenttop-guard.py";
356
- var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
357
- var getHookSource = () => {
358
- const thisFile = fileURLToPath(import.meta.url);
359
- const srcHooksDir = join(dirname(thisFile), "..", "src", "hooks");
360
- const distHooksDir = join(dirname(thisFile), "hooks");
361
- for (const dir of [distHooksDir, srcHooksDir]) {
362
- const path = join(dir, HOOK_FILENAME);
363
- if (existsSync(path)) return path;
364
- }
365
- const npmGlobalPath = join(dirname(thisFile), "..", "hooks", HOOK_FILENAME);
366
- if (existsSync(npmGlobalPath)) return npmGlobalPath;
367
- throw new Error(`cannot find ${HOOK_FILENAME} \u2014 is agenttop installed correctly?`);
368
- };
369
- var getHookTarget = () => {
370
- const claudeHooksDir = join(homedir(), ".claude", "hooks");
371
- mkdirSync(claudeHooksDir, { recursive: true });
372
- return join(claudeHooksDir, HOOK_FILENAME);
373
- };
374
- var readSettings = () => {
375
- if (!existsSync(SETTINGS_PATH)) {
376
- return {};
377
- }
378
- try {
379
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
380
- } catch {
381
- return {};
382
- }
383
- };
384
- var writeSettings = (settings) => {
385
- mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
386
- writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
387
- };
388
- var installHooks = () => {
389
- const source = getHookSource();
390
- const target = getHookTarget();
391
- copyFileSync(source, target);
392
- chmodSync(target, 493);
393
- const settings = readSettings();
394
- const hooks = settings.hooks ?? {};
395
- const postToolUse = hooks.PostToolUse ?? [];
396
- const hookCommand = target;
397
- const allToolsMatcher = postToolUse.find(
398
- (entry) => entry.matcher === "Bash|Read|Grep|Glob|WebFetch|WebSearch"
399
- );
400
- if (allToolsMatcher) {
401
- const alreadyInstalled = allToolsMatcher.hooks.some((h) => h.command.includes("agenttop-guard"));
402
- if (alreadyInstalled) {
403
- process.stdout.write("agenttop hooks already installed\n");
404
- return;
405
- }
406
- allToolsMatcher.hooks.push({ type: "command", command: hookCommand });
407
- } else {
408
- postToolUse.push({
409
- matcher: "Bash|Read|Grep|Glob|WebFetch|WebSearch",
410
- hooks: [{ type: "command", command: hookCommand }]
411
- });
412
- }
413
- hooks.PostToolUse = postToolUse;
414
- settings.hooks = hooks;
415
- writeSettings(settings);
416
- process.stdout.write(`agenttop hooks installed:
417
- `);
418
- process.stdout.write(` hook: ${target}
419
- `);
420
- process.stdout.write(` settings: ${SETTINGS_PATH}
421
- `);
422
- process.stdout.write(` matcher: PostToolUse (Bash|Read|Grep|Glob|WebFetch|WebSearch)
423
- `);
424
- };
425
- var uninstallHooks = () => {
426
- const settings = readSettings();
427
- const hooks = settings.hooks ?? {};
428
- const postToolUse = hooks.PostToolUse ?? [];
429
- let removed = false;
430
- for (const entry of postToolUse) {
431
- const before = entry.hooks.length;
432
- entry.hooks = entry.hooks.filter((h) => !h.command.includes("agenttop-guard"));
433
- if (entry.hooks.length < before) removed = true;
434
- }
435
- hooks.PostToolUse = postToolUse.filter((e) => e.hooks.length > 0);
436
- settings.hooks = hooks;
437
- writeSettings(settings);
438
- if (removed) {
439
- process.stdout.write("agenttop hooks removed from Claude Code settings\n");
440
- } else {
441
- process.stdout.write("agenttop hooks were not installed\n");
442
- }
443
- };
444
-
445
- // src/install-mcp.ts
446
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
447
- import { join as join2 } from "path";
448
- import { homedir as homedir2 } from "os";
449
- var installMcpConfig = () => {
450
- const settingsPath = join2(homedir2(), ".claude", "settings.json");
451
- let settings = {};
452
- try {
453
- settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
454
- } catch {
455
- }
456
- const mcpServers = settings.mcpServers ?? {};
457
- mcpServers.agenttop = { command: "agenttop", args: ["--mcp"] };
458
- settings.mcpServers = mcpServers;
459
- mkdirSync2(join2(homedir2(), ".claude"), { recursive: true });
460
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n");
461
- process.stdout.write("agenttop MCP server registered in Claude Code settings\n");
462
- process.stdout.write(` settings: ${settingsPath}
463
- `);
464
- };
465
-
466
350
  // src/updates.ts
467
351
  import { execSync, exec } from "child_process";
468
- import { readFileSync as readFileSync3 } from "fs";
469
- import { join as join3, dirname as dirname2 } from "path";
470
- import { fileURLToPath as fileURLToPath2 } from "url";
352
+ import { readFileSync } from "fs";
353
+ import { join, dirname } from "path";
354
+ import { fileURLToPath } from "url";
471
355
  var getPackageVersion = () => {
472
356
  try {
473
- const thisFile = fileURLToPath2(import.meta.url);
474
- const pkgPath = join3(dirname2(thisFile), "..", "package.json");
475
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
357
+ const thisFile = fileURLToPath(import.meta.url);
358
+ const pkgPath = join(dirname(thisFile), "..", "package.json");
359
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
476
360
  return pkg.version || "0.0.0";
477
361
  } catch {
478
362
  return "0.0.0";
@@ -606,30 +490,18 @@ var formatTokens = (n) => {
606
490
  return String(n);
607
491
  };
608
492
  var truncate = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
609
- var getDisplayName = (session) => {
610
- if (session.nickname) return session.nickname;
611
- if (session.cwd) {
612
- const parts = session.cwd.replace(/\/+$/, "").split("/");
613
- return parts[parts.length - 1] || session.slug;
614
- }
615
- if (session.project) {
616
- const parts = session.project.replace(/\/+$/, "").split("/");
617
- return parts[parts.length - 1] || session.slug;
618
- }
619
- return session.slug;
620
- };
621
493
  var SIDEBAR_WIDTH = 30;
622
494
  var INNER_WIDTH = SIDEBAR_WIDTH - 4;
623
- var LINES_PER_SESSION = 3;
495
+ var LINES_PER_ITEM = 3;
624
496
  var SessionList = React2.memo(
625
- ({ sessions, selectedIndex, focused, height, filter, viewingArchive }) => {
497
+ ({ visibleItems, selectedIndex, focused, height, filter, viewingArchive, totalSessions }) => {
626
498
  const availableRows = height - 2;
627
- const maxVisible = Math.max(1, Math.floor((availableRows + 1) / LINES_PER_SESSION));
499
+ const maxVisible = Math.max(1, Math.floor((availableRows + 1) / LINES_PER_ITEM));
628
500
  const halfView = Math.floor(maxVisible / 2);
629
- const scrollStart = Math.max(0, Math.min(selectedIndex - halfView, sessions.length - maxVisible));
501
+ const scrollStart = Math.max(0, Math.min(selectedIndex - halfView, visibleItems.length - maxVisible));
630
502
  const start = Math.max(0, scrollStart);
631
- const visible = sessions.slice(start, start + maxVisible);
632
- const canScroll = sessions.length > maxVisible;
503
+ const visible = visibleItems.slice(start, start + maxVisible);
504
+ const canScroll = visibleItems.length > maxVisible;
633
505
  return /* @__PURE__ */ jsxs2(
634
506
  Box2,
635
507
  {
@@ -651,21 +523,104 @@ var SessionList = React2.memo(
651
523
  canScroll && /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, children: [
652
524
  selectedIndex + 1,
653
525
  "/",
654
- sessions.length
655
- ] })
526
+ visibleItems.length
527
+ ] }),
528
+ !canScroll && totalSessions > 0 && /* @__PURE__ */ jsx2(Text2, { color: colors.muted, children: totalSessions })
656
529
  ] }),
657
- sessions.length === 0 && /* @__PURE__ */ jsx2(Box2, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx2(Text2, { color: colors.muted, italic: true, children: filter ? "No matches" : viewingArchive ? "No archived" : "No sessions" }) }),
658
- visible.map((session, vi) => {
530
+ visibleItems.length === 0 && /* @__PURE__ */ jsx2(Box2, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx2(Text2, { color: colors.muted, italic: true, children: filter ? "No matches" : viewingArchive ? "No archived" : "No sessions" }) }),
531
+ visible.map((item, vi) => {
659
532
  const realIdx = start + vi;
660
533
  const isSelected = realIdx === selectedIndex;
534
+ if (item.type === "group") {
535
+ const g = item.group;
536
+ const arrow = g.expanded ? "\u25BE" : "\u25B8";
537
+ const dotColor2 = g.isActive ? colors.success : colors.muted;
538
+ const statusDot2 = g.isActive ? "\u25CF" : "\u25CB";
539
+ const nameColor2 = isSelected ? colors.bright : g.isActive ? colors.secondary : colors.text;
540
+ const label2 = truncate(`${g.key} (${g.sessions.length})`, INNER_WIDTH - 4);
541
+ const model2 = formatModel(g.latestModel);
542
+ return /* @__PURE__ */ jsxs2(
543
+ Box2,
544
+ {
545
+ flexDirection: "column",
546
+ paddingX: 1,
547
+ backgroundColor: isSelected ? colors.selected : void 0,
548
+ children: [
549
+ /* @__PURE__ */ jsxs2(Text2, { color: nameColor2, bold: isSelected, wrap: "truncate", children: [
550
+ arrow,
551
+ " ",
552
+ /* @__PURE__ */ jsx2(Text2, { color: dotColor2, children: statusDot2 }),
553
+ " ",
554
+ label2
555
+ ] }),
556
+ /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, wrap: "truncate", children: [
557
+ " ",
558
+ model2,
559
+ " ",
560
+ formatTokens(g.totalInputTokens),
561
+ "in ",
562
+ formatTokens(g.totalOutputTokens),
563
+ "out"
564
+ ] }),
565
+ vi < visible.length - 1 && /* @__PURE__ */ jsx2(Text2, { color: colors.border, children: "\u2500".repeat(INNER_WIDTH) })
566
+ ]
567
+ },
568
+ `group-${g.key}`
569
+ );
570
+ }
571
+ const session = item.type === "session" ? item.session : item.session;
661
572
  const isActive = session.pid !== null;
662
- const indicator = isSelected ? "\u25B8" : " ";
663
573
  const statusDot = isActive ? "\u25CF" : "\u25CB";
664
- const displayName = truncate(getDisplayName(session), INNER_WIDTH - 4);
574
+ const dotColor = isActive ? colors.success : colors.muted;
665
575
  const totalIn = session.usage.inputTokens + session.usage.cacheReadTokens;
666
576
  const model = formatModel(session.model);
577
+ if (item.type === "session") {
578
+ const nameColor2 = isSelected ? colors.bright : isActive ? colors.secondary : colors.muted;
579
+ const displayName2 = truncate(session.slug, INNER_WIDTH - 6);
580
+ return /* @__PURE__ */ jsxs2(
581
+ Box2,
582
+ {
583
+ flexDirection: "column",
584
+ paddingX: 1,
585
+ backgroundColor: isSelected ? colors.selected : void 0,
586
+ children: [
587
+ /* @__PURE__ */ jsxs2(Text2, { color: nameColor2, bold: isSelected, wrap: "truncate", children: [
588
+ " ",
589
+ " ",
590
+ /* @__PURE__ */ jsx2(Text2, { color: dotColor, children: statusDot }),
591
+ " ",
592
+ displayName2
593
+ ] }),
594
+ /* @__PURE__ */ jsxs2(Text2, { color: colors.muted, wrap: "truncate", children: [
595
+ " ",
596
+ model,
597
+ " ",
598
+ formatTokens(totalIn),
599
+ "in ",
600
+ formatTokens(session.usage.outputTokens),
601
+ "out"
602
+ ] }),
603
+ vi < visible.length - 1 && /* @__PURE__ */ jsx2(Text2, { color: colors.border, children: "\u2500".repeat(INNER_WIDTH) })
604
+ ]
605
+ },
606
+ session.sessionId
607
+ );
608
+ }
609
+ const indicator = isSelected ? "\u25B8" : " ";
667
610
  const nameColor = isSelected ? colors.bright : isActive ? colors.secondary : colors.text;
668
- const dotColor = isActive ? colors.success : colors.muted;
611
+ const getDisplayName2 = (s) => {
612
+ if (s.nickname) return s.nickname;
613
+ if (s.cwd) {
614
+ const parts = s.cwd.replace(/\/+$/, "").split("/");
615
+ return parts[parts.length - 1] || s.slug;
616
+ }
617
+ if (s.project) {
618
+ const parts = s.project.replace(/\/+$/, "").split("/");
619
+ return parts[parts.length - 1] || s.slug;
620
+ }
621
+ return s.slug;
622
+ };
623
+ const displayName = truncate(getDisplayName2(session), INNER_WIDTH - 4);
669
624
  return /* @__PURE__ */ jsxs2(
670
625
  Box2,
671
626
  {
@@ -705,6 +660,16 @@ var SessionList = React2.memo(
705
660
  import React3 from "react";
706
661
  import { Box as Box3, Text as Text3 } from "ink";
707
662
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
663
+ var TAG_COLORS = [
664
+ "#61AFEF",
665
+ "#98C379",
666
+ "#C678DD",
667
+ "#E5C07B",
668
+ "#E06C75",
669
+ "#56B6C2",
670
+ "#D19A66",
671
+ "#BE5046"
672
+ ];
708
673
  var formatTime = (ts) => {
709
674
  const d = new Date(ts);
710
675
  return d.toLocaleTimeString("en-GB", { hour12: false });
@@ -732,7 +697,7 @@ var summarizeInput = (call) => {
732
697
  }
733
698
  };
734
699
  var ActivityFeed = React3.memo(
735
- ({ events, sessionSlug, focused, height, scrollOffset, filter }) => {
700
+ ({ events, sessionSlug, sessionId, isActive, focused, height, scrollOffset, filter, merged, mergedSessions }) => {
736
701
  const viewportRows = height - 2;
737
702
  const totalEvents = events.length;
738
703
  const start = Math.max(0, totalEvents - viewportRows - scrollOffset);
@@ -741,6 +706,12 @@ var ActivityFeed = React3.memo(
741
706
  const isAtBottom = scrollOffset === 0;
742
707
  const isAtTop = start === 0;
743
708
  const canScroll = totalEvents > viewportRows;
709
+ const slugColorMap = /* @__PURE__ */ new Map();
710
+ if (merged && mergedSessions) {
711
+ mergedSessions.forEach((s, i) => {
712
+ slugColorMap.set(s.sessionId, TAG_COLORS[i % TAG_COLORS.length]);
713
+ });
714
+ }
744
715
  return /* @__PURE__ */ jsxs3(
745
716
  Box3,
746
717
  {
@@ -757,6 +728,7 @@ var ActivityFeed = React3.memo(
757
728
  sessionSlug,
758
729
  ")"
759
730
  ] }),
731
+ merged && /* @__PURE__ */ jsx3(Text3, { color: colors.accent, children: " [merged]" }),
760
732
  filter && /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
761
733
  " [",
762
734
  filter.length > 10 ? filter.slice(0, 9) + "\u2026" : filter,
@@ -771,19 +743,46 @@ var ActivityFeed = React3.memo(
771
743
  "]"
772
744
  ] })
773
745
  ] }),
774
- visible.length === 0 && /* @__PURE__ */ jsx3(Box3, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx3(Text3, { color: colors.muted, italic: true, children: sessionSlug ? "Waiting for activity..." : "Select a session" }) }),
775
- visible.map((event, i) => /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, children: [
746
+ visible.length === 0 && /* @__PURE__ */ jsx3(Box3, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx3(Text3, { color: colors.muted, italic: true, children: sessionSlug || merged ? "Waiting for activity..." : "Select a session" }) }),
747
+ visible.map((event, i) => {
748
+ const tag = merged ? event.slug.slice(0, 4) : null;
749
+ const tagColor = merged ? slugColorMap.get(event.sessionId) || colors.muted : void 0;
750
+ return /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, children: [
751
+ tag && /* @__PURE__ */ jsx3(Text3, { color: tagColor, children: tag.padEnd(5) }),
752
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
753
+ formatTime(event.timestamp),
754
+ " "
755
+ ] }),
756
+ /* @__PURE__ */ jsx3(Text3, { color: getToolColor(event.toolName), bold: true, children: event.toolName.padEnd(8) }),
757
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.text, children: [
758
+ " ",
759
+ summarizeInput(event)
760
+ ] })
761
+ ] }, `${event.timestamp}-${i}`);
762
+ }),
763
+ focused && canScroll && !isAtTop && visible.length > 0 && /* @__PURE__ */ jsx3(Box3, { paddingX: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx3(Text3, { color: colors.muted, children: isAtBottom ? "" : "G:bottom " }) }),
764
+ !merged && sessionId && isActive === false && /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, flexDirection: "column", children: [
765
+ /* @__PURE__ */ jsx3(Text3, { color: colors.muted, children: " " }),
776
766
  /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
777
- formatTime(event.timestamp),
778
- " "
779
- ] }),
780
- /* @__PURE__ */ jsx3(Text3, { color: getToolColor(event.toolName), bold: true, children: event.toolName.padEnd(8) }),
781
- /* @__PURE__ */ jsxs3(Text3, { color: colors.text, children: [
782
- " ",
783
- summarizeInput(event)
767
+ "resume: ",
768
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.text, children: [
769
+ "claude --resume ",
770
+ sessionId
771
+ ] })
784
772
  ] })
785
- ] }, `${event.timestamp}-${i}`)),
786
- focused && canScroll && !isAtTop && visible.length > 0 && /* @__PURE__ */ jsx3(Box3, { paddingX: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx3(Text3, { color: colors.muted, children: isAtBottom ? "" : "G:bottom " }) })
773
+ ] }),
774
+ merged && mergedSessions && mergedSessions.some((s) => s.pid === null) && /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, flexDirection: "column", children: [
775
+ /* @__PURE__ */ jsx3(Text3, { color: colors.muted, children: " " }),
776
+ mergedSessions.filter((s) => s.pid === null).map((s) => /* @__PURE__ */ jsxs3(Text3, { color: colors.muted, children: [
777
+ "resume ",
778
+ s.slug.slice(0, 8),
779
+ ": ",
780
+ /* @__PURE__ */ jsxs3(Text3, { color: colors.text, children: [
781
+ "claude --resume ",
782
+ s.sessionId
783
+ ] })
784
+ ] }, s.sessionId))
785
+ ] })
787
786
  ]
788
787
  }
789
788
  );
@@ -1898,6 +1897,8 @@ var SplitPanel = React14.memo(
1898
1897
  {
1899
1898
  events: leftEvents,
1900
1899
  sessionSlug: leftSession?.slug ?? null,
1900
+ sessionId: leftSession?.sessionId,
1901
+ isActive: leftSession ? leftSession.pid !== null : void 0,
1901
1902
  focused: activePanel === "left",
1902
1903
  height,
1903
1904
  scrollOffset: leftScroll,
@@ -1909,6 +1910,8 @@ var SplitPanel = React14.memo(
1909
1910
  {
1910
1911
  events: rightEvents,
1911
1912
  sessionSlug: rightSession?.slug ?? null,
1913
+ sessionId: rightSession?.sessionId,
1914
+ isActive: rightSession ? rightSession.pid !== null : void 0,
1912
1915
  focused: activePanel === "right",
1913
1916
  height,
1914
1917
  scrollOffset: rightScroll,
@@ -1938,9 +1941,69 @@ var SplitPanel = React14.memo(
1938
1941
  import { useState as useState8, useEffect as useEffect5, useCallback as useCallback2, useRef as useRef3 } from "react";
1939
1942
  var ACTIVE_POLL_MS = 1e4;
1940
1943
  var IDLE_POLL_MS = 3e4;
1944
+ var getDisplayName = (session) => {
1945
+ if (session.nickname) return session.nickname;
1946
+ if (session.cwd) {
1947
+ const parts = session.cwd.replace(/\/+$/, "").split("/");
1948
+ return parts[parts.length - 1] || session.slug;
1949
+ }
1950
+ if (session.project) {
1951
+ const parts = session.project.replace(/\/+$/, "").split("/");
1952
+ return parts[parts.length - 1] || session.slug;
1953
+ }
1954
+ return session.slug;
1955
+ };
1956
+ var buildGroups = (sessions, expandedKeys) => {
1957
+ const byName = /* @__PURE__ */ new Map();
1958
+ for (const s of sessions) {
1959
+ const name = getDisplayName(s);
1960
+ const list = byName.get(name);
1961
+ if (list) list.push(s);
1962
+ else byName.set(name, [s]);
1963
+ }
1964
+ const groups = [];
1965
+ for (const [key, list] of byName) {
1966
+ list.sort((a, b) => b.lastActivity - a.lastActivity);
1967
+ const totalIn = list.reduce(
1968
+ (sum, s) => sum + s.usage.inputTokens + s.usage.cacheReadTokens,
1969
+ 0
1970
+ );
1971
+ const totalOut = list.reduce((sum, s) => sum + s.usage.outputTokens, 0);
1972
+ groups.push({
1973
+ key,
1974
+ sessions: list,
1975
+ expanded: expandedKeys.has(key),
1976
+ totalInputTokens: totalIn,
1977
+ totalOutputTokens: totalOut,
1978
+ latestModel: list[0].model,
1979
+ isActive: list.some((s) => s.pid !== null),
1980
+ latestActivity: list[0].lastActivity,
1981
+ earliestStart: Math.min(...list.map((s) => s.startTime))
1982
+ });
1983
+ }
1984
+ groups.sort((a, b) => b.latestActivity - a.latestActivity);
1985
+ return groups;
1986
+ };
1987
+ var buildVisibleItems = (groups) => {
1988
+ const items = [];
1989
+ for (const group of groups) {
1990
+ if (group.sessions.length === 1) {
1991
+ items.push({ type: "ungrouped", session: group.sessions[0] });
1992
+ } else {
1993
+ items.push({ type: "group", group });
1994
+ if (group.expanded) {
1995
+ for (const session of group.sessions) {
1996
+ items.push({ type: "session", session, groupKey: group.key });
1997
+ }
1998
+ }
1999
+ }
2000
+ }
2001
+ return items;
2002
+ };
1941
2003
  var useSessions = (allUsers, filter, archivedIds, viewingArchive) => {
1942
2004
  const [sessions, setSessions] = useState8([]);
1943
2005
  const [selectedIndex, setSelectedIndex] = useState8(0);
2006
+ const [expandedKeys, setExpandedKeys] = useState8(/* @__PURE__ */ new Set());
1944
2007
  const usageOverrides = useRef3(/* @__PURE__ */ new Map());
1945
2008
  const refresh = useCallback2(() => {
1946
2009
  const found = discoverSessions(allUsers);
@@ -1982,16 +2045,28 @@ var useSessions = (allUsers, filter, archivedIds, viewingArchive) => {
1982
2045
  const interval = setInterval(refresh, pollMs);
1983
2046
  return () => clearInterval(interval);
1984
2047
  }, [refresh, sessions.length > 0]);
1985
- const selectedSession = sessions[selectedIndex] ?? null;
2048
+ const groups = buildGroups(sessions, expandedKeys);
2049
+ const visibleItems = buildVisibleItems(groups);
2050
+ const selectedItem = visibleItems[selectedIndex] ?? null;
2051
+ const selectedSession = selectedItem?.type === "ungrouped" ? selectedItem.session : selectedItem?.type === "session" ? selectedItem.session : null;
2052
+ const selectedGroup = selectedItem?.type === "group" ? selectedItem.group : null;
1986
2053
  const selectNext = useCallback2(() => {
1987
- setSelectedIndex((i) => Math.min(i + 1, Math.max(0, sessions.length - 1)));
1988
- }, [sessions.length]);
2054
+ setSelectedIndex((i) => Math.min(i + 1, Math.max(0, visibleItems.length - 1)));
2055
+ }, [visibleItems.length]);
1989
2056
  const selectPrev = useCallback2(() => {
1990
2057
  setSelectedIndex((i) => Math.max(i - 1, 0));
1991
2058
  }, []);
1992
2059
  const selectIndex = useCallback2((i) => {
1993
2060
  setSelectedIndex(i);
1994
2061
  }, []);
2062
+ const toggleExpand = useCallback2((groupKey) => {
2063
+ setExpandedKeys((prev) => {
2064
+ const next = new Set(prev);
2065
+ if (next.has(groupKey)) next.delete(groupKey);
2066
+ else next.add(groupKey);
2067
+ return next;
2068
+ });
2069
+ }, []);
1995
2070
  const addUsage = useCallback2((sessionId, usage) => {
1996
2071
  const existing = usageOverrides.current.get(sessionId);
1997
2072
  if (existing) {
@@ -2005,7 +2080,20 @@ var useSessions = (allUsers, filter, archivedIds, viewingArchive) => {
2005
2080
  usageOverrides.current.set(sessionId, usage);
2006
2081
  }
2007
2082
  }, []);
2008
- return { sessions, selectedSession, selectedIndex, selectNext, selectPrev, selectIndex, refresh, addUsage };
2083
+ return {
2084
+ sessions,
2085
+ groups,
2086
+ visibleItems,
2087
+ selectedSession,
2088
+ selectedGroup,
2089
+ selectedIndex,
2090
+ selectNext,
2091
+ selectPrev,
2092
+ selectIndex,
2093
+ toggleExpand,
2094
+ refresh,
2095
+ addUsage
2096
+ };
2009
2097
  };
2010
2098
 
2011
2099
  // src/ui/hooks/useActivityStream.ts
@@ -2014,20 +2102,28 @@ var MAX_EVENTS = 200;
2014
2102
  var useActivityStream = (session, allUsers) => {
2015
2103
  const [events, setEvents] = useState9([]);
2016
2104
  const watcherRef = useRef4(null);
2105
+ const sessions = session === null ? [] : Array.isArray(session) ? session : [session];
2106
+ const sessionKey = sessions.map((s) => s.sessionId).sort().join(",");
2107
+ const sessionIdSet = new Set(sessions.map((s) => s.sessionId));
2017
2108
  useEffect6(() => {
2018
2109
  setEvents([]);
2019
- if (!session) return;
2110
+ if (sessions.length === 0) return;
2020
2111
  const existingCalls = [];
2021
2112
  const tempWatcher = new Watcher(() => {
2022
2113
  }, allUsers);
2023
- for (const file of session.outputFiles) {
2114
+ const allFiles = sessions.flatMap((s) => s.outputFiles);
2115
+ const seen = /* @__PURE__ */ new Set();
2116
+ for (const file of allFiles) {
2117
+ if (seen.has(file)) continue;
2118
+ seen.add(file);
2024
2119
  existingCalls.push(...tempWatcher.readExisting(file));
2025
2120
  }
2121
+ existingCalls.sort((a, b) => a.timestamp - b.timestamp);
2026
2122
  setEvents(existingCalls.slice(-MAX_EVENTS));
2027
2123
  const handler = (calls) => {
2028
- const sessionCalls = calls.filter((c) => c.sessionId === session.sessionId);
2029
- if (sessionCalls.length === 0) return;
2030
- setEvents((prev) => [...prev, ...sessionCalls].slice(-MAX_EVENTS));
2124
+ const matched = calls.filter((c) => sessionIdSet.has(c.sessionId));
2125
+ if (matched.length === 0) return;
2126
+ setEvents((prev) => [...prev, ...matched].slice(-MAX_EVENTS));
2031
2127
  };
2032
2128
  const watcher = new Watcher(handler, allUsers);
2033
2129
  watcherRef.current = watcher;
@@ -2036,7 +2132,7 @@ var useActivityStream = (session, allUsers) => {
2036
2132
  watcher.stop();
2037
2133
  watcherRef.current = null;
2038
2134
  };
2039
- }, [session?.sessionId, allUsers]);
2135
+ }, [sessionKey, allUsers]);
2040
2136
  return events;
2041
2137
  };
2042
2138
 
@@ -2088,14 +2184,14 @@ var notify = (alert, config) => {
2088
2184
  };
2089
2185
 
2090
2186
  // src/alerts/logger.ts
2091
- import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
2092
- import { dirname as dirname3 } from "path";
2187
+ import { appendFileSync, mkdirSync } from "fs";
2188
+ import { dirname as dirname2 } from "path";
2093
2189
  var AlertLogger = class {
2094
2190
  logPath;
2095
2191
  writeCount = 0;
2096
2192
  constructor(config) {
2097
2193
  this.logPath = resolveAlertLogPath(config);
2098
- mkdirSync3(dirname3(this.logPath), { recursive: true });
2194
+ mkdirSync(dirname2(this.logPath), { recursive: true });
2099
2195
  }
2100
2196
  log(alert) {
2101
2197
  const entry = {
@@ -2313,6 +2409,10 @@ var useKeyHandler = (deps) => {
2313
2409
  }
2314
2410
  return;
2315
2411
  }
2412
+ if (matchKey(d.kb.detail, input, key) && d.activePanel === "sessions" && d.selectedGroup) {
2413
+ d.toggleExpand(d.selectedGroup.key);
2414
+ return;
2415
+ }
2316
2416
  if (matchKey(d.kb.detail, input, key) && d.selectedSession) {
2317
2417
  if (d.splitMode) {
2318
2418
  if (d.activePanel === "left") {
@@ -2450,98 +2550,133 @@ var useUpdateChecker = (disabled, checkOnLaunch, checkInterval) => {
2450
2550
  return updateInfo;
2451
2551
  };
2452
2552
 
2453
- // src/ui/App.tsx
2454
- import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
2455
- var App = ({ options, config: initialConfig, version, firstRun }) => {
2456
- const { exit } = useApp();
2457
- const { stdout } = useStdout3();
2458
- const termHeight = stdout?.rows ?? 40;
2553
+ // src/ui/hooks/useSetupFlow.ts
2554
+ import { useState as useState13, useCallback as useCallback4 } from "react";
2555
+
2556
+ // src/hooks/installer.ts
2557
+ import { existsSync, readFileSync as readFileSync2, writeFileSync, copyFileSync, mkdirSync as mkdirSync2, chmodSync } from "fs";
2558
+ import { join as join2, dirname as dirname3 } from "path";
2559
+ import { homedir } from "os";
2560
+ import { fileURLToPath as fileURLToPath2 } from "url";
2561
+ var HOOK_FILENAME = "agenttop-guard.py";
2562
+ var SETTINGS_PATH = join2(homedir(), ".claude", "settings.json");
2563
+ var getHookSource = () => {
2564
+ const thisFile = fileURLToPath2(import.meta.url);
2565
+ const srcHooksDir = join2(dirname3(thisFile), "..", "src", "hooks");
2566
+ const distHooksDir = join2(dirname3(thisFile), "hooks");
2567
+ for (const dir of [distHooksDir, srcHooksDir]) {
2568
+ const path = join2(dir, HOOK_FILENAME);
2569
+ if (existsSync(path)) return path;
2570
+ }
2571
+ const npmGlobalPath = join2(dirname3(thisFile), "..", "hooks", HOOK_FILENAME);
2572
+ if (existsSync(npmGlobalPath)) return npmGlobalPath;
2573
+ throw new Error(`cannot find ${HOOK_FILENAME} \u2014 is agenttop installed correctly?`);
2574
+ };
2575
+ var getHookTarget = () => {
2576
+ const claudeHooksDir = join2(homedir(), ".claude", "hooks");
2577
+ mkdirSync2(claudeHooksDir, { recursive: true });
2578
+ return join2(claudeHooksDir, HOOK_FILENAME);
2579
+ };
2580
+ var readSettings = () => {
2581
+ if (!existsSync(SETTINGS_PATH)) {
2582
+ return {};
2583
+ }
2584
+ try {
2585
+ return JSON.parse(readFileSync2(SETTINGS_PATH, "utf-8"));
2586
+ } catch {
2587
+ return {};
2588
+ }
2589
+ };
2590
+ var writeSettings = (settings) => {
2591
+ mkdirSync2(dirname3(SETTINGS_PATH), { recursive: true });
2592
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
2593
+ };
2594
+ var installHooks = () => {
2595
+ const source = getHookSource();
2596
+ const target = getHookTarget();
2597
+ copyFileSync(source, target);
2598
+ chmodSync(target, 493);
2599
+ const settings = readSettings();
2600
+ const hooks = settings.hooks ?? {};
2601
+ const postToolUse = hooks.PostToolUse ?? [];
2602
+ const hookCommand = target;
2603
+ const allToolsMatcher = postToolUse.find(
2604
+ (entry) => entry.matcher === "Bash|Read|Grep|Glob|WebFetch|WebSearch"
2605
+ );
2606
+ if (allToolsMatcher) {
2607
+ const alreadyInstalled = allToolsMatcher.hooks.some((h) => h.command.includes("agenttop-guard"));
2608
+ if (alreadyInstalled) {
2609
+ process.stdout.write("agenttop hooks already installed\n");
2610
+ return;
2611
+ }
2612
+ allToolsMatcher.hooks.push({ type: "command", command: hookCommand });
2613
+ } else {
2614
+ postToolUse.push({
2615
+ matcher: "Bash|Read|Grep|Glob|WebFetch|WebSearch",
2616
+ hooks: [{ type: "command", command: hookCommand }]
2617
+ });
2618
+ }
2619
+ hooks.PostToolUse = postToolUse;
2620
+ settings.hooks = hooks;
2621
+ writeSettings(settings);
2622
+ process.stdout.write(`agenttop hooks installed:
2623
+ `);
2624
+ process.stdout.write(` hook: ${target}
2625
+ `);
2626
+ process.stdout.write(` settings: ${SETTINGS_PATH}
2627
+ `);
2628
+ process.stdout.write(` matcher: PostToolUse (Bash|Read|Grep|Glob|WebFetch|WebSearch)
2629
+ `);
2630
+ };
2631
+ var uninstallHooks = () => {
2632
+ const settings = readSettings();
2633
+ const hooks = settings.hooks ?? {};
2634
+ const postToolUse = hooks.PostToolUse ?? [];
2635
+ let removed = false;
2636
+ for (const entry of postToolUse) {
2637
+ const before = entry.hooks.length;
2638
+ entry.hooks = entry.hooks.filter((h) => !h.command.includes("agenttop-guard"));
2639
+ if (entry.hooks.length < before) removed = true;
2640
+ }
2641
+ hooks.PostToolUse = postToolUse.filter((e) => e.hooks.length > 0);
2642
+ settings.hooks = hooks;
2643
+ writeSettings(settings);
2644
+ if (removed) {
2645
+ process.stdout.write("agenttop hooks removed from Claude Code settings\n");
2646
+ } else {
2647
+ process.stdout.write("agenttop hooks were not installed\n");
2648
+ }
2649
+ };
2650
+
2651
+ // src/install-mcp.ts
2652
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
2653
+ import { join as join3 } from "path";
2654
+ import { homedir as homedir2 } from "os";
2655
+ var installMcpConfig = () => {
2656
+ const settingsPath = join3(homedir2(), ".claude", "settings.json");
2657
+ let settings = {};
2658
+ try {
2659
+ settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
2660
+ } catch {
2661
+ }
2662
+ const mcpServers = settings.mcpServers ?? {};
2663
+ mcpServers.agenttop = { command: "agenttop", args: ["--mcp"] };
2664
+ settings.mcpServers = mcpServers;
2665
+ mkdirSync3(join3(homedir2(), ".claude"), { recursive: true });
2666
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2667
+ process.stdout.write("agenttop MCP server registered in Claude Code settings\n");
2668
+ process.stdout.write(` settings: ${settingsPath}
2669
+ `);
2670
+ };
2671
+
2672
+ // src/ui/hooks/useSetupFlow.ts
2673
+ var useSetupFlow = (initialConfig, firstRun) => {
2459
2674
  const [liveConfig, setLiveConfig] = useState13(initialConfig);
2460
- const kb = liveConfig.keybindings;
2461
- const [activePanel, setActivePanel] = useState13("sessions");
2462
- const [activityScroll, setActivityScroll] = useState13(0);
2463
- const [inputMode, setInputMode] = useState13("normal");
2464
2675
  const [showSetup, setShowSetup] = useState13(firstRun);
2465
2676
  const [showThemePicker, setShowThemePicker] = useState13(false);
2466
2677
  const [showTour, setShowTour] = useState13(false);
2467
- const [filter, setFilter] = useState13("");
2468
- const [activityFilter, setActivityFilter] = useState13("");
2469
- const [updateStatus, setUpdateStatus] = useState13("");
2470
- const [showDetail, setShowDetail] = useState13(false);
2471
2678
  const [showSettings, setShowSettings] = useState13(false);
2472
2679
  const [showThemeMenu, setShowThemeMenu] = useState13(false);
2473
- const [viewingArchive, setViewingArchive] = useState13(false);
2474
- const [confirmAction, setConfirmAction] = useState13(
2475
- null
2476
- );
2477
- const [archivedIds, setArchivedIds] = useState13(() => new Set(Object.keys(getArchived())));
2478
- const [splitMode, setSplitMode] = useState13(false);
2479
- const [leftSession, setLeftSession] = useState13(null);
2480
- const [rightSession, setRightSession] = useState13(null);
2481
- const [leftScroll, setLeftScroll] = useState13(0);
2482
- const [rightScroll, setRightScroll] = useState13(0);
2483
- const [leftFilter, setLeftFilter] = useState13("");
2484
- const [rightFilter, setRightFilter] = useState13("");
2485
- const [leftShowDetail, setLeftShowDetail] = useState13(false);
2486
- const [rightShowDetail, setRightShowDetail] = useState13(false);
2487
- const refreshArchived = useCallback4(() => setArchivedIds(new Set(Object.keys(getArchived()))), []);
2488
- const updateInfo = useUpdateChecker(
2489
- options.noUpdates,
2490
- liveConfig.updates.checkOnLaunch,
2491
- liveConfig.updates.checkInterval
2492
- );
2493
- useEffect9(() => {
2494
- applyTheme(resolveTheme(liveConfig.theme, liveConfig.customThemes));
2495
- }, [liveConfig.theme, liveConfig.customThemes]);
2496
- const { sessions, selectedSession, selectedIndex, selectNext, selectPrev, refresh } = useSessions(
2497
- options.allUsers,
2498
- filter || void 0,
2499
- archivedIds,
2500
- viewingArchive
2501
- );
2502
- const rawEvents = useActivityStream(splitMode ? null : selectedSession, options.allUsers);
2503
- const leftRawEvents = useActivityStream(splitMode ? leftSession : null, options.allUsers);
2504
- const rightRawEvents = useActivityStream(splitMode ? rightSession : null, options.allUsers);
2505
- const events = useFilteredEvents(rawEvents, activityFilter);
2506
- const leftEvents = useFilteredEvents(leftRawEvents, leftFilter);
2507
- const rightEvents = useFilteredEvents(rightRawEvents, rightFilter);
2508
- const { alerts } = useAlerts(!options.noSecurity, options.alertLevel, options.allUsers, liveConfig);
2509
- const nicknameInput = useTextInput(
2510
- (value) => {
2511
- if (selectedSession && value.trim()) {
2512
- setNickname(selectedSession.sessionId, value.trim());
2513
- refresh();
2514
- }
2515
- setInputMode("normal");
2516
- },
2517
- () => setInputMode("normal")
2518
- );
2519
- const filterInput = useTextInput(
2520
- (value) => {
2521
- if (activePanel === "sessions") setFilter(value);
2522
- else if (activePanel === "left") setLeftFilter(value);
2523
- else if (activePanel === "right") setRightFilter(value);
2524
- else setActivityFilter(value);
2525
- setInputMode("normal");
2526
- },
2527
- () => {
2528
- if (activePanel === "sessions") setFilter("");
2529
- else if (activePanel === "left") setLeftFilter("");
2530
- else if (activePanel === "right") setRightFilter("");
2531
- else setActivityFilter("");
2532
- setInputMode("normal");
2533
- }
2534
- );
2535
- useEffect9(() => {
2536
- purgeExpiredArchives();
2537
- refreshArchived();
2538
- }, []);
2539
- useEffect9(() => {
2540
- setActivityScroll(0);
2541
- }, [selectedSession?.sessionId]);
2542
- const alertHeight = options.noSecurity ? 0 : 6;
2543
- const mainHeight = termHeight - 3 - alertHeight - 1 - (inputMode !== "normal" ? 1 : 0);
2544
- const viewportRows = mainHeight - 2;
2545
2680
  const handleSettingsClose = useCallback4((c) => {
2546
2681
  setLiveConfig(c);
2547
2682
  saveConfig(c);
@@ -2609,28 +2744,41 @@ var App = ({ options, config: initialConfig, version, firstRun }) => {
2609
2744
  const handleTourSkip = useCallback4(() => {
2610
2745
  setShowTour(false);
2611
2746
  }, []);
2612
- const switchPanel = useCallback4(
2613
- (dir) => {
2614
- if (splitMode) {
2615
- const order = ["sessions", "left", "right"];
2616
- setActivePanel((p) => {
2617
- const idx = order.indexOf(p);
2618
- if (idx === -1) return "sessions";
2619
- return dir === "next" ? order[(idx + 1) % order.length] : order[(idx - 1 + order.length) % order.length];
2620
- });
2621
- } else {
2622
- setActivePanel((p) => p === "sessions" ? "activity" : "sessions");
2623
- }
2624
- },
2625
- [splitMode]
2626
- );
2627
- const getActiveFilter = useCallback4(() => {
2628
- if (activePanel === "sessions") return filter;
2629
- if (activePanel === "left") return leftFilter;
2630
- if (activePanel === "right") return rightFilter;
2631
- return activityFilter;
2632
- }, [activePanel, filter, leftFilter, rightFilter, activityFilter]);
2633
- const clearSplitState = useCallback4(() => {
2747
+ return {
2748
+ liveConfig,
2749
+ setLiveConfig,
2750
+ showSetup,
2751
+ setShowSetup,
2752
+ showThemePicker,
2753
+ showTour,
2754
+ showSettings,
2755
+ setShowSettings,
2756
+ showThemeMenu,
2757
+ handleSettingsClose,
2758
+ handleThemeMenuClose,
2759
+ handleOpenThemeMenu,
2760
+ handleSetupComplete,
2761
+ handleThemePickerSelect,
2762
+ handleThemePickerSkip,
2763
+ handleThemePickerDismiss,
2764
+ handleTourComplete,
2765
+ handleTourSkip
2766
+ };
2767
+ };
2768
+
2769
+ // src/ui/hooks/useSplitPanel.ts
2770
+ import { useState as useState14, useCallback as useCallback5 } from "react";
2771
+ var useSplitPanel = () => {
2772
+ const [splitMode, setSplitMode] = useState14(false);
2773
+ const [leftSession, setLeftSession] = useState14(null);
2774
+ const [rightSession, setRightSession] = useState14(null);
2775
+ const [leftScroll, setLeftScroll] = useState14(0);
2776
+ const [rightScroll, setRightScroll] = useState14(0);
2777
+ const [leftFilter, setLeftFilter] = useState14("");
2778
+ const [rightFilter, setRightFilter] = useState14("");
2779
+ const [leftShowDetail, setLeftShowDetail] = useState14(false);
2780
+ const [rightShowDetail, setRightShowDetail] = useState14(false);
2781
+ const clearSplitState = useCallback5(() => {
2634
2782
  setSplitMode(false);
2635
2783
  setLeftSession(null);
2636
2784
  setRightSession(null);
@@ -2640,9 +2788,8 @@ var App = ({ options, config: initialConfig, version, firstRun }) => {
2640
2788
  setRightFilter("");
2641
2789
  setLeftShowDetail(false);
2642
2790
  setRightShowDetail(false);
2643
- setActivePanel("sessions");
2644
2791
  }, []);
2645
- const resetPanel = useCallback4((side) => {
2792
+ const resetPanel = useCallback5((side) => {
2646
2793
  if (side === "left") {
2647
2794
  setLeftSession(null);
2648
2795
  setLeftScroll(0);
@@ -2655,24 +2802,146 @@ var App = ({ options, config: initialConfig, version, firstRun }) => {
2655
2802
  setRightShowDetail(false);
2656
2803
  }
2657
2804
  }, []);
2658
- useKeyHandler({
2659
- kb,
2660
- activePanel,
2805
+ const switchPanel = useCallback5(
2806
+ (dir, setActivePanel) => {
2807
+ if (splitMode) {
2808
+ const order = ["sessions", "left", "right"];
2809
+ setActivePanel((p) => {
2810
+ const idx = order.indexOf(p);
2811
+ if (idx === -1) return "sessions";
2812
+ return dir === "next" ? order[(idx + 1) % order.length] : order[(idx - 1 + order.length) % order.length];
2813
+ });
2814
+ } else {
2815
+ setActivePanel((p) => p === "sessions" ? "activity" : "sessions");
2816
+ }
2817
+ },
2818
+ [splitMode]
2819
+ );
2820
+ return {
2661
2821
  splitMode,
2662
- inputMode,
2663
- showSetup,
2664
- showSettings: showSettings || showThemeMenu || showThemePicker || showTour,
2665
- showDetail,
2666
- leftShowDetail,
2667
- rightShowDetail,
2668
- confirmAction,
2669
- selectedSession,
2822
+ setSplitMode,
2670
2823
  leftSession,
2824
+ setLeftSession,
2671
2825
  rightSession,
2826
+ setRightSession,
2672
2827
  leftScroll,
2828
+ setLeftScroll,
2673
2829
  rightScroll,
2830
+ setRightScroll,
2674
2831
  leftFilter,
2832
+ setLeftFilter,
2675
2833
  rightFilter,
2834
+ setRightFilter,
2835
+ leftShowDetail,
2836
+ setLeftShowDetail,
2837
+ rightShowDetail,
2838
+ setRightShowDetail,
2839
+ clearSplitState,
2840
+ resetPanel,
2841
+ switchPanel
2842
+ };
2843
+ };
2844
+
2845
+ // src/ui/App.tsx
2846
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
2847
+ var App = ({ options, config: initialConfig, version, firstRun }) => {
2848
+ const { exit } = useApp();
2849
+ const { stdout } = useStdout3();
2850
+ const termHeight = stdout?.rows ?? 40;
2851
+ const setup = useSetupFlow(initialConfig, firstRun);
2852
+ const split = useSplitPanel();
2853
+ const kb = setup.liveConfig.keybindings;
2854
+ const [activePanel, setActivePanel] = useState15("sessions");
2855
+ const [activityScroll, setActivityScroll] = useState15(0);
2856
+ const [inputMode, setInputMode] = useState15("normal");
2857
+ const [filter, setFilter] = useState15("");
2858
+ const [activityFilter, setActivityFilter] = useState15("");
2859
+ const [updateStatus, setUpdateStatus] = useState15("");
2860
+ const [showDetail, setShowDetail] = useState15(false);
2861
+ const [viewingArchive, setViewingArchive] = useState15(false);
2862
+ const [confirmAction, setConfirmAction] = useState15(null);
2863
+ const [archivedIds, setArchivedIds] = useState15(() => new Set(Object.keys(getArchived())));
2864
+ const refreshArchived = useCallback6(() => setArchivedIds(new Set(Object.keys(getArchived()))), []);
2865
+ const updateInfo = useUpdateChecker(options.noUpdates, setup.liveConfig.updates.checkOnLaunch, setup.liveConfig.updates.checkInterval);
2866
+ useEffect9(() => {
2867
+ applyTheme(resolveTheme(setup.liveConfig.theme, setup.liveConfig.customThemes));
2868
+ }, [setup.liveConfig.theme, setup.liveConfig.customThemes]);
2869
+ const { sessions, visibleItems, selectedSession, selectedGroup, selectedIndex, selectNext, selectPrev, toggleExpand, refresh } = useSessions(options.allUsers, filter || void 0, archivedIds, viewingArchive);
2870
+ const activityTarget = split.splitMode ? null : selectedGroup ? selectedGroup.sessions : selectedSession;
2871
+ const rawEvents = useActivityStream(activityTarget, options.allUsers);
2872
+ const leftRawEvents = useActivityStream(split.splitMode ? split.leftSession : null, options.allUsers);
2873
+ const rightRawEvents = useActivityStream(split.splitMode ? split.rightSession : null, options.allUsers);
2874
+ const events = useFilteredEvents(rawEvents, activityFilter);
2875
+ const leftEvents = useFilteredEvents(leftRawEvents, split.leftFilter);
2876
+ const rightEvents = useFilteredEvents(rightRawEvents, split.rightFilter);
2877
+ const { alerts } = useAlerts(!options.noSecurity, options.alertLevel, options.allUsers, setup.liveConfig);
2878
+ const nicknameInput = useTextInput(
2879
+ (value) => {
2880
+ if (selectedSession && value.trim()) {
2881
+ setNickname(selectedSession.sessionId, value.trim());
2882
+ refresh();
2883
+ }
2884
+ setInputMode("normal");
2885
+ },
2886
+ () => setInputMode("normal")
2887
+ );
2888
+ const filterInput = useTextInput(
2889
+ (value) => {
2890
+ if (activePanel === "sessions") setFilter(value);
2891
+ else if (activePanel === "left") split.setLeftFilter(value);
2892
+ else if (activePanel === "right") split.setRightFilter(value);
2893
+ else setActivityFilter(value);
2894
+ setInputMode("normal");
2895
+ },
2896
+ () => {
2897
+ if (activePanel === "sessions") setFilter("");
2898
+ else if (activePanel === "left") split.setLeftFilter("");
2899
+ else if (activePanel === "right") split.setRightFilter("");
2900
+ else setActivityFilter("");
2901
+ setInputMode("normal");
2902
+ }
2903
+ );
2904
+ useEffect9(() => {
2905
+ purgeExpiredArchives();
2906
+ refreshArchived();
2907
+ }, []);
2908
+ useEffect9(() => {
2909
+ setActivityScroll(0);
2910
+ }, [selectedSession?.sessionId, selectedGroup?.key]);
2911
+ const alertHeight = options.noSecurity ? 0 : 6;
2912
+ const mainHeight = termHeight - 3 - alertHeight - 1 - (inputMode !== "normal" ? 1 : 0);
2913
+ const viewportRows = mainHeight - 2;
2914
+ const switchPanel = useCallback6((dir) => split.switchPanel(dir, setActivePanel), [split.switchPanel]);
2915
+ const clearSplitState = useCallback6(() => {
2916
+ split.clearSplitState();
2917
+ setActivePanel("sessions");
2918
+ }, [split.clearSplitState]);
2919
+ const getActiveFilter = useCallback6(() => {
2920
+ if (activePanel === "sessions") return filter;
2921
+ if (activePanel === "left") return split.leftFilter;
2922
+ if (activePanel === "right") return split.rightFilter;
2923
+ return activityFilter;
2924
+ }, [activePanel, filter, split.leftFilter, split.rightFilter, activityFilter]);
2925
+ useKeyHandler({
2926
+ kb,
2927
+ activePanel,
2928
+ splitMode: split.splitMode,
2929
+ inputMode,
2930
+ showSetup: setup.showSetup,
2931
+ showSettings: setup.showSettings || setup.showThemeMenu || setup.showThemePicker || setup.showTour,
2932
+ showDetail,
2933
+ leftShowDetail: split.leftShowDetail,
2934
+ rightShowDetail: split.rightShowDetail,
2935
+ confirmAction,
2936
+ selectedSession,
2937
+ selectedGroup,
2938
+ toggleExpand,
2939
+ leftSession: split.leftSession,
2940
+ rightSession: split.rightSession,
2941
+ leftScroll: split.leftScroll,
2942
+ rightScroll: split.rightScroll,
2943
+ leftFilter: split.leftFilter,
2944
+ rightFilter: split.rightFilter,
2676
2945
  filter,
2677
2946
  activityFilter,
2678
2947
  viewingArchive,
@@ -2687,24 +2956,24 @@ var App = ({ options, config: initialConfig, version, firstRun }) => {
2687
2956
  refresh,
2688
2957
  switchPanel,
2689
2958
  clearSplitState,
2690
- resetPanel,
2959
+ resetPanel: split.resetPanel,
2691
2960
  getActiveFilter,
2692
2961
  setActivePanel,
2693
2962
  setInputMode,
2694
2963
  setFilter,
2695
2964
  setActivityFilter,
2696
- setLeftFilter,
2697
- setRightFilter,
2965
+ setLeftFilter: split.setLeftFilter,
2966
+ setRightFilter: split.setRightFilter,
2698
2967
  setShowDetail,
2699
- setLeftShowDetail,
2700
- setRightShowDetail,
2701
- setShowSettings,
2968
+ setLeftShowDetail: split.setLeftShowDetail,
2969
+ setRightShowDetail: split.setRightShowDetail,
2970
+ setShowSettings: setup.setShowSettings,
2702
2971
  setViewingArchive,
2703
- setSplitMode,
2704
- setLeftSession,
2705
- setRightSession,
2706
- setLeftScroll,
2707
- setRightScroll,
2972
+ setSplitMode: split.setSplitMode,
2973
+ setLeftSession: split.setLeftSession,
2974
+ setRightSession: split.setRightSession,
2975
+ setLeftScroll: split.setLeftScroll,
2976
+ setRightScroll: split.setRightScroll,
2708
2977
  setActivityScroll,
2709
2978
  setConfirmAction,
2710
2979
  setUpdateStatus,
@@ -2747,96 +3016,31 @@ var App = ({ options, config: initialConfig, version, firstRun }) => {
2747
3016
  installUpdate().then(() => setUpdateStatus(`updated to v${updateInfo?.latest} \u2014 restart to apply`)).catch(() => setUpdateStatus("update failed"));
2748
3017
  }
2749
3018
  });
2750
- if (showSetup) {
3019
+ if (setup.showSetup) {
2751
3020
  const steps = [
2752
- ...liveConfig.prompts.hook === "pending" ? [
2753
- {
2754
- title: "Install Claude Code hook?",
2755
- description: "Adds a PostToolUse hook that blocks prompt injection attempts in real-time."
2756
- }
2757
- ] : [],
2758
- ...liveConfig.prompts.mcp === "pending" ? [
2759
- {
2760
- title: "Install MCP server?",
2761
- description: "Registers agenttop as an MCP server so Claude Code can query session status and alerts."
2762
- }
2763
- ] : []
3021
+ ...setup.liveConfig.prompts.hook === "pending" ? [{ title: "Install Claude Code hook?", description: "Adds a PostToolUse hook that blocks prompt injection attempts in real-time." }] : [],
3022
+ ...setup.liveConfig.prompts.mcp === "pending" ? [{ title: "Install MCP server?", description: "Registers agenttop as an MCP server so Claude Code can query session status and alerts." }] : []
2764
3023
  ];
2765
3024
  if (steps.length === 0) {
2766
- setShowSetup(false);
2767
- if (liveConfig.prompts.theme === "pending") setShowThemePicker(true);
2768
- else if (liveConfig.prompts.tour === "pending") setShowTour(true);
3025
+ setup.setShowSetup(false);
2769
3026
  return null;
2770
3027
  }
2771
- return /* @__PURE__ */ jsx15(SetupModal, { steps, onComplete: handleSetupComplete });
3028
+ return /* @__PURE__ */ jsx15(SetupModal, { steps, onComplete: setup.handleSetupComplete });
2772
3029
  }
2773
- if (showThemePicker) {
2774
- return /* @__PURE__ */ jsx15(
2775
- ThemePickerModal,
2776
- {
2777
- onSelect: handleThemePickerSelect,
2778
- onSkip: handleThemePickerSkip,
2779
- onDismiss: handleThemePickerDismiss
2780
- }
2781
- );
2782
- }
2783
- if (showTour) return /* @__PURE__ */ jsx15(GuidedTour, { onComplete: handleTourComplete, onSkip: handleTourSkip });
2784
- if (showThemeMenu) return /* @__PURE__ */ jsx15(ThemeMenu, { config: liveConfig, onClose: handleThemeMenuClose });
2785
- if (showSettings)
2786
- return /* @__PURE__ */ jsx15(SettingsMenu, { config: liveConfig, onClose: handleSettingsClose, onOpenThemeMenu: handleOpenThemeMenu });
3030
+ if (setup.showThemePicker) return /* @__PURE__ */ jsx15(ThemePickerModal, { onSelect: setup.handleThemePickerSelect, onSkip: setup.handleThemePickerSkip, onDismiss: setup.handleThemePickerDismiss });
3031
+ if (setup.showTour) return /* @__PURE__ */ jsx15(GuidedTour, { onComplete: setup.handleTourComplete, onSkip: setup.handleTourSkip });
3032
+ if (setup.showThemeMenu) return /* @__PURE__ */ jsx15(ThemeMenu, { config: setup.liveConfig, onClose: setup.handleThemeMenuClose });
3033
+ if (setup.showSettings) return /* @__PURE__ */ jsx15(SettingsMenu, { config: setup.liveConfig, onClose: setup.handleSettingsClose, onOpenThemeMenu: setup.handleOpenThemeMenu });
2787
3034
  if (confirmAction) {
2788
- return /* @__PURE__ */ jsx15(Box15, { flexDirection: "column", height: termHeight, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx15(
2789
- ConfirmModal,
2790
- {
2791
- title: confirmAction.title,
2792
- message: confirmAction.message,
2793
- onConfirm: confirmAction.onConfirm,
2794
- onCancel: () => setConfirmAction(null)
2795
- }
2796
- ) });
3035
+ return /* @__PURE__ */ jsx15(Box15, { flexDirection: "column", height: termHeight, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx15(ConfirmModal, { title: confirmAction.title, message: confirmAction.message, onConfirm: confirmAction.onConfirm, onCancel: () => setConfirmAction(null) }) });
2797
3036
  }
2798
- const filterLabel = activePanel === "sessions" ? "sessions" : activePanel === "left" ? "left" : activePanel === "right" ? "right" : "activity";
2799
- const rightPanel = splitMode ? /* @__PURE__ */ jsx15(
2800
- SplitPanel,
2801
- {
2802
- activePanel,
2803
- leftSession,
2804
- rightSession,
2805
- leftEvents,
2806
- rightEvents,
2807
- leftScroll,
2808
- rightScroll,
2809
- leftFilter,
2810
- rightFilter,
2811
- leftShowDetail,
2812
- rightShowDetail,
2813
- height: mainHeight
2814
- }
2815
- ) : showDetail && selectedSession ? /* @__PURE__ */ jsx15(SessionDetail, { session: selectedSession, focused: activePanel === "activity", height: mainHeight }) : /* @__PURE__ */ jsx15(
2816
- ActivityFeed,
2817
- {
2818
- events,
2819
- sessionSlug: selectedSession?.slug ?? null,
2820
- focused: activePanel === "activity",
2821
- height: mainHeight,
2822
- scrollOffset: activityScroll,
2823
- filter: activityFilter || void 0
2824
- }
2825
- );
3037
+ const activitySlug = selectedGroup ? selectedGroup.key : selectedSession?.slug ?? null;
3038
+ const isMerged = selectedGroup !== null;
3039
+ const rightPanel = split.splitMode ? /* @__PURE__ */ jsx15(SplitPanel, { activePanel, leftSession: split.leftSession, rightSession: split.rightSession, leftEvents, rightEvents, leftScroll: split.leftScroll, rightScroll: split.rightScroll, leftFilter: split.leftFilter, rightFilter: split.rightFilter, leftShowDetail: split.leftShowDetail, rightShowDetail: split.rightShowDetail, height: mainHeight }) : showDetail && selectedSession ? /* @__PURE__ */ jsx15(SessionDetail, { session: selectedSession, focused: activePanel === "activity", height: mainHeight }) : /* @__PURE__ */ jsx15(ActivityFeed, { events, sessionSlug: activitySlug, sessionId: selectedSession?.sessionId, isActive: selectedGroup ? selectedGroup.isActive : selectedSession ? selectedSession.pid !== null : void 0, focused: activePanel === "activity", height: mainHeight, scrollOffset: activityScroll, filter: activityFilter || void 0, merged: isMerged, mergedSessions: selectedGroup?.sessions });
2826
3040
  return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", height: termHeight, children: [
2827
3041
  /* @__PURE__ */ jsx15(StatusBar, { sessionCount: sessions.length, alertCount: alerts.length, version, updateInfo }),
2828
3042
  /* @__PURE__ */ jsxs15(Box15, { flexGrow: 1, height: mainHeight, children: [
2829
- /* @__PURE__ */ jsx15(
2830
- SessionList,
2831
- {
2832
- sessions,
2833
- selectedIndex,
2834
- focused: activePanel === "sessions",
2835
- height: mainHeight,
2836
- filter: filter || void 0,
2837
- viewingArchive
2838
- }
2839
- ),
3043
+ /* @__PURE__ */ jsx15(SessionList, { visibleItems, selectedIndex, focused: activePanel === "sessions", height: mainHeight, filter: filter || void 0, viewingArchive, totalSessions: sessions.length }),
2840
3044
  rightPanel
2841
3045
  ] }),
2842
3046
  !options.noSecurity && /* @__PURE__ */ jsx15(AlertBar, { alerts }),
@@ -2846,12 +3050,12 @@ var App = ({ options, config: initialConfig, version, firstRun }) => {
2846
3050
  /* @__PURE__ */ jsx15(Text14, { color: colors.muted, children: "_" })
2847
3051
  ] }),
2848
3052
  inputMode === "filter" && /* @__PURE__ */ jsxs15(Box15, { paddingX: 1, children: [
2849
- /* @__PURE__ */ jsx15(Text14, { color: colors.muted, children: filterLabel }),
3053
+ /* @__PURE__ */ jsx15(Text14, { color: colors.muted, children: activePanel === "sessions" ? "sessions" : activePanel === "left" ? "left" : activePanel === "right" ? "right" : "activity" }),
2850
3054
  /* @__PURE__ */ jsx15(Text14, { color: colors.primary, children: "/" }),
2851
3055
  /* @__PURE__ */ jsx15(Text14, { color: colors.bright, children: filterInput.value }),
2852
3056
  /* @__PURE__ */ jsx15(Text14, { color: colors.muted, children: "_" })
2853
3057
  ] }),
2854
- inputMode === "normal" && /* @__PURE__ */ jsx15(FooterBar, { keybindings: kb, updateStatus, viewingArchive, splitMode })
3058
+ inputMode === "normal" && /* @__PURE__ */ jsx15(FooterBar, { keybindings: kb, updateStatus, viewingArchive, splitMode: split.splitMode })
2855
3059
  ] });
2856
3060
  };
2857
3061