@zgeoff/atc 0.1.6 → 0.1.7
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 +1 -1
- package/package.json +1 -1
- package/src/index.ts +18 -3
- package/src/pick-tab-target.ts +31 -0
- package/src/ui.ts +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ fine nested inside zellij/tmux (give the pane locked mode so Ctrl-Space reaches
|
|
|
43
43
|
| `R` | home | restore last fleet after a daemon death — respawns every session via `claude --resume <id>` |
|
|
44
44
|
| `j`/`k`/`↑`/`↓` | overlay/picker | move |
|
|
45
45
|
| `Enter` | overlay | attach (auto-acks) |
|
|
46
|
-
| `Tab` | overlay | attach the most urgent needs-you session
|
|
46
|
+
| `Tab` | overlay | attach the most urgent needs-you session, else the latest turn-done one |
|
|
47
47
|
| `/` | overlay | fuzzy filter by name/dir (chars in order), `⏎` attach top match, `esc` clear |
|
|
48
48
|
| `a` | overlay | ack notification without attaching |
|
|
49
49
|
| `H` | overlay | eject to headless: the terminal dies, a headless Agent SDK run resumes the same session |
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { basename } from 'node:path';
|
|
|
2
2
|
import { bootDaemonClient } from './boot-daemon';
|
|
3
3
|
import { loadConfig } from './config';
|
|
4
4
|
import { collectDirs, findFuzzyScore, formatDir, pickMatches } from './dirs';
|
|
5
|
+
import { pickTabTarget } from './pick-tab-target';
|
|
5
6
|
import type { EventMsg } from './protocol';
|
|
6
7
|
import { isRecord } from './report';
|
|
7
8
|
import { countSessionStates, sortGroupedSessionViews, sortSessionViews } from './sessions';
|
|
@@ -39,6 +40,12 @@ let confirmKill = false;
|
|
|
39
40
|
|
|
40
41
|
// The client's mirror of the daemon's fleet, kept fresh by events.
|
|
41
42
|
let fleet: MirrorSession[] = [];
|
|
43
|
+
|
|
44
|
+
// When each session last flipped to done, so the Tab jump can prefer the
|
|
45
|
+
// most recently finished turn. Sessions already done before this client
|
|
46
|
+
// connected have no entry and rank last.
|
|
47
|
+
const doneAt = new Map<string, number>();
|
|
48
|
+
|
|
42
49
|
let focusedID: string | null = null;
|
|
43
50
|
let fleetCount = 0;
|
|
44
51
|
|
|
@@ -471,6 +478,10 @@ function applyDaemonEvent(e: EventMsg) {
|
|
|
471
478
|
if (s !== undefined) {
|
|
472
479
|
const next = SESSION_STATES.find((x) => x === e['state']);
|
|
473
480
|
|
|
481
|
+
if (next === 'done' && s.state !== 'done') {
|
|
482
|
+
doneAt.set(s.id, Date.now());
|
|
483
|
+
}
|
|
484
|
+
|
|
474
485
|
if (next !== undefined) {
|
|
475
486
|
s.state = next;
|
|
476
487
|
}
|
|
@@ -524,6 +535,10 @@ function applyDaemonEvent(e: EventMsg) {
|
|
|
524
535
|
case 'session.removed': {
|
|
525
536
|
fleet = fleet.filter((x) => x.id !== e['s']);
|
|
526
537
|
|
|
538
|
+
if (typeof e['s'] === 'string') {
|
|
539
|
+
doneAt.delete(e['s']);
|
|
540
|
+
}
|
|
541
|
+
|
|
527
542
|
if (focusedID === e['s']) {
|
|
528
543
|
focusedID = null;
|
|
529
544
|
}
|
|
@@ -695,10 +710,10 @@ function applyOverlayKey(buf: Buffer) {
|
|
|
695
710
|
const ch = buf.toString();
|
|
696
711
|
|
|
697
712
|
if (buf[0] === KEY.tab) {
|
|
698
|
-
const
|
|
713
|
+
const target = pickTabTarget(fleet, (id) => doneAt.get(id));
|
|
699
714
|
|
|
700
|
-
if (
|
|
701
|
-
void attach(
|
|
715
|
+
if (target !== undefined) {
|
|
716
|
+
void attach(target.id);
|
|
702
717
|
}
|
|
703
718
|
|
|
704
719
|
return;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SessionState } from './sessions';
|
|
2
|
+
import { sortSessionViews } from './sessions';
|
|
3
|
+
|
|
4
|
+
interface TabCandidate {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly state: SessionState;
|
|
7
|
+
readonly createdAt: number;
|
|
8
|
+
readonly alive: boolean;
|
|
9
|
+
readonly kind: 'pty' | 'jsonl';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The Tab jump target: the most urgent alive needs-you session, else the
|
|
14
|
+
* alive terminal session whose turn finished most recently. Headless
|
|
15
|
+
* sessions never qualify for the fallback — they have no terminal to
|
|
16
|
+
* attach — and sessions with no recorded finish time rank last.
|
|
17
|
+
*/
|
|
18
|
+
export function pickTabTarget<T extends TabCandidate>(
|
|
19
|
+
list: readonly T[],
|
|
20
|
+
findDoneAt: (id: string) => number | undefined,
|
|
21
|
+
): T | undefined {
|
|
22
|
+
const needy = sortSessionViews(list).find((s) => s.state === 'needs_you' && s.alive);
|
|
23
|
+
|
|
24
|
+
if (needy !== undefined) {
|
|
25
|
+
return needy;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return sortSessionViews(list)
|
|
29
|
+
.filter((s) => s.state === 'done' && s.alive && s.kind === 'pty')
|
|
30
|
+
.toSorted((a, b) => (findDoneAt(b.id) ?? 0) - (findDoneAt(a.id) ?? 0))[0];
|
|
31
|
+
}
|
package/src/ui.ts
CHANGED
|
@@ -274,7 +274,7 @@ export function drawHelp() {
|
|
|
274
274
|
|
|
275
275
|
const lines = [
|
|
276
276
|
'⏎ attach the selected session',
|
|
277
|
-
'⇥ attach the most urgent needs-you
|
|
277
|
+
'⇥ attach the most urgent needs-you, else latest done',
|
|
278
278
|
'a ack its notification without attaching',
|
|
279
279
|
'H eject to a headless run',
|
|
280
280
|
'P revive a dead or headless session',
|