@zgeoff/atc 0.1.9 → 0.1.10

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.
@@ -1,7 +1,9 @@
1
1
  import { Database } from 'bun:sqlite';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
+ import { toAgentKind } from './agent-adapter';
4
+ import type { AgentKind } from './agent-adapter';
3
5
  import type { HookEvent } from './hooks';
4
- import { isRecord } from './report';
6
+ import { parseFleetEntry } from './sessions';
5
7
  import type { FleetEntry } from './sessions';
6
8
 
7
9
  /**
@@ -21,11 +23,12 @@ export class StateStore {
21
23
 
22
24
  this.db.run(`
23
25
  CREATE TABLE IF NOT EXISTS fleet (
24
- claude_id TEXT PRIMARY KEY,
26
+ agent_session_id TEXT PRIMARY KEY,
25
27
  name TEXT NOT NULL,
26
28
  cwd TEXT NOT NULL,
27
29
  pinned INTEGER NOT NULL DEFAULT 0,
28
- last_attached INTEGER
30
+ last_attached INTEGER,
31
+ agent TEXT NOT NULL DEFAULT 'claude'
29
32
  );
30
33
  CREATE TABLE IF NOT EXISTS events (
31
34
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -39,10 +42,14 @@ export class StateStore {
39
42
  cwd TEXT PRIMARY KEY,
40
43
  last_spawn INTEGER NOT NULL
41
44
  );
45
+ CREATE TABLE IF NOT EXISTS prefs (
46
+ key TEXT PRIMARY KEY,
47
+ value TEXT NOT NULL
48
+ );
42
49
  `);
43
50
 
44
- // Stores created before pinning and attach recency existed lack the
45
- // columns; the create above only applies to fresh databases.
51
+ // Stores created before pinning, attach recency, or agent kind existed
52
+ // lack the columns; the create above only applies to fresh databases.
46
53
  const fleetColumns = new Set(
47
54
  this.db
48
55
  .query<{ name: string }, []>('PRAGMA table_info(fleet)')
@@ -50,6 +57,12 @@ export class StateStore {
50
57
  .map((c) => c.name),
51
58
  );
52
59
 
60
+ // Stores created before the id column was agent-neutral carry it under
61
+ // its Claude-era name.
62
+ if (fleetColumns.has('claude_id')) {
63
+ this.db.run('ALTER TABLE fleet RENAME COLUMN claude_id TO agent_session_id');
64
+ }
65
+
53
66
  if (!fleetColumns.has('pinned')) {
54
67
  this.db.run('ALTER TABLE fleet ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0');
55
68
  }
@@ -58,6 +71,10 @@ export class StateStore {
58
71
  this.db.run('ALTER TABLE fleet ADD COLUMN last_attached INTEGER');
59
72
  }
60
73
 
74
+ if (!fleetColumns.has('agent')) {
75
+ this.db.run("ALTER TABLE fleet ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude'");
76
+ }
77
+
61
78
  if (legacyFleetPath !== undefined) {
62
79
  this.adoptLegacyFleet(legacyFleetPath);
63
80
  }
@@ -67,23 +84,25 @@ export class StateStore {
67
84
  const rows = this.db
68
85
  .query<
69
86
  {
70
- claude_id: string;
87
+ agent_session_id: string;
71
88
  name: string;
72
89
  cwd: string;
73
90
  pinned: number;
74
91
  last_attached: number | null;
92
+ agent: string | null;
75
93
  },
76
94
  []
77
- >('SELECT claude_id, name, cwd, pinned, last_attached FROM fleet')
95
+ >('SELECT agent_session_id, name, cwd, pinned, last_attached, agent FROM fleet')
78
96
  .all();
79
97
 
80
98
  const entries: FleetEntry[] = [];
81
99
 
82
100
  for (const row of rows) {
83
101
  entries.push({
84
- claudeId: row.claude_id,
102
+ agentSessionID: row.agent_session_id,
85
103
  name: row.name,
86
104
  cwd: row.cwd,
105
+ agent: toAgentKind(row.agent),
87
106
  ...(row.pinned === 0 ? {} : { pinned: true }),
88
107
  ...(row.last_attached === null ? {} : { lastAttachedAt: row.last_attached }),
89
108
  });
@@ -109,13 +128,20 @@ export class StateStore {
109
128
  this.db.run('DELETE FROM fleet');
110
129
 
111
130
  const insert = this.db.query(
112
- 'INSERT OR REPLACE INTO fleet (claude_id, name, cwd, pinned, last_attached) VALUES (?1, ?2, ?3, ?4, ?5)',
131
+ 'INSERT OR REPLACE INTO fleet (agent_session_id, name, cwd, pinned, last_attached, agent) VALUES (?1, ?2, ?3, ?4, ?5, ?6)',
113
132
  );
114
133
 
115
134
  for (const entry of all) {
116
135
  const pinned = entry.pinned === true ? 1 : 0;
117
136
 
118
- insert.run(entry.claudeId, entry.name, entry.cwd, pinned, entry.lastAttachedAt ?? null);
137
+ insert.run(
138
+ entry.agentSessionID,
139
+ entry.name,
140
+ entry.cwd,
141
+ pinned,
142
+ entry.lastAttachedAt ?? null,
143
+ entry.agent,
144
+ );
119
145
  }
120
146
  });
121
147
 
@@ -124,7 +150,7 @@ export class StateStore {
124
150
 
125
151
  recordEvent(e: HookEvent): void {
126
152
  const rawMessage = e.payload['message'];
127
- const rawSessionID = e.payload['session_id'];
153
+ const rawSessionID = e.payload['session_id'] ?? e.payload['sessionId'];
128
154
  const message = typeof rawMessage === 'string' ? rawMessage : null;
129
155
  const sessionID = typeof rawSessionID === 'string' ? rawSessionID : null;
130
156
 
@@ -151,6 +177,22 @@ export class StateStore {
151
177
  return rows.map((row) => row.cwd);
152
178
  }
153
179
 
180
+ loadLastUsedAgent(): AgentKind {
181
+ const row = this.db
182
+ .query<{ value: string }, []>("SELECT value FROM prefs WHERE key = 'last_used_agent'")
183
+ .get();
184
+
185
+ return toAgentKind(row?.value);
186
+ }
187
+
188
+ writeLastUsedAgent(agent: AgentKind): void {
189
+ this.db
190
+ .query(
191
+ 'INSERT INTO prefs (key, value) VALUES (?1, ?2) ON CONFLICT(key) DO UPDATE SET value = ?2',
192
+ )
193
+ .run('last_used_agent', agent);
194
+ }
195
+
154
196
  stop(): void {
155
197
  this.db.close();
156
198
  }
@@ -169,13 +211,15 @@ export class StateStore {
169
211
  return;
170
212
  }
171
213
 
172
- const entries = parsed.filter(
173
- (entry): entry is FleetEntry =>
174
- isRecord(entry) &&
175
- typeof entry['name'] === 'string' &&
176
- typeof entry['cwd'] === 'string' &&
177
- typeof entry['claudeId'] === 'string',
178
- );
214
+ const entries: FleetEntry[] = [];
215
+
216
+ for (const entry of parsed) {
217
+ const parsedEntry = parseFleetEntry(entry);
218
+
219
+ if (parsedEntry !== undefined) {
220
+ entries.push(parsedEntry);
221
+ }
222
+ }
179
223
 
180
224
  this.writeFleet(entries);
181
225
  } catch {}
package/src/ui.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import type { AgentKind } from './agent-adapter';
1
2
  import { formatDir } from './dirs';
3
+ import { formatOverlayAgentMark } from './format-overlay-agent-mark';
2
4
  import { RESET_INPUT_MODES } from './reset-input-modes';
3
5
  import { PINNED_GROUP_KEY } from './sessions';
4
6
  import type { SessionState } from './sessions';
@@ -159,8 +161,9 @@ function dimRow(width: number, text: string): Row {
159
161
  // Selection-dependent hints need the liveness facts of each row.
160
162
  export interface OverlaySessionView extends SessionView {
161
163
  readonly alive: boolean;
162
- readonly kind: 'pty' | 'jsonl';
164
+ readonly kind: 'pty' | 'headless';
163
165
  readonly resumable: boolean;
166
+ readonly agent: AgentKind;
164
167
  readonly pinned: boolean;
165
168
  readonly repoRoot: string;
166
169
  }
@@ -202,14 +205,18 @@ export function drawOverlay(view: OverlayView) {
202
205
  const name = truncate(s.name, 16).padEnd(16);
203
206
  const state = STATE_LABEL[s.state].padEnd(9);
204
207
  const dir = view.grouped ? '' : ` ${truncate(formatDir(s.cwd), 18).padEnd(18)}`;
205
- const msgWidth = Math.max(4, width - 4 - 3 - 17 - 10 - (view.grouped ? 0 : 19));
208
+ const msgWidth = Math.max(4, width - 4 - 4 - 17 - 10 - (view.grouped ? 0 : 19));
206
209
  const msg = truncate(s.lastMsg, msgWidth).padEnd(msgWidth);
207
210
  const unread = s.unread ? `${ESC}[1;33m!${ESC}[0m` : ' ';
208
211
  const pin = s.pinned ? `${ESC}[93m⋆${ESC}[0m` : ' ';
212
+ const mark = formatOverlayAgentMark(s.agent);
213
+ const styledMark = mark === 'g' ? `${ESC}[90m${mark}${ESC}[0m` : mark;
209
214
  const body = `${name} ${state}${dir} ${msg}`;
210
215
  const styledBody = sel ? `${ESC}[7m${body}${ESC}[0m` : body;
211
216
 
212
- rowsList.push(boxRow(width, `${GLYPH[s.state]}${unread}${pin}${styledBody}`, 3 + body.length));
217
+ rowsList.push(
218
+ boxRow(width, `${GLYPH[s.state]}${unread}${pin}${styledMark}${styledBody}`, 4 + body.length),
219
+ );
213
220
  }
214
221
 
215
222
  rowsList.push(boxDivider(width));
@@ -245,15 +252,15 @@ export function drawOverlay(view: OverlayView) {
245
252
  const GLOBAL_HINT = 'g groups · n new · ? keys';
246
253
 
247
254
  // Only the actions valid for the selected row appear; the full reference
248
- // lives behind ?.
249
- function buildOverlayHint(s: OverlaySessionView | undefined): string {
255
+ // lives behind ?. Grok has no headless handoff, so H is omitted there.
256
+ export function buildOverlayHint(s: OverlaySessionView | undefined): string {
250
257
  if (s === undefined) {
251
258
  return GLOBAL_HINT;
252
259
  }
253
260
 
254
261
  const actions: string[] = [];
255
262
 
256
- if (s.kind === 'jsonl' && s.alive) {
263
+ if (s.kind === 'headless' && s.alive) {
257
264
  actions.push('P reattach', 'K kill');
258
265
  } else if (s.alive) {
259
266
  actions.push('⏎ attach');
@@ -262,7 +269,11 @@ function buildOverlayHint(s: OverlaySessionView | undefined): string {
262
269
  actions.push('a ack');
263
270
  }
264
271
 
265
- actions.push('H headless', 'y yank', 'Y eject', 'K kill');
272
+ if (s.agent !== 'grok') {
273
+ actions.push('H headless');
274
+ }
275
+
276
+ actions.push('y yank', 'Y eject', 'K kill');
266
277
  } else {
267
278
  if (s.resumable) {
268
279
  actions.push('P revive', 'y yank');
@@ -285,13 +296,14 @@ export function drawHelp() {
285
296
  '⏎ attach the selected session',
286
297
  '⇥ attach the most urgent needs-you, else latest done',
287
298
  'a ack its notification without attaching',
288
- 'H eject to a headless run',
299
+ 'H eject to a headless run (Claude only)',
289
300
  'P revive a dead or headless session',
290
301
  'y yank the resume command to the clipboard',
291
302
  'Y yank the resume command, then kill here',
292
303
  'K kill (K again on a dead session forgets it)',
293
304
  'p pin or unpin — pinned sessions stay on top',
294
305
  'g toggle grouping by repository',
306
+ ' Grok rows show a dim g after the pin mark',
295
307
  'n new session',
296
308
  'r adopt an external session',
297
309
  '/ filter · ↑↓/jk move · q quit',
@@ -359,10 +371,10 @@ export function drawHome(fleetCount = 0, leaderLabel = '^Space') {
359
371
  out(ansi.clear + ansi.hideCursor);
360
372
 
361
373
  const msgs = [
362
- 'atc — control tower for claude sessions',
374
+ 'atc — control tower for Claude and Grok sessions',
363
375
  '',
364
376
  'n spawn a session',
365
- 'r adopt an existing session (claude --resume)',
377
+ 'r adopt an existing session',
366
378
  ...(fleetCount > 0 ? [`R restore last fleet (${fleetCount} sessions)`] : []),
367
379
  `${leaderLabel.padEnd(7)} session list`,
368
380
  'q quit',