abmux 0.0.5 → 0.0.7

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 +492 -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.7",
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,137 @@ 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
+ const reservedLines = 3;
1098
+ const { scrollOffset, visibleCount } = useScroll(
1099
+ clampedCursor,
1100
+ lines.length,
1101
+ availableRows - reservedLines
1102
+ );
1103
+ const visibleLines = useMemo5(
1104
+ () => lines.slice(scrollOffset, scrollOffset + visibleCount),
1105
+ [lines, scrollOffset, visibleCount]
1106
+ );
1107
+ const moveCursor = useCallback3(
1108
+ (next) => {
1109
+ setCursor(Math.max(0, Math.min(lines.length - 1, next)));
1110
+ },
1111
+ [lines.length]
1112
+ );
1113
+ useInput3(
1114
+ (input, key) => {
1115
+ if (input === "q") {
1116
+ exit();
1117
+ return;
1118
+ }
1119
+ if (key.escape || key.leftArrow) {
1120
+ onBack();
1121
+ return;
1122
+ }
1123
+ if (key.upArrow) {
1124
+ moveCursor(clampedCursor - 1);
1125
+ return;
1126
+ }
1127
+ if (key.downArrow) {
1128
+ moveCursor(clampedCursor + 1);
1129
+ }
1130
+ },
1131
+ { isActive: isFocused }
1132
+ );
1133
+ return /* @__PURE__ */ jsxs5(
1134
+ Box7,
1135
+ {
1136
+ flexDirection: "column",
1137
+ height: availableRows,
1138
+ borderStyle: "round",
1139
+ borderColor: isFocused ? "green" : "gray",
1140
+ children: [
1141
+ /* @__PURE__ */ jsxs5(Box7, { paddingLeft: 1, gap: 1, children: [
1142
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: isFocused ? "green" : "gray", children: "Overview" }),
1143
+ isLoading && /* @__PURE__ */ jsx7(Spinner, { label: "" })
1144
+ ] }),
1145
+ /* @__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) => {
1146
+ const globalIndex = scrollOffset + i;
1147
+ const isHighlighted = isFocused && globalIndex === clampedCursor;
1148
+ if (line.type === "spacer") {
1149
+ return /* @__PURE__ */ jsx7(Box7, { height: 1 }, line.key);
1150
+ }
1151
+ if (line.type === "summary") {
1152
+ return /* @__PURE__ */ jsx7(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : void 0, wrap: "wrap", children: line.text }) }, line.key);
1153
+ }
1154
+ if (line.type === "session") {
1155
+ return /* @__PURE__ */ jsx7(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : "cyan", children: line.sessionName }) }, line.key);
1156
+ }
1157
+ return /* @__PURE__ */ jsxs5(Box7, { paddingLeft: 3, gap: 1, children: [
1158
+ line.statusLabel ? /* @__PURE__ */ jsxs5(Text7, { color: isHighlighted ? "green" : line.statusColor, children: [
1159
+ "[",
1160
+ line.statusLabel,
1161
+ "]"
1162
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : "gray", children: "[--]" }),
1163
+ /* @__PURE__ */ jsx7(Text7, { color: isHighlighted ? "green" : void 0, wrap: "wrap", children: line.description })
1164
+ ] }, line.key);
1165
+ }) })
1166
+ ]
1167
+ }
1168
+ );
1169
+ };
1170
+
886
1171
  // src/components/ConfirmView.tsx
887
- import { Box as Box7, Text as Text7, useInput as useInput3 } from "ink";
1172
+ import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
888
1173
 
889
1174
  // src/hooks/use-terminal-size.ts
890
1175
  import { useStdout } from "ink";
