@sandropadin/tend 0.1.0 → 0.2.0

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 CHANGED
@@ -28,9 +28,9 @@ without color: **`●` red = blocked**, **`⠧` yellow (animated spinner) = work
28
28
  `tend` needs **tmux** and **Node ≥ 20**. Install it from npm:
29
29
 
30
30
  ```sh
31
- npm install -g @spadin/tend # global — puts `tend` on your PATH
31
+ npm install -g @sandropadin/tend # global — puts `tend` on your PATH
32
32
  # or run it without installing:
33
- npx @spadin/tend
33
+ npx @sandropadin/tend
34
34
  ```
35
35
 
36
36
  **From source** (to hack on it) — Node ≥ 22.18 runs the TypeScript directly, no
@@ -131,10 +131,11 @@ Enter sends the selected agent to B.
131
131
  set -g status-right "#(cd #{pane_current_path} && tend --json | jq -r '...')"
132
132
  ```
133
133
 
134
- Or bind a key to a quick popup:
134
+ Or bind a key to a quick popup dashboard — `--popup` makes it exit after you
135
+ pick an agent, so the popup closes and drops you onto that pane:
135
136
 
136
137
  ```tmux
137
- bind-key g display-popup -E "tend; read"
138
+ bind-key g display-popup -E "tend --popup"
138
139
  ```
139
140
 
140
141
  ## How it works
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ function parseArgs(argv) {
29
29
  interval: 800,
30
30
  onceDelay: 350,
31
31
  other: false,
32
+ popup: false,
32
33
  };
33
34
  const args = [...argv];
34
35
  while (args.length) {
@@ -55,6 +56,9 @@ function parseArgs(argv) {
55
56
  case "--other":
56
57
  opts.other = true;
57
58
  break;
59
+ case "--popup":
60
+ opts.popup = true;
61
+ break;
58
62
  case "jump":
59
63
  opts.mode = "jump";
60
64
  opts.target = args.shift();
@@ -113,6 +117,7 @@ function printHelp() {
113
117
  " --other open jumps in the one other attached client",
114
118
  "",
115
119
  "Options:",
120
+ " --popup exit the dashboard after a jump — for tmux display-popup",
116
121
  " --interval <ms> dashboard refresh interval (default 800)",
117
122
  " --once-delay <ms> activity sampling window for --once (default 350)",
118
123
  "",
@@ -273,7 +278,7 @@ async function main() {
273
278
  if (interactiveTty && !opts.readonly) {
274
279
  // Optional preset target window from --to/--other; `o` cycles it live.
275
280
  const initialTarget = opts.targetClient ?? (opts.other ? await resolveTargetClient(opts) : null);
276
- await runPicker(scanOpts(opts), opts.interval, initialTarget ?? undefined);
281
+ await runPicker(scanOpts(opts), opts.interval, initialTarget ?? undefined, opts.popup);
277
282
  }
278
283
  else if (opts.readonly) {
279
284
  await runWatch(opts); // display-only repaint (explicit)
package/dist/pick.js CHANGED
@@ -21,7 +21,7 @@ const CLEAR_EOL = "\x1b[K"; // erase from cursor to end of *line*
21
21
  const CLEAR_BELOW = "\x1b[J"; // erase from cursor to end of screen
22
22
  const ANIM_MS = 90; // spinner tick — faster than the scan interval
23
23
  const shortTty = (tty) => tty.replace(/^\/dev\//, "");
24
- export async function runPicker(opts, intervalMs, initialTarget) {
24
+ export async function runPicker(opts, intervalMs, initialTarget, exitOnJump = false) {
25
25
  let statuses = [];
26
26
  let memory = new Map();
27
27
  let cursorPaneId = null; // track selection by id across rescans
@@ -133,6 +133,13 @@ export async function runPicker(opts, intervalMs, initialTarget) {
133
133
  input.removeListener("data", onKey);
134
134
  out.write(SHOW_CURSOR + ALT_OFF);
135
135
  };
136
+ // Tear down and exit after a jump that has already completed (the switch-client
137
+ // paths). Used in --popup mode so `display-popup -E` closes onto the agent.
138
+ const finishAfterJump = () => {
139
+ done = true;
140
+ cleanup();
141
+ process.exit(0);
142
+ };
136
143
  const jump = async () => {
137
144
  const target = selectable()[currentIndex()];
138
145
  if (!target)
@@ -141,6 +148,8 @@ export async function runPicker(opts, intervalMs, initialTarget) {
141
148
  if (targetTty) {
142
149
  try {
143
150
  await jumpToPane(target.pane, { client: targetTty });
151
+ if (exitOnJump)
152
+ finishAfterJump();
144
153
  statusMsg = `→ sent ${target.agent} (${target.pane.id}) to ${shortTty(targetTty)}`;
145
154
  }
146
155
  catch {
@@ -149,9 +158,13 @@ export async function runPicker(opts, intervalMs, initialTarget) {
149
158
  render();
150
159
  return;
151
160
  }
152
- // Targeting this window, inside tmux: switch our client but keep running.
161
+ // Targeting this window, inside tmux: switch our client. Normally we keep the
162
+ // dashboard alive in its own pane; in --popup mode we exit so the popup closes
163
+ // onto the agent instead of lingering on top of it.
153
164
  if (insideTmux()) {
154
165
  await jumpToPane(target.pane);
166
+ if (exitOnJump)
167
+ finishAfterJump();
155
168
  statusMsg = `→ jumped to ${target.agent} in ${target.pane.sessionName} (${target.pane.id})`;
156
169
  render();
157
170
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandropadin/tend",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight status detector for AI coding agents running inside tmux. Reports blocked / working / idle per pane by scraping the terminal, plus git branch state.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ interface Options {
41
41
  target?: string; // pane id for jump/debug
42
42
  targetClient?: string; // --to <tty>: open jumps in this client (window)
43
43
  other: boolean; // --other: open jumps in the one other attached client
44
+ popup: boolean; // --popup: dashboard exits after a jump (for tmux display-popup)
44
45
  }
45
46
 
46
47
  function parseArgs(argv: string[]): Options {
@@ -53,6 +54,7 @@ function parseArgs(argv: string[]): Options {
53
54
  interval: 800,
54
55
  onceDelay: 350,
55
56
  other: false,
57
+ popup: false,
56
58
  };
57
59
  const args = [...argv];
58
60
  while (args.length) {
@@ -79,6 +81,9 @@ function parseArgs(argv: string[]): Options {
79
81
  case "--other":
80
82
  opts.other = true;
81
83
  break;
84
+ case "--popup":
85
+ opts.popup = true;
86
+ break;
82
87
  case "jump":
83
88
  opts.mode = "jump";
84
89
  opts.target = args.shift();
@@ -139,6 +144,7 @@ function printHelp(): void {
139
144
  " --other open jumps in the one other attached client",
140
145
  "",
141
146
  "Options:",
147
+ " --popup exit the dashboard after a jump — for tmux display-popup",
142
148
  " --interval <ms> dashboard refresh interval (default 800)",
143
149
  " --once-delay <ms> activity sampling window for --once (default 350)",
144
150
  "",
@@ -313,7 +319,7 @@ async function main(): Promise<void> {
313
319
  // Optional preset target window from --to/--other; `o` cycles it live.
314
320
  const initialTarget =
315
321
  opts.targetClient ?? (opts.other ? await resolveTargetClient(opts) : null);
316
- await runPicker(scanOpts(opts), opts.interval, initialTarget ?? undefined);
322
+ await runPicker(scanOpts(opts), opts.interval, initialTarget ?? undefined, opts.popup);
317
323
  } else if (opts.readonly) {
318
324
  await runWatch(opts); // display-only repaint (explicit)
319
325
  } else if (opts.mode === "watch") {
package/src/pick.ts CHANGED
@@ -31,6 +31,7 @@ export async function runPicker(
31
31
  opts: ScanOptions,
32
32
  intervalMs: number,
33
33
  initialTarget?: string,
34
+ exitOnJump = false, // --popup: end the session after a jump so the popup closes
34
35
  ): Promise<void> {
35
36
  let statuses: AgentStatus[] = [];
36
37
  let memory = new Map<string, PaneMemory>();
@@ -145,6 +146,14 @@ export async function runPicker(
145
146
  out.write(SHOW_CURSOR + ALT_OFF);
146
147
  };
147
148
 
149
+ // Tear down and exit after a jump that has already completed (the switch-client
150
+ // paths). Used in --popup mode so `display-popup -E` closes onto the agent.
151
+ const finishAfterJump = () => {
152
+ done = true;
153
+ cleanup();
154
+ process.exit(0);
155
+ };
156
+
148
157
  const jump = async () => {
149
158
  const target = selectable()[currentIndex()];
150
159
  if (!target) return;
@@ -153,6 +162,7 @@ export async function runPicker(
153
162
  if (targetTty) {
154
163
  try {
155
164
  await jumpToPane(target.pane, { client: targetTty });
165
+ if (exitOnJump) finishAfterJump();
156
166
  statusMsg = `→ sent ${target.agent} (${target.pane.id}) to ${shortTty(targetTty)}`;
157
167
  } catch {
158
168
  statusMsg = `⚠ ${shortTty(targetTty)} unavailable`;
@@ -161,9 +171,12 @@ export async function runPicker(
161
171
  return;
162
172
  }
163
173
 
164
- // Targeting this window, inside tmux: switch our client but keep running.
174
+ // Targeting this window, inside tmux: switch our client. Normally we keep the
175
+ // dashboard alive in its own pane; in --popup mode we exit so the popup closes
176
+ // onto the agent instead of lingering on top of it.
165
177
  if (insideTmux()) {
166
178
  await jumpToPane(target.pane);
179
+ if (exitOnJump) finishAfterJump();
167
180
  statusMsg = `→ jumped to ${target.agent} in ${target.pane.sessionName} (${target.pane.id})`;
168
181
  render();
169
182
  return;