unsnooze 1.16.0 → 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 +28 -0
- package/README.md +41 -2
- package/package.json +1 -1
- package/src/multiplexers/herdr.js +67 -11
- package/src/multiplexers/session-name.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
19
|
+
## 1.16.1 — 2026-08-16
|
|
20
|
+
|
|
21
|
+
- **`unsnooze status` no longer offers an attach command for headless sessions.**
|
|
22
|
+
A headless "pane" is a detached pid, but the attach hint fell through to the
|
|
23
|
+
default `tmux attach -t <session>` — a command that at best does nothing and
|
|
24
|
+
at worst attaches to an unrelated tmux session that happens to share the name.
|
|
25
|
+
Headless output lives in `~/.unsnooze/headless/<session>.log`, and status now
|
|
26
|
+
says nothing rather than something wrong. Found while building the capability
|
|
27
|
+
table for the new **[Supported terminals](https://unsnooze.dev/docs/#terminals)**
|
|
28
|
+
docs section, which sets out exactly what each of tmux, Zellij, herdr, cmux
|
|
29
|
+
and headless does and does not do.
|
|
30
|
+
|
|
3
31
|
## 1.16.0 — 2026-08-16
|
|
4
32
|
|
|
5
33
|
- **Claude Design, and native Windows along the way.** ([#13](https://github.com/saaranshM/unsnooze/issues/13))
|
package/README.md
CHANGED
|
@@ -665,6 +665,38 @@ watcher stops (no pane context) always use native.
|
|
|
665
665
|
- zsh or bash (wrappers go into `~/.zshrc` / `~/.bashrc`), or PowerShell
|
|
666
666
|
(wrappers go into `$PROFILE.CurrentUserAllHosts`)
|
|
667
667
|
|
|
668
|
+
### Supported terminals
|
|
669
|
+
|
|
670
|
+
unsnooze drives four terminal multiplexers, and works without one.
|
|
671
|
+
|
|
672
|
+
| | [tmux](https://github.com/tmux/tmux) ≥ 3.2 | [Zellij](https://zellij.dev) | [herdr](https://herdr.dev) ≥ 0.8.0 | [cmux](https://cmux.dev) | `headless` |
|
|
673
|
+
|---|:---:|:---:|:---:|:---:|:---:|
|
|
674
|
+
| Detect a limit stop | pane + hook + transcript | pane + hook + transcript | pane + hook + transcript | pane + hook + transcript | hook + transcript |
|
|
675
|
+
| Resume a live session in place | ✅ | ✅ | ✅ | ✅ | — |
|
|
676
|
+
| Answer Claude's limit menu | ✅ | ✅ | ✅ | ✅ | — |
|
|
677
|
+
| Revive a session whose pane died | ✅ | ✅ | ✅¹ | ✅² | ✅³ |
|
|
678
|
+
| Wrap a bare `claude` into a managed session | ✅ | ✅ | ✅ | — | n/a⁴ |
|
|
679
|
+
| `unsnooze reap` idle sessions | ✅ | ✅ | ✅ | — | — |
|
|
680
|
+
| `attach:` hint in `unsnooze status` | ✅ | ✅ | ✅ | — | — |
|
|
681
|
+
| OSC notifications to your terminal | ✅ | — | — | — | — |
|
|
682
|
+
|
|
683
|
+
¹ Into a fresh session name (`unsnooze-2`), never by restarting the stopped one —
|
|
684
|
+
herdr restores saved agent panes itself, so a revival on top would resume the
|
|
685
|
+
same conversation twice.
|
|
686
|
+
² Into a fresh workspace; cmux has no joinable named-session model.
|
|
687
|
+
³ As a detached process. Its output goes to `~/.unsnooze/headless/<session>.log`,
|
|
688
|
+
since there is no scrollback to read it out of later.
|
|
689
|
+
⁴ Nothing to wrap into — your own terminal is the session.
|
|
690
|
+
|
|
691
|
+
Pick one explicitly with `unsnooze config set multiplexer tmux|zellij|herdr|cmux|headless`.
|
|
692
|
+
On `auto`, unsnooze uses the multiplexer you are currently inside; failing that,
|
|
693
|
+
the only one installed (tmux breaks ties); failing that, `headless`.
|
|
694
|
+
|
|
695
|
+
**Install:** `brew install tmux` or `brew install zellij` on macOS,
|
|
696
|
+
`sudo apt install tmux` on Linux/WSL. herdr comes from
|
|
697
|
+
[herdr.dev](https://herdr.dev) — Homebrew still ships 0.7.3, which `available()`
|
|
698
|
+
refuses, so take the release binary.
|
|
699
|
+
|
|
668
700
|
### Watching without a multiplexer
|
|
669
701
|
|
|
670
702
|
A terminal pane is only one of unsnooze's three detection channels. The Claude
|
|
@@ -719,7 +751,7 @@ explicitly with `unsnooze config set multiplexer tmux`, `zellij`, or `herdr`.
|
|
|
719
751
|
|
|
720
752
|
#### herdr specifics
|
|
721
753
|
|
|
722
|
-
herdr differs from tmux and Zellij in
|
|
754
|
+
herdr differs from tmux and Zellij in four ways that are visible in use:
|
|
723
755
|
|
|
724
756
|
- **unsnooze never restarts a stopped herdr session.** herdr restores saved
|
|
725
757
|
agent panes when a session restarts (`resume_agents_on_restore` is on by
|
|
@@ -729,10 +761,17 @@ herdr differs from tmux and Zellij in three ways that are visible in use:
|
|
|
729
761
|
reusing the name. If you restart a stopped session yourself and herdr brings
|
|
730
762
|
the agent back, unsnooze may still open a fresh pane for it; that case is not
|
|
731
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.)
|
|
732
771
|
- **Multi-line resume messages are carried, not typed.** herdr starts a pane
|
|
733
772
|
command by typing it into the pane's shell, where a newline would submit half
|
|
734
773
|
a command and a tab would be a completion request. Any such argument is passed
|
|
735
|
-
through the
|
|
774
|
+
through the new tab's environment instead and referenced by the typed line, so
|
|
736
775
|
a multi-paragraph `resumeMessage` reaches the agent as one argument, byte for
|
|
737
776
|
byte. (A NUL byte is still refused — an environment value cannot hold one.)
|
|
738
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
|
|
@@ -52,6 +52,11 @@ export function attachHint(muxName, sessionName) {
|
|
|
52
52
|
// is no command that attaches to a surface from outside cmux, so omit the
|
|
53
53
|
// hint rather than print a `tmux attach` that would silently do nothing.
|
|
54
54
|
if (muxName === 'cmux') return null;
|
|
55
|
+
// headless has no session at all — its "pane" is a detached pid. The default
|
|
56
|
+
// below would print `tmux attach -t <name>`, which at best does nothing and
|
|
57
|
+
// at worst attaches to an unrelated tmux session that happens to share the
|
|
58
|
+
// name. Its output lives in ~/.unsnooze/headless/<session>.log instead.
|
|
59
|
+
if (muxName === 'headless') return null;
|
|
55
60
|
if (muxName === 'herdr') return `herdr session attach ${sessionName}`;
|
|
56
61
|
if (muxName === 'zellij') return `zellij attach ${sessionName}`;
|
|
57
62
|
return `tmux attach -t ${sessionName}`;
|