abmux 0.0.7 → 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.
- package/README.md +100 -24
- package/dist/cli/index.js +315 -182
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
# abmux
|
|
2
2
|
|
|
3
|
-
AI Board on tmux — A
|
|
3
|
+
AI Board on tmux — A TUI for managing multiple Claude Code sessions from a single terminal screen.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
19
|
+
## Getting Started
|
|
30
20
|
|
|
31
21
|
```sh
|
|
32
|
-
abmux
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
45
|
-
pnpm test # Run tests
|
|
46
|
-
pnpm typecheck # Type check
|
|
47
|
-
pnpm lint:check # Lint
|
|
48
|
-
pnpm format:check #
|
|
49
|
-
pnpm build # Bundle
|
|
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
|
@@ -532,7 +532,12 @@ var createManagerUsecase = (context) => {
|
|
|
532
532
|
const { tmux, sessionDetection } = context.services;
|
|
533
533
|
const windowTarget = (up) => `${up.pane.sessionName}:${String(up.pane.windowIndex)}`;
|
|
534
534
|
return {
|
|
535
|
-
createSession: async ({
|
|
535
|
+
createSession: async ({
|
|
536
|
+
sessionName,
|
|
537
|
+
cwd,
|
|
538
|
+
prompt,
|
|
539
|
+
worktree
|
|
540
|
+
}) => {
|
|
536
541
|
const exists2 = await context.infra.tmuxCli.hasSession(sessionName);
|
|
537
542
|
if (!exists2) {
|
|
538
543
|
await context.infra.tmuxCli.newSession({ name: sessionName, cwd });
|
|
@@ -544,7 +549,8 @@ var createManagerUsecase = (context) => {
|
|
|
544
549
|
const target = sessionPanes[0]?.paneId;
|
|
545
550
|
if (!target) return;
|
|
546
551
|
const escapedPrompt = escapeShellArg(prompt);
|
|
547
|
-
|
|
552
|
+
const worktreeFlag = worktree ? " -w" : "";
|
|
553
|
+
await tmux.sendCommand({ target, command: `claude${worktreeFlag} -- ${escapedPrompt}` });
|
|
548
554
|
},
|
|
549
555
|
list: async () => {
|
|
550
556
|
const panes = await tmux.listPanes();
|
|
@@ -597,7 +603,7 @@ var createUsecases = (context) => ({
|
|
|
597
603
|
// package.json
|
|
598
604
|
var package_default = {
|
|
599
605
|
name: "abmux",
|
|
600
|
-
version: "0.0.
|
|
606
|
+
version: "0.0.9",
|
|
601
607
|
repository: {
|
|
602
608
|
type: "git",
|
|
603
609
|
url: "https://github.com/cut0/abmux.git"
|
|
@@ -667,8 +673,8 @@ import { createElement } from "react";
|
|
|
667
673
|
|
|
668
674
|
// src/components/ManagerView.tsx
|
|
669
675
|
import { basename as basename2 } from "node:path";
|
|
670
|
-
import { Box as
|
|
671
|
-
import { useCallback as
|
|
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";
|
|
672
678
|
|
|
673
679
|
// src/components/shared/Header.tsx
|
|
674
680
|
import { Box, Text } from "ink";
|
|
@@ -769,10 +775,11 @@ var SessionListPanel = ({
|
|
|
769
775
|
onSelect,
|
|
770
776
|
onCursorChange,
|
|
771
777
|
onDeleteSession,
|
|
772
|
-
onAddSession
|
|
778
|
+
onAddSession,
|
|
779
|
+
cursorRef
|
|
773
780
|
}) => {
|
|
774
781
|
const { exit } = useApp();
|
|
775
|
-
const [cursor, setCursor] = useState(0);
|
|
782
|
+
const [cursor, setCursor] = useState(cursorRef?.current ?? 0);
|
|
776
783
|
const sortedSessions = useMemo3(
|
|
777
784
|
() => sortSessions(sessions, currentSession),
|
|
778
785
|
[sessions, currentSession]
|
|
@@ -782,6 +789,7 @@ var SessionListPanel = ({
|
|
|
782
789
|
if (clampedCursor !== cursor) {
|
|
783
790
|
setCursor(clampedCursor);
|
|
784
791
|
}
|
|
792
|
+
if (cursorRef) cursorRef.current = clampedCursor;
|
|
785
793
|
const reservedLines = 1;
|
|
786
794
|
const { scrollOffset, visibleCount } = useScroll(
|
|
787
795
|
clampedCursor,
|
|
@@ -855,6 +863,7 @@ var SessionListPanel = ({
|
|
|
855
863
|
|
|
856
864
|
// src/components/PaneListPanel.tsx
|
|
857
865
|
import { Box as Box6, Text as Text6 } from "ink";
|
|
866
|
+
import { useRef as useRef2 } from "react";
|
|
858
867
|
|
|
859
868
|
// src/components/PaneListView.tsx
|
|
860
869
|
import { Box as Box5, Text as Text5, useApp as useApp2, useInput as useInput2 } from "ink";
|
|
@@ -902,16 +911,19 @@ var PaneListView = ({
|
|
|
902
911
|
onUnhighlight,
|
|
903
912
|
onBack,
|
|
904
913
|
onNewSession,
|
|
905
|
-
|
|
914
|
+
onOpenEditor,
|
|
915
|
+
onKillPane,
|
|
916
|
+
cursorRef
|
|
906
917
|
}) => {
|
|
907
918
|
const { exit } = useApp2();
|
|
908
|
-
const [cursor, setCursor] = useState2(0);
|
|
919
|
+
const [cursor, setCursor] = useState2(cursorRef?.current ?? 0);
|
|
909
920
|
const highlightedRef = useRef(void 0);
|
|
910
921
|
const panes = useMemo4(() => group.tabs.flatMap((t) => t.panes), [group]);
|
|
911
922
|
const clampedCursor = cursor >= panes.length ? Math.max(0, panes.length - 1) : cursor;
|
|
912
923
|
if (clampedCursor !== cursor) {
|
|
913
924
|
setCursor(clampedCursor);
|
|
914
925
|
}
|
|
926
|
+
if (cursorRef) cursorRef.current = clampedCursor;
|
|
915
927
|
const reservedLines = 1;
|
|
916
928
|
const { scrollOffset, visibleCount } = useScroll(
|
|
917
929
|
clampedCursor,
|
|
@@ -980,6 +992,10 @@ var PaneListView = ({
|
|
|
980
992
|
onNewSession(selectedSession);
|
|
981
993
|
return;
|
|
982
994
|
}
|
|
995
|
+
if (input === "v") {
|
|
996
|
+
onOpenEditor(selectedSession);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
983
999
|
if (key.return) {
|
|
984
1000
|
const pane = panes[clampedCursor];
|
|
985
1001
|
if (pane) onNavigate(pane);
|
|
@@ -1000,7 +1016,7 @@ var PaneListView = ({
|
|
|
1000
1016
|
")"
|
|
1001
1017
|
] })
|
|
1002
1018
|
] }),
|
|
1003
|
-
/* @__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(
|
|
1004
1020
|
PaneItem,
|
|
1005
1021
|
{
|
|
1006
1022
|
unifiedPane: up,
|
|
@@ -1023,8 +1039,15 @@ var PaneListPanel = ({
|
|
|
1023
1039
|
onUnhighlight,
|
|
1024
1040
|
onBack,
|
|
1025
1041
|
onNewSession,
|
|
1026
|
-
|
|
1042
|
+
onOpenEditor,
|
|
1043
|
+
onKillPane,
|
|
1044
|
+
cursorRef
|
|
1027
1045
|
}) => {
|
|
1046
|
+
const prevSessionRef = useRef2(selectedSession);
|
|
1047
|
+
if (prevSessionRef.current !== selectedSession) {
|
|
1048
|
+
prevSessionRef.current = selectedSession;
|
|
1049
|
+
if (cursorRef) cursorRef.current = 0;
|
|
1050
|
+
}
|
|
1028
1051
|
if (!selectedSession) {
|
|
1029
1052
|
return /* @__PURE__ */ jsx6(Box6, { paddingLeft: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No session selected" }) });
|
|
1030
1053
|
}
|
|
@@ -1040,7 +1063,9 @@ var PaneListPanel = ({
|
|
|
1040
1063
|
onUnhighlight,
|
|
1041
1064
|
onBack,
|
|
1042
1065
|
onNewSession,
|
|
1043
|
-
|
|
1066
|
+
onOpenEditor,
|
|
1067
|
+
onKillPane,
|
|
1068
|
+
cursorRef
|
|
1044
1069
|
},
|
|
1045
1070
|
selectedSession
|
|
1046
1071
|
);
|
|
@@ -1058,10 +1083,11 @@ var SessionOverviewPanel = ({
|
|
|
1058
1083
|
isLoading,
|
|
1059
1084
|
isFocused,
|
|
1060
1085
|
availableRows,
|
|
1061
|
-
onBack
|
|
1086
|
+
onBack,
|
|
1087
|
+
cursorRef
|
|
1062
1088
|
}) => {
|
|
1063
1089
|
const { exit } = useApp3();
|
|
1064
|
-
const [cursor, setCursor] = useState3(0);
|
|
1090
|
+
const [cursor, setCursor] = useState3(cursorRef?.current ?? 0);
|
|
1065
1091
|
const lines = useMemo5(() => {
|
|
1066
1092
|
const summaryLines = overallSummary ? [
|
|
1067
1093
|
{ key: "summary", type: "summary", text: overallSummary },
|
|
@@ -1094,6 +1120,7 @@ var SessionOverviewPanel = ({
|
|
|
1094
1120
|
return [...summaryLines, ...sessionLines];
|
|
1095
1121
|
}, [overallSummary, items, groups]);
|
|
1096
1122
|
const clampedCursor = cursor >= lines.length ? Math.max(0, lines.length - 1) : cursor;
|
|
1123
|
+
if (cursorRef) cursorRef.current = clampedCursor;
|
|
1097
1124
|
const reservedLines = 3;
|
|
1098
1125
|
const { scrollOffset, visibleCount } = useScroll(
|
|
1099
1126
|
clampedCursor,
|
|
@@ -1170,6 +1197,7 @@ var SessionOverviewPanel = ({
|
|
|
1170
1197
|
|
|
1171
1198
|
// src/components/ConfirmView.tsx
|
|
1172
1199
|
import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
|
|
1200
|
+
import { useCallback as useCallback4, useState as useState5 } from "react";
|
|
1173
1201
|
|
|
1174
1202
|
// src/hooks/use-terminal-size.ts
|
|
1175
1203
|
import { useStdout } from "ink";
|
|
@@ -1199,20 +1227,35 @@ var useTerminalSize = () => {
|
|
|
1199
1227
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1200
1228
|
var ConfirmView = ({ selectedDir, prompt, onConfirm, onCancel }) => {
|
|
1201
1229
|
const { rows } = useTerminalSize();
|
|
1230
|
+
const [worktree, setWorktree] = useState5(true);
|
|
1202
1231
|
const previewLines = prompt.split("\n");
|
|
1203
|
-
const maxPreview = Math.min(previewLines.length, rows -
|
|
1232
|
+
const maxPreview = Math.min(previewLines.length, rows - 7);
|
|
1233
|
+
const handleToggleWorktree = useCallback4(() => {
|
|
1234
|
+
setWorktree((prev) => !prev);
|
|
1235
|
+
}, []);
|
|
1204
1236
|
useInput4((_input, key) => {
|
|
1205
1237
|
if (key.return) {
|
|
1206
|
-
onConfirm();
|
|
1238
|
+
onConfirm({ worktree });
|
|
1207
1239
|
return;
|
|
1208
1240
|
}
|
|
1209
1241
|
if (key.escape) {
|
|
1210
1242
|
onCancel();
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (key.tab) {
|
|
1246
|
+
handleToggleWorktree();
|
|
1211
1247
|
}
|
|
1212
1248
|
});
|
|
1213
1249
|
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", height: rows, children: [
|
|
1214
1250
|
/* @__PURE__ */ jsx8(Header, { title: `${APP_TITLE} \u2014 ${selectedDir}` }),
|
|
1215
|
-
/* @__PURE__ */
|
|
1251
|
+
/* @__PURE__ */ jsxs6(Box8, { marginBottom: 1, gap: 2, children: [
|
|
1252
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "New Claude session:" }),
|
|
1253
|
+
/* @__PURE__ */ jsxs6(Text8, { color: worktree ? "green" : "gray", children: [
|
|
1254
|
+
"worktree: [",
|
|
1255
|
+
worktree ? "ON" : "OFF",
|
|
1256
|
+
"]"
|
|
1257
|
+
] })
|
|
1258
|
+
] }),
|
|
1216
1259
|
/* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingLeft: 2, children: [
|
|
1217
1260
|
previewLines.slice(0, maxPreview).map((line, i) => /* @__PURE__ */ jsx8(Text8, { color: "white", children: line }, i)),
|
|
1218
1261
|
previewLines.length > maxPreview && /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
|
|
@@ -1223,6 +1266,7 @@ var ConfirmView = ({ selectedDir, prompt, onConfirm, onCancel }) => {
|
|
|
1223
1266
|
] }),
|
|
1224
1267
|
/* @__PURE__ */ jsxs6(Box8, { gap: 2, children: [
|
|
1225
1268
|
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Enter confirm" }),
|
|
1269
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Tab worktree" }),
|
|
1226
1270
|
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc cancel" })
|
|
1227
1271
|
] })
|
|
1228
1272
|
] });
|
|
@@ -1266,7 +1310,7 @@ var DeleteSessionView = ({ sessionName, paneCount, onConfirm, onCancel }) => {
|
|
|
1266
1310
|
|
|
1267
1311
|
// src/components/DirectorySearchView.tsx
|
|
1268
1312
|
import { Box as Box10, Text as Text10, useInput as useInput6 } from "ink";
|
|
1269
|
-
import { useMemo as useMemo6, useState as
|
|
1313
|
+
import { useMemo as useMemo6, useState as useState6 } from "react";
|
|
1270
1314
|
import { basename } from "node:path";
|
|
1271
1315
|
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1272
1316
|
var formatPath = (path) => {
|
|
@@ -1278,8 +1322,8 @@ var formatPath = (path) => {
|
|
|
1278
1322
|
};
|
|
1279
1323
|
var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
|
|
1280
1324
|
const { rows } = useTerminalSize();
|
|
1281
|
-
const [query, setQuery] =
|
|
1282
|
-
const [cursor, setCursor] =
|
|
1325
|
+
const [query, setQuery] = useState6("");
|
|
1326
|
+
const [cursor, setCursor] = useState6(0);
|
|
1283
1327
|
const filtered = useMemo6(() => {
|
|
1284
1328
|
if (!query) return directories;
|
|
1285
1329
|
const lower = query.toLowerCase();
|
|
@@ -1354,10 +1398,53 @@ var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
|
|
|
1354
1398
|
] });
|
|
1355
1399
|
};
|
|
1356
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
|
+
|
|
1357
1444
|
// src/hooks/use-interval.ts
|
|
1358
|
-
import { useEffect as useEffect2, useRef as
|
|
1445
|
+
import { useEffect as useEffect2, useRef as useRef3 } from "react";
|
|
1359
1446
|
var useInterval = (fn, intervalMs, enabled = true) => {
|
|
1360
|
-
const fnRef =
|
|
1447
|
+
const fnRef = useRef3(fn);
|
|
1361
1448
|
fnRef.current = fn;
|
|
1362
1449
|
useEffect2(() => {
|
|
1363
1450
|
if (!enabled) return;
|
|
@@ -1380,13 +1467,7 @@ var swallow = async (fn) => {
|
|
|
1380
1467
|
};
|
|
1381
1468
|
|
|
1382
1469
|
// src/components/ManagerView.tsx
|
|
1383
|
-
import { jsx as
|
|
1384
|
-
var MODE = {
|
|
1385
|
-
split: "split",
|
|
1386
|
-
confirm: "confirm",
|
|
1387
|
-
deleteSession: "deleteSession",
|
|
1388
|
-
addSession: "addSession"
|
|
1389
|
-
};
|
|
1470
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1390
1471
|
var FOCUS = {
|
|
1391
1472
|
left: "left",
|
|
1392
1473
|
right: "right",
|
|
@@ -1394,65 +1475,91 @@ var FOCUS = {
|
|
|
1394
1475
|
};
|
|
1395
1476
|
var POLL_INTERVAL = 3e3;
|
|
1396
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
|
+
};
|
|
1397
1492
|
var ManagerView = ({
|
|
1398
1493
|
actions,
|
|
1399
1494
|
currentSession,
|
|
1400
1495
|
directories,
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
restoredCwd
|
|
1496
|
+
remountState,
|
|
1497
|
+
snapshotRef
|
|
1404
1498
|
}) => {
|
|
1405
1499
|
const { rows, columns } = useTerminalSize();
|
|
1406
|
-
const [
|
|
1407
|
-
|
|
1408
|
-
|
|
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 }
|
|
1409
1507
|
});
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const
|
|
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 () => {
|
|
1508
|
+
const patch = useCallback5((partial) => {
|
|
1509
|
+
setState((prev) => ({ ...prev, ...partial }));
|
|
1510
|
+
}, []);
|
|
1511
|
+
const refresh = useCallback5(async () => {
|
|
1422
1512
|
try {
|
|
1423
1513
|
const fetched = await actions.fetchSessions();
|
|
1424
|
-
|
|
1514
|
+
setState((prev) => {
|
|
1425
1515
|
const fetchedNames = new Set(fetched.map((s) => s.name));
|
|
1426
1516
|
const userOnly = prev.sessions.filter(
|
|
1427
1517
|
(s) => !fetchedNames.has(s.name) && s.groups.length === 0
|
|
1428
1518
|
);
|
|
1429
|
-
return { sessions: [...userOnly, ...fetched],
|
|
1519
|
+
return { ...prev, sessions: [...userOnly, ...fetched], sessionsLoading: false };
|
|
1430
1520
|
});
|
|
1431
1521
|
} catch {
|
|
1432
|
-
|
|
1522
|
+
patch({ sessionsLoading: false });
|
|
1433
1523
|
}
|
|
1434
|
-
}, [actions]);
|
|
1524
|
+
}, [actions, patch]);
|
|
1435
1525
|
useInterval(() => void refresh(), POLL_INTERVAL);
|
|
1436
1526
|
useInterval(
|
|
1437
1527
|
() => {
|
|
1438
1528
|
if (overviewInFlightRef.current) return;
|
|
1439
1529
|
overviewInFlightRef.current = true;
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
setOverviewResult(result);
|
|
1530
|
+
void actions.fetchOverview(state.sessions).then((result) => {
|
|
1531
|
+
patch({ overviewResult: result });
|
|
1443
1532
|
}).catch(() => {
|
|
1444
1533
|
}).finally(() => {
|
|
1445
|
-
setOverviewLoading(false);
|
|
1446
1534
|
overviewInFlightRef.current = false;
|
|
1447
1535
|
});
|
|
1448
1536
|
},
|
|
1449
1537
|
OVERVIEW_POLL_INTERVAL,
|
|
1450
|
-
!
|
|
1538
|
+
!state.sessionsLoading
|
|
1451
1539
|
);
|
|
1452
|
-
|
|
1540
|
+
useInput8(
|
|
1541
|
+
(_input, key) => {
|
|
1542
|
+
if (key.tab) {
|
|
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 };
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
{ isActive: state.view.mode === "split" }
|
|
1550
|
+
);
|
|
1551
|
+
const resolvedSession = state.selectedSession ?? state.sessions[0]?.name;
|
|
1552
|
+
snapshotRef.current = {
|
|
1553
|
+
focus: state.focus,
|
|
1554
|
+
selectedSession: resolvedSession,
|
|
1555
|
+
sessionListCursor: cursorsRef.current.session.current,
|
|
1556
|
+
paneListCursor: cursorsRef.current.pane.current,
|
|
1557
|
+
overviewCursor: cursorsRef.current.overview.current,
|
|
1558
|
+
overviewResult: state.overviewResult ?? { overallSummary: "", sessions: [] }
|
|
1559
|
+
};
|
|
1453
1560
|
const selectedManagedSession = useMemo7(
|
|
1454
|
-
() =>
|
|
1455
|
-
[
|
|
1561
|
+
() => state.sessions.find((s) => s.name === resolvedSession),
|
|
1562
|
+
[state.sessions, resolvedSession]
|
|
1456
1563
|
);
|
|
1457
1564
|
const selectedGroup = useMemo7(() => {
|
|
1458
1565
|
if (!selectedManagedSession) return void 0;
|
|
@@ -1461,10 +1568,7 @@ var ManagerView = ({
|
|
|
1461
1568
|
tabs: selectedManagedSession.groups.flatMap((g) => g.tabs)
|
|
1462
1569
|
};
|
|
1463
1570
|
}, [selectedManagedSession]);
|
|
1464
|
-
const allGroups = useMemo7(
|
|
1465
|
-
() => sessionsState.sessions.flatMap((s) => s.groups),
|
|
1466
|
-
[sessionsState.sessions]
|
|
1467
|
-
);
|
|
1571
|
+
const allGroups = useMemo7(() => state.sessions.flatMap((s) => s.groups), [state.sessions]);
|
|
1468
1572
|
const statusCounts = useMemo7(
|
|
1469
1573
|
() => allGroups.flatMap((g) => g.tabs).flatMap((t) => t.panes).filter((p) => p.kind === "claude" && p.claudeStatus).reduce((acc, p) => {
|
|
1470
1574
|
const s = p.claudeStatus;
|
|
@@ -1474,53 +1578,54 @@ var ManagerView = ({
|
|
|
1474
1578
|
}, {}),
|
|
1475
1579
|
[allGroups]
|
|
1476
1580
|
);
|
|
1477
|
-
const handleOpenAddSession =
|
|
1478
|
-
|
|
1479
|
-
}, []);
|
|
1480
|
-
const handleAddSessionSelect =
|
|
1581
|
+
const handleOpenAddSession = useCallback5(() => {
|
|
1582
|
+
patch({ view: { mode: "addSession" } });
|
|
1583
|
+
}, [patch]);
|
|
1584
|
+
const handleAddSessionSelect = useCallback5((path) => {
|
|
1481
1585
|
const name = basename2(path);
|
|
1482
|
-
|
|
1586
|
+
setState((prev) => {
|
|
1483
1587
|
const exists2 = prev.sessions.some((s) => s.name === name);
|
|
1484
|
-
if (exists2) return prev;
|
|
1485
1588
|
return {
|
|
1486
1589
|
...prev,
|
|
1487
|
-
|
|
1590
|
+
view: { mode: "split" },
|
|
1591
|
+
selectedSession: name,
|
|
1592
|
+
sessions: exists2 ? prev.sessions : [{ name, path, groups: [] }, ...prev.sessions]
|
|
1488
1593
|
};
|
|
1489
1594
|
});
|
|
1490
|
-
setSelectedSession(name);
|
|
1491
|
-
setMode(MODE.split);
|
|
1492
|
-
}, []);
|
|
1493
|
-
const handleCancelAddSession = useCallback4(() => {
|
|
1494
|
-
setMode(MODE.split);
|
|
1495
|
-
}, []);
|
|
1496
|
-
const handleDeleteSession = useCallback4((name) => {
|
|
1497
|
-
setPendingDeleteSession(name);
|
|
1498
|
-
setMode(MODE.deleteSession);
|
|
1499
1595
|
}, []);
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1596
|
+
const handleCancelAddSession = useCallback5(() => {
|
|
1597
|
+
patch({ view: { mode: "split" } });
|
|
1598
|
+
}, [patch]);
|
|
1599
|
+
const handleDeleteSession = useCallback5(
|
|
1600
|
+
(name) => {
|
|
1601
|
+
patch({ view: { mode: "deleteSession", sessionName: name } });
|
|
1602
|
+
},
|
|
1603
|
+
[patch]
|
|
1604
|
+
);
|
|
1605
|
+
const handleConfirmDelete = useCallback5(() => {
|
|
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) => ({
|
|
1504
1610
|
...prev,
|
|
1505
|
-
|
|
1611
|
+
view: { mode: "split" },
|
|
1612
|
+
sessions: prev.sessions.filter((s) => s.name !== sessionName),
|
|
1613
|
+
selectedSession: prev.selectedSession === sessionName ? void 0 : prev.selectedSession
|
|
1506
1614
|
}));
|
|
1507
|
-
if (resolvedSession === pendingDeleteSession) {
|
|
1508
|
-
setSelectedSession(void 0);
|
|
1509
|
-
}
|
|
1510
1615
|
if (session) {
|
|
1511
1616
|
const killAll = Promise.all(
|
|
1512
1617
|
session.groups.map((g) => swallow(() => actions.killSession(g.sessionName)))
|
|
1513
1618
|
);
|
|
1514
1619
|
void killAll.then(() => void refresh());
|
|
1515
1620
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}, []);
|
|
1523
|
-
const
|
|
1621
|
+
}, [state.view, state.sessions, actions, refresh]);
|
|
1622
|
+
const handleCancelDelete = useCallback5(() => {
|
|
1623
|
+
patch({ view: { mode: "split" } });
|
|
1624
|
+
}, [patch]);
|
|
1625
|
+
const handleNewSession = useCallback5(() => {
|
|
1626
|
+
patch({ view: { mode: "promptInput" } });
|
|
1627
|
+
}, [patch]);
|
|
1628
|
+
const handleOpenEditor = useCallback5(
|
|
1524
1629
|
(sessionName) => {
|
|
1525
1630
|
const cwd = selectedManagedSession?.path;
|
|
1526
1631
|
if (!cwd) return;
|
|
@@ -1528,61 +1633,77 @@ var ManagerView = ({
|
|
|
1528
1633
|
},
|
|
1529
1634
|
[actions, selectedManagedSession]
|
|
1530
1635
|
);
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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]);
|
|
1647
|
+
const handleConfirmNew = useCallback5(
|
|
1648
|
+
({ worktree }) => {
|
|
1649
|
+
if (state.view.mode !== "confirm") return;
|
|
1650
|
+
if (!resolvedSession) return;
|
|
1651
|
+
void actions.createSession(resolvedSession, state.view.cwd, state.view.prompt, worktree).then(() => void refresh());
|
|
1652
|
+
patch({ view: { mode: "split" } });
|
|
1653
|
+
},
|
|
1654
|
+
[state.view, resolvedSession, actions, refresh, patch]
|
|
1655
|
+
);
|
|
1656
|
+
const handleCancelConfirm = useCallback5(() => {
|
|
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
|
+
);
|
|
1671
|
+
const handleNavigate = useCallback5(
|
|
1551
1672
|
(up) => {
|
|
1552
1673
|
void actions.navigateToPane(up);
|
|
1553
1674
|
},
|
|
1554
1675
|
[actions]
|
|
1555
1676
|
);
|
|
1556
|
-
const handleBack =
|
|
1557
|
-
|
|
1558
|
-
}, []);
|
|
1559
|
-
const handleKillPane =
|
|
1677
|
+
const handleBack = useCallback5(() => {
|
|
1678
|
+
patch({ focus: FOCUS.left });
|
|
1679
|
+
}, [patch]);
|
|
1680
|
+
const handleKillPane = useCallback5(
|
|
1560
1681
|
async (paneId) => {
|
|
1561
1682
|
await swallow(() => actions.killPane(paneId));
|
|
1562
1683
|
void refresh();
|
|
1563
1684
|
},
|
|
1564
1685
|
[actions, refresh]
|
|
1565
1686
|
);
|
|
1566
|
-
const handleHighlight =
|
|
1687
|
+
const handleHighlight = useCallback5(
|
|
1567
1688
|
async (up) => {
|
|
1568
1689
|
await swallow(() => actions.highlightWindow(up));
|
|
1569
1690
|
},
|
|
1570
1691
|
[actions]
|
|
1571
1692
|
);
|
|
1572
|
-
const handleUnhighlight =
|
|
1693
|
+
const handleUnhighlight = useCallback5(
|
|
1573
1694
|
async (up) => {
|
|
1574
1695
|
await swallow(() => actions.unhighlightWindow(up));
|
|
1575
1696
|
},
|
|
1576
1697
|
[actions]
|
|
1577
1698
|
);
|
|
1578
|
-
if (
|
|
1579
|
-
return /* @__PURE__ */
|
|
1580
|
-
/* @__PURE__ */
|
|
1581
|
-
/* @__PURE__ */
|
|
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" })
|
|
1582
1703
|
] });
|
|
1583
1704
|
}
|
|
1584
|
-
if (mode ===
|
|
1585
|
-
return /* @__PURE__ */
|
|
1705
|
+
if (state.view.mode === "addSession") {
|
|
1706
|
+
return /* @__PURE__ */ jsx12(
|
|
1586
1707
|
DirectorySearchView,
|
|
1587
1708
|
{
|
|
1588
1709
|
directories,
|
|
@@ -1591,105 +1712,119 @@ var ManagerView = ({
|
|
|
1591
1712
|
}
|
|
1592
1713
|
);
|
|
1593
1714
|
}
|
|
1594
|
-
if (mode ===
|
|
1595
|
-
const
|
|
1715
|
+
if (state.view.mode === "deleteSession") {
|
|
1716
|
+
const { sessionName } = state.view;
|
|
1717
|
+
const deleteSession = state.sessions.find((s) => s.name === sessionName);
|
|
1596
1718
|
const paneCount = deleteSession?.groups.reduce(
|
|
1597
1719
|
(sum, g) => sum + g.tabs.reduce((s, t) => s + t.panes.length, 0),
|
|
1598
1720
|
0
|
|
1599
1721
|
) ?? 0;
|
|
1600
|
-
return /* @__PURE__ */
|
|
1722
|
+
return /* @__PURE__ */ jsx12(
|
|
1601
1723
|
DeleteSessionView,
|
|
1602
1724
|
{
|
|
1603
|
-
sessionName
|
|
1725
|
+
sessionName,
|
|
1604
1726
|
paneCount,
|
|
1605
1727
|
onConfirm: handleConfirmDelete,
|
|
1606
1728
|
onCancel: handleCancelDelete
|
|
1607
1729
|
}
|
|
1608
1730
|
);
|
|
1609
1731
|
}
|
|
1610
|
-
if (mode ===
|
|
1611
|
-
return /* @__PURE__ */
|
|
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(
|
|
1612
1744
|
ConfirmView,
|
|
1613
1745
|
{
|
|
1614
1746
|
selectedDir: resolvedSession ?? "",
|
|
1615
|
-
prompt:
|
|
1747
|
+
prompt: state.view.prompt,
|
|
1616
1748
|
onConfirm: handleConfirmNew,
|
|
1617
1749
|
onCancel: handleCancelConfirm
|
|
1618
1750
|
}
|
|
1619
1751
|
);
|
|
1620
1752
|
}
|
|
1621
|
-
const
|
|
1622
|
-
const
|
|
1623
|
-
const topHeight = Math.floor(contentHeight / 2);
|
|
1753
|
+
const contentHeight = rows - FIXED_ROWS;
|
|
1754
|
+
const topHeight = Math.floor(contentHeight * TOP_HEIGHT_RATIO);
|
|
1624
1755
|
const bottomHeight = contentHeight - topHeight;
|
|
1625
|
-
const leftWidth = Math.floor(columns
|
|
1756
|
+
const leftWidth = Math.floor(columns * LEFT_WIDTH_RATIO);
|
|
1626
1757
|
const rightWidth = columns - leftWidth;
|
|
1627
|
-
return /* @__PURE__ */
|
|
1628
|
-
/* @__PURE__ */
|
|
1629
|
-
/* @__PURE__ */
|
|
1630
|
-
/* @__PURE__ */
|
|
1631
|
-
|
|
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,
|
|
1632
1763
|
{
|
|
1633
1764
|
flexDirection: "column",
|
|
1634
1765
|
width: leftWidth,
|
|
1635
1766
|
borderStyle: "round",
|
|
1636
|
-
borderColor: focus === FOCUS.left ? "green" : "gray",
|
|
1637
|
-
children: /* @__PURE__ */
|
|
1767
|
+
borderColor: state.focus === FOCUS.left ? "green" : "gray",
|
|
1768
|
+
children: /* @__PURE__ */ jsx12(
|
|
1638
1769
|
SessionListPanel,
|
|
1639
1770
|
{
|
|
1640
|
-
sessions:
|
|
1771
|
+
sessions: state.sessions,
|
|
1641
1772
|
currentSession,
|
|
1642
|
-
isFocused: focus === FOCUS.left,
|
|
1773
|
+
isFocused: state.focus === FOCUS.left,
|
|
1643
1774
|
availableRows: topHeight - 2,
|
|
1644
1775
|
onSelect: handleSessionSelect,
|
|
1645
1776
|
onCursorChange: handleSessionCursorChange,
|
|
1646
1777
|
onDeleteSession: handleDeleteSession,
|
|
1647
|
-
onAddSession: handleOpenAddSession
|
|
1778
|
+
onAddSession: handleOpenAddSession,
|
|
1779
|
+
cursorRef: cursorsRef.current.session
|
|
1648
1780
|
}
|
|
1649
1781
|
)
|
|
1650
1782
|
}
|
|
1651
1783
|
),
|
|
1652
|
-
/* @__PURE__ */
|
|
1653
|
-
|
|
1784
|
+
/* @__PURE__ */ jsx12(
|
|
1785
|
+
Box12,
|
|
1654
1786
|
{
|
|
1655
1787
|
flexDirection: "column",
|
|
1656
1788
|
width: rightWidth,
|
|
1657
1789
|
borderStyle: "round",
|
|
1658
|
-
borderColor: focus === FOCUS.right ? "green" : "gray",
|
|
1659
|
-
children: /* @__PURE__ */
|
|
1790
|
+
borderColor: state.focus === FOCUS.right ? "green" : "gray",
|
|
1791
|
+
children: /* @__PURE__ */ jsx12(
|
|
1660
1792
|
PaneListPanel,
|
|
1661
1793
|
{
|
|
1662
1794
|
selectedSession: resolvedSession,
|
|
1663
1795
|
group: selectedGroup,
|
|
1664
|
-
isFocused: focus === FOCUS.right,
|
|
1796
|
+
isFocused: state.focus === FOCUS.right,
|
|
1665
1797
|
availableRows: topHeight - 2,
|
|
1666
1798
|
onNavigate: handleNavigate,
|
|
1667
1799
|
onHighlight: handleHighlight,
|
|
1668
1800
|
onUnhighlight: handleUnhighlight,
|
|
1669
1801
|
onBack: handleBack,
|
|
1670
1802
|
onNewSession: handleNewSession,
|
|
1671
|
-
|
|
1803
|
+
onOpenEditor: handleOpenEditor,
|
|
1804
|
+
onKillPane: handleKillPane,
|
|
1805
|
+
cursorRef: cursorsRef.current.pane
|
|
1672
1806
|
}
|
|
1673
1807
|
)
|
|
1674
1808
|
}
|
|
1675
1809
|
)
|
|
1676
1810
|
] }),
|
|
1677
|
-
/* @__PURE__ */
|
|
1811
|
+
/* @__PURE__ */ jsx12(
|
|
1678
1812
|
SessionOverviewPanel,
|
|
1679
1813
|
{
|
|
1680
|
-
overallSummary: overviewResult
|
|
1681
|
-
items: overviewResult
|
|
1814
|
+
overallSummary: state.overviewResult?.overallSummary ?? "",
|
|
1815
|
+
items: state.overviewResult?.sessions ?? [],
|
|
1682
1816
|
groups: allGroups,
|
|
1683
|
-
isLoading:
|
|
1684
|
-
isFocused: focus === FOCUS.bottom,
|
|
1817
|
+
isLoading: !state.overviewResult,
|
|
1818
|
+
isFocused: state.focus === FOCUS.bottom,
|
|
1685
1819
|
availableRows: bottomHeight,
|
|
1686
|
-
onBack: handleBack
|
|
1820
|
+
onBack: handleBack,
|
|
1821
|
+
cursorRef: cursorsRef.current.overview
|
|
1687
1822
|
}
|
|
1688
1823
|
),
|
|
1689
|
-
/* @__PURE__ */
|
|
1824
|
+
/* @__PURE__ */ jsx12(
|
|
1690
1825
|
StatusBar,
|
|
1691
1826
|
{
|
|
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",
|
|
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",
|
|
1693
1828
|
statusCounts
|
|
1694
1829
|
}
|
|
1695
1830
|
)
|
|
@@ -1700,9 +1835,8 @@ var ManagerView = ({
|
|
|
1700
1835
|
var createTuiCommand = ({ usecases, services, infra }) => async () => {
|
|
1701
1836
|
const directories = await services.directoryScan.scan();
|
|
1702
1837
|
let instance;
|
|
1703
|
-
|
|
1704
|
-
let
|
|
1705
|
-
let pendingCwd;
|
|
1838
|
+
const snapshotRef = { current: void 0 };
|
|
1839
|
+
let remountState;
|
|
1706
1840
|
const actions = {
|
|
1707
1841
|
fetchSessions: async () => {
|
|
1708
1842
|
const result = await usecases.manager.list();
|
|
@@ -1736,8 +1870,8 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
|
|
|
1736
1870
|
const groups = sessions.flatMap((s) => s.groups);
|
|
1737
1871
|
return await usecases.manager.fetchOverview(groups);
|
|
1738
1872
|
},
|
|
1739
|
-
createSession: async (sessionName, cwd, prompt) => {
|
|
1740
|
-
await usecases.manager.createSession({ sessionName, cwd, prompt });
|
|
1873
|
+
createSession: async (sessionName, cwd, prompt, worktree) => {
|
|
1874
|
+
await usecases.manager.createSession({ sessionName, cwd, prompt, worktree });
|
|
1741
1875
|
},
|
|
1742
1876
|
killSession: async (sessionName) => {
|
|
1743
1877
|
await usecases.manager.killSession(sessionName);
|
|
@@ -1752,11 +1886,10 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
|
|
|
1752
1886
|
await usecases.manager.unhighlightWindow(up);
|
|
1753
1887
|
},
|
|
1754
1888
|
openEditor: (sessionName, cwd) => {
|
|
1889
|
+
const snapshot = snapshotRef.current;
|
|
1755
1890
|
instance.unmount();
|
|
1756
1891
|
const prompt = infra.editor.open();
|
|
1757
|
-
|
|
1758
|
-
pendingSession = sessionName;
|
|
1759
|
-
pendingCwd = cwd;
|
|
1892
|
+
remountState = { prompt, session: sessionName, cwd, snapshot };
|
|
1760
1893
|
instance = renderApp();
|
|
1761
1894
|
return prompt;
|
|
1762
1895
|
},
|
|
@@ -1764,18 +1897,17 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
|
|
|
1764
1897
|
const target = `${up.pane.sessionName}:${String(up.pane.windowIndex)}`;
|
|
1765
1898
|
await infra.tmuxCli.selectWindow(target);
|
|
1766
1899
|
await infra.tmuxCli.selectPane(up.pane.paneId);
|
|
1900
|
+
const snapshot = snapshotRef.current;
|
|
1767
1901
|
instance.unmount();
|
|
1768
1902
|
await infra.tmuxCli.attachSession(up.pane.sessionName);
|
|
1903
|
+
remountState = { snapshot };
|
|
1769
1904
|
instance = renderApp();
|
|
1770
1905
|
}
|
|
1771
1906
|
};
|
|
1772
1907
|
const renderApp = () => {
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
pendingPrompt = void 0;
|
|
1777
|
-
pendingSession = void 0;
|
|
1778
|
-
pendingCwd = void 0;
|
|
1908
|
+
const state = remountState;
|
|
1909
|
+
remountState = void 0;
|
|
1910
|
+
snapshotRef.current = void 0;
|
|
1779
1911
|
const rawCwd = process.cwd();
|
|
1780
1912
|
const currentSession = basename3(findMatchingDirectory(rawCwd, directories) ?? rawCwd);
|
|
1781
1913
|
return render(
|
|
@@ -1783,9 +1915,8 @@ var createTuiCommand = ({ usecases, services, infra }) => async () => {
|
|
|
1783
1915
|
actions,
|
|
1784
1916
|
currentSession,
|
|
1785
1917
|
directories,
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
restoredCwd: cwd
|
|
1918
|
+
remountState: state,
|
|
1919
|
+
snapshotRef
|
|
1789
1920
|
}),
|
|
1790
1921
|
{ concurrent: true }
|
|
1791
1922
|
);
|
|
@@ -1801,18 +1932,20 @@ var createNewCommand = ({ usecases }) => async (args) => {
|
|
|
1801
1932
|
const { values, positionals } = parseArgs({
|
|
1802
1933
|
args,
|
|
1803
1934
|
options: {
|
|
1804
|
-
dir: { type: "string" }
|
|
1935
|
+
dir: { type: "string" },
|
|
1936
|
+
worktree: { type: "boolean", default: true }
|
|
1805
1937
|
},
|
|
1806
1938
|
allowPositionals: true
|
|
1807
1939
|
});
|
|
1808
1940
|
const prompt = positionals[0];
|
|
1809
1941
|
if (!prompt) {
|
|
1810
|
-
console.error("Usage: abmux new <prompt> [--dir <path>]");
|
|
1942
|
+
console.error("Usage: abmux new <prompt> [--dir <path>] [--no-worktree]");
|
|
1811
1943
|
process.exit(1);
|
|
1812
1944
|
}
|
|
1813
1945
|
const dir = values.dir ?? process.cwd();
|
|
1814
1946
|
const sessionName = basename4(dir);
|
|
1815
|
-
|
|
1947
|
+
const worktree = values.worktree ?? true;
|
|
1948
|
+
await usecases.manager.createSession({ sessionName, cwd: dir, prompt, worktree });
|
|
1816
1949
|
console.log(`Session "${sessionName}" created.`);
|
|
1817
1950
|
};
|
|
1818
1951
|
|