abmux 0.0.8 → 0.0.9

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 (3) hide show
  1. package/README.md +100 -24
  2. package/dist/cli/index.js +233 -186
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,14 +1,8 @@
1
1
  # abmux
2
2
 
3
- AI Board on tmux — A terminal UI for managing Claude Code sessions on tmux.
3
+ AI Board on tmux — A TUI for managing multiple Claude Code sessions from a single terminal screen.
4
4
 
5
- ## Features
6
-
7
- - Manage tmux sessions grouped by project
8
- - Detect Claude session status: waiting for input, thinking, running tools, idle
9
- - Launch new Claude Code sessions with prompts via your `$EDITOR`
10
- - Fuzzy search directories to add new sessions
11
- - CLI commands for scripting: `new`, `open`, `kill`, `list`
5
+ Monitor, create, switch between, and delete Claude Code sessions running on tmux, all in one place.
12
6
 
13
7
  ## Requirements
14
8
 
@@ -19,34 +13,116 @@ AI Board on tmux — A terminal UI for managing Claude Code sessions on tmux.
19
13
  ## Install
20
14
 
21
15
  ```sh
22
- npm install -g abmux
23
- # or
24
- yarn global add abmux
25
- # or
26
16
  pnpm add -g abmux
27
17
  ```
28
18
 
29
- ## Usage
19
+ ## Getting Started
30
20
 
31
21
  ```sh
32
- abmux # Start TUI
33
- abmux new <prompt> [--dir <path>] # Create session and launch Claude
34
- abmux open [session] # Attach to session
35
- abmux kill [session] # Kill session
22
+ abmux
23
+ ```
24
+
25
+ Run without arguments to launch the TUI.
26
+
27
+ CLI commands are also available for scripting:
28
+
29
+ ```sh
30
+ abmux new <prompt> [--dir <path>] # Create a session
31
+ abmux open [session] # Attach to a session
32
+ abmux kill [session] # Kill a session
36
33
  abmux list # List sessions
37
- abmux --help # Show help
38
34
  ```
39
35
 
36
+ ## Screen Layout
37
+
38
+ The main screen is split into three panels:
39
+
40
+ ```
41
+ ┌─────────────────────────────────────────────────────────────────┐
42
+ │ abmux - v0.0.x │
43
+ ├───────────────────────┬─────────────────────────────────────────┤
44
+ │ │ │
45
+ │ Session List │ Pane List │
46
+ │ (Left Panel) │ (Right Panel) │
47
+ │ │ │
48
+ │ > my-project (cwd) │ ⠋ [thinking] Refactoring... %5 │
49
+ │ other-project │ ✳ [running] Fixing tests %8 │
50
+ │ │ ○ [idle] Waiting %12 │
51
+ │ │ ● vim %3 │
52
+ │ │ │
53
+ ├───────────────────────┴─────────────────────────────────────────┤
54
+ │ │
55
+ │ Session Overview (Bottom Panel) │
56
+ │ Summaries of what Claude is working on in each session │
57
+ │ │
58
+ ├──────────────────────────────────────────────────────────────────┤
59
+ │ ↑/↓ move Enter select Tab next n add q quit ● 2 thinking │
60
+ └──────────────────────────────────────────────────────────────────┘
61
+ ```
62
+
63
+ Press `Tab` to cycle focus: Left → Right → Bottom. The focused panel is highlighted with a green border.
64
+
65
+ ## Panels
66
+
67
+ ### Left Panel: Session List
68
+
69
+ Lists tmux sessions grouped by project directory. The session matching your current directory is marked with `(cwd)`.
70
+
71
+ | Key | Action |
72
+ | ------------- | ----------------------------------- |
73
+ | `↑` / `↓` | Move cursor |
74
+ | `Enter` / `→` | Select session, move to right panel |
75
+ | `n` | Add session via directory search |
76
+ | `d` | Delete session |
77
+ | `q` | Quit |
78
+
79
+ ### Right Panel: Pane List
80
+
81
+ Shows all panes in the selected session. Claude Code panes display their status; other panes (editors, shells) are also listed.
82
+
83
+ | Key | Action |
84
+ | ----------- | ------------------------------- |
85
+ | `↑` / `↓` | Move cursor |
86
+ | `Enter` | Attach to pane (switch to tmux) |
87
+ | `n` | Create a new Claude session |
88
+ | `v` | Open session in `$EDITOR` |
89
+ | `d` | Kill pane |
90
+ | `Esc` / `←` | Back to left panel |
91
+
92
+ ### Bottom Panel: Session Overview
93
+
94
+ Displays AI-generated summaries of what Claude is doing in each session. Auto-refreshes every 60 seconds.
95
+
96
+ | Key | Action |
97
+ | ----------- | ------------------ |
98
+ | `↑` / `↓` | Scroll |
99
+ | `Tab` | Next panel |
100
+ | `Esc` / `←` | Back to left panel |
101
+
102
+ ## Status Icons
103
+
104
+ The right panel and the status bar show Claude session states with these icons:
105
+
106
+ | Icon | Status | Meaning |
107
+ | ------------- | -------- | ------------------------- |
108
+ | `⠋` (braille) | thinking | Claude is reasoning |
109
+ | `✳` | running | Executing a tool |
110
+ | `❓` | confirm | Waiting for user approval |
111
+ | `❯` | waiting | Ready for input |
112
+ | `○` | idle | Idle |
113
+
114
+ Non-Claude panes show `●` (busy) or `○` (available).
115
+
40
116
  ## Development
