abmux 0.0.5 → 0.0.6

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +494 -136
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -144,10 +144,97 @@ var createEditor = () => ({
144
144
  }
145
145
  });
146
146
 
147
+ // src/infra/claude-cli.ts
148
+ import { execFile as execFile2, execFileSync as execFileSync2 } from "node:child_process";
149
+
150
+ // src/models/claude-session.ts
151
+ var SESSION_SUMMARY_SCHEMA = {
152
+ type: "object",
153
+ properties: {
154
+ overallSummary: { type: "string" },
155
+ sessions: {
156
+ type: "array",
157
+ items: {
158
+ type: "object",
159
+ properties: {
160
+ sessionName: { type: "string" },
161
+ panes: {
162
+ type: "array",
163
+ items: {
164
+ type: "object",
165
+ properties: {
166
+ paneTitle: { type: "string" },
167
+ description: { type: "string" }
168
+ },
169
+ required: ["paneTitle", "description"]
170
+ }
171
+ }
172
+ },
173
+ required: ["sessionName", "panes"]
174
+ }
175
+ }
176
+ },
177
+ required: ["overallSummary", "sessions"]
178
+ };
179
+
180
+ // src/infra/claude-cli.ts
181
+ var resolveClaudePath = () => {
182
+ try {
183
+ return execFileSync2("which", ["claude"], { encoding: "utf-8" }).trim();
184
+ } catch {
185
+ return "claude";
186
+ }
187
+ };
188
+ var claudePath = resolveClaudePath();
189
+ var EMPTY_RESULT = { overallSummary: "", sessions: [] };
190
+ var execClaude = (args) => new Promise((resolve, reject) => {
191
+ execFile2(claudePath, args, { timeout: 12e4 }, (error, stdout, stderr) => {
192
+ if (error) {
193
+ reject(new Error(`claude failed: ${stderr || error.message}`));
194
+ return;
195
+ }
196
+ resolve(stdout.trim());
197
+ });
198
+ });
199
+ var parseResult = (raw) => {
200
+ const output = JSON.parse(raw);
201
+ if (output.is_error) return EMPTY_RESULT;
202
+ const parsed = output.structured_output;
203
+ if (typeof parsed !== "object" || parsed === null || !("sessions" in parsed) || !Array.isArray(parsed.sessions)) {
204
+ return EMPTY_RESULT;
205
+ }
206
+ const result = parsed;
207
+ return {
208
+ overallSummary: result.overallSummary ?? "",
209
+ sessions: result.sessions
210
+ };
211
+ };
212
+ var createClaudeCli = () => ({
213
+ querySessionSummary: async (prompt) => {
214
+ try {
215
+ const raw = await execClaude([
216
+ "-p",
217
+ "--output-format",
218
+ "json",
219
+ "--json-schema",
220
+ JSON.stringify(SESSION_SUMMARY_SCHEMA),
221
+ "--no-session-persistence",
222
+ "--model",
223
+ "haiku",
224
+ prompt
225
+ ]);
226
+ return parseResult(raw);
227
+ } catch {
228
+ return EMPTY_RESULT;
229
+ }
230
+ }
231
+ });
232
+
147
233
  // src/infra/index.ts
148
234
  var createInfra = () => ({
149
235
  tmuxCli: createTmuxCli(),
150
- editor: createEditor()
236
+ editor: createEditor(),
237
+ claudeCli: createClaudeCli()
151
238
  });
152
239
 
153
240
  // src/models/session.ts
@@ -387,11 +474,51 @@ var createDirectoryScanService = () => ({
387
474
  }
388
475
  });
389
476
 
477
+ // src/services/session-summary-service.ts
478
+ var EMPTY_RESULT2 = { overallSummary: "", sessions: [] };
479
+ var formatGroupsForPrompt = (groups) => {
480
+ const lines = [];
481
+ for (const group of groups) {
482
+ const panes = group.tabs.flatMap((t) => t.panes);
483
+ const paneDescriptions = panes.map((p) => {
484
+ if (p.kind === "claude") {
485
+ const status = p.claudeStatus ? SESSION_STATUS_LABEL[p.claudeStatus] : "idle";
486
+ const title = p.claudeTitle ?? "";
487
+ return ` - [claude] status=${status} title="${title}"`;
488
+ }
489
+ return ` - [${p.kind}] title="${p.pane.title}"`;
490
+ });
491
+ lines.push(`session: ${group.sessionName}`);
492
+ lines.push(` panes (${String(panes.length)}):`);
493
+ lines.push(...paneDescriptions);
494
+ }
495
+ return lines.join("\n");
496
+ };
497
+ var buildPrompt = (groups) => {
498
+ const data = formatGroupsForPrompt(groups);
499
+ return [
500
+ "\u4EE5\u4E0B\u306F tmux \u30BB\u30C3\u30B7\u30E7\u30F3\u3068\u30DA\u30A4\u30F3\u306E\u4E00\u89A7\u3067\u3059\u3002",
501
+ "overallSummary: \u5168\u4F53\u3068\u3057\u3066\u4ECA\u3069\u3046\u3044\u3046\u4F5C\u696D\u304C\u884C\u308F\u308C\u3066\u3044\u3066\u3001\u3069\u3046\u3044\u3046\u72B6\u614B\u304B\u3092\u65E5\u672C\u8A9E\u3067\u7C21\u6F54\u306B1\u301C2\u6587\u3067\u8AAC\u660E\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
502
+ "sessions.panes: \u5404\u30DA\u30A4\u30F3\u304C\u4F55\u3092\u3057\u3066\u3044\u308B\u304B\u3001\u65E5\u672C\u8A9E\u3067\u7C21\u6F54\u306B1\u6587\u305A\u3064\u8AAC\u660E\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
503
+ "sessionName \u306F\u305D\u306E\u307E\u307E\u3001paneTitle \u306F\u30DA\u30A4\u30F3\u306E title \u3092\u305D\u306E\u307E\u307E\u5165\u308C\u3066\u304F\u3060\u3055\u3044\u3002",
504
+ "",
505
+ data
506
+ ].join("\n");
507
+ };
508
+ var createSessionSummaryService = (context) => ({
509
+ fetchSummary: async (groups) => {
510
+ if (groups.length === 0) return EMPTY_RESULT2;
511
+ const prompt = buildPrompt(groups);
512
+ return await context.infra.claudeCli.querySessionSummary(prompt);
513
+ }
514
+ });
515
+
390
516
  // src/services/index.ts
391
517
  var createServices = (context) => ({
392
518
  tmux: createTmuxService(context),
393
519
  sessionDetection: createSessionDetectionService(),
394
- directoryScan: createDirectoryScanService()
520
+ directoryScan: createDirectoryScanService(),
521
+ sessionSummary: createSessionSummaryService(context)
395
522
  });