891
- import { useEffect, useState as useState3 } from "react";
1176
+ import { useEffect, useState as useState4 } from "react";
892
1177
  var useTerminalSize = () => {
893
1178
  const { stdout } = useStdout();
894
- const [size, setSize] = useState3({
1179
+ const [size, setSize] = useState4({
895
1180
  rows: stdout.rows ?? 24,
896
1181
  columns: stdout.columns ?? 80
897
1182
  });
@@ -911,12 +1196,12 @@ var useTerminalSize = () => {
911
1196
  };
912
1197
 
913
1198
  // src/components/ConfirmView.tsx
914
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1199
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
915
1200
  var ConfirmView = ({ selectedDir, prompt, onConfirm, onCancel }) => {
916
1201
  const { rows } = useTerminalSize();
917
1202
  const previewLines = prompt.split("\n");
918
1203
  const maxPreview = Math.min(previewLines.length, rows - 6);
919
- useInput3((_input, key) => {
1204
+ useInput4((_input, key) => {
920
1205
  if (key.return) {
921
1206
  onConfirm();
922
1207
  return;
@@ -925,30 +1210,30 @@ var ConfirmView = ({ selectedDir, prompt, onConfirm, onCancel }) => {
925
1210
  onCancel();
926
1211
  }
927
1212
  });
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: [
1213
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", height: rows, children: [
1214
+ /* @__PURE__ */ jsx8(Header, { title: `${APP_TITLE} \u2014 ${selectedDir}` }),
1215
+ /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text8, { bold: true, children: "New Claude session:" }) }),
1216
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingLeft: 2, children: [
1217
+ previewLines.slice(0, maxPreview).map((line, i) => /* @__PURE__ */ jsx8(Text8, { color: "white", children: line }, i)),
1218
+ previewLines.length > maxPreview && /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
934
1219
  "... (",
935
1220
  previewLines.length - maxPreview,
936
1221
  " more lines)"
937
1222
  ] })
938
1223
  ] }),
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" })
1224
+ /* @__PURE__ */ jsxs6(Box8, { gap: 2, children: [
1225
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Enter confirm" }),
1226
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc cancel" })
942
1227
  ] })
943
1228
  ] });
944
1229
  };
945
1230
 
946
1231
  // 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";
1232
+ import { Box as Box9, Text as Text9, useInput as useInput5 } from "ink";
1233
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
949
1234
  var DeleteSessionView = ({ sessionName, paneCount, onConfirm, onCancel }) => {
950
1235
  const { rows } = useTerminalSize();
951
- useInput4((_input, key) => {
1236
+ useInput5((_input, key) => {
952
1237
  if (key.return) {
953
1238
  onConfirm();
954
1239
  return;
@@ -957,33 +1242,33 @@ var DeleteSessionView = ({ sessionName, paneCount, onConfirm, onCancel }) => {
957
1242
  onCancel();
958
1243
  }
959
1244
  });
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: [
1245
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", height: rows, children: [
1246
+ /* @__PURE__ */ jsx9(Header, { title: APP_TITLE }),
1247
+ /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", gap: 1, paddingLeft: 2, children: [
1248
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "red", children: "Delete session?" }),
1249
+ /* @__PURE__ */ jsxs7(Text9, { children: [
965
1250
  "Session: ",
966
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: sessionName })
1251
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: sessionName })
967
1252
  ] }),
968
- /* @__PURE__ */ jsxs5(Text8, { children: [
1253
+ /* @__PURE__ */ jsxs7(Text9, { children: [
969
1254
  "Panes: ",
970
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: paneCount })
1255
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: paneCount })
971
1256
  ] }),
972
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "All processes in this session will be terminated." })
1257
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "All processes in this session will be terminated." })
973
1258
  ] }),
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" })
1259
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1 }),
1260
+ /* @__PURE__ */ jsxs7(Box9, { gap: 2, children: [
1261
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter confirm" }),
1262
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Esc cancel" })
978
1263
  ] })
979
1264
  ] });
980
1265
  };
981
1266
 
982
1267
  // 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";
1268
+ import { Box as Box10, Text as Text10, useInput as useInput6 } from "ink";
1269
+ import { useMemo as useMemo6, useState as useState5 } from "react";
985
1270
  import { basename } from "node:path";
