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.
- package/README.md +54 -0
- package/dist/cli/index.js +1514 -0
- 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
|
+
});
|