396
523
 
397
524
  // src/utils/ShellUtils.ts
@@ -424,6 +551,9 @@ var createManagerUsecase = (context) => {
424
551
  const sessionGroups = sessionDetection.groupBySession({ panes });
425
552
  return { sessionGroups };
426
553
  },
554
+ fetchOverview: async (groups) => {
555
+ return await context.services.sessionSummary.fetchSummary(groups);
556
+ },
427
557
  enrichStatus: async (up) => {
428
558
  if (up.kind !== "claude") return up;
429
559
  try {
@@ -467,7 +597,7 @@ var createUsecases = (context) => ({
467
597
  // package.json
468
598
  var package_default = {
469
599
  name: "abmux",
470
- version: "0.0.5",
600
+ version: "0.0.6",
471
601
  repository: {
472
602
  type: "git",
473
603
  url: "https://github.com/cut0/abmux.git"
@@ -537,8 +667,8 @@ import { createElement } from "react";
537
667
 
538
668
  // src/components/ManagerView.tsx
539
669
  import { basename as basename2 } from "node:path";
540
- import { Box as Box10, Text as Text10 } from "ink";
541
- import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo5, useState as useState5 } from "react";
670
+ import { Box as Box11 } from "ink";
671
+ import { useCallback as useCallback4, useMemo as useMemo7, useRef as useRef3, useState as useState6 } from "react";
542
672
 
543
673
  // src/components/shared/Header.tsx
544
674
  import { Box, Text } from "ink";
@@ -549,24 +679,57 @@ var Header = ({ title }) => {
549
679
 
550
680
  // src/components/shared/StatusBar.tsx
551
681
  import { Box as Box2, Text as Text2 } from "ink";
552
- import { jsx as jsx2 } from "react/jsx-runtime";
682
+ import { useMemo } from "react";
683
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
684
+ var STATUS_ICON = {
685
+ [SESSION_STATUS.waitingInput]: "\u276F",
686
+ [SESSION_STATUS.waitingConfirm]: "\u2753",
687
+ [SESSION_STATUS.thinking]: "\u25CF",
688
+ [SESSION_STATUS.toolRunning]: "\u2733",
689
+ [SESSION_STATUS.idle]: "\u25CB"
690
+ };
553
691
  var COLOR_MAP = {
554
692
  success: "green",
555
693
  error: "red",
556
694
  info: "gray"
557
695
  };
558
- var StatusBar = ({ message, type = "info" }) => {
559
- return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: COLOR_MAP[type], children: message }) });
696
+ var STATUS_ORDER = [
697
+ SESSION_STATUS.thinking,
698
+ SESSION_STATUS.toolRunning,
699
+ SESSION_STATUS.waitingConfirm,
700
+ SESSION_STATUS.waitingInput,
701
+ SESSION_STATUS.idle
702
+ ];
703
+ var StatusBar = ({ message, type = "info", statusCounts }) => {
704
+ const summaryEntries = useMemo(() => {
705
+ if (!statusCounts) return [];
706
+ return STATUS_ORDER.filter((s) => (statusCounts[s] ?? 0) > 0).map((s) => ({
707
+ status: s,
708
+ icon: STATUS_ICON[s],
709
+ label: SESSION_STATUS_LABEL[s],
710
+ color: SESSION_STATUS_COLOR[s],
711
+ count: statusCounts[s] ?? 0
712
+ }));
713
+ }, [statusCounts]);
714
+ return /* @__PURE__ */ jsxs(Box2, { children: [
715
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, children: /* @__PURE__ */ jsx2(Text2, { color: COLOR_MAP[type], children: message }) }),
716
+ summaryEntries.length > 0 && /* @__PURE__ */ jsx2(Box2, { gap: 1, children: summaryEntries.map((entry) => /* @__PURE__ */ jsxs(Text2, { color: entry.color, children: [
717
+ entry.icon,
718
+ String(entry.count),
719
+ " ",
720
+ entry.label
721
+ ] }, entry.status)) })
722
+ ] });
560
723
  };
561
724
 
562
725
  // src/components/shared/DirectorySelect.tsx
563
726
  import { Box as Box3, Text as Text3, useApp, useInput } from "ink";
564
- import { useCallback, useMemo as useMemo2, useState } from "react";
727
+ import { useCallback, useMemo as useMemo3, useState } from "react";
565
728
 
566
729
  // src/hooks/use-scroll.ts
567
- import { useMemo } from "react";
730
+ import { useMemo as useMemo2 } from "react";
568
731
  var useScroll = (cursor, totalItems, availableRows) => {
569
- return useMemo(() => {
732
+ return useMemo2(() => {
570
733
  const visibleCount = Math.max(1, availableRows);
571
734
  if (totalItems <= visibleCount) {
572
735
  return { scrollOffset: 0, visibleCount };
@@ -592,7 +755,7 @@ var findMatchingDirectory = (path, directories) => directories.filter((dir) => p
592
755
  );
593
756
 
594
757
  // src/components/shared/DirectorySelect.tsx
595
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
758
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
596
759
  var sortSessions = (sessions, currentSession) => {
597
760
  const current = sessions.filter((s) => s.name === currentSession);
598
761
  const rest = sessions.filter((s) => s.name !== currentSession);
@@ -610,11 +773,11 @@ var SessionListPanel = ({
610
773
  }) => {
611
774
  const { exit } = useApp();
612
775
  const [cursor, setCursor] = useState(0);
613
- const sortedSessions = useMemo2(
776
+ const sortedSessions = useMemo3(
614
777
  () => sortSessions(sessions, currentSession),
615
778
  [sessions, currentSession]
616
779
  );
617
- const names = useMemo2(() => sortedSessions.map((s) => s.name), [sortedSessions]);
780
+ const names = useMemo3(() => sortedSessions.map((s) => s.name), [sortedSessions]);
618
781
  const clampedCursor = cursor >= names.length ? Math.max(0, names.length - 1) : cursor;
619
782
  if (clampedCursor !== cursor) {
620
783
  setCursor(clampedCursor);
@@ -665,10 +828,10 @@ var SessionListPanel = ({
665
828
  },
666
829
  { isActive: isFocused }
667
830
  );
668
- return /* @__PURE__ */ jsxs(Box3, { flexDirection: "column", children: [
669
- /* @__PURE__ */ jsxs(Box3, { paddingLeft: 1, children: [
831
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
832
+ /* @__PURE__ */ jsxs2(Box3, { paddingLeft: 1, children: [
670
833
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: isFocused ? "green" : "gray", children: "Sessions" }),
671
- /* @__PURE__ */ jsxs(Text3, { dimColor: true, children: [
834
+ /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
672
835
  " ",
673
836
  "(",
674
837
  clampedCursor + 1,
@@ -681,7 +844,7 @@ var SessionListPanel = ({
681
844
  const globalIndex = scrollOffset + i;
682
845
  const isHighlighted = globalIndex === clampedCursor;
683
846
  const isCurrent = session.name === currentSession;
684
- return /* @__PURE__ */ jsxs(Box3, { paddingLeft: 1, gap: 1, children: [
847
+ return /* @__PURE__ */ jsxs2(Box3, { paddingLeft: 1, gap: 1, children: [
685
848
  /* @__PURE__ */ jsx3(Text3, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
686
849
  /* @__PURE__ */ jsx3(Text3, { color: isHighlighted ? "green" : "cyan", bold: isHighlighted, wrap: "truncate", children: session.path ? formatCwd(session.path) : session.name }),
687
850
  isCurrent && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "(cwd)" })
@@ -695,21 +858,21 @@ import { Box as Box6, Text as Text6 } from "ink";
695
858
 
696
859
  // src/components/PaneListView.tsx
697
860
  import { Box as Box5, Text as Text5, useApp as useApp2, useInput as useInput2 } from "ink";
698
- import { useCallback as useCallback2, useMemo as useMemo3, useRef, useState as useState2 } from "react";
861
+ import { useCallback as useCallback2, useMemo as useMemo4, useRef, useState as useState2 } from "react";
699
862
 
700
863
  // src/components/sessions/PaneItem.tsx
701
864
  import { Box as Box4, Text as Text4 } from "ink";
702
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
865
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
703
866
  var PaneItem = ({ unifiedPane, isHighlighted }) => {
704
867
  const { pane, kind, claudeStatus, claudeTitle } = unifiedPane;
705
868
  if (kind === "claude") {
706
869
  const icon = pane.title.charAt(0);
707
870
  const statusLabel = claudeStatus ? SESSION_STATUS_LABEL[claudeStatus] : "";
708
871
  const statusColor = claudeStatus ? SESSION_STATUS_COLOR[claudeStatus] : "gray";
709
- return /* @__PURE__ */ jsxs2(Box4, { paddingLeft: 3, gap: 1, children: [
872
+ return /* @__PURE__ */ jsxs3(Box4, { paddingLeft: 3, gap: 1, children: [
710
873
  /* @__PURE__ */ jsx4(Text4, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
711
874
  /* @__PURE__ */ jsx4(Text4, { color: "#FF8C00", children: icon }),
712
- /* @__PURE__ */ jsxs2(Text4, { color: statusColor, children: [
875
+ /* @__PURE__ */ jsxs3(Text4, { color: statusColor, children: [
713
876
  "[",
714
877
  statusLabel,
715
878
  "]"
@@ -718,7 +881,7 @@ var PaneItem = ({ unifiedPane, isHighlighted }) => {
718
881
  /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pane.paneId })
719
882
  ] });
720
883
  }
721
- return /* @__PURE__ */ jsxs2(Box4, { paddingLeft: 3, gap: 1, children: [
884
+ return /* @__PURE__ */ jsxs3(Box4, { paddingLeft: 3, gap: 1, children: [
722
885
  /* @__PURE__ */ jsx4(Text4, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
723
886
  kind === "available" && /* @__PURE__ */ jsx4(Text4, { color: "#4AA8D8", children: "\u25CB" }),
724
887
  kind === "busy" && /* @__PURE__ */ jsx4(Text4, { color: "#E05252", children: "\u25CF" }),
@@ -728,7 +891,7 @@ var PaneItem = ({ unifiedPane, isHighlighted }) => {
728
891
  };
729
892
 
730
893
  // src/components/PaneListView.tsx
731
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
894
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
732
895
  var PaneListView = ({
733
896
  selectedSession,
734
897
  group,
@@ -744,7 +907,7 @@ var PaneListView = ({
744
907
  const { exit } = useApp2();
745
908
  const [cursor, setCursor] = useState2(0);
746
909
  const highlightedRef = useRef(void 0);
747
- const panes = useMemo3(() => group.tabs.flatMap((t) => t.panes), [group]);
910
+ const panes = useMemo4(() => group.tabs.flatMap((t) => t.panes), [group]);
748
911
  const clampedCursor = cursor >= panes.length ? Math.max(0, panes.length - 1) : cursor;
749
912
  if (clampedCursor !== cursor) {
750
913
  setCursor(clampedCursor);
@@ -755,7 +918,7 @@ var PaneListView = ({
755
918
  panes.length,
756
919
  availableRows - reservedLines
757
920
  );
758
- const visiblePanes = useMemo3(
921
+ const visiblePanes = useMemo4(
759
922
  () => panes.slice(scrollOffset, scrollOffset + visibleCount),
760
923
  [panes, scrollOffset, visibleCount]
761
924
  );
@@ -824,10 +987,10 @@ var PaneListView = ({
824
987
  },
825
988
  { isActive: isFocused }
826
989
  );
827
- return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
828
- /* @__PURE__ */ jsxs3(Box5, { paddingLeft: 1, children: [
990
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
991
+ /* @__PURE__ */ jsxs4(Box5, { paddingLeft: 1, children: [
829
992
  /* @__PURE__ */ jsx5(Text5, { bold: true, color: isFocused ? "green" : "gray", children: "Panes" }),
830
- /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
993
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
831
994
  " ",
832
995
  selectedSession,
833
996
  " (",
@@ -883,15 +1046,140 @@ var PaneListPanel = ({
883
1046
  );
884
1047
  };
885
1048
 
1049
+ // src/components/SessionOverviewPanel.tsx
1050
+ import { Spinner } from "@inkjs/ui";
1051
+ import { Box as Box7, Text as Text7, useApp as useApp3, useInput as useInput3 } from "ink";
1052
+ import { useCallback as useCallback3, useMemo as useMemo5, useState as useState3 } from "react";
1053
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1054
+ var SessionOverviewPanel = ({
1055
+ overallSummary,
1056
+ items,
1057
+ groups,
1058
+ isLoading,
1059
+ isFocused,
1060
+ availableRows,
1061
+ onBack
1062
+ }) => {
1063
+ const { exit } = useApp3();
1064
+ const [cursor, setCursor] = useState3(0);
1065
+ const lines = useMemo5(() => {
1066
+ const summaryLines = overallSummary ? [
1067
+ { key: "summary", type: "summary", text: overallSummary },
1068
+ { key: "spacer:summary", type: "spacer" }
1069
+ ] : [];
1070
+ const sessionLines = items.flatMap((item, idx) => {
1071
+ const group = groups.find((g) => g.sessionName === item.sessionName);
1072
+ const allPanes = group?.tabs.flatMap((t) => t.panes) ?? [];
1073
+ const paneLines = item.panes.map((paneSummary) => {
1074
+ const matched = allPanes.find(
1075
+ (p) => (p.claudeTitle ?? p.pane.title) === paneSummary.paneTitle
1076
+ );
1077
+ const statusLabel = matched?.claudeStatus ? SESSION_STATUS_LABEL[matched.claudeStatus] : void 0;
1078
+ const statusColor = matched?.claudeStatus ? SESSION_STATUS_COLOR[matched.claudeStatus] : void 0;
1079
+ return {
1080
+ key: `p:${item.sessionName}:${paneSummary.paneTitle}`,
1081
+ type: "pane",
1082
+ statusLabel,
1083
+ statusColor,
1084
+ description: paneSummary.description
1085
+ };
1086
+ });
1087
+ const spacer = idx > 0 ? [{ key: `spacer:${item.sessionName}`, type: "spacer" }] : [];
1088
+ return [
1089
+ ...spacer,
1090
+ { key: `s:${item.sessionName}`, type: "session", sessionName: item.sessionName },
1091
+ ...paneLines
1092
+ ];
1093
+ });
1094
+ return [...summaryLines, ...sessionLines];
1095
+ }, [overallSummary, items, groups]);
1096
+ const clampedCursor = cursor >= lines.length ? Math.max(0, lines.length - 1) : cursor;
1097
+ if (clampedCursor !== cursor) {
1098
+ setCursor(clampedCursor);
1099
+ }
1100
+ const reservedLines = 3;
1101
+ const { scrollOffset, visibleCount } = useScroll(
1102
+ clampedCursor,
1103
+ lines.length,
1104
+ availableRows - reservedLines
1105
+ );
1106
+ const visibleLines = useMemo5(
1107
+ () => lines.slice(scrollOffset, scrollOffset + visibleCount),
1108
+ [lines, scrollOffset, visibleCount]
1109
+ );
1110
+ const moveCursor = useCallback3(
1111
+ (next) => {
1112
+ setCursor(Math.max(0, Math.min(lines.length - 1, next)));
1113
+ },
1114
+ [lines.length]
1115
+ );
1116
+ useInput3(
1117
+ (input, key) => {
1118
+ if (input === "q") {
1119
+ exit();
1120
+ return;
1121
+ }
1122
+ if (key.escape || key.leftArrow) {
1123
+ onBack();
1124
+ return;
1125
+ }
1126
+ if (key.upArrow) {
1127
+ moveCursor(clampedCursor - 1);
1128
+ return;
1129
+ }
1130
+ if (key.downArrow) {
1131
+ moveCursor(clampedCursor + 1);
1132
+ }
1133
+ },
1134
+ { isActive: isFocused }
1135
+ );
1136
+ return /* @__PURE__ */ jsxs5(
1137
+ Box7,
1138
+ {
1139
+ flexDirection: "column",
1140
+ height: availableRows,
1141
+ borderStyle: "round",
1142
+ borderColor: isFocused ? "green" : "gray",
1143
+ children: [
1144
+ /* @__PURE__ */ jsxs5(Box7, { paddingLeft: 1, gap: 1, children: [
1145
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: isFocused ? "green" : "gray", children: "Overview" }),
1146
+ isLoading && /* @__PURE__ */ jsx7(Spinner, { label: "" })
1147
+ ] }),
1148
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: lines.length === 0 && !isLoading ? /* @__PURE__ */ jsx7(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "No sessions." }) }) : visibleLines.map((line, i) => {
1149
+ const globalIndex = scrollOffset + i;
1150
+ const isHighlighted = isFocused && globalIndex === clampedCursor;
1151
+ if (line.type === "spacer") {
1152
+ return /* @__PURE__ */ jsx7(Box7, { height: 1 }, line.key);
1153
+ }
1154
+ if (line.type === "summary") {
1155
+ return /* @__PURE__ */ jsx7(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : void 0, wrap: "wrap", children: line.text }) }, line.key);
1156
+ }
1157
+ if (line.type === "session") {
1158
+ return /* @__PURE__ */ jsx7(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : "cyan", children: line.sessionName }) }, line.key);
1159
+ }
1160
+ return /* @__PURE__ */ jsxs5(Box7, { paddingLeft: 3, gap: 1, children: [
1161
+ line.statusLabel ? /* @__PURE__ */ jsxs5(Text7, { color: isHighlighted ? "green" : line.statusColor, children: [
1162
+ "[",
1163
+ line.statusLabel,
1164
+ "]"
1165
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : "gray", children: "[--]" }),
1166
+ /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : void 0, wrap: "wrap", children: line.description })
1167
+ ] }, line.key);
1168
+ }) })
1169
+ ]
1170
+ }
1171
+ );
1172
+ };
1173
+
886
1174
  // src/components/ConfirmView.tsx
887
- import { Box as Box7, Text as Text7, useInput as useInput3 } from "ink";
1175
+ import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
888
1176
 
889
1177
  // src/hooks/use-terminal-size.ts
890
1178
  import { useStdout } from "ink";
891
- import { useEffect, useState as useState3 } from "react";
1179
+ import { useEffect, useState as useState4 } from "react";
892
1180
  var useTerminalSize = () => {
893
1181
  const { stdout } = useStdout();
894
- const [size, setSize] = useState3({
1182
+ const [size, setSize] = useState4({
895
1183
  rows: stdout.rows ?? 24,
896
1184
  columns: stdout.columns ?? 80
897
1185
  });
@@ -911,12 +1199,12 @@ var useTerminalSize = () => {
911
1199
  };
912
1200
 
913
1201
  // src/components/ConfirmView.tsx
914
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1202
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
915
1203
  var ConfirmView = ({ selectedDir, prompt, onConfirm, onCancel }) => {
916
1204
  const { rows } = useTerminalSize();
917
1205
  const previewLines = prompt.split("\n");
918
1206
  const maxPreview = Math.min(previewLines.length, rows - 6);
919
- useInput3((_input, key) => {
1207
+ useInput4((_input, key) => {
920
1208
  if (key.return) {
921
1209
  onConfirm();
922
1210
  return;
@@ -925,30 +1213,30 @@ var ConfirmView = ({ selectedDir, prompt, onConfirm, onCancel }) => {
925
1213
  onCancel();
926
1214
  }
927
1215
  });
928
- return /* @__PURE__ */ jsxs4(Box7, { flexDirection: "column", height: rows, children: [
929
- /* @__PURE__ */ jsx7(Header, { title: `${APP_TITLE} \u2014 ${selectedDir}` }),
930
- /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { bold: true, children: "New Claude session:" }) }),
931
- /* @__PURE__ */ jsxs4(Box7, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingLeft: 2, children: [
932
- previewLines.slice(0, maxPreview).map((line, i) => /* @__PURE__ */ jsx7(Text7, { color: "white", children: line }, i)),
933
- previewLines.length > maxPreview && /* @__PURE__ */ jsxs4(Text7, { dimColor: true, children: [
1216
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", height: rows, children: [
1217
+ /* @__PURE__ */ jsx8(Header, { title: `${APP_TITLE} \u2014 ${selectedDir}` }),
1218
+ /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: "New Claude session:" }) }),
1219
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingLeft: 2, children: [
1220
+ previewLines.slice(0, maxPreview).map((line, i) => /* @__PURE__ */ jsx8(Text8, { color: "white", children: line }, i)),
1221
+ previewLines.length > maxPreview && /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
934
1222
  "... (",
935
1223
  previewLines.length - maxPreview,
936
1224
  " more lines)"
937
1225
  ] })
938
1226
  ] }),
939
- /* @__PURE__ */ jsxs4(Box7, { gap: 2, children: [
940
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Enter confirm" }),
941
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Esc cancel" })
1227
+ /* @__PURE__ */ jsxs6(Box8, { gap: 2, children: [
1228
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Enter confirm" }),
1229
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc cancel" })
942
1230
  ] })
943
1231
  ] });
944
1232
  };
945
1233
 
946
1234
  // src/components/DeleteSessionView.tsx
947
- import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
948
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1235
+ import { Box as Box9, Text as Text9, useInput as useInput5 } from "ink";
1236
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
949
1237
  var DeleteSessionView = ({ sessionName, paneCount, onConfirm, onCancel }) => {
950
1238
  const { rows } = useTerminalSize();
951
- useInput4((_input, key) => {
1239
+ useInput5((_input, key) => {
952
1240
  if (key.return) {
953
1241
  onConfirm();
954
1242
  return;
@@ -957,33 +1245,33 @@ var DeleteSessionView = ({ sessionName, paneCount, onConfirm, onCancel }) => {
957
1245
  onCancel();
958
1246
  }
959
1247
  });
960
- return /* @__PURE__ */ jsxs5(Box8, { flexDirection: "column", height: rows, children: [
961
- /* @__PURE__ */ jsx8(Header, { title: APP_TITLE }),
962
- /* @__PURE__ */ jsxs5(Box8, { flexDirection: "column", gap: 1, paddingLeft: 2, children: [
963
- /* @__PURE__ */ jsx8(Text8, { bold: true, color: "red", children: "Delete session?" }),
964
- /* @__PURE__ */ jsxs5(Text8, { children: [
1248
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", height: rows, children: [
1249
+ /* @__PURE__ */ jsx9(Header, { title: APP_TITLE }),
1250
+ /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", gap: 1, paddingLeft: 2, children: [
1251
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "red", children: "Delete session?" }),
1252
+ /* @__PURE__ */ jsxs7(Text9, { children: [
965
1253
  "Session: ",
966
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: sessionName })
1254
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: sessionName })
967
1255
  ] }),
968
- /* @__PURE__ */ jsxs5(Text8, { children: [
1256
+ /* @__PURE__ */ jsxs7(Text9, { children: [
969
1257
  "Panes: ",
970
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: paneCount })
1258
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: paneCount })
971
1259
  ] }),
972
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "All processes in this session will be terminated." })
1260
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "All processes in this session will be terminated." })
973
1261
  ] }),
974
- /* @__PURE__ */ jsx8(Box8, { flexGrow: 1 }),
975
- /* @__PURE__ */ jsxs5(Box8, { gap: 2, children: [
976
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Enter confirm" }),
977
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc cancel" })
1262
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1 }),
1263
+ /* @__PURE__ */ jsxs7(Box9, { gap: 2, children: [
1264
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter confirm" }),
1265
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Esc cancel" })
978
1266
  ] })
979
1267
  ] });
980
1268
  };
981
1269
 
982
1270
  // src/components/DirectorySearchView.tsx
983
- import { Box as Box9, Text as Text9, useInput as useInput5 } from "ink";
984
- import { useMemo as useMemo4, useState as useState4 } from "react";
1271
+ import { Box as Box10, Text as Text10, useInput as useInput6 } from "ink";
1272
+ import { useMemo as useMemo6, useState as useState5 } from "react";
985
1273
  import { basename } from "node:path";
986
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1274
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
987
1275
  var formatPath = (path) => {
988
1276
  const home = process.env["HOME"] ?? "";
989
1277
  if (home && path.startsWith(home)) {
@@ -993,9 +1281,9 @@ var formatPath = (path) => {
993
1281
  };
994
1282
  var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
995
1283
  const { rows } = useTerminalSize();
996
- const [query, setQuery] = useState4("");
997
- const [cursor, setCursor] = useState4(0);
998
- const filtered = useMemo4(() => {
1284
+ const [query, setQuery] = useState5("");
1285
+ const [cursor, setCursor] = useState5(0);
1286
+ const filtered = useMemo6(() => {
999
1287
  if (!query) return directories;
1000
1288
  const lower = query.toLowerCase();
1001
1289
  return directories.filter((d) => {
@@ -1010,11 +1298,11 @@ var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
1010
1298
  }
1011
1299
  const listHeight = rows - 6;
1012
1300
  const { scrollOffset, visibleCount } = useScroll(clampedCursor, filtered.length, listHeight);
1013
- const visibleItems = useMemo4(
1301
+ const visibleItems = useMemo6(
1014
1302
  () => filtered.slice(scrollOffset, scrollOffset + visibleCount),
1015
1303
  [filtered, scrollOffset, visibleCount]
1016
1304
  );
1017
- useInput5((input, key) => {
1305
+ useInput6((input, key) => {
1018
1306
  if (key.escape) {
1019
1307
  onCancel();
1020
1308
  return;
@@ -1042,33 +1330,50 @@ var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
1042
1330
  setCursor(0);
1043
1331
  }
1044
1332
  });
1045
- return /* @__PURE__ */ jsxs6(Box9, { flexDirection: "column", height: rows, children: [
1046
- /* @__PURE__ */ jsx9(Header, { title: `${APP_TITLE} \u2014 Add Session` }),
1047
- /* @__PURE__ */ jsxs6(Box9, { paddingLeft: 1, gap: 1, children: [
1048
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: ">" }),
1049
- /* @__PURE__ */ jsx9(Text9, { children: query }),
1050
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: query ? "" : "type to filter..." })
1333
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", height: rows, children: [
1334
+ /* @__PURE__ */ jsx10(Header, { title: `${APP_TITLE} \u2014 Add Session` }),
1335
+ /* @__PURE__ */ jsxs8(Box10, { paddingLeft: 1, gap: 1, children: [
1336
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: ">" }),
1337
+ /* @__PURE__ */ jsx10(Text10, { children: query }),
1338
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: query ? "" : "type to filter..." })
1051
1339
  ] }),
1052
- /* @__PURE__ */ jsx9(Box9, { paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text9, { dimColor: true, children: [
1340
+ /* @__PURE__ */ jsx10(Box10, { paddingLeft: 1, children: /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
1053
1341
  filtered.length,
1054
1342
  "/",
1055
1343
  directories.length
1056
1344
  ] }) }),
1057
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleItems.map((dir, i) => {
1345
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleItems.map((dir, i) => {
1058
1346
  const globalIndex = scrollOffset + i;
1059
1347
  const isHighlighted = globalIndex === clampedCursor;
1060
- return /* @__PURE__ */ jsxs6(Box9, { paddingLeft: 1, gap: 1, children: [
1061
- /* @__PURE__ */ jsx9(Text9, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
1062
- /* @__PURE__ */ jsx9(Text9, { color: isHighlighted ? "green" : void 0, bold: isHighlighted, children: formatPath(dir) })
1348
+ return /* @__PURE__ */ jsxs8(Box10, { paddingLeft: 1, gap: 1, children: [
1349
+ /* @__PURE__ */ jsx10(Text10, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
1350
+ /* @__PURE__ */ jsx10(Text10, { color: isHighlighted ? "green" : void 0, bold: isHighlighted, children: formatPath(dir) })
1063
1351
  ] }, dir);
1064
1352
  }) }),
1065
- /* @__PURE__ */ jsxs6(Box9, { gap: 2, children: [
1066
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter select" }),
1067
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Esc cancel" })
1353
+ /* @__PURE__ */ jsxs8(Box10, { gap: 2, children: [
1354
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Enter select" }),
1355
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Esc cancel" })
1068
1356
  ] })
1069
1357
  ] });
1070
1358
  };
1071
1359
 
1360
+ // src/hooks/use-interval.ts
1361
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
1362
+ var useInterval = (fn, intervalMs, enabled = true) => {
1363
+ const fnRef = useRef2(fn);
1364
+ fnRef.current = fn;
1365
+ useEffect2(() => {
1366
+ if (!enabled) return;
1367
+ fnRef.current();
1368
+ const timer = setInterval(() => {
1369
+ fnRef.current();
1370
+ }, intervalMs);
1371
+ return () => {
1372
+ clearInterval(timer);
1373
+ };
1374
+ }, [intervalMs, enabled]);
1375
+ };
1376
+
1072
1377
  // src/utils/PromiseUtils.ts
1073
1378
  var swallow = async (fn) => {
1074
1379
  try {
@@ -1078,7 +1383,7 @@ var swallow = async (fn) => {
1078
1383
  };
1079
1384
 
1080
1385
  // src/components/ManagerView.tsx
1081
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1386
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1082
1387
  var MODE = {
1083
1388
  split: "split",
1084
1389
  confirm: "confirm",
@@ -1087,9 +1392,11 @@ var MODE = {
1087
1392
  };
1088
1393
  var FOCUS = {
1089
1394
  left: "left",
1090
- right: "right"
1395
+ right: "right",
1396
+ bottom: "bottom"
1091
1397
  };
1092
1398
  var POLL_INTERVAL = 3e3;
1399
+ var OVERVIEW_POLL_INTERVAL = 6e4;
1093
1400
  var ManagerView = ({
1094
1401
  actions,
1095
1402
  currentSession,
@@ -1099,16 +1406,22 @@ var ManagerView = ({
1099
1406
  restoredCwd
1100
1407
  }) => {
1101
1408
  const { rows, columns } = useTerminalSize();
1102
- const [sessionsState, setSessionsState] = useState5({
1409
+ const [sessionsState, setSessionsState] = useState6({
1103
1410
  sessions: [],
1104
1411
  isLoading: true
1105
1412
  });
1106
- const [mode, setMode] = useState5(restoredPrompt ? MODE.confirm : MODE.split);
1107
- const [focus, setFocus] = useState5(FOCUS.left);
1108
- const [selectedSession, setSelectedSession] = useState5(restoredSession);
1109
- const [pendingPrompt, setPendingPrompt] = useState5(restoredPrompt ?? "");
1110
- const [pendingDeleteSession, setPendingDeleteSession] = useState5(void 0);
1111
- const refresh = useCallback3(async () => {
1413
+ const [mode, setMode] = useState6(restoredPrompt ? MODE.confirm : MODE.split);
1414
+ const [focus, setFocus] = useState6(FOCUS.left);
1415
+ const [selectedSession, setSelectedSession] = useState6(restoredSession);
1416
+ const [pendingPrompt, setPendingPrompt] = useState6(restoredPrompt ?? "");
1417
+ const [pendingDeleteSession, setPendingDeleteSession] = useState6(void 0);
1418
+ const [overviewResult, setOverviewResult] = useState6({
1419
+ overallSummary: "",
1420
+ sessions: []
1421
+ });
1422
+ const [overviewLoading, setOverviewLoading] = useState6(true);
1423
+ const overviewInFlightRef = useRef3(false);
1424
+ const refresh = useCallback4(async () => {
1112
1425
  try {
1113
1426
  const fetched = await actions.fetchSessions();
1114
1427
  setSessionsState((prev) => {
@@ -1122,31 +1435,51 @@ var ManagerView = ({
1122
1435
  setSessionsState((prev) => ({ ...prev, isLoading: false }));
1123
1436
  }
1124
1437
  }, [actions]);
1125
- useEffect2(() => {
1126
- void refresh();
1127
- const timer = setInterval(() => {
1128
- void refresh();
1129
- }, POLL_INTERVAL);
1130
- return () => {
1131
- clearInterval(timer);
1132
- };
1133
- }, [refresh]);
1438
+ useInterval(() => void refresh(), POLL_INTERVAL);
1439
+ useInterval(
1440
+ () => {
1441
+ if (overviewInFlightRef.current) return;
1442
+ overviewInFlightRef.current = true;
1443
+ setOverviewLoading(true);
1444
+ void actions.fetchOverview(sessionsState.sessions).then((result) => {
1445
+ setOverviewResult(result);
1446
+ }).catch(() => {
1447
+ }).finally(() => {
1448
+ setOverviewLoading(false);
1449
+ overviewInFlightRef.current = false;
1450
+ });
1451
+ },
1452
+ OVERVIEW_POLL_INTERVAL,
1453
+ !sessionsState.isLoading
1454
+ );
1134
1455
  const resolvedSession = selectedSession ?? sessionsState.sessions[0]?.name;
1135
- const selectedManagedSession = useMemo5(
1456
+ const selectedManagedSession = useMemo7(
1136
1457
  () => sessionsState.sessions.find((s) => s.name === resolvedSession),
1137
1458
  [sessionsState.sessions, resolvedSession]
1138
1459
  );
1139
- const selectedGroup = useMemo5(() => {
1460
+ const selectedGroup = useMemo7(() => {
1140
1461
  if (!selectedManagedSession) return void 0;
1141
1462
  return {
1142
1463
  sessionName: selectedManagedSession.name,
1143
1464
  tabs: selectedManagedSession.groups.flatMap((g) => g.tabs)
1144
1465
  };
1145
1466
  }, [selectedManagedSession]);
1146
- const handleOpenAddSession = useCallback3(() => {
1467
+ const allGroups = useMemo7(
1468
+ () => sessionsState.sessions.flatMap((s) => s.groups),
1469
+ [sessionsState.sessions]
1470
+ );
1471
+ const statusCounts = useMemo7(
1472
+ () => allGroups.flatMap((g) => g.tabs).flatMap((t) => t.panes).filter((p) => p.kind === "claude" && p.claudeStatus).reduce((acc, p) => {
1473
+ const s = p.claudeStatus;
1474
+ if (!s) return acc;
1475
+ return { ...acc, [s]: (acc[s] ?? 0) + 1 };
1476
+ }, {}),
1477
+ [allGroups]
1478
+ );
1479
+ const handleOpenAddSession = useCallback4(() => {
1147
1480
  setMode(MODE.addSession);
1148
1481
  }, []);
1149
- const handleAddSessionSelect = useCallback3((path) => {
1482
+ const handleAddSessionSelect = useCallback4((path) => {
1150
1483
  const name = basename2(path);
1151
1484
  setSessionsState((prev) => {
1152
1485
  const exists2 = prev.sessions.some((s) => s.name === name);
@@ -1159,14 +1492,14 @@ var ManagerView = ({
1159
1492
  setSelectedSession(name);
1160
1493
  setMode(MODE.split);
1161
1494
  }, []);
1162
- const handleCancelAddSession = useCallback3(() => {
1495
+ const handleCancelAddSession = useCallback4(() => {
1163
1496
  setMode(MODE.split);
1164
1497
  }, []);
1165
- const handleDeleteSession = useCallback3((name) => {
1498
+ const handleDeleteSession = useCallback4((name) => {
1166
1499
  setPendingDeleteSession(name);
1167
1500
  setMode(MODE.deleteSession);
1168
1501
  }, []);
1169
- const handleConfirmDelete = useCallback3(() => {
1502
+ const handleConfirmDelete = useCallback4(() => {
1170
1503
  if (!pendingDeleteSession) return;
1171
1504
  const session = sessionsState.sessions.find((s) => s.name === pendingDeleteSession);
1172
1505
  setSessionsState((prev) => ({
@@ -1185,11 +1518,11 @@ var ManagerView = ({
1185
1518
  setPendingDeleteSession(void 0);
1186
1519
  setMode(MODE.split);
1187
1520
  }, [pendingDeleteSession, resolvedSession, sessionsState.sessions, actions, refresh]);
1188
- const handleCancelDelete = useCallback3(() => {
1521
+ const handleCancelDelete = useCallback4(() => {
1189
1522
  setPendingDeleteSession(void 0);
1190
1523
  setMode(MODE.split);
1191
1524
  }, []);
1192
- const handleNewSession = useCallback3(
1525
+ const handleNewSession = useCallback4(
1193
1526
  (sessionName) => {
1194
1527
  const cwd = selectedManagedSession?.path;
1195
1528
  if (!cwd) return;
@@ -1197,7 +1530,7 @@ var ManagerView = ({
1197
1530
  },
1198
1531
  [actions, selectedManagedSession]
1199
1532
  );
1200
- const handleConfirmNew = useCallback3(() => {
1533
+ const handleConfirmNew = useCallback4(() => {
1201
1534
  if (!resolvedSession) return;
1202
1535
  const cwd = restoredCwd ?? selectedManagedSession?.path;
1203
1536
  if (!cwd) return;
@@ -1205,53 +1538,53 @@ var ManagerView = ({
1205
1538
  setPendingPrompt("");
1206
1539
  setMode(MODE.split);
1207
1540
  }, [resolvedSession, restoredCwd, selectedManagedSession, pendingPrompt, actions, refresh]);
1208
- const handleCancelConfirm = useCallback3(() => {
1541
+ const handleCancelConfirm = useCallback4(() => {
1209
1542
  setPendingPrompt("");
1210
1543
  setMode(MODE.split);
1211
1544
  }, []);
1212
- const handleSessionSelect = useCallback3((name) => {
1545
+ const handleSessionSelect = useCallback4((name) => {
1213
1546
  setSelectedSession(name);
1214
1547
  setFocus(FOCUS.right);
1215
1548
  }, []);
1216
- const handleSessionCursorChange = useCallback3((name) => {
1549
+ const handleSessionCursorChange = useCallback4((name) => {
1217
1550
  setSelectedSession(name);
1218
1551
  }, []);
1219
- const handleNavigate = useCallback3(
1552
+ const handleNavigate = useCallback4(
1220
1553
  (up) => {
1221
1554
  void actions.navigateToPane(up);
1222
1555
  },
1223
1556
  [actions]
1224
1557
  );
1225
- const handleBack = useCallback3(() => {
1558
+ const handleBack = useCallback4(() => {
1226
1559
  setFocus(FOCUS.left);
1227
1560
  }, []);
1228
- const handleKillPane = useCallback3(
1561
+ const handleKillPane = useCallback4(
1229
1562
  async (paneId) => {
1230
1563
  await swallow(() => actions.killPane(paneId));
1231
1564
  void refresh();
1232
1565
  },
1233
1566
  [actions, refresh]
1234
1567
  );
1235
- const handleHighlight = useCallback3(
1568
+ const handleHighlight = useCallback4(
1236
1569
  async (up) => {
1237
1570
  await swallow(() => actions.highlightWindow(up));
1238
1571
  },
1239
1572
  [actions]
1240
1573
  );
1241
- const handleUnhighlight = useCallback3(
1574
+ const handleUnhighlight = useCallback4(
1242
1575
  async (up) => {
1243
1576
  await swallow(() => actions.unhighlightWindow(up));
1244
1577
  },
1245
1578
  [actions]
1246
1579
  );
1247
1580
  if (sessionsState.isLoading) {
1248
- return /* @__PURE__ */ jsxs7(Box10, { flexDirection: "column", height: rows, children: [
1249
- /* @__PURE__ */ jsx10(Header, { title: `${APP_TITLE} v${APP_VERSION}` }),
1250
- /* @__PURE__ */ jsx10(StatusBar, { message: "Loading...", type: "info" })
1581
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: rows, children: [
1582
+ /* @__PURE__ */ jsx11(Header, { title: `${APP_TITLE} v${APP_VERSION}` }),
1583
+ /* @__PURE__ */ jsx11(StatusBar, { message: "Loading...", type: "info" })
1251
1584
  ] });
1252
1585
  }
1253
1586
  if (mode === MODE.addSession) {
1254
- return /* @__PURE__ */ jsx10(
1587
+ return /* @__PURE__ */ jsx11(
1255
1588
  DirectorySearchView,
1256
1589
  {
1257
1590
  directories,
@@ -1266,7 +1599,7 @@ var ManagerView = ({
1266
1599
  (sum, g) => sum + g.tabs.reduce((s, t) => s + t.panes.length, 0),
1267
1600
  0
1268
1601
  ) ?? 0;
1269
- return /* @__PURE__ */ jsx10(
1602
+ return /* @__PURE__ */ jsx11(
1270
1603
  DeleteSessionView,
1271
1604
  {
1272
1605
  sessionName: pendingDeleteSession,
@@ -1277,7 +1610,7 @@ var ManagerView = ({
1277
1610
  );
1278
1611
  }
1279
1612
  if (mode === MODE.confirm && pendingPrompt) {
1280
- return /* @__PURE__ */ jsx10(
1613
+ return /* @__PURE__ */ jsx11(
1281
1614
  ConfirmView,
1282
1615
  {
1283
1616
  selectedDir: resolvedSession ?? "",
@@ -1287,26 +1620,29 @@ var ManagerView = ({
1287
1620
  }
1288
1621
  );
1289
1622
  }
1290
- const panelHeight = rows - 5;
1623
+ const fixedRows = 3;
1624
+ const contentHeight = rows - fixedRows;
1625
+ const topHeight = Math.floor(contentHeight / 2);
1626
+ const bottomHeight = contentHeight - topHeight;
1291
1627
  const leftWidth = Math.floor(columns / 3);
1292
1628
  const rightWidth = columns - leftWidth;
1293
- return /* @__PURE__ */ jsxs7(Box10, { flexDirection: "column", height: rows, children: [
1294
- /* @__PURE__ */ jsx10(Header, { title: `${APP_TITLE} - v${APP_VERSION}` }),
1295
- /* @__PURE__ */ jsxs7(Box10, { flexDirection: "row", flexGrow: 1, children: [
1296
- /* @__PURE__ */ jsx10(
1297
- Box10,
1629
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: rows, children: [
1630
+ /* @__PURE__ */ jsx11(Header, { title: `${APP_TITLE} - v${APP_VERSION}` }),
1631
+ /* @__PURE__ */ jsxs9(Box11, { flexDirection: "row", height: topHeight, children: [
1632
+ /* @__PURE__ */ jsx11(
1633
+ Box11,
1298
1634
  {
1299
1635
  flexDirection: "column",
1300
1636
  width: leftWidth,
1301
1637
  borderStyle: "round",
1302
1638
  borderColor: focus === FOCUS.left ? "green" : "gray",
1303
- children: /* @__PURE__ */ jsx10(
1639
+ children: /* @__PURE__ */ jsx11(
1304
1640
  SessionListPanel,
1305
1641
  {
1306
1642
  sessions: sessionsState.sessions,
1307
1643
  currentSession,
1308
1644
  isFocused: focus === FOCUS.left,
1309
- availableRows: panelHeight,
1645
+ availableRows: topHeight - 2,
1310
1646
  onSelect: handleSessionSelect,
1311
1647
  onCursorChange: handleSessionCursorChange,
1312
1648
  onDeleteSession: handleDeleteSession,
@@ -1315,20 +1651,20 @@ var ManagerView = ({
1315
1651
  )
1316
1652
  }
1317
1653
  ),
1318
- /* @__PURE__ */ jsx10(
1319
- Box10,
1654
+ /* @__PURE__ */ jsx11(
1655
+ Box11,
1320
1656
  {
1321
1657
  flexDirection: "column",
1322
1658
  width: rightWidth,
1323
1659
  borderStyle: "round",
1324
1660
  borderColor: focus === FOCUS.right ? "green" : "gray",
1325
- children: /* @__PURE__ */ jsx10(
1661
+ children: /* @__PURE__ */ jsx11(
1326
1662
  PaneListPanel,
1327
1663
  {
1328
1664
  selectedSession: resolvedSession,
1329
1665
  group: selectedGroup,
1330
1666
  isFocused: focus === FOCUS.right,
1331
- availableRows: panelHeight,
1667
+ availableRows: topHeight - 2,
1332
1668
  onNavigate: handleNavigate,
1333
1669
  onHighlight: handleHighlight,
1334
1670
  onUnhighlight: handleUnhighlight,
@@ -1340,7 +1676,25 @@ var ManagerView = ({
1340
1676
  }
1341
1677
  )
1342
1678
  ] }),
1343
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: focus === FOCUS.left ? "\u2191/\u2193 move Enter/\u2192 select n add d delete q quit" : "\u2191/\u2193 move Enter focus n new d kill Esc/\u2190 back q quit" })
1679
+ /* @__PURE__ */ jsx11(
1680
+ SessionOverviewPanel,
1681
+ {
1682
+ overallSummary: overviewResult.overallSummary,
1683
+ items: overviewResult.sessions,
1684
+ groups: allGroups,
1685
+ isLoading: overviewLoading,
1686
+ isFocused: focus === FOCUS.bottom,
1687
+ availableRows: bottomHeight,
1688
+ onBack: handleBack
1689
+ }
1690
+ ),
1691
+ /* @__PURE__ */ jsx11(
1692
+ StatusBar,
1693
+ {
1694
+ message: focus === FOCUS.left ? "\u2191/\u2193 move Enter/\u2192 select n add d delete q quit" : focus === FOCUS.right ? "\u2191/\u2193 move Enter focus n new d kill Esc/\u2190 back q quit" : "\u2191/\u2193 scroll Esc/\u2190 back q quit",
1695
+ statusCounts
1696
+ }
1697
+ )
1344
1698
  ] });
1345
1699
  };
1346
1700
 
@@ -1380,6 +1734,10 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
1380
1734
  groups: items.map((item) => item.group)
1381
1735
  }));
1382
1736
  },
1737
+ fetchOverview: async (sessions) => {
1738
+ const groups = sessions.flatMap((s) => s.groups);
1739
+ return await usecases.manager.fetchOverview(groups);
1740
+ },
1383
1741
  createSession: async (sessionName, cwd, prompt) => {
1384
1742
  await usecases.manager.createSession({ sessionName, cwd, prompt });
1385
1743
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmux",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cut0/abmux.git"