unsnooze 1.16.1 → 1.16.2
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/CHANGELOG.md +16 -0
- package/README.md +9 -2
- package/package.json +1 -1
- package/src/multiplexers/herdr.js +67 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.2 — 2026-08-22
|
|
4
|
+
|
|
5
|
+
- **A herdr revival now opens a tab in your project's workspace, not a new
|
|
6
|
+
workspace of its own.** ([#15](https://github.com/saaranshM/unsnooze/issues/15))
|
|
7
|
+
`newWindow` means "one more terminal for this work" — a window in tmux, a tab
|
|
8
|
+
in Zellij — and it was mapped to `herdr workspace create`. But a herdr
|
|
9
|
+
workspace is the *project*: it is per repo, the way a tmux session is. So
|
|
10
|
+
every reset left another top-level workspace for the same repo, and a week of
|
|
11
|
+
them was a sidebar full of duplicates. unsnooze now asks herdr which
|
|
12
|
+
workspace is already open on that directory and adds a tab there, creating a
|
|
13
|
+
workspace only when there is genuinely none — a session it just started, or a
|
|
14
|
+
directory herdr has nothing open on. A tab rather than a split, because a
|
|
15
|
+
revived agent draws a full-screen TUI and halving the pane you are looking at
|
|
16
|
+
would leave two terminals too narrow to use; neither takes focus away from
|
|
17
|
+
what you are doing.
|
|
18
|
+
|
|
3
19
|
## 1.16.1 — 2026-08-16
|
|
4
20
|
|
|
5
21
|
- **`unsnooze status` no longer offers an attach command for headless sessions.**
|
package/README.md
CHANGED
|
@@ -751,7 +751,7 @@ explicitly with `unsnooze config set multiplexer tmux`, `zellij`, or `herdr`.
|
|
|
751
751
|
|
|
752
752
|
#### herdr specifics
|
|
753
753
|
|
|
754
|
-
herdr differs from tmux and Zellij in
|
|
754
|
+
herdr differs from tmux and Zellij in four ways that are visible in use:
|
|
755
755
|
|
|
756
756
|
- **unsnooze never restarts a stopped herdr session.** herdr restores saved
|
|
757
757
|
agent panes when a session restarts (`resume_agents_on_restore` is on by
|
|
@@ -761,10 +761,17 @@ herdr differs from tmux and Zellij in three ways that are visible in use:
|
|
|
761
761
|
reusing the name. If you restart a stopped session yourself and herdr brings
|
|
762
762
|
the agent back, unsnooze may still open a fresh pane for it; that case is not
|
|
763
763
|
yet detected.
|
|
764
|
+
- **A revived session opens a tab, not a workspace.** A herdr workspace is the
|
|
765
|
+
project — per repo, the way a tmux *session* is — while a tab is what tmux
|
|
766
|
+
calls a window. So a revival opens a new tab in whichever workspace herdr
|
|
767
|
+
already has open on that directory, and only creates a workspace when there
|
|
768
|
+
is none. (A tab rather than a split: a revived agent draws a full-screen TUI,
|
|
769
|
+
and halving the pane you are looking at would leave two terminals too narrow
|
|
770
|
+
to use. Neither steals focus.)
|
|
764
771
|
- **Multi-line resume messages are carried, not typed.** herdr starts a pane
|
|
765
772
|
command by typing it into the pane's shell, where a newline would submit half
|
|
766
773
|
a command and a tab would be a completion request. Any such argument is passed
|
|
767
|
-
through the
|
|
774
|
+
through the new tab's environment instead and referenced by the typed line, so
|
|
768
775
|
a multi-paragraph `resumeMessage` reaches the agent as one argument, byte for
|
|
769
776
|
byte. (A NUL byte is still refused — an environment value cannot hold one.)
|
|
770
777
|
- **A custom `HERDR_SOCKET_PATH` that does not match the session being
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unsnooze",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.2",
|
|
4
4
|
"description": "Automatically resume Claude Code, Codex CLI, Grok, Qwen Code, Kimi CLI, OpenCode, and Antigravity sessions when 5-hour or weekly usage limits reset—on macOS, Linux and Windows, with or without a terminal multiplexer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// remains alive with its shell prompt. The resumer must therefore verify
|
|
3
3
|
// agent ownership before injecting, and revival can reuse a surviving pane
|
|
4
4
|
// only when Task 3 wires that path deliberately.
|
|
5
|
-
// Launch env is delivered via workspace create --env. Keep this narrow because
|
|
5
|
+
// Launch env is delivered via tab/workspace create --env. Keep this narrow because
|
|
6
6
|
// pane run types argv into the pane shell, but preserve the two Claude config
|
|
7
7
|
// roots: desktop/isolated Claude sessions cannot be resumed without them.
|
|
8
8
|
|
|
@@ -276,6 +276,66 @@ export function createHerdr({ spawner = defaultSpawner, env = process.env } = {}
|
|
|
276
276
|
return syncOutput(result);
|
|
277
277
|
};
|
|
278
278
|
|
|
279
|
+
const listPaneRows = async session => {
|
|
280
|
+
const result = parseResult(await inSession(session, 'pane', 'list'));
|
|
281
|
+
return Array.isArray(result?.panes) ? result.panes : [];
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const samePath = (a, b) => {
|
|
285
|
+
if (typeof a !== 'string' || typeof b !== 'string' || !a || !b) return false;
|
|
286
|
+
const trim = value => (value.length > 1 ? value.replace(/\/+$/, '') : value);
|
|
287
|
+
return trim(a) === trim(b);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// The workspace herdr already has open on this project, if any.
|
|
291
|
+
//
|
|
292
|
+
// A herdr workspace is the project, not the window: it is where a repo
|
|
293
|
+
// lives, the way a tmux SESSION is, and creating one per revival left a
|
|
294
|
+
// user with N top-level workspaces for one repo (#15). herdr reports no cwd
|
|
295
|
+
// on a workspace, so the project is read off its panes instead — `pane
|
|
296
|
+
// list` walks workspaces in order, so the first match is the oldest
|
|
297
|
+
// workspace on this path, which is the user's own rather than one of ours.
|
|
298
|
+
const workspaceOnProject = async (session, cwd) => {
|
|
299
|
+
try {
|
|
300
|
+
const match = (await listPaneRows(session)).find(pane =>
|
|
301
|
+
samePath(pane?.cwd, cwd) || samePath(pane?.foreground_cwd, cwd));
|
|
302
|
+
return match?.workspace_id ? String(match.workspace_id) : null;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const rootPaneId = (created, what) => {
|
|
309
|
+
const pane = created?.root_pane?.pane_id;
|
|
310
|
+
if (!pane) throw new Error(`unsnooze: unexpected herdr ${what} shape: no root_pane`);
|
|
311
|
+
return String(pane);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// One more full-size terminal for `cwd`, in the workspace that project
|
|
315
|
+
// already occupies. A TAB, not a pane split: `newWindow` means the same
|
|
316
|
+
// thing on every backend — a tmux window, a zellij tab — and a revived
|
|
317
|
+
// agent draws a full-screen TUI, so halving the pane the user is looking at
|
|
318
|
+
// would leave two terminals too narrow to use. Only when herdr has no
|
|
319
|
+
// workspace on this project (a session we just started has none at all)
|
|
320
|
+
// does a new workspace get created.
|
|
321
|
+
const openTerminal = async (session, cwd, envArgs) => {
|
|
322
|
+
const workspace = await workspaceOnProject(session, cwd);
|
|
323
|
+
if (workspace) {
|
|
324
|
+
try {
|
|
325
|
+
return rootPaneId(parseResult(await inSession(
|
|
326
|
+
session, 'tab', 'create', '--workspace', workspace,
|
|
327
|
+
'--cwd', cwd, '--label', 'unsnooze', ...envArgs,
|
|
328
|
+
)), 'tab');
|
|
329
|
+
} catch {
|
|
330
|
+
// The workspace can close between the listing and the create. A
|
|
331
|
+
// revival into a fresh workspace still revives; failing does not.
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return rootPaneId(parseResult(await inSession(
|
|
335
|
+
session, 'workspace', 'create', '--cwd', cwd, '--label', 'unsnooze', ...envArgs,
|
|
336
|
+
)), 'workspace');
|
|
337
|
+
};
|
|
338
|
+
|
|
279
339
|
const backend = {
|
|
280
340
|
name: 'herdr',
|
|
281
341
|
owner,
|
|
@@ -477,9 +537,8 @@ export function createHerdr({ spawner = defaultSpawner, env = process.env } = {}
|
|
|
477
537
|
|
|
478
538
|
async listSessionPanes(sessionName) {
|
|
479
539
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return panes.map(entry => String(entry.pane_id)).filter(Boolean);
|
|
540
|
+
return (await listPaneRows(sessionName))
|
|
541
|
+
.map(entry => String(entry.pane_id)).filter(Boolean);
|
|
483
542
|
} catch {
|
|
484
543
|
return [];
|
|
485
544
|
}
|
|
@@ -515,14 +574,11 @@ export function createHerdr({ spawner = defaultSpawner, env = process.env } = {}
|
|
|
515
574
|
}
|
|
516
575
|
// Build the command first: an argument that cannot be typed (a
|
|
517
576
|
// multi-line resume message) travels as environment instead, and that
|
|
518
|
-
// environment has to be on the
|
|
577
|
+
// environment has to be on the tab before its pane exists.
|
|
519
578
|
const { line, env: argEnv } = shellCommand(launchSpec.file, launchSpec.args || []);
|
|
520
|
-
const
|
|
521
|
-
target,
|
|
522
|
-
|
|
523
|
-
));
|
|
524
|
-
const pane = workspace?.root_pane?.pane_id;
|
|
525
|
-
if (!pane) throw new Error('unsnooze: unexpected herdr workspace shape: no root_pane');
|
|
579
|
+
const pane = await openTerminal(
|
|
580
|
+
target, cwd, envFlags({ ...launchSpec?.env, ...argEnv }),
|
|
581
|
+
);
|
|
526
582
|
// One argument, already quoted: `pane run` joins its operands with
|
|
527
583
|
// spaces and types them into the pane's shell, so the quoting has to be
|
|
528
584
|
// ours. It also submits the line itself (PaneSendInput carries
|