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 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 three ways that are visible in use:
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 workspace environment instead and referenced by the typed line, so
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.1",
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
- const result = parseResult(await inSession(sessionName, 'pane', 'list'));
481
- const panes = Array.isArray(result?.panes) ? result.panes : [];
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 workspace before the pane exists.
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 workspace = parseResult(await inSession(
521
- target, 'workspace', 'create', '--cwd', cwd, '--label', 'unsnooze',
522
- ...envFlags({ ...launchSpec?.env, ...argEnv }),
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