986
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1271
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
987
1272
  var formatPath = (path) => {
988
1273
  const home = process.env["HOME"] ?? "";
989
1274
  if (home && path.startsWith(home)) {
@@ -993,9 +1278,9 @@ var formatPath = (path) => {
993
1278
  };
994
1279
  var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
995
1280
  const { rows } = useTerminalSize();
996
- const [query, setQuery] = useState4("");
997
- const [cursor, setCursor] = useState4(0);
998
- const filtered = useMemo4(() => {
1281
+ const [query, setQuery] = useState5("");
1282
+ const [cursor, setCursor] = useState5(0);
1283
+ const filtered = useMemo6(() => {
999
1284
  if (!query) return directories;
1000
1285
  const lower = query.toLowerCase();
1001
1286
  return directories.filter((d) => {
@@ -1010,11 +1295,11 @@ var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
1010
1295
  }
1011
1296
  const listHeight = rows - 6;
1012
1297
  const { scrollOffset, visibleCount } = useScroll(clampedCursor, filtered.length, listHeight);
1013
- const visibleItems = useMemo4(
1298
+ const visibleItems = useMemo6(
1014
1299
  () => filtered.slice(scrollOffset, scrollOffset + visibleCount),
1015
1300
  [filtered, scrollOffset, visibleCount]
1016
1301
  );
1017
- useInput5((input, key) => {
1302
+ useInput6((input, key) => {
1018
1303
  if (key.escape) {
1019
1304
  onCancel();
1020
1305
  return;
@@ -1042,33 +1327,50 @@ var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
1042
1327
  setCursor(0);
1043
1328
  }
1044
1329
  });
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..." })
1330
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", height: rows, children: [
1331
+ /* @__PURE__ */ jsx10(Header, { title: `${APP_TITLE} \u2014 Add Session` }),
1332
+ /* @__PURE__ */ jsxs8(Box10, { paddingLeft: 1, gap: 1, children: [
1333
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: ">" }),
1334
+ /* @__PURE__ */ jsx10(Text10, { children: query }),
1335
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: query ? "" : "type to filter..." })
1051
1336
  ] }),
1052
- /* @__PURE__ */ jsx9(Box9, { paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text9, { dimColor: true, children: [
1337
+ /* @__PURE__ */ jsx10(Box10, { paddingLeft: 1, children: /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
1053
1338
  filtered.length,
1054
1339
  "/",
1055
1340
  directories.length
1056
1341
  ] }) }),
1057
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleItems.map((dir, i) => {
1342
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleItems.map((dir, i) => {
1058
1343
  const globalIndex = scrollOffset + i;
1059
1344
  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) })
1345
+ return /* @__PURE__ */ jsxs8(Box10, { paddingLeft: 1, gap: 1, children: [
1346
+ /* @__PURE__ */ jsx10(Text10, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
1347
+ /* @__PURE__ */ jsx10(Text10, { color: isHighlighted ? "green" : void 0, bold: isHighlighted, children: formatPath(dir) })
1063
1348
  ] }, dir);
1064
1349
  }) }),
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" })
1350
+ /* @__PURE__ */ jsxs8(Box10, { gap: 2, children: [
1351
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Enter select" }),
1352
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Esc cancel" })
1068
1353
  ] })
1069
1354
  ] });
1070
1355
  };
1071
1356
 
1357
+ // src/hooks/use-interval.ts
1358
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
1359
+ var useInterval = (fn, intervalMs, enabled = true) => {
1360
+ const fnRef = useRef2(fn);
1361
+ fnRef.current = fn;
1362
+ useEffect2(() => {
1363
+ if (!enabled) return;
1364
+ fnRef.current();
1365
+ const timer = setInterval(() => {
1366
+ fnRef.current();
1367
+ }, intervalMs);
1368
+ return () => {
1369
+ clearInterval(timer);
1370
+ };
1371
+ }, [intervalMs, enabled]);
1372
+ };
1373
+
1072
1374
  // src/utils/PromiseUtils.ts
1073
1375
  var swallow = async (fn) => {
1074
1376
  try {
@@ -1078,7 +1380,7 @@ var swallow = async (fn) => {
1078
1380
  };
1079
1381
 
1080
1382
  // src/components/ManagerView.tsx
1081
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1383
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1082
1384
  var MODE = {
1083
1385
  split: "split",
1084
1386
  confirm: "confirm",
@@ -1087,9 +1389,11 @@ var MODE = {
1087
1389
  };
1088
1390
  var FOCUS = {
1089
1391
  left: "left",
1090
- right: "right"
1392
+ right: "right",
1393
+ bottom: "bottom"
1091
1394
  };
1092
1395
  var POLL_INTERVAL = 3e3;
1396
+ var OVERVIEW_POLL_INTERVAL = 6e4;
1093
1397
  var ManagerView = ({
1094
1398
  actions,
1095
1399
  currentSession,
@@ -1099,16 +1403,22 @@ var ManagerView = ({
1099
1403
  restoredCwd
1100
1404
  }) => {
1101
1405
  const { rows, columns } = useTerminalSize();
1102
- const [sessionsState, setSessionsState] = useState5({
1406
+ const [sessionsState, setSessionsState] = useState6({
1103
1407
  sessions: [],
1104
1408
  isLoading: true
1105
1409
  });
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 () => {
1410
+ const [mode, setMode] = useState6(restoredPrompt ? MODE.confirm : MODE.split);
1411
+ const [focus, setFocus] = useState6(FOCUS.left);
1412
+ const [selectedSession, setSelectedSession] = useState6(restoredSession);
1413
+ const [pendingPrompt, setPendingPrompt] = useState6(restoredPrompt ?? "");
1414
+ const [pendingDeleteSession, setPendingDeleteSession] = useState6(void 0);
1415
+ const [overviewResult, setOverviewResult] = useState6({
1416
+ overallSummary: "",
1417
+ sessions: []
1418
+ });
1419
+ const [overviewLoading, setOverviewLoading] = useState6(true);
1420
+ const overviewInFlightRef = useRef3(false);
1421
+ const refresh = useCallback4(async () => {
1112
1422
  try {
1113
1423
  const fetched = await actions.fetchSessions();
1114
1424
  setSessionsState((prev) => {
@@ -1122,31 +1432,52 @@ var ManagerView = ({
1122
1432
  setSessionsState((prev) => ({ ...prev, isLoading: false }));
1123
1433
  }
1124
1434
  }, [actions]);
1125
- useEffect2(() => {
1126
- void refresh();
1127
- const timer = setInterval(() => {
1128
- void refresh();
1129
- }, POLL_INTERVAL);
1130
- return () => {
1131
- clearInterval(timer);
1132
- };
1133
- }, [refresh]);
1435
+ useInterval(() => void refresh(), POLL_INTERVAL);
1436
+ useInterval(
1437
+ () => {
1438
+ if (overviewInFlightRef.current) return;
1439
+ overviewInFlightRef.current = true;
1440
+ setOverviewLoading(true);
1441
+ void actions.fetchOverview(sessionsState.sessions).then((result) => {
1442
+ setOverviewResult(result);
1443
+ }).catch(() => {
1444
+ }).finally(() => {
1445
+ setOverviewLoading(false);
1446
+ overviewInFlightRef.current = false;
1447
+ });
1448
+ },
1449
+ OVERVIEW_POLL_INTERVAL,
1450
+ !sessionsState.isLoading
1451
+ );
1134
1452
  const resolvedSession = selectedSession ?? sessionsState.sessions[0]?.name;
1135
- const selectedManagedSession = useMemo5(
1453
+ const selectedManagedSession = useMemo7(
1136
1454
  () => sessionsState.sessions.find((s) => s.name === resolvedSession),
1137
1455
  [sessionsState.sessions, resolvedSession]
1138
1456
  );
1139
- const selectedGroup = useMemo5(() => {
1457
+ const selectedGroup = useMemo7(() => {
1140
1458
  if (!selectedManagedSession) return void 0;
1141
1459
  return {
1142
1460
  sessionName: selectedManagedSession.name,
1143
1461
  tabs: selectedManagedSession.groups.flatMap((g) => g.tabs)
1144
1462
  };
1145
1463
  }, [selectedManagedSession]);
1146
- const handleOpenAddSession = useCallback3(() => {
1464
+ const allGroups = useMemo7(
1465
+ () => sessionsState.sessions.flatMap((s) => s.groups),
1466
+ [sessionsState.sessions]
1467
+ );
1468
+ const statusCounts = useMemo7(
1469
+ () => allGroups.flatMap((g) => g.tabs).flatMap((t) => t.panes).filter((p) => p.kind === "claude" && p.claudeStatus).reduce((acc, p) => {
1470
+ const s = p.claudeStatus;
1471
+ if (!s) return acc;
1472
+ acc[s] = (acc[s] ?? 0) + 1;
1473
+ return acc;
1474
+ }, {}),
1475
+ [allGroups]
1476
+ );
1477
+ const handleOpenAddSession = useCallback4(() => {
1147
1478
  setMode(MODE.addSession);
1148
1479
  }, []);
1149
- const handleAddSessionSelect = useCallback3((path) => {
1480
+ const handleAddSessionSelect = useCallback4((path) => {
1150
1481
  const name = basename2(path);
1151
1482
  setSessionsState((prev) => {
1152
1483
  const exists2 = prev.sessions.some((s) => s.name === name);
@@ -1159,14 +1490,14 @@ var ManagerView = ({
1159
1490
  setSelectedSession(name);
1160
1491
  setMode(MODE.split);
1161
1492
  }, []);
1162
- const handleCancelAddSession = useCallback3(() => {
1493
+ const handleCancelAddSession = useCallback4(() => {
1163
1494
  setMode(MODE.split);
1164
1495
  }, []);
1165
- const handleDeleteSession = useCallback3((name) => {
1496
+ const handleDeleteSession = useCallback4((name) => {
1166
1497
  setPendingDeleteSession(name);
1167
1498
  setMode(MODE.deleteSession);
1168
1499
  }, []);
1169
- const handleConfirmDelete = useCallback3(() => {
1500
+ const handleConfirmDelete = useCallback4(() => {
1170
1501
  if (!pendingDeleteSession) return;
1171
1502
  const session = sessionsState.sessions.find((s) => s.name === pendingDeleteSession);
1172
1503
  setSessionsState((prev) => ({
@@ -1185,11 +1516,11 @@ var ManagerView = ({
1185
1516
  setPendingDeleteSession(void 0);
1186
1517
  setMode(MODE.split);
1187
1518
  }, [pendingDeleteSession, resolvedSession, sessionsState.sessions, actions, refresh]);
1188
- const handleCancelDelete = useCallback3(() => {
1519
+ const handleCancelDelete = useCallback4(() => {
1189
1520
  setPendingDeleteSession(void 0);
1190
1521
  setMode(MODE.split);
1191
1522
  }, []);
1192
- const handleNewSession = useCallback3(
1523
+ const handleNewSession = useCallback4(
1193
1524
  (sessionName) => {
1194
1525
  const cwd = selectedManagedSession?.path;
1195
1526
  if (!cwd) return;
@@ -1197,7 +1528,7 @@ var ManagerView = ({
1197
1528
  },
1198
1529
  [actions, selectedManagedSession]
1199
1530
  );
1200
- const handleConfirmNew = useCallback3(() => {
1531
+ const handleConfirmNew = useCallback4(() => {
1201
1532
  if (!resolvedSession) return;
1202
1533
  const cwd = restoredCwd ?? selectedManagedSession?.path;
1203
1534
  if (!cwd) return;
@@ -1205,53 +1536,53 @@ var ManagerView = ({
1205
1536
  setPendingPrompt("");
1206
1537
  setMode(MODE.split);
1207
1538
  }, [resolvedSession, restoredCwd, selectedManagedSession, pendingPrompt, actions, refresh]);
1208
- const handleCancelConfirm = useCallback3(() => {
1539
+ const handleCancelConfirm = useCallback4(() => {
1209
1540
  setPendingPrompt("");
1210
1541
  setMode(MODE.split);
1211
1542
  }, []);
1212
- const handleSessionSelect = useCallback3((name) => {
1543
+ const handleSessionSelect = useCallback4((name) => {
1213
1544
  setSelectedSession(name);
1214
1545
  setFocus(FOCUS.right);
1215
1546
  }, []);
1216
- const handleSessionCursorChange = useCallback3((name) => {
1547
+ const handleSessionCursorChange = useCallback4((name) => {
1217
1548
  setSelectedSession(name);
1218
1549
  }, []);
1219
- const handleNavigate = useCallback3(
1550
+ const handleNavigate = useCallback4(
1220
1551
  (up) => {
1221
1552
  void actions.navigateToPane(up);
1222
1553
  },
1223
1554
  [actions]
1224
1555
  );
1225
- const handleBack = useCallback3(() => {
1556
+ const handleBack = useCallback4(() => {
1226
1557
  setFocus(FOCUS.left);
1227
1558
  }, []);
1228
- const handleKillPane = useCallback3(
1559
+ const handleKillPane = useCallback4(
1229
1560
  async (paneId) => {
1230
1561
  await swallow(() => actions.killPane(paneId));
1231
1562
  void refresh();
1232
1563
  },
1233
1564
  [actions, refresh]
1234
1565
  );
1235
- const handleHighlight = useCallback3(
1566
+ const handleHighlight = useCallback4(
1236
1567
  async (up) => {
1237
1568
  await swallow(() => actions.highlightWindow(up));
1238
1569
  },
1239
1570
  [actions]
1240
1571
  );
1241
- const handleUnhighlight = useCallback3(
1572
+ const handleUnhighlight = useCallback4(
1242
1573
  async (up) => {
1243
1574
  await swallow(() => actions.unhighlightWindow(up));
1244
1575
  },
1245
1576
  [actions]
1246
1577
  );
1247
1578
  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" })
1579
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: rows, children: [
1580
+ /* @__PURE__ */ jsx11(Header, { title: `${APP_TITLE} v${APP_VERSION}` }),
1581
+ /* @__PURE__ */ jsx11(StatusBar, { message: "Loading...", type: "info" })
1251
1582
  ] });
1252
1583
  }
1253
1584
  if (mode === MODE.addSession) {
1254
- return /* @__PURE__ */ jsx10(
1585
+ return /* @__PURE__ */ jsx11(
1255
1586
  DirectorySearchView,
1256
1587
  {
1257
1588
  directories,
@@ -1266,7 +1597,7 @@ var ManagerView = ({
1266
1597
  (sum, g) => sum + g.tabs.reduce((s, t) => s + t.panes.length, 0),
1267
1598
  0
1268
1599
  ) ?? 0;
1269
- return /* @__PURE__ */ jsx10(
1600
+ return /* @__PURE__ */ jsx11(
1270
1601
  DeleteSessionView,
1271
1602
  {
1272
1603
  sessionName: pendingDeleteSession,
@@ -1277,7 +1608,7 @@ var ManagerView = ({
1277
1608
  );
1278
1609
  }
1279
1610
  if (mode === MODE.confirm && pendingPrompt) {
1280
- return /* @__PURE__ */ jsx10(
1611
+ return /* @__PURE__ */ jsx11(
1281
1612
  ConfirmView,
1282
1613
  {
1283
1614
  selectedDir: resolvedSession ?? "",
@@ -1287,26 +1618,29 @@ var ManagerView = ({
1287
1618
  }
1288
1619
  );
1289
1620
  }
1290
- const panelHeight = rows - 5;
1621
+ const fixedRows = 3;
1622
+ const contentHeight = rows - fixedRows;
1623
+ const topHeight = Math.floor(contentHeight / 2);
1624
+ const bottomHeight = contentHeight - topHeight;
1291
1625
  const leftWidth = Math.floor(columns / 3);
1292
1626
  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,
1627
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: rows, children: [
1628
+ /* @__PURE__ */ jsx11(Header, { title: `${APP_TITLE} - v${APP_VERSION}` }),
1629
+ /* @__PURE__ */ jsxs9(Box11, { flexDirection: "row", height: topHeight, children: [
1630
+ /* @__PURE__ */ jsx11(
1631
+ Box11,
1298
1632
  {
1299
1633
  flexDirection: "column",
1300
1634
  width: leftWidth,
1301
1635
  borderStyle: "round",
1302
1636
  borderColor: focus === FOCUS.left ? "green" : "gray",
1303
- children: /* @__PURE__ */ jsx10(
1637
+ children: /* @__PURE__ */ jsx11(
1304
1638
  SessionListPanel,
1305
1639
  {
1306
1640
  sessions: sessionsState.sessions,
1307
1641
  currentSession,
1308
1642
  isFocused: focus === FOCUS.left,
1309
- availableRows: panelHeight,
1643
+ availableRows: topHeight - 2,
1310
1644
  onSelect: handleSessionSelect,
1311
1645
  onCursorChange: handleSessionCursorChange,
1312
1646
  onDeleteSession: handleDeleteSession,
@@ -1315,20 +1649,20 @@ var ManagerView = ({
1315
1649
  )
1316
1650
  }
1317
1651
  ),
1318
- /* @__PURE__ */ jsx10(
1319
- Box10,
1652
+ /* @__PURE__ */ jsx11(
1653
+ Box11,
1320
1654
  {
1321
1655
  flexDirection: "column",
1322
1656
  width: rightWidth,
1323
1657
  borderStyle: "round",
1324
1658
  borderColor: focus === FOCUS.right ? "green" : "gray",
1325
- children: /* @__PURE__ */ jsx10(
1659
+ children: /* @__PURE__ */ jsx11(
1326
1660
  PaneListPanel,
1327
1661
  {
1328
1662
  selectedSession: resolvedSession,
1329
1663
  group: selectedGroup,
1330
1664
  isFocused: focus === FOCUS.right,
1331
- availableRows: panelHeight,
1665
+ availableRows: topHeight - 2,
1332
1666
  onNavigate: handleNavigate,
1333
1667
  onHighlight: handleHighlight,
1334
1668
  onUnhighlight: handleUnhighlight,
@@ -1340,7 +1674,25 @@ var ManagerView = ({
1340
1674
  }
1341
1675
  )
1342
1676
  ] }),
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" })
1677
+ /* @__PURE__ */ jsx11(
1678
+ SessionOverviewPanel,
1679
+ {
1680
+ overallSummary: overviewResult.overallSummary,
1681
+ items: overviewResult.sessions,
1682
+ groups: allGroups,
1683
+ isLoading: overviewLoading,
1684
+ isFocused: focus === FOCUS.bottom,
1685
+ availableRows: bottomHeight,
1686
+ onBack: handleBack
1687
+ }
1688
+ ),
1689
+ /* @__PURE__ */ jsx11(
1690
+ StatusBar,
1691
+ {
1692
+ 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",
1693
+ statusCounts
1694
+ }
1695
+ )
1344
1696
  ] });
1345
1697
  };
1346
1698
 
@@ -1380,6 +1732,10 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
1380
1732
  groups: items.map((item) => item.group)
1381
1733
  }));
1382
1734
  },
1735
+ fetchOverview: async (sessions) => {
1736
+ const groups = sessions.flatMap((s) => s.groups);
1737
+ return await usecases.manager.fetchOverview(groups);
1738
+ },
1383
1739
  createSession: async (sessionName, cwd, prompt) => {
1384
1740
  await usecases.manager.createSession({ sessionName, cwd, prompt });
1385
1741
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmux",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cut0/abmux.git"