41
117
 
42
118
  ```sh
43
119
  pnpm install
44
- pnpm start # Run in development mode (tsx)
45
- pnpm test # Run tests with Vitest
46
- pnpm typecheck # Type check without emitting
47
- pnpm lint:check # Lint with oxlint
48
- pnpm format:check # Check formatting with oxfmt
49
- pnpm build # Bundle with esbuild
120
+ pnpm start # Run in dev mode
121
+ pnpm test # Run tests
122
+ pnpm typecheck # Type check
123
+ pnpm lint:check # Lint
124
+ pnpm format:check # Format check
125
+ pnpm build # Bundle
50
126
  ```
51
127
 
52
128
  ## License
package/dist/cli/index.js CHANGED
@@ -603,7 +603,7 @@ var createUsecases = (context) => ({
603
603
  // package.json
604
604
  var package_default = {
605
605
  name: "abmux",
606
- version: "0.0.8",
606
+ version: "0.0.9",
607
607
  repository: {
608
608
  type: "git",
609
609
  url: "https://github.com/cut0/abmux.git"
@@ -673,8 +673,8 @@ import { createElement } from "react";
673
673
 
674
674
  // src/components/ManagerView.tsx
675
675
  import { basename as basename2 } from "node:path";
676
- import { Box as Box11, useInput as useInput7 } from "ink";
677
- import { useCallback as useCallback5, useMemo as useMemo7, useRef as useRef3, useState as useState7 } from "react";
676
+ import { Box as Box12, useInput as useInput8 } from "ink";
677
+ import { useCallback as useCallback5, useMemo as useMemo7, useRef as useRef4, useState as useState8 } from "react";
678
678
 
679
679
  // src/components/shared/Header.tsx
680
680
  import { Box, Text } from "ink";
@@ -776,11 +776,10 @@ var SessionListPanel = ({
776
776
  onCursorChange,
777
777
  onDeleteSession,
778
778
  onAddSession,
779
- initialCursor,
780
779
  cursorRef
781
780
  }) => {
782
781
  const { exit } = useApp();
783
- const [cursor, setCursor] = useState(initialCursor ?? 0);
782
+ const [cursor, setCursor] = useState(cursorRef?.current ?? 0);
784
783
  const sortedSessions = useMemo3(
785
784
  () => sortSessions(sessions, currentSession),
786
785
  [sessions, currentSession]
@@ -864,6 +863,7 @@ var SessionListPanel = ({
864
863
 
865
864
  // src/components/PaneListPanel.tsx
866
865
  import { Box as Box6, Text as Text6 } from "ink";
866
+ import { useRef as useRef2 } from "react";
867
867
 
868
868
  // src/components/PaneListView.tsx
869
869
  import { Box as Box5, Text as Text5, useApp as useApp2, useInput as useInput2 } from "ink";
@@ -911,12 +911,12 @@ var PaneListView = ({
911
911
  onUnhighlight,
912
912
  onBack,
913
913
  onNewSession,
914
+ onOpenEditor,
914
915
  onKillPane,
915
- initialCursor,
916
916
  cursorRef
917
917
  }) => {
918
918
  const { exit } = useApp2();
919
- const [cursor, setCursor] = useState2(initialCursor ?? 0);
919
+ const [cursor, setCursor] = useState2(cursorRef?.current ?? 0);
920
920
  const highlightedRef = useRef(void 0);
921
921
  const panes = useMemo4(() => group.tabs.flatMap((t) => t.panes), [group]);
922
922
  const clampedCursor = cursor >= panes.length ? Math.max(0, panes.length - 1) : cursor;
@@ -992,6 +992,10 @@ var PaneListView = ({
992
992
  onNewSession(selectedSession);
993
993
  return;
994
994
  }
995
+ if (input === "v") {
996
+ onOpenEditor(selectedSession);
997
+ return;
998
+ }
995
999
  if (key.return) {
996
1000
  const pane = panes[clampedCursor];
997
1001
  if (pane) onNavigate(pane);
@@ -1012,7 +1016,7 @@ var PaneListView = ({
1012
1016
  ")"
1013
1017
  ] })
1014
1018
  ] }),
1015
- /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: panes.length === 0 ? /* @__PURE__ */ jsx5(Box5, { paddingLeft: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No panes. Press n to create." }) }) : visiblePanes.map((up, i) => /* @__PURE__ */ jsx5(
1019
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: panes.length === 0 ? /* @__PURE__ */ jsx5(Box5, { paddingLeft: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No panes. Press n or v to create." }) }) : visiblePanes.map((up, i) => /* @__PURE__ */ jsx5(
1016
1020
  PaneItem,
1017
1021
  {
1018
1022
  unifiedPane: up,
@@ -1035,10 +1039,15 @@ var PaneListPanel = ({
1035
1039
  onUnhighlight,
1036
1040
  onBack,
1037
1041
  onNewSession,
1042
+ onOpenEditor,
1038
1043
  onKillPane,
1039
- initialCursor,
1040
1044
  cursorRef
1041
1045
  }) => {
1046
+ const prevSessionRef = useRef2(selectedSession);
1047
+ if (prevSessionRef.current !== selectedSession) {
1048
+ prevSessionRef.current = selectedSession;
1049
+ if (cursorRef) cursorRef.current = 0;
1050
+ }
1042
1051
  if (!selectedSession) {
1043
1052
  return /* @__PURE__ */ jsx6(Box6, { paddingLeft: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No session selected" }) });
1044
1053
  }
@@ -1054,8 +1063,8 @@ var PaneListPanel = ({
1054
1063
  onUnhighlight,
1055
1064
  onBack,
1056
1065
  onNewSession,
1066
+ onOpenEditor,
1057
1067
  onKillPane,
1058
- initialCursor,
1059
1068
  cursorRef
1060
1069
  },
1061
1070
  selectedSession
@@ -1075,11 +1084,10 @@ var SessionOverviewPanel = ({
1075
1084
  isFocused,
1076
1085
  availableRows,
1077
1086
  onBack,
1078
- initialCursor,
1079
1087
  cursorRef
1080
1088
  }) => {
1081
1089
  const { exit } = useApp3();
1082
- const [cursor, setCursor] = useState3(initialCursor ?? 0);
1090
+ const [cursor, setCursor] = useState3(cursorRef?.current ?? 0);
1083
1091
  const lines = useMemo5(() => {
1084
1092
  const summaryLines = overallSummary ? [
1085
1093
  { key: "summary", type: "summary", text: overallSummary },
@@ -1390,10 +1398,53 @@ var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
1390
1398
  ] });
1391
1399
  };
1392
1400
 
1401
+ // src/components/PromptInputView.tsx
1402
+ import { Box as Box11, Text as Text11, useInput as useInput7 } from "ink";
1403
+ import { useState as useState7 } from "react";
1404
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1405
+ var PromptInputView = ({ selectedDir, onSubmit, onCancel }) => {
1406
+ const { rows } = useTerminalSize();
1407
+ const [value, setValue] = useState7("");
1408
+ useInput7((input, key) => {
1409
+ if (key.escape) {
1410
+ onCancel();
1411
+ return;
1412
+ }
1413
+ if (key.return) {
1414
+ const trimmed = value.trim();
1415
+ if (trimmed) onSubmit(trimmed);
1416
+ return;
1417
+ }
1418
+ if (key.backspace || key.delete) {
1419
+ setValue((prev) => prev.slice(0, -1));
1420
+ return;
1421
+ }
1422
+ if (input && !key.ctrl && !key.meta) {
1423
+ setValue((prev) => prev + input);
1424
+ }
1425
+ });
1426
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: rows, children: [
1427
+ /* @__PURE__ */ jsx11(Header, { title: `${APP_TITLE} \u2014 ${selectedDir}` }),
1428
+ /* @__PURE__ */ jsx11(Box11, { marginBottom: 1, children: /* @__PURE__ */ jsx11(Text11, { bold: true, children: "New prompt:" }) }),
1429
+ /* @__PURE__ */ jsxs9(Box11, { paddingLeft: 1, flexGrow: 1, children: [
1430
+ /* @__PURE__ */ jsxs9(Text11, { color: "green", children: [
1431
+ ">",
1432
+ " "
1433
+ ] }),
1434
+ /* @__PURE__ */ jsx11(Text11, { children: value }),
1435
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: value ? "" : "type your prompt..." })
1436
+ ] }),
1437
+ /* @__PURE__ */ jsxs9(Box11, { gap: 2, children: [
1438
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Enter submit" }),
1439
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Esc cancel" })
1440
+ ] })
1441
+ ] });
1442
+ };
1443
+
1393
1444
  // src/hooks/use-interval.ts
1394
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
1445
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1395
1446
  var useInterval = (fn, intervalMs, enabled = true) => {
1396
- const fnRef = useRef2(fn);
1447
+ const fnRef = useRef3(fn);
1397
1448
  fnRef.current = fn;
1398
1449
  useEffect2(() => {
1399
1450
  if (!enabled) return;
@@ -1416,13 +1467,7 @@ var swallow = async (fn) => {
1416
1467
  };
1417
1468
 
1418
1469
  // src/components/ManagerView.tsx
1419
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1420
- var MODE = {
1421
- split: "split",
1422
- confirm: "confirm",
1423
- deleteSession: "deleteSession",
1424
- addSession: "addSession"
1425
- };
1470
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1426
1471
  var FOCUS = {
1427
1472
  left: "left",
1428
1473
  right: "right",
@@ -1430,95 +1475,91 @@ var FOCUS = {
1430
1475
  };
1431
1476
  var POLL_INTERVAL = 3e3;
1432
1477
  var OVERVIEW_POLL_INTERVAL = 6e4;
1478
+ var FIXED_ROWS = 3;
1479
+ var TOP_HEIGHT_RATIO = 1 / 2;
1480
+ var LEFT_WIDTH_RATIO = 1 / 3;
1481
+ var initState = (remountState) => {
1482
+ const snapshot = remountState?.snapshot;
1483
+ return {
1484
+ view: remountState?.prompt ? { mode: "confirm", prompt: remountState.prompt, cwd: remountState.cwd ?? "" } : { mode: "split" },
1485
+ focus: snapshot?.focus ?? FOCUS.left,
1486
+ selectedSession: remountState?.session ?? snapshot?.selectedSession,
1487
+ sessions: [],
1488
+ sessionsLoading: true,
1489
+ overviewResult: snapshot?.overviewResult
1490
+ };
1491
+ };
1433
1492
  var ManagerView = ({
1434
1493
  actions,
1435
1494
  currentSession,
1436
1495
  directories,
1437
- restoredPrompt,
1438
- restoredSession,
1439
- restoredCwd,
1440
- snapshotRef,
1441
- restoredState
1496
+ remountState,
1497
+ snapshotRef
1442
1498
  }) => {
1443
1499
  const { rows, columns } = useTerminalSize();
1444
- const [sessionsState, setSessionsState] = useState7({
1445
- sessions: [],
1446
- isLoading: true
1500
+ const [state, setState] = useState8(() => initState(remountState));
1501
+ const overviewInFlightRef = useRef4(false);
1502
+ const snapshot = remountState?.snapshot;
1503
+ const cursorsRef = useRef4({
1504
+ session: { current: snapshot?.sessionListCursor ?? 0 },
1505
+ pane: { current: snapshot?.paneListCursor ?? 0 },
1506
+ overview: { current: snapshot?.overviewCursor ?? 0 }
1447
1507
  });
1448
- const [mode, setMode] = useState7(restoredPrompt ? MODE.confirm : MODE.split);
1449
- const [focus, setFocus] = useState7(restoredState?.focus ?? FOCUS.left);
1450
- const [selectedSession, setSelectedSession] = useState7(
1451
- restoredSession ?? restoredState?.selectedSession
1452
- );
1453
- const [pendingPrompt, setPendingPrompt] = useState7(restoredPrompt ?? "");
1454
- const [pendingDeleteSession, setPendingDeleteSession] = useState7(void 0);
1455
- const [overviewResult, setOverviewResult] = useState7(
1456
- restoredState?.overviewResult ?? { overallSummary: "", sessions: [] }
1457
- );
1458
- const [overviewLoading, setOverviewLoading] = useState7(
1459
- restoredState?.overviewResult ? false : true
1460
- );
1461
- const overviewInFlightRef = useRef3(false);
1462
- const sessionCursorRef = useRef3(restoredState?.sessionListCursor ?? 0);
1463
- const paneCursorRef = useRef3(restoredState?.paneListCursor ?? 0);
1464
- const overviewCursorRef = useRef3(restoredState?.overviewCursor ?? 0);
1465
- const paneRestoredRef = useRef3(false);
1508
+ const patch = useCallback5((partial) => {
1509
+ setState((prev) => ({ ...prev, ...partial }));
1510
+ }, []);
1466
1511
  const refresh = useCallback5(async () => {
1467
1512
  try {
1468
1513
  const fetched = await actions.fetchSessions();
1469
- setSessionsState((prev) => {
1514
+ setState((prev) => {
1470
1515
  const fetchedNames = new Set(fetched.map((s) => s.name));
1471
1516
  const userOnly = prev.sessions.filter(
1472
1517
  (s) => !fetchedNames.has(s.name) && s.groups.length === 0
1473
1518
  );
1474
- return { sessions: [...userOnly, ...fetched], isLoading: false };
1519
+ return { ...prev, sessions: [...userOnly, ...fetched], sessionsLoading: false };
1475
1520
  });
1476
1521
  } catch {
1477
- setSessionsState((prev) => ({ ...prev, isLoading: false }));
1522
+ patch({ sessionsLoading: false });
1478
1523
  }
1479
- }, [actions]);
1524
+ }, [actions, patch]);
1480
1525
  useInterval(() => void refresh(), POLL_INTERVAL);
1481
1526
  useInterval(
1482
1527
  () => {
1483
1528
  if (overviewInFlightRef.current) return;
1484
1529
  overviewInFlightRef.current = true;
1485
- void actions.fetchOverview(sessionsState.sessions).then((result) => {
1486
- setOverviewResult(result);
1530
+ void actions.fetchOverview(state.sessions).then((result) => {
1531
+ patch({ overviewResult: result });
1487
1532
  }).catch(() => {
1488
1533
  }).finally(() => {
1489
- setOverviewLoading(false);
1490
1534
  overviewInFlightRef.current = false;
1491
1535
  });
1492
1536
  },
1493
1537
  OVERVIEW_POLL_INTERVAL,
1494
- !sessionsState.isLoading
1538
+ !state.sessionsLoading
1495
1539
  );
1496
- useInput7(
1540
+ useInput8(
1497
1541
  (_input, key) => {
1498
1542
  if (key.tab) {
1499
- setFocus((prev) => {
1500
- if (prev === FOCUS.left) return FOCUS.right;
1501
- if (prev === FOCUS.right) return FOCUS.bottom;
1502
- return FOCUS.left;
1543
+ setState((prev) => {
1544
+ const next = prev.focus === FOCUS.left ? FOCUS.right : prev.focus === FOCUS.right ? FOCUS.bottom : FOCUS.left;
1545
+ return { ...prev, focus: next };
1503
1546
  });
1504
1547
  }
1505
1548
  },
1506
- { isActive: mode === MODE.split }
1549
+ { isActive: state.view.mode === "split" }
1507
1550
  );
1508
- const resolvedSession = selectedSession ?? sessionsState.sessions[0]?.name;
1509
- const paneInitialCursor = !paneRestoredRef.current && restoredState?.selectedSession === resolvedSession ? restoredState?.paneListCursor : void 0;
1510
- if (paneInitialCursor !== void 0) paneRestoredRef.current = true;
1551
+ const resolvedSession = state.selectedSession ?? state.sessions[0]?.name;
1511
1552
  snapshotRef.current = {
1512
- focus,
1553
+ focus: state.focus,
1513
1554
  selectedSession: resolvedSession,
1514
- sessionListCursor: sessionCursorRef.current,
1515
- paneListCursor: paneCursorRef.current,
1516
- overviewCursor: overviewCursorRef.current,
1517
- overviewResult
1555
+ sessionListCursor: cursorsRef.current.session.current,
1556
+ paneListCursor: cursorsRef.current.pane.current,
1557
+ overviewCursor: cursorsRef.current.overview.current,
1558
+ overviewResult: state.overviewResult ?? { overallSummary: "", sessions: [] }
1518
1559
  };
1519
1560
  const selectedManagedSession = useMemo7(
1520
- () => sessionsState.sessions.find((s) => s.name === resolvedSession),
1521
- [sessionsState.sessions, resolvedSession]
1561
+ () => state.sessions.find((s) => s.name === resolvedSession),
1562
+ [state.sessions, resolvedSession]
1522
1563
  );
1523
1564
  const selectedGroup = useMemo7(() => {
1524
1565
  if (!selectedManagedSession) return void 0;
@@ -1527,10 +1568,7 @@ var ManagerView = ({
1527
1568
  tabs: selectedManagedSession.groups.flatMap((g) => g.tabs)
1528
1569
  };
1529
1570
  }, [selectedManagedSession]);
1530
- const allGroups = useMemo7(
1531
- () => sessionsState.sessions.flatMap((s) => s.groups),
1532
- [sessionsState.sessions]
1533
- );
1571
+ const allGroups = useMemo7(() => state.sessions.flatMap((s) => s.groups), [state.sessions]);
1534
1572
  const statusCounts = useMemo7(
1535
1573
  () => allGroups.flatMap((g) => g.tabs).flatMap((t) => t.panes).filter((p) => p.kind === "claude" && p.claudeStatus).reduce((acc, p) => {
1536
1574
  const s = p.claudeStatus;
@@ -1541,52 +1579,53 @@ var ManagerView = ({
1541
1579
  [allGroups]
1542
1580
  );
1543
1581
  const handleOpenAddSession = useCallback5(() => {
1544
- setMode(MODE.addSession);
1545
- }, []);
1582
+ patch({ view: { mode: "addSession" } });
1583
+ }, [patch]);
1546
1584
  const handleAddSessionSelect = useCallback5((path) => {
1547
1585
  const name = basename2(path);
1548
- setSessionsState((prev) => {
1586
+ setState((prev) => {
1549
1587
  const exists2 = prev.sessions.some((s) => s.name === name);
1550
- if (exists2) return prev;
1551
1588
  return {
1552
1589
  ...prev,
1553
- sessions: [{ name, path, groups: [] }, ...prev.sessions]
1590
+ view: { mode: "split" },
1591
+ selectedSession: name,
1592
+ sessions: exists2 ? prev.sessions : [{ name, path, groups: [] }, ...prev.sessions]
1554
1593
  };
1555
1594
  });
1556
- setSelectedSession(name);
1557
- setMode(MODE.split);
1558
1595
  }, []);
1559
1596
  const handleCancelAddSession = useCallback5(() => {
1560
- setMode(MODE.split);
1561
- }, []);
1562
- const handleDeleteSession = useCallback5((name) => {
1563
- setPendingDeleteSession(name);
1564
- setMode(MODE.deleteSession);
1565
- }, []);
1597
+ patch({ view: { mode: "split" } });
1598
+ }, [patch]);
1599
+ const handleDeleteSession = useCallback5(
1600
+ (name) => {
1601
+ patch({ view: { mode: "deleteSession", sessionName: name } });
1602
+ },
1603
+ [patch]
1604
+ );
1566
1605
  const handleConfirmDelete = useCallback5(() => {
1567
- if (!pendingDeleteSession) return;
1568
- const session = sessionsState.sessions.find((s) => s.name === pendingDeleteSession);
1569
- setSessionsState((prev) => ({
1606
+ if (state.view.mode !== "deleteSession") return;
1607
+ const { sessionName } = state.view;
1608
+ const session = state.sessions.find((s) => s.name === sessionName);
1609
+ setState((prev) => ({
1570
1610
  ...prev,
1571
- sessions: prev.sessions.filter((s) => s.name !== pendingDeleteSession)
1611
+ view: { mode: "split" },
1612
+ sessions: prev.sessions.filter((s) => s.name !== sessionName),
1613
+ selectedSession: prev.selectedSession === sessionName ? void 0 : prev.selectedSession
1572
1614
  }));
1573
- if (resolvedSession === pendingDeleteSession) {
1574
- setSelectedSession(void 0);
1575
- }
1576
1615
  if (session) {
1577
1616
  const killAll = Promise.all(
1578
1617
  session.groups.map((g) => swallow(() => actions.killSession(g.sessionName)))
1579
1618
  );
1580
1619
  void killAll.then(() => void refresh());
1581
1620
  }
1582
- setPendingDeleteSession(void 0);
1583
- setMode(MODE.split);
1584
- }, [pendingDeleteSession, resolvedSession, sessionsState.sessions, actions, refresh]);
1621
+ }, [state.view, state.sessions, actions, refresh]);
1585
1622
  const handleCancelDelete = useCallback5(() => {
1586
- setPendingDeleteSession(void 0);
1587
- setMode(MODE.split);
1588
- }, []);
1589
- const handleNewSession = useCallback5(
1623
+ patch({ view: { mode: "split" } });
1624
+ }, [patch]);
1625
+ const handleNewSession = useCallback5(() => {
1626
+ patch({ view: { mode: "promptInput" } });
1627
+ }, [patch]);
1628
+ const handleOpenEditor = useCallback5(
1590
1629
  (sessionName) => {
1591
1630
  const cwd = selectedManagedSession?.path;
1592
1631
  if (!cwd) return;
@@ -1594,28 +1633,41 @@ var ManagerView = ({
1594
1633
  },
1595
1634
  [actions, selectedManagedSession]
1596
1635
  );
1636
+ const handlePromptInputSubmit = useCallback5(
1637
+ (prompt) => {
1638
+ const cwd = selectedManagedSession?.path;
1639
+ if (!cwd) return;
1640
+ patch({ view: { mode: "confirm", prompt, cwd } });
1641
+ },
1642
+ [patch, selectedManagedSession]
1643
+ );
1644
+ const handlePromptInputCancel = useCallback5(() => {
1645
+ patch({ view: { mode: "split" } });
1646
+ }, [patch]);
1597
1647
  const handleConfirmNew = useCallback5(
1598
1648
  ({ worktree }) => {
1649
+ if (state.view.mode !== "confirm") return;
1599
1650
  if (!resolvedSession) return;
1600
- const cwd = restoredCwd ?? selectedManagedSession?.path;
1601
- if (!cwd) return;
1602
- void actions.createSession(resolvedSession, cwd, pendingPrompt, worktree).then(() => void refresh());
1603
- setPendingPrompt("");
1604
- setMode(MODE.split);
1651
+ void actions.createSession(resolvedSession, state.view.cwd, state.view.prompt, worktree).then(() => void refresh());
1652
+ patch({ view: { mode: "split" } });
1605
1653
  },
1606
- [resolvedSession, restoredCwd, selectedManagedSession, pendingPrompt, actions, refresh]
1654
+ [state.view, resolvedSession, actions, refresh, patch]
1607
1655
  );
1608
1656
  const handleCancelConfirm = useCallback5(() => {
1609
- setPendingPrompt("");
1610
- setMode(MODE.split);
1611
- }, []);
1612
- const handleSessionSelect = useCallback5((name) => {
1613
- setSelectedSession(name);
1614
- setFocus(FOCUS.right);
1615
- }, []);
1616
- const handleSessionCursorChange = useCallback5((name) => {
1617
- setSelectedSession(name);
1618
- }, []);
1657
+ patch({ view: { mode: "split" } });
1658
+ }, [patch]);
1659
+ const handleSessionSelect = useCallback5(
1660
+ (name) => {
1661
+ patch({ selectedSession: name, focus: FOCUS.right });
1662
+ },
1663
+ [patch]
1664
+ );
1665
+ const handleSessionCursorChange = useCallback5(
1666
+ (name) => {
1667
+ patch({ selectedSession: name });
1668
+ },
1669
+ [patch]
1670
+ );
1619
1671
  const handleNavigate = useCallback5(
1620
1672
  (up) => {
1621
1673
  void actions.navigateToPane(up);
@@ -1623,8 +1675,8 @@ var ManagerView = ({
1623
1675
  [actions]
1624
1676
  );
1625
1677
  const handleBack = useCallback5(() => {
1626
- setFocus(FOCUS.left);
1627
- }, []);
1678
+ patch({ focus: FOCUS.left });
1679
+ }, [patch]);
1628
1680
  const handleKillPane = useCallback5(
1629
1681
  async (paneId) => {
1630
1682
  await swallow(() => actions.killPane(paneId));
@@ -1644,14 +1696,14 @@ var ManagerView = ({
1644
1696
  },
1645
1697
  [actions]
1646
1698
  );
1647
- if (sessionsState.isLoading) {
1648
- return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: rows, children: [
1649
- /* @__PURE__ */ jsx11(Header, { title: `${APP_TITLE} v${APP_VERSION}` }),
1650
- /* @__PURE__ */ jsx11(StatusBar, { message: "Loading...", type: "info" })
1699
+ if (state.sessionsLoading) {
1700
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", height: rows, children: [
1701
+ /* @__PURE__ */ jsx12(Header, { title: `${APP_TITLE} v${APP_VERSION}` }),
1702
+ /* @__PURE__ */ jsx12(StatusBar, { message: "Loading...", type: "info" })
1651
1703
  ] });
1652
1704
  }
1653
- if (mode === MODE.addSession) {
1654
- return /* @__PURE__ */ jsx11(
1705
+ if (state.view.mode === "addSession") {
1706
+ return /* @__PURE__ */ jsx12(
1655
1707
  DirectorySearchView,
1656
1708
  {
1657
1709
  directories,
@@ -1660,111 +1712,119 @@ var ManagerView = ({
1660
1712
  }
1661
1713
  );
1662
1714
  }
1663
- if (mode === MODE.deleteSession && pendingDeleteSession) {
1664
- const deleteSession = sessionsState.sessions.find((s) => s.name === pendingDeleteSession);
1715
+ if (state.view.mode === "deleteSession") {
1716
+ const { sessionName } = state.view;
1717
+ const deleteSession = state.sessions.find((s) => s.name === sessionName);
1665
1718
  const paneCount = deleteSession?.groups.reduce(
1666
1719
  (sum, g) => sum + g.tabs.reduce((s, t) => s + t.panes.length, 0),
1667
1720
  0
1668
1721
  ) ?? 0;
1669
- return /* @__PURE__ */ jsx11(
1722
+ return /* @__PURE__ */ jsx12(
1670
1723
  DeleteSessionView,
1671
1724
  {
1672
- sessionName: pendingDeleteSession,
1725
+ sessionName,
1673
1726
  paneCount,
1674
1727
  onConfirm: handleConfirmDelete,
1675
1728
  onCancel: handleCancelDelete
1676
1729
  }
1677
1730
  );
1678
1731
  }
1679
- if (mode === MODE.confirm && pendingPrompt) {
1680
- return /* @__PURE__ */ jsx11(
1732
+ if (state.view.mode === "promptInput") {
1733
+ return /* @__PURE__ */ jsx12(
1734
+ PromptInputView,
1735
+ {
1736
+ selectedDir: resolvedSession ?? "",
1737
+ onSubmit: handlePromptInputSubmit,
1738
+ onCancel: handlePromptInputCancel
1739
+ }
1740
+ );
1741
+ }
1742
+ if (state.view.mode === "confirm") {
1743
+ return /* @__PURE__ */ jsx12(
1681
1744
  ConfirmView,
1682
1745
  {
1683
1746
  selectedDir: resolvedSession ?? "",
1684
- prompt: pendingPrompt,
1747
+ prompt: state.view.prompt,
1685
1748
  onConfirm: handleConfirmNew,
1686
1749
  onCancel: handleCancelConfirm
1687
1750
  }
1688
1751
  );
1689
1752
  }
1690
- const fixedRows = 3;
1691
- const contentHeight = rows - fixedRows;
1692
- const topHeight = Math.floor(contentHeight / 2);
1753
+ const contentHeight = rows - FIXED_ROWS;
1754
+ const topHeight = Math.floor(contentHeight * TOP_HEIGHT_RATIO);
1693
1755
  const bottomHeight = contentHeight - topHeight;
1694
- const leftWidth = Math.floor(columns / 3);
1756
+ const leftWidth = Math.floor(columns * LEFT_WIDTH_RATIO);
1695
1757
  const rightWidth = columns - leftWidth;
1696
- return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: rows, children: [
1697
- /* @__PURE__ */ jsx11(Header, { title: `${APP_TITLE} - v${APP_VERSION}` }),
1698
- /* @__PURE__ */ jsxs9(Box11, { flexDirection: "row", height: topHeight, children: [
1699
- /* @__PURE__ */ jsx11(
1700
- Box11,
1758
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", height: rows, children: [
1759
+ /* @__PURE__ */ jsx12(Header, { title: `${APP_TITLE} - v${APP_VERSION}` }),
1760
+ /* @__PURE__ */ jsxs10(Box12, { flexDirection: "row", height: topHeight, children: [
1761
+ /* @__PURE__ */ jsx12(
1762
+ Box12,
1701
1763
  {
1702
1764
  flexDirection: "column",
1703
1765
  width: leftWidth,
1704
1766
  borderStyle: "round",
1705
- borderColor: focus === FOCUS.left ? "green" : "gray",
1706
- children: /* @__PURE__ */ jsx11(
1767
+ borderColor: state.focus === FOCUS.left ? "green" : "gray",
1768
+ children: /* @__PURE__ */ jsx12(
1707
1769
  SessionListPanel,
1708
1770
  {
1709
- sessions: sessionsState.sessions,
1771
+ sessions: state.sessions,
1710
1772
  currentSession,
1711
- isFocused: focus === FOCUS.left,
1773
+ isFocused: state.focus === FOCUS.left,
1712
1774
  availableRows: topHeight - 2,
1713
1775
  onSelect: handleSessionSelect,
1714
1776
  onCursorChange: handleSessionCursorChange,
1715
1777
  onDeleteSession: handleDeleteSession,
1716
1778
  onAddSession: handleOpenAddSession,
1717
- initialCursor: restoredState?.sessionListCursor,
1718
- cursorRef: sessionCursorRef
1779
+ cursorRef: cursorsRef.current.session
1719
1780
  }
1720
1781
  )
1721
1782
  }
1722
1783
  ),
1723
- /* @__PURE__ */ jsx11(
1724
- Box11,
1784
+ /* @__PURE__ */ jsx12(
1785
+ Box12,
1725
1786
  {
1726
1787
  flexDirection: "column",
1727
1788
  width: rightWidth,
1728
1789
  borderStyle: "round",
1729
- borderColor: focus === FOCUS.right ? "green" : "gray",
1730
- children: /* @__PURE__ */ jsx11(
1790
+ borderColor: state.focus === FOCUS.right ? "green" : "gray",
1791
+ children: /* @__PURE__ */ jsx12(
1731
1792
  PaneListPanel,
1732
1793
  {
1733
1794
  selectedSession: resolvedSession,
1734
1795
  group: selectedGroup,
1735
- isFocused: focus === FOCUS.right,
1796
+ isFocused: state.focus === FOCUS.right,
1736
1797
  availableRows: topHeight - 2,
1737
1798
  onNavigate: handleNavigate,
1738
1799
  onHighlight: handleHighlight,
1739
1800
  onUnhighlight: handleUnhighlight,
1740
1801
  onBack: handleBack,
1741
1802
  onNewSession: handleNewSession,
1803
+ onOpenEditor: handleOpenEditor,
1742
1804
  onKillPane: handleKillPane,
1743
- initialCursor: paneInitialCursor,
1744
- cursorRef: paneCursorRef
1805
+ cursorRef: cursorsRef.current.pane
1745
1806
  }
1746
1807
  )
1747
1808
  }
1748
1809
  )
1749
1810
  ] }),
1750
- /* @__PURE__ */ jsx11(
1811
+ /* @__PURE__ */ jsx12(
1751
1812
  SessionOverviewPanel,
1752
1813
  {
1753
- overallSummary: overviewResult.overallSummary,
1754
- items: overviewResult.sessions,
1814
+ overallSummary: state.overviewResult?.overallSummary ?? "",
1815
+ items: state.overviewResult?.sessions ?? [],
1755
1816
  groups: allGroups,
1756
- isLoading: overviewLoading,
1757
- isFocused: focus === FOCUS.bottom,
1817
+ isLoading: !state.overviewResult,
1818
+ isFocused: state.focus === FOCUS.bottom,
1758
1819
  availableRows: bottomHeight,
1759
1820
  onBack: handleBack,
1760
- initialCursor: restoredState?.overviewCursor,
1761
- cursorRef: overviewCursorRef
1821
+ cursorRef: cursorsRef.current.overview
1762
1822
  }
1763
1823
  ),
1764
- /* @__PURE__ */ jsx11(
1824
+ /* @__PURE__ */ jsx12(
1765
1825
  StatusBar,
1766
1826
  {
1767
- message: focus === FOCUS.left ? "\u2191/\u2193 move Enter/\u2192 select Tab next n add d delete q quit" : focus === FOCUS.right ? "\u2191/\u2193 move Enter focus Tab next n new d kill Esc/\u2190 back q quit" : "\u2191/\u2193 scroll Tab next Esc/\u2190 back q quit",
1827
+ message: state.focus === FOCUS.left ? "\u2191/\u2193 move Enter/\u2192 select Tab next n add d delete q quit" : state.focus === FOCUS.right ? "\u2191/\u2193 move Enter focus Tab next n new v vim d kill Esc/\u2190 back q quit" : "\u2191/\u2193 scroll Tab next Esc/\u2190 back q quit",
1768
1828
  statusCounts
1769
1829
  }
1770
1830
  )
@@ -1775,11 +1835,8 @@ var ManagerView = ({
1775
1835
  var createTuiCommand = ({ usecases, services, infra }) => async () => {
1776
1836
  const directories = await services.directoryScan.scan();
1777
1837
  let instance;
1778
- let pendingPrompt;
1779
- let pendingSession;
1780
- let pendingCwd;
1781
1838
  const snapshotRef = { current: void 0 };
1782
- let restoredState;
1839
+ let remountState;
1783
1840
  const actions = {
1784
1841
  fetchSessions: async () => {
1785
1842
  const result = await usecases.manager.list();
@@ -1829,12 +1886,10 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
1829
1886
  await usecases.manager.unhighlightWindow(up);
1830
1887
  },
1831
1888
  openEditor: (sessionName, cwd) => {
1832
- restoredState = snapshotRef.current;
1889
+ const snapshot = snapshotRef.current;
1833
1890
  instance.unmount();
1834
1891
  const prompt = infra.editor.open();
1835
- pendingPrompt = prompt;
1836
- pendingSession = sessionName;
1837
- pendingCwd = cwd;
1892
+ remountState = { prompt, session: sessionName, cwd, snapshot };
1838
1893
  instance = renderApp();
1839
1894
  return prompt;
1840
1895
  },
@@ -1842,21 +1897,16 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
1842
1897
  const target = `${up.pane.sessionName}:${String(up.pane.windowIndex)}`;
1843
1898
  await infra.tmuxCli.selectWindow(target);
1844
1899
  await infra.tmuxCli.selectPane(up.pane.paneId);
1845
- restoredState = snapshotRef.current;
1900
+ const snapshot = snapshotRef.current;
1846
1901
  instance.unmount();
1847
1902
  await infra.tmuxCli.attachSession(up.pane.sessionName);
1903
+ remountState = { snapshot };
1848
1904
  instance = renderApp();
1849
1905
  }
1850
1906
  };
1851
1907
  const renderApp = () => {
1852
- const prompt = pendingPrompt;
1853
- const session = pendingSession;
1854
- const cwd = pendingCwd;
1855
- const snapshot = restoredState;
1856
- pendingPrompt = void 0;
1857
- pendingSession = void 0;
1858
- pendingCwd = void 0;
1859
- restoredState = void 0;
1908
+ const state = remountState;
1909
+ remountState = void 0;
1860
1910
  snapshotRef.current = void 0;
1861
1911
  const rawCwd = process.cwd();
1862
1912
  const currentSession = basename3(findMatchingDirectory(rawCwd, directories) ?? rawCwd);
@@ -1865,11 +1915,8 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
1865
1915
  actions,
1866
1916
  currentSession,
1867
1917
  directories,
1868
- restoredPrompt: prompt,
1869
- restoredSession: session,
1870
- restoredCwd: cwd,
1871
- snapshotRef,
1872
- restoredState: snapshot
1918
+ remountState: state,
1919
+ snapshotRef
1873
1920
  }),
1874
1921
  { concurrent: true }
1875
1922
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmux",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cut0/abmux.git"