@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 +5 -4
- package/dist/index.js +6 -1
- package/dist/pick.js +15 -2
- package/package.json +1 -1
- package/src/index.ts +7 -1
- package/src/pick.ts +14 -1
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 @
|
|
31
|
+
npm install -g @sandropadin/tend # global — puts `tend` on your PATH
|
|
32
32
|
# or run it without installing:
|
|
33
|
-
npx @
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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;
|