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/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-BXd3a0v9.js"></script>
52
- <link rel="stylesheet" crossorigin href="/assets/index-DJNPeU_h.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-DJNPeU_h.css"></noscript>
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">
@@ -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
- for (let i = 1; i < 1000 && !name; i++) {
80
- const cand = takeoverSessionName(o.cwdLabel, i, agent.takeoverPrefix);
81
- if (!existing.has(cand)) name = cand;
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: cwd ? path.basename(cwd) : '',
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 || '',