handmux 0.12.2 → 0.12.3
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 +17 -4
- package/README.zh-CN.md +17 -4
- package/package.json +3 -3
- package/public/assets/{index-DJNPeU_h.css → index-CpBDvLzi.css} +1 -1
- package/public/assets/{index-BXd3a0v9.js → index-yrOcjnt0.js} +25 -25
- package/public/index.html +2 -2
- package/src/agents/scanUtils.js +17 -0
- package/src/orphans.js +17 -6
package/public/index.html
CHANGED
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
@keyframes bootDot { 0%,100% { opacity: .22; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
|
|
49
49
|
@media (prefers-reduced-motion: reduce) { .boot-glow, .boot-dots i { animation: none; opacity: .8; } }
|
|
50
50
|
</style>
|
|
51
|
-
<script type="module" crossorigin src="/assets/index-
|
|
52
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
51
|
+
<script type="module" crossorigin src="/assets/index-yrOcjnt0.js"></script>
|
|
52
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CpBDvLzi.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-CpBDvLzi.css"></noscript>
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div id="boot-splash" aria-hidden="true">
|
package/src/agents/scanUtils.js
CHANGED
|
@@ -200,6 +200,23 @@ export function takeoverSessionName(cwdLabel, n, prefix = 'cc') {
|
|
|
200
200
|
return `${prefix}-${base}-${n}`.slice(0, 16);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// Coerce a user-typed takeover name into a valid tmux session name (isValidSessionName: [A-Za-z0-9-], ≤16):
|
|
204
|
+
// non-alnum runs → a single '-', trimmed of edge hyphens, capped at 16. Returns '' if nothing usable is
|
|
205
|
+
// left (caller then falls back to the generated name).
|
|
206
|
+
export function sanitizeSessionName(s) {
|
|
207
|
+
return String(s || '').replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 16).replace(/-+$/, '');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// First name in `<base>, base-2, base-3, …` (each capped at 16) that isn't already taken. null if none free.
|
|
211
|
+
export function freeSessionName(base, taken) {
|
|
212
|
+
if (!taken.has(base)) return base;
|
|
213
|
+
for (let i = 2; i < 1000; i++) {
|
|
214
|
+
const cand = `${base}-${i}`.slice(0, 16);
|
|
215
|
+
if (!taken.has(cand)) return cand;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
203
220
|
export const isShell = (c) => /^-?(zsh|bash|sh|fish|dash|tcsh|csh|ksh)$/.test(String(c || ''));
|
|
204
221
|
|
|
205
222
|
export async function lsofCwd(run, pid) {
|
package/src/orphans.js
CHANGED
|
@@ -21,7 +21,7 @@ import { AGENTS, getAgent } from './agents/index.js';
|
|
|
21
21
|
import { isSessionId } from './tmux/commands.js';
|
|
22
22
|
import {
|
|
23
23
|
defaultRun, parseAgentProcs, parsePaneMembership, findOrphans,
|
|
24
|
-
takeoverSessionName, isShell, lsofCwd, isSessionUuid,
|
|
24
|
+
takeoverSessionName, sanitizeSessionName, freeSessionName, isShell, lsofCwd, isSessionUuid,
|
|
25
25
|
} from './agents/scanUtils.js';
|
|
26
26
|
|
|
27
27
|
// Back-compat re-exports: these were originally defined here and are imported by tests and callers by this
|
|
@@ -53,7 +53,7 @@ export async function takeoverOrphan(
|
|
|
53
53
|
delay = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
54
54
|
pollTries = 16, pollMs = 400,
|
|
55
55
|
},
|
|
56
|
-
{ pid, sessionId, target = { mode: 'new' }, kill = true } = {},
|
|
56
|
+
{ pid, sessionId, target = { mode: 'new' }, kill = true, name: wantName } = {},
|
|
57
57
|
) {
|
|
58
58
|
if (!Number.isInteger(pid) || pid <= 0) return { error: 'bad pid', status: 400 };
|
|
59
59
|
// Both agents use UUID session ids; validate up front (takeover types the id into a shell via send-keys).
|
|
@@ -76,10 +76,18 @@ export async function takeoverOrphan(
|
|
|
76
76
|
wid = await commands.newWindow(sid, o.cwd, agent.procName, cmd);
|
|
77
77
|
} else {
|
|
78
78
|
const existing = new Set((await commands.listSessions()).map((s) => s.name));
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
// Honour a user-typed name (sanitized to tmux rules, deduped with a numeric suffix); fall back to the
|
|
80
|
+
// generated `<prefix>-<dir>-<n>` when the client sent nothing.
|
|
81
|
+
const wanted = sanitizeSessionName(wantName);
|
|
82
|
+
if (wanted) {
|
|
83
|
+
name = freeSessionName(wanted, existing);
|
|
84
|
+
} else {
|
|
85
|
+
for (let i = 1; i < 1000 && !name; i++) {
|
|
86
|
+
const cand = takeoverSessionName(o.cwdLabel, i, agent.takeoverPrefix);
|
|
87
|
+
if (!existing.has(cand)) name = cand;
|
|
88
|
+
}
|
|
82
89
|
}
|
|
90
|
+
if (!name) return { error: 'no free session name', status: 409 };
|
|
83
91
|
sid = await commands.newSession(name, o.cwd, cmd);
|
|
84
92
|
wid = (await commands.listWindows(sid))[0]?.id;
|
|
85
93
|
}
|
|
@@ -121,12 +129,15 @@ export async function scanOrphans({
|
|
|
121
129
|
const cwd = await lsofCwd(run, o.pid);
|
|
122
130
|
const dir = dirOverrides[agent.sessions.dirOptKey] || agent.sessions.dir(home);
|
|
123
131
|
const meta = cwd ? await agent.sessions.resolve(dir, cwd, { busyMs, now }) : {};
|
|
132
|
+
const cwdLabel = cwd ? path.basename(cwd) : '';
|
|
124
133
|
results.push({
|
|
125
134
|
pid: o.pid,
|
|
126
135
|
agent: agent.id,
|
|
127
136
|
agentLabel: agent.label,
|
|
128
137
|
cwd: cwd || '',
|
|
129
|
-
cwdLabel
|
|
138
|
+
cwdLabel,
|
|
139
|
+
// Default name the takeover sheet prefills (the same `<prefix>-<dir>-1` the server would auto-pick).
|
|
140
|
+
suggestedName: takeoverSessionName(cwdLabel, 1, agent.takeoverPrefix),
|
|
130
141
|
sessionId: meta.sessionId || null,
|
|
131
142
|
state: meta.state || 'unknown',
|
|
132
143
|
snippet: meta.snippet || '',
|