abmux 0.0.1

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 +54 -0
  2. package/dist/cli/index.js +1514 -0
  3. package/package.json +47 -0
@@ -0,0 +1,1514 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/infra/tmux-cli.ts
4
+ import { execFile, execFileSync, spawnSync } from "node:child_process";
5
+ var resolveTmuxPath = () => {
6
+ try {
7
+ return execFileSync("which", ["tmux"], { encoding: "utf-8" }).trim();
8
+ } catch {
9
+ return "tmux";
10
+ }
11
+ };
12
+ var tmuxPath = resolveTmuxPath();
13
+ var DELIMITER = " ";
14
+ var PANE_FORMAT = [
15
+ "#{session_name}",
16
+ "#{window_index}",
17
+ "#{pane_index}",
18
+ "#{pane_id}",
19
+ "#{pane_current_path}",
20
+ "#{pane_title}",
21
+ "#{window_name}",
22
+ "#{pane_active}",
23
+ "#{pane_width}",
24
+ "#{pane_height}"
25
+ ].join(DELIMITER);
26
+ var execTmux = (args) => new Promise((resolve, reject) => {
27
+ execFile(tmuxPath, args, (error, stdout, stderr) => {
28
+ if (error) {
29
+ reject(new Error(`tmux ${args[0]} failed: ${stderr || error.message}`));
30
+ return;
31
+ }
32
+ resolve(stdout.trim());
33
+ });
34
+ });
35
+ var parsePaneLine = (line) => {
36
+ const parts = line.split(DELIMITER);
37
+ if (parts.length < 10) return void 0;
38
+ return {
39
+ sessionName: parts[0] ?? "",
40
+ windowIndex: parseInt(parts[1] ?? "0", 10),
41
+ paneIndex: parseInt(parts[2] ?? "0", 10),
42
+ paneId: parts[3] ?? "",
43
+ cwd: parts[4] ?? "",
44
+ title: parts[5] ?? "",
45
+ windowName: parts[6] ?? "",
46
+ isActive: parts[7] === "1",
47
+ paneWidth: parseInt(parts[8] ?? "0", 10),
48
+ paneHeight: parseInt(parts[9] ?? "0", 10)
49
+ };
50
+ };
51
+ var createTmuxCli = () => ({
52
+ listPanes: async () => {
53
+ try {
54
+ const output = await execTmux(["list-panes", "-a", "-F", PANE_FORMAT]);
55
+ if (!output) return [];
56
+ return output.split("\n").map(parsePaneLine).filter((p) => p !== void 0);
57
+ } catch {
58
+ return [];
59
+ }
60
+ },
61
+ newSession: async (input) => {
62
+ const args = ["new-session", "-d", "-s", input.name, "-P", "-F", "#{session_name}"];
63
+ if (input.cwd) {
64
+ args.push("-c", input.cwd);
65
+ }
66
+ return await execTmux(args);
67
+ },
68
+ newWindow: async (input) => {
69
+ const args = ["new-window", "-t", input.target];
70
+ if (input.cwd) {
71
+ args.push("-c", input.cwd);
72
+ }
73
+ if (input.command) {
74
+ args.push(...input.command);
75
+ }
76
+ await execTmux(args);
77
+ },
78
+ splitWindow: async (input) => {
79
+ const flag = input.direction === "h" ? "-h" : "-v";
80
+ const args = ["split-window", flag, "-t", input.target];
81
+ if (input.cwd) {
82
+ args.push("-c", input.cwd);
83
+ }
84
+ await execTmux(args);
85
+ },
86
+ sendKeys: async (input) => {
87
+ await execTmux(["send-keys", "-t", input.target, input.keys, "Enter"]);
88
+ },
89
+ capturePane: async (target) => {
90
+ return await execTmux(["capture-pane", "-t", target, "-p"]);
91
+ },
92
+ selectPane: async (target) => {
93
+ await execTmux(["select-pane", "-t", target]);
94
+ },
95
+ selectWindow: async (target) => {
96
+ await execTmux(["select-window", "-t", target]);
97
+ },
98
+ renameWindow: async (input) => {
99
+ await execTmux(["rename-window", "-t", input.target, input.name]);
100
+ },
101
+ attachSession: async (sessionName) => {
102
+ spawnSync(tmuxPath, ["attach-session", "-t", sessionName], {
103
+ stdio: "inherit"
104
+ });
105
+ },
106
+ killPane: async (target) => {
107
+ await execTmux(["kill-pane", "-t", target]);
108
+ },
109
+ killSession: async (sessionName) => {
110
+ await execTmux(["kill-session", "-t", sessionName]);
111
+ },
112
+ hasSession: async (name) => {
113
+ try {
114
+ await execTmux(["has-session", "-t", name]);
115
+ return true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+ });
121
+
122
+ // src/infra/editor.ts
123
+ import { spawnSync as spawnSync2 } from "node:child_process";
124
+ import { writeFileSync, readFileSync, unlinkSync, mkdtempSync } from "node:fs";
125
+ import { join } from "node:path";
126
+ import { tmpdir } from "node:os";
127
+ var createEditor = () => ({
128
+ open: () => {
129
+ const dir = mkdtempSync(join(tmpdir(), "abmux-"));
130
+ const filePath = join(dir, "PROMPT.md");
131
+ writeFileSync(filePath, "", "utf-8");
132
+ const editor = process.env["EDITOR"] ?? "vim";
133
+ const result = spawnSync2(editor, [filePath], {
134
+ stdio: "inherit"
135
+ });
136
+ if (result.status !== 0) return void 0;
137
+ try {
138
+ const content = readFileSync(filePath, "utf-8").trim();
139
+ unlinkSync(filePath);
140
+ return content || void 0;
141
+ } catch {
142
+ return void 0;
143
+ }
144
+ }
145
+ });
146
+
147
+ // src/infra/index.ts
148
+ var createInfra = () => ({
149
+ tmuxCli: createTmuxCli(),
150
+ editor: createEditor()
151
+ });
152
+
153
+ // src/models/session.ts
154
+ var SESSION_STATUS = {
155
+ waitingInput: "waiting-input",
156
+ waitingConfirm: "waiting-confirm",
157
+ thinking: "thinking",
158
+ toolRunning: "tool-running",
159
+ idle: "idle"
160
+ };
161
+ var SESSION_STATUS_LABEL = {
162
+ [SESSION_STATUS.waitingInput]: "waiting",
163
+ [SESSION_STATUS.waitingConfirm]: "confirm",
164
+ [SESSION_STATUS.thinking]: "thinking",
165
+ [SESSION_STATUS.toolRunning]: "running",
166
+ [SESSION_STATUS.idle]: "idle"
167
+ };
168
+ var SESSION_STATUS_COLOR = {
169
+ [SESSION_STATUS.waitingInput]: "green",
170
+ [SESSION_STATUS.waitingConfirm]: "yellow",
171
+ [SESSION_STATUS.thinking]: "blue",
172
+ [SESSION_STATUS.toolRunning]: "magenta",
173
+ [SESSION_STATUS.idle]: "gray"
174
+ };
175
+ var PANE_KIND = {
176
+ claude: "claude",
177
+ available: "available",
178
+ busy: "busy"
179
+ };
180
+
181
+ // src/services/session-detection-service.ts
182
+ var BUSY_TITLES = /* @__PURE__ */ new Set([
183
+ "nvim",
184
+ "vim",
185
+ "vi",
186
+ "node",
187
+ "python",
188
+ "python3",
189
+ "ruby",
190
+ "cargo",
191
+ "go"
192
+ ]);
193
+ var isClaudePrefix = (char) => {
194
+ if (char === "\u2733") return true;
195
+ const code = char.charCodeAt(0);
196
+ return code >= 10240 && code <= 10495;
197
+ };
198
+ var classifyPane = (pane) => {
199
+ const firstChar = pane.title.charAt(0);
200
+ if (isClaudePrefix(firstChar)) return PANE_KIND.claude;
201
+ if (BUSY_TITLES.has(pane.title)) return PANE_KIND.busy;
202
+ return PANE_KIND.available;
203
+ };
204
+ var detectStatusFromTitle = (title) => {
205
+ const firstChar = title.charAt(0);
206
+ if (firstChar === "\u2733") return SESSION_STATUS.toolRunning;
207
+ const code = firstChar.charCodeAt(0);
208
+ if (code >= 10240 && code <= 10495) return SESSION_STATUS.thinking;
209
+ return SESSION_STATUS.idle;
210
+ };
211
+ var detectStatusFromText = (paneText) => {
212
+ const lines = paneText.split("\n");
213
+ const lastLines = lines.slice(-20);
214
+ const joined = lastLines.join("\n");
215
+ if (/Do you want to proceed\?|Esc to cancel/.test(joined)) {
216
+ return SESSION_STATUS.waitingConfirm;
217
+ }
218
+ if (/Running…/.test(joined)) {
219
+ return SESSION_STATUS.toolRunning;
220
+ }
221
+ if (/ろーでぃんぐ…|Thinking|⏳/.test(joined)) {
222
+ return SESSION_STATUS.thinking;
223
+ }
224
+ const trimmedLines = lastLines.map((l) => l.trim()).filter((l) => l.length > 0);
225
+ const lastNonEmpty = trimmedLines[trimmedLines.length - 1] ?? "";
226
+ if (/^❯\s*$/.test(lastNonEmpty) || /-- INSERT --/.test(joined)) {
227
+ return SESSION_STATUS.waitingInput;
228
+ }
229
+ return SESSION_STATUS.idle;
230
+ };
231
+ var formatCwd = (cwd) => {
232
+ const home = process.env["HOME"] ?? "";
233
+ if (home && cwd.startsWith(home)) {
234
+ return `~${cwd.slice(home.length)}`;
235
+ }
236
+ return cwd;
237
+ };
238
+ var toUnifiedPane = (pane) => {
239
+ const kind = classifyPane(pane);
240
+ if (kind === PANE_KIND.claude) {
241
+ return {
242
+ pane,
243
+ kind,
244
+ claudeStatus: detectStatusFromTitle(pane.title),
245
+ claudeTitle: pane.title.slice(2)
246
+ };
247
+ }
248
+ return { pane, kind };
249
+ };
250
+ var createSessionDetectionService = () => ({
251
+ groupBySession: ({ panes }) => {
252
+ const windowKey = (pane) => `${pane.sessionName}:${String(pane.windowIndex)}`;
253
+ const windowMap = /* @__PURE__ */ new Map();
254
+ for (const pane of panes) {
255
+ const key = windowKey(pane);
256
+ const unified = toUnifiedPane(pane);
257
+ const existing = windowMap.get(key);
258
+ if (existing) {
259
+ existing.panes = [...existing.panes, unified];
260
+ if (pane.isActive) {
261
+ existing.activePaneTitle = pane.title;
262
+ }
263
+ } else {
264
+ windowMap.set(key, {
265
+ sessionName: pane.sessionName,
266
+ windowIndex: pane.windowIndex,
267
+ cwd: formatCwd(pane.cwd),
268
+ windowName: pane.windowName,
269
+ activePaneTitle: pane.isActive ? pane.title : "",
270
+ panes: [unified]
271
+ });
272
+ }
273
+ }
274
+ const windowGroups = [...windowMap.entries()].toSorted(([a], [b]) => a.localeCompare(b)).map(([, group]) => ({
275
+ windowIndex: group.windowIndex,
276
+ windowName: group.windowName || group.activePaneTitle || `Window ${String(group.windowIndex)}`,
277
+ sessionName: group.sessionName,
278
+ panes: group.panes.toSorted((a, b) => {
279
+ const kindOrder = { claude: 0, available: 1, busy: 2 };
280
+ const kindDiff = kindOrder[a.kind] - kindOrder[b.kind];
281
+ if (kindDiff !== 0) return kindDiff;
282
+ if (a.kind === "claude" && b.kind === "claude") {
283
+ const statusOrder = {
284
+ "waiting-confirm": 0,
285
+ "waiting-input": 1,
286
+ thinking: 2,
287
+ "tool-running": 3,
288
+ idle: 4
289
+ };
290
+ const sa = statusOrder[a.claudeStatus ?? "idle"] ?? 4;
291
+ const sb = statusOrder[b.claudeStatus ?? "idle"] ?? 4;
292
+ if (sa !== sb) return sa - sb;
293
+ }
294
+ return a.pane.paneIndex - b.pane.paneIndex;
295
+ })
296
+ }));
297
+ const sessionMap = /* @__PURE__ */ new Map();
298
+ for (const win of windowGroups) {
299
+ const existing = sessionMap.get(win.sessionName);
300
+ if (existing) {
301
+ existing.push({
302
+ windowIndex: win.windowIndex,
303
+ windowName: win.windowName,
304
+ panes: win.panes
305
+ });
306
+ } else {
307
+ sessionMap.set(win.sessionName, [
308
+ { windowIndex: win.windowIndex, windowName: win.windowName, panes: win.panes }
309
+ ]);
310
+ }
311
+ }
312
+ return [...sessionMap.entries()].map(([sessionName, tabs]) => ({ sessionName, tabs }));
313
+ },
314
+ detectStatusFromText
315
+ });
316
+
317
+ // src/services/tmux-service.ts
318
+ var createTmuxService = (context) => {
319
+ const { tmuxCli } = context.infra;
320
+ return {
321
+ listPanes: async () => {
322
+ return await tmuxCli.listPanes();
323
+ },
324
+ createNewWindow: async (input) => {
325
+ await tmuxCli.newWindow({ target: input.target, cwd: input.cwd });
326
+ },
327
+ splitWindow: async (input) => {
328
+ await tmuxCli.splitWindow({
329
+ target: input.target,
330
+ direction: input.direction,
331
+ cwd: input.cwd
332
+ });
333
+ },
334
+ sendCommand: async (input) => {
335
+ await tmuxCli.sendKeys({
336
+ target: input.target,
337
+ keys: input.command
338
+ });
339
+ },
340
+ sendKeys: async (input) => {
341
+ await tmuxCli.sendKeys({
342
+ target: input.target,
343
+ keys: input.keys
344
+ });
345
+ },
346
+ attachSession: async (sessionName) => {
347
+ await tmuxCli.attachSession(sessionName);
348
+ },
349
+ killPane: async (target) => {
350
+ await tmuxCli.killPane(target);
351
+ },
352
+ killSession: async (sessionName) => {
353
+ await tmuxCli.killSession(sessionName);
354
+ },
355
+ renameWindow: async (input) => {
356
+ await tmuxCli.renameWindow({ target: input.target, name: input.name });
357
+ },
358
+ getText: async (target) => {
359
+ return await tmuxCli.capturePane(target);
360
+ }
361
+ };
362
+ };
363
+
364
+ // src/services/directory-scan-service.ts
365
+ import { readdir, access } from "node:fs/promises";
366
+ import { join as join2 } from "node:path";
367
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
368
+ "node_modules",
369
+ ".git",
370
+ ".hg",
371
+ ".svn",
372
+ "dist",
373
+ "build",
374
+ ".cache",
375
+ ".pnpm-store",
376
+ ".Trash",
377
+ "Library",
378
+ "Applications",
379
+ ".claude"
380
+ ]);
381
+ var MAX_DEPTH = 5;
382
+ var exists = async (path) => {
383
+ try {
384
+ await access(path);
385
+ return true;
386
+ } catch {
387
+ return false;
388
+ }
389
+ };
390
+ var findProjects = async (dir, depth) => {
391
+ if (depth > MAX_DEPTH) return [];
392
+ try {
393
+ const entries = await readdir(dir, { withFileTypes: true });
394
+ const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name));
395
+ const results = [];
396
+ const childScans = [];
397
+ for (const entry of dirs) {
398
+ const fullPath = join2(dir, entry.name);
399
+ childScans.push(
400
+ exists(join2(fullPath, ".git")).then(async (isProject) => {
401
+ if (isProject) return [fullPath];
402
+ return await findProjects(fullPath, depth + 1);
403
+ })
404
+ );
405
+ }
406
+ const nested = await Promise.all(childScans);
407
+ for (const paths of nested) {
408
+ for (const p of paths) {
409
+ results.push(p);
410
+ }
411
+ }
412
+ return results;
413
+ } catch {
414
+ return [];
415
+ }
416
+ };
417
+ var createDirectoryScanService = () => ({
418
+ scan: async () => {
419
+ const home = process.env["HOME"] ?? "";
420
+ if (!home) return [];
421
+ const results = await findProjects(home, 0);
422
+ return results.toSorted((a, b) => a.localeCompare(b));
423
+ }
424
+ });
425
+
426
+ // src/services/index.ts
427
+ var createServices = (context) => ({
428
+ tmux: createTmuxService(context),
429
+ sessionDetection: createSessionDetectionService(),
430
+ directoryScan: createDirectoryScanService()
431
+ });
432
+
433
+ // src/utils/ShellUtils.ts
434
+ var escapeShellArg = (arg) => {
435
+ if (arg === "") return "''";
436
+ return `'${arg.replace(/'/g, "'\\''")}'`;
437
+ };
438
+
439
+ // src/usecases/manager-usecase.ts
440
+ var createManagerUsecase = (context) => {
441
+ const { tmux, sessionDetection } = context.services;
442
+ const windowTarget = (up) => `${up.pane.sessionName}:${String(up.pane.windowIndex)}`;
443
+ return {
444
+ createSession: async ({ sessionName, cwd, prompt }) => {
445
+ const exists2 = await context.infra.tmuxCli.hasSession(sessionName);
446
+ if (!exists2) {
447
+ await context.infra.tmuxCli.newSession({ name: sessionName, cwd });
448
+ } else {
449
+ await context.infra.tmuxCli.newWindow({ target: sessionName, cwd });
450
+ }
451
+ const panes = await context.infra.tmuxCli.listPanes();
452
+ const sessionPanes = panes.filter((p) => p.sessionName === sessionName).toSorted((a, b) => b.windowIndex - a.windowIndex || b.paneIndex - a.paneIndex);
453
+ const target = sessionPanes[0]?.paneId;
454
+ if (!target) return;
455
+ const escapedPrompt = escapeShellArg(prompt);
456
+ await tmux.sendCommand({ target, command: `claude -w -- ${escapedPrompt}` });
457
+ },
458
+ list: async () => {
459
+ const panes = await tmux.listPanes();
460
+ const sessionGroups = sessionDetection.groupBySession({ panes });
461
+ return { sessionGroups };
462
+ },
463
+ enrichStatus: async (up) => {
464
+ if (up.kind !== "claude") return up;
465
+ try {
466
+ const paneText = await tmux.getText(up.pane.paneId);
467
+ const status = sessionDetection.detectStatusFromText(paneText);
468
+ return { ...up, claudeStatus: status };
469
+ } catch {
470
+ return up;
471
+ }
472
+ },
473
+ navigateTo: async (up) => {
474
+ await tmux.attachSession(up.pane.sessionName);
475
+ },
476
+ highlightWindow: async (up) => {
477
+ const title = up.claudeTitle ?? up.pane.title;
478
+ await tmux.renameWindow({
479
+ target: windowTarget(up),
480
+ name: `\u25B6 ${title}`
481
+ });
482
+ },
483
+ unhighlightWindow: async (up) => {
484
+ await tmux.renameWindow({
485
+ target: windowTarget(up),
486
+ name: ""
487
+ });
488
+ },
489
+ killPane: async (paneId) => {
490
+ await tmux.killPane(paneId);
491
+ },
492
+ killSession: async (sessionName) => {
493
+ await tmux.killSession(sessionName);
494
+ }
495
+ };
496
+ };
497
+
498
+ // src/usecases/index.ts
499
+ var createUsecases = (context) => ({
500
+ manager: createManagerUsecase(context)
501
+ });
502
+
503
+ // package.json
504
+ var package_default = {
505
+ name: "abmux",
506
+ version: "0.0.1",
507
+ repository: {
508
+ type: "git",
509
+ url: "https://github.com/cut0/abmux.git"
510
+ },
511
+ bin: {
512
+ abmux: "./dist/cli/index.js"
513
+ },
514
+ files: [
515
+ "dist"
516
+ ],
517
+ type: "module",
518
+ publishConfig: {
519
+ access: "public"
520
+ },
521
+ scripts: {
522
+ start: "tsx src/cli/index.ts",
523
+ test: "vitest run",
524
+ typecheck: "tsc --noEmit",
525
+ "lint:check": "oxlint src/",
526
+ "lint:fix": "oxlint --fix src/",
527
+ "format:check": "oxfmt --check .",
528
+ "format:fix": "oxfmt .",
529
+ build: "tsx build.ts",
530
+ release: "pnpm build && changeset publish"
531
+ },
532
+ dependencies: {
533
+ "@inkjs/ui": "2.0.0",
534
+ ink: "6.8.0",
535
+ react: "19.2.4"
536
+ },
537
+ devDependencies: {
538
+ "@changesets/changelog-github": "0.5.1",
539
+ "@changesets/cli": "2.29.4",
540
+ "@types/node": "25.5.2",
541
+ "@types/react": "19.2.14",
542
+ esbuild: "0.25.2",
543
+ oxfmt: "0.44.0",
544
+ oxlint: "1.59.0",
545
+ tsx: "4.21.0",
546
+ typescript: "6.0.2",
547
+ vitest: "4.1.2"
548
+ },
549
+ packageManager: "pnpm@10.30.1"
550
+ };
551
+
552
+ // src/constants.ts
553
+ var APP_TITLE = "abmux - AI Board on tmux";
554
+ var APP_VERSION = package_default.version;
555
+
556
+ // src/cli/help.ts
557
+ var printHelp = () => {
558
+ console.log(`${APP_TITLE} v${APP_VERSION}
559
+
560
+ Usage:
561
+ abmux Start TUI
562
+ abmux new <prompt> [--dir <path>] Create session and launch Claude
563
+ abmux open [session] Attach to session
564
+ abmux kill [session] Kill session
565
+ abmux list List sessions
566
+ abmux --help Show this help`);
567
+ };
568
+
569
+ // src/cli/tui-command.ts
570
+ import { basename as basename3 } from "node:path";
571
+ import { render } from "ink";
572
+ import { createElement } from "react";
573
+
574
+ // src/components/ManagerView.tsx
575
+ import { basename as basename2 } from "node:path";
576
+ import { Box as Box10, Text as Text10 } from "ink";
577
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef2, useState as useState5 } from "react";
578
+
579
+ // src/components/shared/Header.tsx
580
+ import { Box, Text } from "ink";
581
+ import { jsx } from "react/jsx-runtime";
582
+ var Header = ({ title }) => {
583
+ return /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: title }) });
584
+ };
585
+
586
+ // src/components/shared/StatusBar.tsx
587
+ import { Box as Box2, Text as Text2 } from "ink";
588
+ import { jsx as jsx2 } from "react/jsx-runtime";
589
+ var COLOR_MAP = {
590
+ success: "green",
591
+ error: "red",
592
+ info: "gray"
593
+ };
594
+ var StatusBar = ({ message, type = "info" }) => {
595
+ return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: COLOR_MAP[type], children: message }) });
596
+ };
597
+
598
+ // src/components/shared/DirectorySelect.tsx
599
+ import { Box as Box3, Text as Text3, useApp, useInput } from "ink";
600
+ import { useCallback, useMemo as useMemo2, useState } from "react";
601
+
602
+ // src/hooks/use-scroll.ts
603
+ import { useMemo } from "react";
604
+ var useScroll = (cursor, totalItems, availableRows) => {
605
+ return useMemo(() => {
606
+ const visibleCount = Math.max(1, availableRows);
607
+ if (totalItems <= visibleCount) {
608
+ return { scrollOffset: 0, visibleCount };
609
+ }
610
+ let offset = cursor - Math.floor(visibleCount / 2);
611
+ if (offset < 0) offset = 0;
612
+ if (offset + visibleCount > totalItems) offset = totalItems - visibleCount;
613
+ return { scrollOffset: offset, visibleCount };
614
+ }, [cursor, totalItems, availableRows]);
615
+ };
616
+
617
+ // src/components/shared/DirectorySelect.tsx
618
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
619
+ var sortSessionGroups = (groups, currentSession) => {
620
+ const current = groups.filter((g) => g.sessionName === currentSession);
621
+ const rest = groups.filter((g) => g.sessionName !== currentSession);
622
+ return [...current, ...rest];
623
+ };
624
+ var SessionListPanel = ({
625
+ sessionGroups,
626
+ currentSession,
627
+ isFocused,
628
+ availableRows,
629
+ onSelect,
630
+ onCursorChange,
631
+ onDeleteSession,
632
+ onAddSession
633
+ }) => {
634
+ const { exit } = useApp();
635
+ const [cursor, setCursor] = useState(0);
636
+ const sortedGroups = useMemo2(
637
+ () => sortSessionGroups(sessionGroups, currentSession),
638
+ [sessionGroups, currentSession]
639
+ );
640
+ const sessions = useMemo2(() => sortedGroups.map((g) => g.sessionName), [sortedGroups]);
641
+ const clampedCursor = cursor >= sessions.length ? Math.max(0, sessions.length - 1) : cursor;
642
+ if (clampedCursor !== cursor) {
643
+ setCursor(clampedCursor);
644
+ }
645
+ const reservedLines = 1;
646
+ const { scrollOffset, visibleCount } = useScroll(
647
+ clampedCursor,
648
+ sessions.length,
649
+ availableRows - reservedLines
650
+ );
651
+ const visibleSessions = sessions.slice(scrollOffset, scrollOffset + visibleCount);
652
+ const moveCursor = useCallback(
653
+ (next) => {
654
+ const clamped = Math.max(0, Math.min(sessions.length - 1, next));
655
+ setCursor(clamped);
656
+ const name = sessions[clamped];
657
+ if (name) onCursorChange(name);
658
+ },
659
+ [sessions, onCursorChange]
660
+ );
661
+ useInput(
662
+ (input, key) => {
663
+ if (input === "q") {
664
+ exit();
665
+ return;
666
+ }
667
+ if (key.upArrow) {
668
+ moveCursor(clampedCursor - 1);
669
+ return;
670
+ }
671
+ if (key.downArrow) {
672
+ moveCursor(clampedCursor + 1);
673
+ return;
674
+ }
675
+ if (key.return || key.rightArrow) {
676
+ const name = sessions[clampedCursor];
677
+ if (name) onSelect(name);
678
+ return;
679
+ }
680
+ if (input === "d" && onDeleteSession) {
681
+ const name = sessions[clampedCursor];
682
+ if (name) onDeleteSession(name);
683
+ return;
684
+ }
685
+ if (input === "n" && onAddSession) {
686
+ onAddSession();
687
+ }
688
+ },
689
+ { isActive: isFocused }
690
+ );
691
+ return /* @__PURE__ */ jsxs(Box3, { flexDirection: "column", children: [
692
+ /* @__PURE__ */ jsxs(Box3, { paddingLeft: 1, children: [
693
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: isFocused ? "green" : "gray", children: "Sessions" }),
694
+ /* @__PURE__ */ jsxs(Text3, { dimColor: true, children: [
695
+ " ",
696
+ "(",
697
+ clampedCursor + 1,
698
+ "/",
699
+ sessions.length,
700
+ ")"
701
+ ] })
702
+ ] }),
703
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleSessions.map((name, i) => {
704
+ const globalIndex = scrollOffset + i;
705
+ const isHighlighted = globalIndex === clampedCursor;
706
+ const isCurrent = name === currentSession;
707
+ return /* @__PURE__ */ jsxs(Box3, { paddingLeft: 1, gap: 1, children: [
708
+ /* @__PURE__ */ jsx3(Text3, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
709
+ /* @__PURE__ */ jsx3(Text3, { color: isHighlighted ? "green" : "cyan", bold: isHighlighted, wrap: "truncate", children: name }),
710
+ isCurrent && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "(cwd)" })
711
+ ] }, name);
712
+ }) })
713
+ ] });
714
+ };
715
+
716
+ // src/components/PaneListPanel.tsx
717
+ import { Box as Box6, Text as Text6 } from "ink";
718
+
719
+ // src/components/PaneListView.tsx
720
+ import { Box as Box5, Text as Text5, useApp as useApp2, useInput as useInput2 } from "ink";
721
+ import { useCallback as useCallback2, useMemo as useMemo3, useRef, useState as useState2 } from "react";
722
+
723
+ // src/components/sessions/PaneItem.tsx
724
+ import { Box as Box4, Text as Text4 } from "ink";
725
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
726
+ var PaneItem = ({ unifiedPane, isHighlighted }) => {
727
+ const { pane, kind, claudeStatus, claudeTitle } = unifiedPane;
728
+ if (kind === "claude") {
729
+ const icon = pane.title.charAt(0);
730
+ const statusLabel = claudeStatus ? SESSION_STATUS_LABEL[claudeStatus] : "";
731
+ const statusColor = claudeStatus ? SESSION_STATUS_COLOR[claudeStatus] : "gray";
732
+ return /* @__PURE__ */ jsxs2(Box4, { paddingLeft: 3, gap: 1, children: [
733
+ /* @__PURE__ */ jsx4(Text4, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
734
+ /* @__PURE__ */ jsx4(Text4, { color: "#FF8C00", children: icon }),
735
+ /* @__PURE__ */ jsxs2(Text4, { color: statusColor, children: [
736
+ "[",
737
+ statusLabel,
738
+ "]"
739
+ ] }),
740
+ /* @__PURE__ */ jsx4(Text4, { color: isHighlighted ? "green" : void 0, bold: isHighlighted, children: claudeTitle }),
741
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pane.paneId })
742
+ ] });
743
+ }
744
+ return /* @__PURE__ */ jsxs2(Box4, { paddingLeft: 3, gap: 1, children: [
745
+ /* @__PURE__ */ jsx4(Text4, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
746
+ kind === "available" && /* @__PURE__ */ jsx4(Text4, { color: "#4AA8D8", children: "\u25CB" }),
747
+ kind === "busy" && /* @__PURE__ */ jsx4(Text4, { color: "#E05252", children: "\u25CF" }),
748
+ /* @__PURE__ */ jsx4(Text4, { color: isHighlighted ? "green" : void 0, bold: isHighlighted, children: pane.title || "(empty)" }),
749
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pane.paneId })
750
+ ] });
751
+ };
752
+
753
+ // src/components/PaneListView.tsx
754
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
755
+ var PaneListView = ({
756
+ selectedSession,
757
+ group,
758
+ isFocused,
759
+ availableRows,
760
+ onNavigate,
761
+ onHighlight,
762
+ onUnhighlight,
763
+ onBack,
764
+ onNewSession,
765
+ onKillPane
766
+ }) => {
767
+ const { exit } = useApp2();
768
+ const [cursor, setCursor] = useState2(0);
769
+ const highlightedRef = useRef(void 0);
770
+ const panes = useMemo3(() => group.tabs.flatMap((t) => t.panes), [group]);
771
+ const clampedCursor = cursor >= panes.length ? Math.max(0, panes.length - 1) : cursor;
772
+ if (clampedCursor !== cursor) {
773
+ setCursor(clampedCursor);
774
+ }
775
+ const reservedLines = 1;
776
+ const { scrollOffset, visibleCount } = useScroll(
777
+ clampedCursor,
778
+ panes.length,
779
+ availableRows - reservedLines
780
+ );
781
+ const visiblePanes = useMemo3(
782
+ () => panes.slice(scrollOffset, scrollOffset + visibleCount),
783
+ [panes, scrollOffset, visibleCount]
784
+ );
785
+ const highlight = useCallback2(
786
+ (up) => {
787
+ const prev = highlightedRef.current;
788
+ if (prev && prev !== up) onUnhighlight(prev);
789
+ if (up) onHighlight(up);
790
+ highlightedRef.current = up;
791
+ },
792
+ [onHighlight, onUnhighlight]
793
+ );
794
+ const clearHighlight = useCallback2(() => {
795
+ const prev = highlightedRef.current;
796
+ if (prev) onUnhighlight(prev);
797
+ highlightedRef.current = void 0;
798
+ }, [onUnhighlight]);
799
+ const moveCursor = useCallback2(
800
+ (next) => {
801
+ const clamped = Math.max(0, Math.min(panes.length - 1, next));
802
+ setCursor(clamped);
803
+ highlight(panes[clamped]);
804
+ },
805
+ [panes, highlight]
806
+ );
807
+ const didInitRef = useRef(false);
808
+ if (!didInitRef.current && panes.length > 0) {
809
+ didInitRef.current = true;
810
+ highlight(panes[clampedCursor]);
811
+ }
812
+ useInput2(
813
+ (input, key) => {
814
+ if (input === "q") {
815
+ clearHighlight();
816
+ exit();
817
+ return;
818
+ }
819
+ if (key.escape || key.leftArrow) {
820
+ clearHighlight();
821
+ onBack();
822
+ return;
823
+ }
824
+ if (key.upArrow) {
825
+ moveCursor(clampedCursor - 1);
826
+ return;
827
+ }
828
+ if (key.downArrow) {
829
+ moveCursor(clampedCursor + 1);
830
+ return;
831
+ }
832
+ if (input === "d") {
833
+ const pane = panes[clampedCursor];
834
+ if (pane) {
835
+ void onKillPane(pane.pane.paneId);
836
+ }
837
+ return;
838
+ }
839
+ if (input === "n") {
840
+ onNewSession(selectedSession);
841
+ return;
842
+ }
843
+ if (key.return) {
844
+ const pane = panes[clampedCursor];
845
+ if (pane) onNavigate(pane);
846
+ }
847
+ },
848
+ { isActive: isFocused }
849
+ );
850
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
851
+ /* @__PURE__ */ jsxs3(Box5, { paddingLeft: 1, children: [
852
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: isFocused ? "green" : "gray", children: "Panes" }),
853
+ /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
854
+ " ",
855
+ selectedSession,
856
+ " (",
857
+ panes.length > 0 ? clampedCursor + 1 : 0,
858
+ "/",
859
+ panes.length,
860
+ ")"
861
+ ] })
862
+ ] }),
863
+ /* @__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(
864
+ PaneItem,
865
+ {
866
+ unifiedPane: up,
867
+ isHighlighted: scrollOffset + i === clampedCursor
868
+ },
869
+ up.pane.paneId
870
+ )) })
871
+ ] });
872
+ };
873
+
874
+ // src/components/PaneListPanel.tsx
875
+ import { jsx as jsx6 } from "react/jsx-runtime";
876
+ var PaneListPanel = ({
877
+ selectedSession,
878
+ group,
879
+ isFocused,
880
+ availableRows,
881
+ onNavigate,
882
+ onHighlight,
883
+ onUnhighlight,
884
+ onBack,
885
+ onNewSession,
886
+ onKillPane
887
+ }) => {
888
+ if (!selectedSession) {
889
+ return /* @__PURE__ */ jsx6(Box6, { paddingLeft: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No session selected" }) });
890
+ }
891
+ return /* @__PURE__ */ jsx6(
892
+ PaneListView,
893
+ {
894
+ selectedSession,
895
+ group: group ?? { sessionName: selectedSession, tabs: [] },
896
+ isFocused,
897
+ availableRows,
898
+ onNavigate,
899
+ onHighlight,
900
+ onUnhighlight,
901
+ onBack,
902
+ onNewSession,
903
+ onKillPane
904
+ },
905
+ selectedSession
906
+ );
907
+ };
908
+
909
+ // src/components/ConfirmView.tsx
910
+ import { Box as Box7, Text as Text7, useInput as useInput3 } from "ink";
911
+
912
+ // src/hooks/use-terminal-size.ts
913
+ import { useStdout } from "ink";
914
+ import { useEffect, useState as useState3 } from "react";
915
+ var useTerminalSize = () => {
916
+ const { stdout } = useStdout();
917
+ const [size, setSize] = useState3({
918
+ rows: stdout.rows ?? 24,
919
+ columns: stdout.columns ?? 80
920
+ });
921
+ useEffect(() => {
922
+ const handleResize = () => {
923
+ setSize({
924
+ rows: stdout.rows ?? 24,
925
+ columns: stdout.columns ?? 80
926
+ });
927
+ };
928
+ stdout.on("resize", handleResize);
929
+ return () => {
930
+ stdout.off("resize", handleResize);
931
+ };
932
+ }, [stdout]);
933
+ return size;
934
+ };
935
+
936
+ // src/components/ConfirmView.tsx
937
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
938
+ var ConfirmView = ({ selectedDir, prompt, onConfirm, onCancel }) => {
939
+ const { rows } = useTerminalSize();
940
+ const previewLines = prompt.split("\n");
941
+ const maxPreview = Math.min(previewLines.length, rows - 6);
942
+ useInput3((_input, key) => {
943
+ if (key.return) {
944
+ onConfirm();
945
+ return;
946
+ }
947
+ if (key.escape) {
948
+ onCancel();
949
+ }
950
+ });
951
+ return /* @__PURE__ */ jsxs4(Box7, { flexDirection: "column", height: rows, children: [
952
+ /* @__PURE__ */ jsx7(Header, { title: `${APP_TITLE} \u2014 ${selectedDir}` }),
953
+ /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { bold: true, children: "New Claude session:" }) }),
954
+ /* @__PURE__ */ jsxs4(Box7, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingLeft: 2, children: [
955
+ previewLines.slice(0, maxPreview).map((line, i) => /* @__PURE__ */ jsx7(Text7, { color: "white", children: line }, i)),
956
+ previewLines.length > maxPreview && /* @__PURE__ */ jsxs4(Text7, { dimColor: true, children: [
957
+ "... (",
958
+ previewLines.length - maxPreview,
959
+ " more lines)"
960
+ ] })
961
+ ] }),
962
+ /* @__PURE__ */ jsxs4(Box7, { gap: 2, children: [
963
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Enter confirm" }),
964
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Esc cancel" })
965
+ ] })
966
+ ] });
967
+ };
968
+
969
+ // src/components/DeleteSessionView.tsx
970
+ import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
971
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
972
+ var DeleteSessionView = ({
973
+ sessionName,
974
+ paneCount,
975
+ onConfirm,
976
+ onCancel
977
+ }) => {
978
+ const { rows } = useTerminalSize();
979
+ useInput4((_input, key) => {
980
+ if (key.return) {
981
+ onConfirm();
982
+ return;
983
+ }
984
+ if (key.escape) {
985
+ onCancel();
986
+ }
987
+ });
988
+ return /* @__PURE__ */ jsxs5(Box8, { flexDirection: "column", height: rows, children: [
989
+ /* @__PURE__ */ jsx8(Header, { title: APP_TITLE }),
990
+ /* @__PURE__ */ jsxs5(Box8, { flexDirection: "column", gap: 1, paddingLeft: 2, children: [
991
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "red", children: "Delete session?" }),
992
+ /* @__PURE__ */ jsxs5(Text8, { children: [
993
+ "Session: ",
994
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: sessionName })
995
+ ] }),
996
+ /* @__PURE__ */ jsxs5(Text8, { children: [
997
+ "Panes: ",
998
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: paneCount })
999
+ ] }),
1000
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "All processes in this session will be terminated." })
1001
+ ] }),
1002
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1 }),
1003
+ /* @__PURE__ */ jsxs5(Box8, { gap: 2, children: [
1004
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Enter confirm" }),
1005
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Esc cancel" })
1006
+ ] })
1007
+ ] });
1008
+ };
1009
+
1010
+ // src/components/DirectorySearchView.tsx
1011
+ import { Box as Box9, Text as Text9, useInput as useInput5 } from "ink";
1012
+ import { useMemo as useMemo4, useState as useState4 } from "react";
1013
+ import { basename } from "node:path";
1014
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1015
+ var formatPath = (path) => {
1016
+ const home = process.env["HOME"] ?? "";
1017
+ if (home && path.startsWith(home)) {
1018
+ return `~${path.slice(home.length)}`;
1019
+ }
1020
+ return path;
1021
+ };
1022
+ var DirectorySearchView = ({ directories, onSelect, onCancel }) => {
1023
+ const { rows } = useTerminalSize();
1024
+ const [query, setQuery] = useState4("");
1025
+ const [cursor, setCursor] = useState4(0);
1026
+ const filtered = useMemo4(() => {
1027
+ if (!query) return directories;
1028
+ const lower = query.toLowerCase();
1029
+ return directories.filter((d) => {
1030
+ const name = basename(d).toLowerCase();
1031
+ const formatted = formatPath(d).toLowerCase();
1032
+ return name.includes(lower) || formatted.includes(lower);
1033
+ });
1034
+ }, [directories, query]);
1035
+ const clampedCursor = cursor >= filtered.length ? Math.max(0, filtered.length - 1) : cursor;
1036
+ if (clampedCursor !== cursor) {
1037
+ setCursor(clampedCursor);
1038
+ }
1039
+ const listHeight = rows - 6;
1040
+ const { scrollOffset, visibleCount } = useScroll(clampedCursor, filtered.length, listHeight);
1041
+ const visibleItems = useMemo4(
1042
+ () => filtered.slice(scrollOffset, scrollOffset + visibleCount),
1043
+ [filtered, scrollOffset, visibleCount]
1044
+ );
1045
+ useInput5((input, key) => {
1046
+ if (key.escape) {
1047
+ onCancel();
1048
+ return;
1049
+ }
1050
+ if (key.return) {
1051
+ const selected = filtered[clampedCursor];
1052
+ if (selected) onSelect(selected);
1053
+ return;
1054
+ }
1055
+ if (key.upArrow) {
1056
+ setCursor((c) => Math.max(0, c - 1));
1057
+ return;
1058
+ }
1059
+ if (key.downArrow) {
1060
+ setCursor((c) => Math.min(filtered.length - 1, c + 1));
1061
+ return;
1062
+ }
1063
+ if (key.backspace || key.delete) {
1064
+ setQuery((q) => q.slice(0, -1));
1065
+ setCursor(0);
1066
+ return;
1067
+ }
1068
+ if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow) {
1069
+ setQuery((q) => q + input);
1070
+ setCursor(0);
1071
+ }
1072
+ });
1073
+ return /* @__PURE__ */ jsxs6(Box9, { flexDirection: "column", height: rows, children: [
1074
+ /* @__PURE__ */ jsx9(Header, { title: `${APP_TITLE} \u2014 Add Session` }),
1075
+ /* @__PURE__ */ jsxs6(Box9, { paddingLeft: 1, gap: 1, children: [
1076
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: ">" }),
1077
+ /* @__PURE__ */ jsx9(Text9, { children: query }),
1078
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: query ? "" : "type to filter..." })
1079
+ ] }),
1080
+ /* @__PURE__ */ jsx9(Box9, { paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text9, { dimColor: true, children: [
1081
+ filtered.length,
1082
+ "/",
1083
+ directories.length
1084
+ ] }) }),
1085
+ /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleItems.map((dir, i) => {
1086
+ const globalIndex = scrollOffset + i;
1087
+ const isHighlighted = globalIndex === clampedCursor;
1088
+ return /* @__PURE__ */ jsxs6(Box9, { paddingLeft: 1, gap: 1, children: [
1089
+ /* @__PURE__ */ jsx9(Text9, { color: isHighlighted ? "green" : void 0, children: isHighlighted ? "\u25B6" : " " }),
1090
+ /* @__PURE__ */ jsx9(Text9, { color: isHighlighted ? "green" : void 0, bold: isHighlighted, children: formatPath(dir) })
1091
+ ] }, dir);
1092
+ }) }),
1093
+ /* @__PURE__ */ jsxs6(Box9, { gap: 2, children: [
1094
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter select" }),
1095
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Esc cancel" })
1096
+ ] })
1097
+ ] });
1098
+ };
1099
+
1100
+ // src/utils/PromiseUtils.ts
1101
+ var swallow = async (fn) => {
1102
+ try {
1103
+ await fn();
1104
+ } catch {
1105
+ }
1106
+ };
1107
+
1108
+ // src/components/ManagerView.tsx
1109
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1110
+ var MODE = {
1111
+ split: "split",
1112
+ confirm: "confirm",
1113
+ deleteSession: "deleteSession",
1114
+ addSession: "addSession"
1115
+ };
1116
+ var FOCUS = {
1117
+ left: "left",
1118
+ right: "right"
1119
+ };
1120
+ var POLL_INTERVAL = 3e3;
1121
+ var ManagerView = ({
1122
+ actions,
1123
+ currentSession,
1124
+ currentCwd,
1125
+ directories,
1126
+ restoredPrompt,
1127
+ restoredSession
1128
+ }) => {
1129
+ const { rows, columns } = useTerminalSize();
1130
+ const [fetchState, setFetchState] = useState5({ data: [], isLoading: true });
1131
+ const [mode, setMode] = useState5(restoredPrompt ? MODE.confirm : MODE.split);
1132
+ const [focus, setFocus] = useState5(FOCUS.left);
1133
+ const [selectedSession, setSelectedSession] = useState5(restoredSession);
1134
+ const [pendingPrompt, setPendingPrompt] = useState5(restoredPrompt ?? "");
1135
+ const [pendingDeleteSession, setPendingDeleteSession] = useState5(void 0);
1136
+ const sessionCwdMap = useRef2(/* @__PURE__ */ new Map());
1137
+ const refresh = useCallback3(async () => {
1138
+ try {
1139
+ const groups = await actions.fetchSessions();
1140
+ const knownNames = new Set(groups.map((g) => g.sessionName));
1141
+ const missing = [];
1142
+ for (const name of sessionCwdMap.current.keys()) {
1143
+ if (!knownNames.has(name)) {
1144
+ missing.push({ sessionName: name, tabs: [] });
1145
+ }
1146
+ }
1147
+ setFetchState({ data: [...missing, ...groups], isLoading: false });
1148
+ } catch {
1149
+ setFetchState((prev) => ({ ...prev, isLoading: false }));
1150
+ }
1151
+ }, [actions]);
1152
+ useEffect2(() => {
1153
+ void refresh();
1154
+ const timer = setInterval(() => {
1155
+ void refresh();
1156
+ }, POLL_INTERVAL);
1157
+ return () => {
1158
+ clearInterval(timer);
1159
+ };
1160
+ }, [refresh]);
1161
+ const resolvedSession = selectedSession ?? fetchState.data[0]?.sessionName;
1162
+ const selectedGroup = useMemo5(
1163
+ () => fetchState.data.find((g) => g.sessionName === resolvedSession),
1164
+ [fetchState.data, resolvedSession]
1165
+ );
1166
+ const handleOpenAddSession = useCallback3(() => {
1167
+ setMode(MODE.addSession);
1168
+ }, []);
1169
+ const handleAddSessionSelect = useCallback3(
1170
+ (path) => {
1171
+ const name = basename2(path);
1172
+ sessionCwdMap.current.set(name, path);
1173
+ const exists2 = fetchState.data.some((g) => g.sessionName === name);
1174
+ if (!exists2) {
1175
+ setFetchState((prev) => ({
1176
+ ...prev,
1177
+ data: [{ sessionName: name, tabs: [] }, ...prev.data]
1178
+ }));
1179
+ }
1180
+ setSelectedSession(name);
1181
+ setMode(MODE.split);
1182
+ },
1183
+ [fetchState.data]
1184
+ );
1185
+ const handleCancelAddSession = useCallback3(() => {
1186
+ setMode(MODE.split);
1187
+ }, []);
1188
+ const handleDeleteSession = useCallback3((name) => {
1189
+ setPendingDeleteSession(name);
1190
+ setMode(MODE.deleteSession);
1191
+ }, []);
1192
+ const handleConfirmDelete = useCallback3(() => {
1193
+ if (!pendingDeleteSession) return;
1194
+ sessionCwdMap.current.delete(pendingDeleteSession);
1195
+ if (resolvedSession === pendingDeleteSession) {
1196
+ setSelectedSession(void 0);
1197
+ }
1198
+ void swallow(() => actions.killSession(pendingDeleteSession)).then(() => void refresh());
1199
+ setPendingDeleteSession(void 0);
1200
+ setMode(MODE.split);
1201
+ }, [pendingDeleteSession, resolvedSession, actions, refresh]);
1202
+ const handleCancelDelete = useCallback3(() => {
1203
+ setPendingDeleteSession(void 0);
1204
+ setMode(MODE.split);
1205
+ }, []);
1206
+ const handleNewSession = useCallback3(
1207
+ (sessionName) => {
1208
+ actions.openEditor(sessionName);
1209
+ },
1210
+ [actions]
1211
+ );
1212
+ const handleConfirmNew = useCallback3(() => {
1213
+ if (!resolvedSession) return;
1214
+ const cwd = sessionCwdMap.current.get(resolvedSession) ?? currentCwd;
1215
+ void actions.createSession(resolvedSession, cwd, pendingPrompt).then(() => void refresh());
1216
+ setPendingPrompt("");
1217
+ setMode(MODE.split);
1218
+ }, [resolvedSession, currentCwd, pendingPrompt, actions, refresh]);
1219
+ const handleCancelConfirm = useCallback3(() => {
1220
+ setPendingPrompt("");
1221
+ setMode(MODE.split);
1222
+ }, []);
1223
+ const handleSessionSelect = useCallback3((name) => {
1224
+ setSelectedSession(name);
1225
+ setFocus(FOCUS.right);
1226
+ }, []);
1227
+ const handleSessionCursorChange = useCallback3((name) => {
1228
+ setSelectedSession(name);
1229
+ }, []);
1230
+ const handleNavigate = useCallback3(
1231
+ (up) => {
1232
+ actions.attachSession(up.pane.sessionName);
1233
+ },
1234
+ [actions]
1235
+ );
1236
+ const handleBack = useCallback3(() => {
1237
+ setFocus(FOCUS.left);
1238
+ }, []);
1239
+ const handleKillPane = useCallback3(
1240
+ async (paneId) => {
1241
+ await swallow(() => actions.killPane(paneId));
1242
+ void refresh();
1243
+ },
1244
+ [actions, refresh]
1245
+ );
1246
+ const handleHighlight = useCallback3(
1247
+ async (up) => {
1248
+ await swallow(() => actions.highlightWindow(up));
1249
+ },
1250
+ [actions]
1251
+ );
1252
+ const handleUnhighlight = useCallback3(
1253
+ async (up) => {
1254
+ await swallow(() => actions.unhighlightWindow(up));
1255
+ },
1256
+ [actions]
1257
+ );
1258
+ if (fetchState.isLoading) {
1259
+ return /* @__PURE__ */ jsxs7(Box10, { flexDirection: "column", height: rows, children: [
1260
+ /* @__PURE__ */ jsx10(Header, { title: `${APP_TITLE} v${APP_VERSION}` }),
1261
+ /* @__PURE__ */ jsx10(StatusBar, { message: "Loading...", type: "info" })
1262
+ ] });
1263
+ }
1264
+ if (mode === MODE.addSession) {
1265
+ return /* @__PURE__ */ jsx10(
1266
+ DirectorySearchView,
1267
+ {
1268
+ directories,
1269
+ onSelect: handleAddSessionSelect,
1270
+ onCancel: handleCancelAddSession
1271
+ }
1272
+ );
1273
+ }
1274
+ if (mode === MODE.deleteSession && pendingDeleteSession) {
1275
+ const deleteGroup = fetchState.data.find((g) => g.sessionName === pendingDeleteSession);
1276
+ const paneCount = deleteGroup?.tabs.reduce((sum, t) => sum + t.panes.length, 0) ?? 0;
1277
+ return /* @__PURE__ */ jsx10(
1278
+ DeleteSessionView,
1279
+ {
1280
+ sessionName: pendingDeleteSession,
1281
+ paneCount,
1282
+ onConfirm: handleConfirmDelete,
1283
+ onCancel: handleCancelDelete
1284
+ }
1285
+ );
1286
+ }
1287
+ if (mode === MODE.confirm && pendingPrompt) {
1288
+ return /* @__PURE__ */ jsx10(
1289
+ ConfirmView,
1290
+ {
1291
+ selectedDir: resolvedSession ?? "",
1292
+ prompt: pendingPrompt,
1293
+ onConfirm: handleConfirmNew,
1294
+ onCancel: handleCancelConfirm
1295
+ }
1296
+ );
1297
+ }
1298
+ const panelHeight = rows - 5;
1299
+ const leftWidth = Math.floor(columns / 3);
1300
+ const rightWidth = columns - leftWidth;
1301
+ return /* @__PURE__ */ jsxs7(Box10, { flexDirection: "column", height: rows, children: [
1302
+ /* @__PURE__ */ jsx10(Header, { title: `${APP_TITLE} - v${APP_VERSION}` }),
1303
+ /* @__PURE__ */ jsxs7(Box10, { flexDirection: "row", flexGrow: 1, children: [
1304
+ /* @__PURE__ */ jsx10(
1305
+ Box10,
1306
+ {
1307
+ flexDirection: "column",
1308
+ width: leftWidth,
1309
+ borderStyle: "round",
1310
+ borderColor: focus === FOCUS.left ? "green" : "gray",
1311
+ children: /* @__PURE__ */ jsx10(
1312
+ SessionListPanel,
1313
+ {
1314
+ sessionGroups: fetchState.data,
1315
+ currentSession,
1316
+ isFocused: focus === FOCUS.left,
1317
+ availableRows: panelHeight,
1318
+ onSelect: handleSessionSelect,
1319
+ onCursorChange: handleSessionCursorChange,
1320
+ onDeleteSession: handleDeleteSession,
1321
+ onAddSession: handleOpenAddSession
1322
+ }
1323
+ )
1324
+ }
1325
+ ),
1326
+ /* @__PURE__ */ jsx10(
1327
+ Box10,
1328
+ {
1329
+ flexDirection: "column",
1330
+ width: rightWidth,
1331
+ borderStyle: "round",
1332
+ borderColor: focus === FOCUS.right ? "green" : "gray",
1333
+ children: /* @__PURE__ */ jsx10(
1334
+ PaneListPanel,
1335
+ {
1336
+ selectedSession: resolvedSession,
1337
+ group: selectedGroup,
1338
+ isFocused: focus === FOCUS.right,
1339
+ availableRows: panelHeight,
1340
+ onNavigate: handleNavigate,
1341
+ onHighlight: handleHighlight,
1342
+ onUnhighlight: handleUnhighlight,
1343
+ onBack: handleBack,
1344
+ onNewSession: handleNewSession,
1345
+ onKillPane: handleKillPane
1346
+ }
1347
+ )
1348
+ }
1349
+ )
1350
+ ] }),
1351
+ /* @__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" })
1352
+ ] });
1353
+ };
1354
+
1355
+ // src/cli/tui-command.ts
1356
+ var createTuiCommand = ({ usecases, services, infra }) => async () => {
1357
+ const directories = await services.directoryScan.scan();
1358
+ let instance;
1359
+ let pendingPrompt;
1360
+ let pendingSession;
1361
+ const actions = {
1362
+ fetchSessions: async () => {
1363
+ const result = await usecases.manager.list();
1364
+ return await Promise.all(
1365
+ result.sessionGroups.map(async (group) => ({
1366
+ sessionName: group.sessionName,
1367
+ tabs: await Promise.all(
1368
+ group.tabs.map(async (tab) => ({
1369
+ windowIndex: tab.windowIndex,
1370
+ windowName: tab.windowName,
1371
+ panes: await Promise.all(
1372
+ tab.panes.map((up) => usecases.manager.enrichStatus(up))
1373
+ )
1374
+ }))
1375
+ )
1376
+ }))
1377
+ );
1378
+ },
1379
+ createSession: async (sessionName, cwd, prompt) => {
1380
+ await usecases.manager.createSession({ sessionName, cwd, prompt });
1381
+ },
1382
+ killSession: async (sessionName) => {
1383
+ await usecases.manager.killSession(sessionName);
1384
+ },
1385
+ killPane: async (paneId) => {
1386
+ await usecases.manager.killPane(paneId);
1387
+ },
1388
+ highlightWindow: async (up) => {
1389
+ await usecases.manager.highlightWindow(up);
1390
+ },
1391
+ unhighlightWindow: async (up) => {
1392
+ await usecases.manager.unhighlightWindow(up);
1393
+ },
1394
+ openEditor: (sessionName) => {
1395
+ instance.unmount();
1396
+ const prompt = infra.editor.open();
1397
+ pendingPrompt = prompt;
1398
+ pendingSession = sessionName;
1399
+ instance = renderApp();
1400
+ return prompt;
1401
+ },
1402
+ attachSession: (sessionName) => {
1403
+ instance.unmount();
1404
+ void infra.tmuxCli.attachSession(sessionName);
1405
+ instance = renderApp();
1406
+ }
1407
+ };
1408
+ const renderApp = () => {
1409
+ const prompt = pendingPrompt;
1410
+ const session = pendingSession;
1411
+ pendingPrompt = void 0;
1412
+ pendingSession = void 0;
1413
+ return render(
1414
+ createElement(ManagerView, {
1415
+ actions,
1416
+ currentSession: basename3(process.cwd()),
1417
+ currentCwd: process.cwd(),
1418
+ directories,
1419
+ restoredPrompt: prompt,
1420
+ restoredSession: session
1421
+ })
1422
+ );
1423
+ };
1424
+ instance = renderApp();
1425
+ await instance.waitUntilExit();
1426
+ };
1427
+
1428
+ // src/cli/new-command.ts
1429
+ import { basename as basename4 } from "node:path";
1430
+ import { parseArgs } from "node:util";
1431
+ var createNewCommand = ({ usecases }) => async (args) => {
1432
+ const { values, positionals } = parseArgs({
1433
+ args,
1434
+ options: {
1435
+ dir: { type: "string" }
1436
+ },
1437
+ allowPositionals: true
1438
+ });
1439
+ const prompt = positionals[0];
1440
+ if (!prompt) {
1441
+ console.error("Usage: abmux new <prompt> [--dir <path>]");
1442
+ process.exit(1);
1443
+ }
1444
+ const dir = values.dir ?? process.cwd();
1445
+ const sessionName = basename4(dir);
1446
+ await usecases.manager.createSession({ sessionName, cwd: dir, prompt });
1447
+ console.log(`Session "${sessionName}" created.`);
1448
+ };
1449
+
1450
+ // src/cli/open-command.ts
1451
+ import { basename as basename5 } from "node:path";
1452
+ var createOpenCommand = ({ infra }) => async (args) => {
1453
+ const session = args[0] ?? basename5(process.cwd());
1454
+ const exists2 = await infra.tmuxCli.hasSession(session);
1455
+ if (!exists2) {
1456
+ console.error(`Session "${session}" not found.`);
1457
+ process.exit(1);
1458
+ }
1459
+ await infra.tmuxCli.attachSession(session);
1460
+ };
1461
+
1462
+ // src/cli/kill-command.ts
1463
+ import { basename as basename6 } from "node:path";
1464
+ var createKillCommand = ({ usecases }) => async (args) => {
1465
+ const session = args[0] ?? basename6(process.cwd());
1466
+ await usecases.manager.killSession(session);
1467
+ console.log(`Session "${session}" killed.`);
1468
+ };
1469
+
1470
+ // src/cli/list-command.ts
1471
+ var createListCommand = ({ usecases }) => async () => {
1472
+ const result = await usecases.manager.list();
1473
+ if (result.sessionGroups.length === 0) {
1474
+ console.log("No sessions found.");
1475
+ return;
1476
+ }
1477
+ for (const group of result.sessionGroups) {
1478
+ const paneCount = group.tabs.reduce((sum, t) => sum + t.panes.length, 0);
1479
+ console.log(`${group.sessionName} (${String(paneCount)} panes)`);
1480
+ }
1481
+ };
1482
+
1483
+ // src/cli/index.ts
1484
+ var main = async () => {
1485
+ const infra = createInfra();
1486
+ const services = createServices({ infra });
1487
+ const usecases = createUsecases({ services, infra });
1488
+ const [command, ...args] = process.argv.slice(2);
1489
+ if (command === "--help" || command === "-h") {
1490
+ printHelp();
1491
+ return;
1492
+ }
1493
+ const commands = {
1494
+ new: createNewCommand({ usecases }),
1495
+ open: createOpenCommand({ infra }),
1496
+ kill: createKillCommand({ usecases }),
1497
+ list: createListCommand({ usecases })
1498
+ };
1499
+ const handler = commands[command ?? ""];
1500
+ if (command && !handler) {
1501
+ console.error(`Unknown command: ${command}`);
1502
+ printHelp();
1503
+ process.exit(1);
1504
+ }
1505
+ if (handler) {
1506
+ await handler(args);
1507
+ return;
1508
+ }
1509
+ await createTuiCommand({ usecases, services, infra })();
1510
+ };
1511
+ main().catch((err) => {
1512
+ console.error(err);
1513
+ process.exit(1);
1514
+ });