@zgeoff/atc 0.1.12 → 1.0.1

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.
Files changed (75) hide show
  1. package/README.md +50 -12
  2. package/package.json +7 -2
  3. package/src/{agent-adapter.ts → agents/agent-adapter.ts} +31 -14
  4. package/src/{build-cli-command.ts → agents/build-cli-command.ts} +1 -1
  5. package/src/agents/build-hook-settings.ts +59 -0
  6. package/src/{claude-adapter.ts → agents/claude-adapter.ts} +41 -80
  7. package/src/{codex-adapter.ts → agents/codex-adapter.ts} +41 -30
  8. package/src/agents/gateway-adapter.ts +106 -0
  9. package/src/{grok-adapter.ts → agents/grok-adapter.ts} +83 -46
  10. package/src/agents/resolve-agent-home.ts +13 -0
  11. package/src/agents/truncate-detail.ts +7 -0
  12. package/src/agents/write-hook-settings.ts +37 -0
  13. package/src/cli.ts +25 -14
  14. package/src/{boot-daemon.ts → client/boot-daemon.ts} +8 -7
  15. package/src/client/build-client-machine.ts +119 -0
  16. package/src/client/collect-agent-picks.ts +31 -0
  17. package/src/{daemon-client.ts → client/daemon-client.ts} +4 -4
  18. package/src/{dirs.ts → client/dirs.ts} +8 -0
  19. package/src/client/format-overlay-agent-mark.ts +15 -0
  20. package/src/{index.ts → client/index.ts} +181 -550
  21. package/src/client/keys.ts +111 -0
  22. package/src/client/parse-daemon-event.ts +114 -0
  23. package/src/{pick-tab-target.ts → client/pick-tab-target.ts} +2 -2
  24. package/src/client/plan-vacated-rows.ts +26 -0
  25. package/src/client/spawn-picker.ts +305 -0
  26. package/src/client/to-mirror-session.ts +65 -0
  27. package/src/{ui.ts → client/ui.ts} +27 -8
  28. package/src/{attach-registry.ts → daemon/attach-registry.ts} +12 -10
  29. package/src/daemon/build-session-event.ts +43 -0
  30. package/src/{daemon-connection.ts → daemon/daemon-connection.ts} +194 -108
  31. package/src/{daemon.ts → daemon/daemon.ts} +148 -313
  32. package/src/{hooks.ts → daemon/hooks.ts} +6 -4
  33. package/src/daemon/mint-session-id.ts +12 -0
  34. package/src/{permission-registry.ts → daemon/permission-registry.ts} +5 -3
  35. package/src/daemon/restore-fleet.ts +141 -0
  36. package/src/daemon/run-eject-handoff.ts +33 -0
  37. package/src/{screen-model.ts → daemon/screen-model.ts} +1 -1
  38. package/src/daemon/session-runtime.ts +64 -0
  39. package/src/{sessions.ts → daemon/sessions.ts} +77 -133
  40. package/src/{start-headless-run.ts → daemon/start-headless-run.ts} +9 -3
  41. package/src/daemon/start-headless-turn.ts +57 -0
  42. package/src/hook-report.ts +2 -2
  43. package/src/mcp-server.ts +33 -44
  44. package/src/protocol/parse-request-params.ts +31 -0
  45. package/src/{protocol.ts → protocol/protocol.ts} +1 -1
  46. package/src/protocol/request-param-schemas.ts +109 -0
  47. package/src/shared/agent-session-id.ts +8 -0
  48. package/src/shared/build-optional-boolean.ts +10 -0
  49. package/src/shared/build-optional-string-array.ts +14 -0
  50. package/src/shared/build-optional-string.ts +10 -0
  51. package/src/shared/collect-gateways.ts +113 -0
  52. package/src/{config.ts → shared/config.ts} +47 -31
  53. package/src/{get-build.ts → shared/get-build.ts} +21 -6
  54. package/src/shared/get-record.ts +20 -0
  55. package/src/shared/session-id.ts +8 -0
  56. package/src/shared/to-agent-session-id.ts +12 -0
  57. package/src/shared/to-session-id.ts +11 -0
  58. package/src/shared/to-shell-arg.ts +7 -0
  59. package/src/statusline.ts +2 -2
  60. package/src/store/bun-sqlite-driver.ts +96 -0
  61. package/src/store/fleet-entry.ts +72 -0
  62. package/src/store/run-migrations.ts +242 -0
  63. package/src/store/state-store.ts +226 -0
  64. package/src/format-overlay-agent-mark.ts +0 -17
  65. package/src/state-store.ts +0 -236
  66. /package/src/{normalize-hook-event.ts → agents/normalize-hook-event.ts} +0 -0
  67. /package/src/{print-codex-hook-file.ts → agents/print-codex-hook-file.ts} +0 -0
  68. /package/src/{print-grok-hook-file.ts → agents/print-grok-hook-file.ts} +0 -0
  69. /package/src/{build-leader-chords.ts → client/build-leader-chords.ts} +0 -0
  70. /package/src/{daemon-error.ts → protocol/daemon-error.ts} +0 -0
  71. /package/src/{outbound-queue.ts → protocol/outbound-queue.ts} +0 -0
  72. /package/src/{collect-clean-env.ts → shared/collect-clean-env.ts} +0 -0
  73. /package/src/{report.ts → shared/report.ts} +0 -0
  74. /package/src/{reset-input-modes.ts → shared/reset-input-modes.ts} +0 -0
  75. /package/src/{resolve-repo-root.ts → shared/resolve-repo-root.ts} +0 -0
@@ -1,9 +1,11 @@
1
- import type { AgentKind } from './agent-adapter';
1
+ import type { AgentID } from '../agents/agent-adapter';
2
+ import { PINNED_GROUP_KEY } from '../daemon/sessions';
3
+ import type { SessionState } from '../daemon/sessions';
4
+ import { RESET_INPUT_MODES } from '../shared/reset-input-modes';
2
5
  import { formatDir } from './dirs';
3
6
  import { formatOverlayAgentMark } from './format-overlay-agent-mark';
4
- import { RESET_INPUT_MODES } from './reset-input-modes';
5
- import { PINNED_GROUP_KEY } from './sessions';
6
- import type { SessionState } from './sessions';
7
+ import { planVacatedRows } from './plan-vacated-rows';
8
+ import type { BoxExtent } from './plan-vacated-rows';
7
9
 
8
10
  // The slice of a session the drawing layer needs; satisfied by both the
9
11
  // daemon's sessions and the wire descriptors a client mirrors.
@@ -119,12 +121,24 @@ interface Row {
119
121
  readonly width: number;
120
122
  }
121
123
 
124
+ // The extent of the last box drawn, so the next draw can erase the rows a
125
+ // shrinking or shifting box leaves behind. Erasing an already-blank row is a
126
+ // no-op, so a stale extent after a full clear is harmless.
127
+ let lastBoxExtent: BoxExtent | null = null;
128
+
122
129
  function drawBox(rowsList: readonly Row[]) {
123
130
  const boxWidth = Math.max(...rowsList.map((r) => r.width));
124
131
  const top = Math.max(1, Math.floor((rows() - 1 - rowsList.length) / 2));
125
132
  const left = Math.max(1, Math.floor((cols() - boxWidth) / 2));
133
+ const extent: BoxExtent = { top, height: rowsList.length };
126
134
  let buf = ansi.hideCursor;
127
135
 
136
+ for (const row of planVacatedRows(lastBoxExtent, extent)) {
137
+ buf += `${ansi.moveTo(row, 1)}${ESC}[2K`;
138
+ }
139
+
140
+ lastBoxExtent = extent;
141
+
128
142
  for (const [i, r] of rowsList.entries()) {
129
143
  buf += ansi.moveTo(top + i, left) + r.styled;
130
144
  }
@@ -163,13 +177,18 @@ export interface OverlaySessionView extends SessionView {
163
177
  readonly alive: boolean;
164
178
  readonly kind: 'pty' | 'headless';
165
179
  readonly resumable: boolean;
166
- readonly agent: AgentKind;
180
+ readonly canEject: boolean;
181
+ readonly agent: AgentID;
167
182
  readonly pinned: boolean;
168
183
  readonly repoRoot: string;
169
184
  }
170
185
 
171
186
  export interface OverlayView {
172
187
  sessions: readonly OverlaySessionView[];
188
+
189
+ // The overlay column character per agent id, for the ids that do not use
190
+ // the built-in one.
191
+ agentMarks: Readonly<Record<AgentID, string>>;
173
192
  selected: number;
174
193
  confirmKill: boolean;
175
194
  filter: string | null;
@@ -209,8 +228,8 @@ export function drawOverlay(view: OverlayView) {
209
228
  const msg = truncate(s.lastMsg, msgWidth).padEnd(msgWidth);
210
229
  const unread = s.unread ? `${ESC}[1;33m!${ESC}[0m` : ' ';
211
230
  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;
231
+ const mark = formatOverlayAgentMark(s.agent, view.agentMarks);
232
+ const styledMark = mark === ' ' ? mark : `${ESC}[90m${mark}${ESC}[0m`;
214
233
  const body = `${name} ${state}${dir} ${msg}`;
215
234
  const styledBody = sel ? `${ESC}[7m${body}${ESC}[0m` : body;
216
235
 
@@ -269,7 +288,7 @@ export function buildOverlayHint(s: OverlaySessionView | undefined): string {
269
288
  actions.push('a ack');
270
289
  }
271
290
 
272
- if (s.agent === 'claude') {
291
+ if (s.canEject) {
273
292
  actions.push('H headless');
274
293
  }
275
294
 
@@ -1,3 +1,5 @@
1
+ import type { SessionID } from '../shared/session-id';
2
+
1
3
  export interface Dims {
2
4
  readonly cols: number;
3
5
  readonly rows: number;
@@ -10,16 +12,16 @@ export interface Dims {
10
12
  * the whole screen.
11
13
  */
12
14
  export class AttachRegistry<TClient> {
13
- private readonly bySession = new Map<string, Map<TClient, Dims>>();
15
+ private readonly bySession = new Map<SessionID, Map<TClient, Dims>>();
14
16
 
15
- attach(sessionID: string, client: TClient, dims: Dims): void {
17
+ attach(sessionID: SessionID, client: TClient, dims: Dims): void {
16
18
  const clients = this.bySession.get(sessionID) ?? new Map<TClient, Dims>();
17
19
 
18
20
  clients.set(client, dims);
19
21
  this.bySession.set(sessionID, clients);
20
22
  }
21
23
 
22
- detach(sessionID: string, client: TClient): void {
24
+ detach(sessionID: SessionID, client: TClient): void {
23
25
  const clients = this.bySession.get(sessionID);
24
26
 
25
27
  clients?.delete(client);
@@ -29,8 +31,8 @@ export class AttachRegistry<TClient> {
29
31
  }
30
32
  }
31
33
 
32
- detachAll(client: TClient): string[] {
33
- const affected: string[] = [];
34
+ detachAll(client: TClient): SessionID[] {
35
+ const affected: SessionID[] = [];
34
36
 
35
37
  for (const [sessionID, clients] of this.bySession) {
36
38
  if (clients.delete(client)) {
@@ -45,15 +47,15 @@ export class AttachRegistry<TClient> {
45
47
  return affected;
46
48
  }
47
49
 
48
- removeSession(sessionID: string): void {
50
+ removeSession(sessionID: SessionID): void {
49
51
  this.bySession.delete(sessionID);
50
52
  }
51
53
 
52
- hasClient(sessionID: string, client: TClient): boolean {
54
+ hasClient(sessionID: SessionID, client: TClient): boolean {
53
55
  return this.bySession.get(sessionID)?.has(client) ?? false;
54
56
  }
55
57
 
56
- updateDims(sessionID: string, client: TClient, dims: Dims): boolean {
58
+ updateDims(sessionID: SessionID, client: TClient, dims: Dims): boolean {
57
59
  const clients = this.bySession.get(sessionID);
58
60
 
59
61
  if (clients === undefined || !clients.has(client)) {
@@ -65,11 +67,11 @@ export class AttachRegistry<TClient> {
65
67
  return true;
66
68
  }
67
69
 
68
- collectClients(sessionID: string): TClient[] {
70
+ collectClients(sessionID: SessionID): TClient[] {
69
71
  return [...(this.bySession.get(sessionID)?.keys() ?? [])];
70
72
  }
71
73
 
72
- findEffectiveDims(sessionID: string): Dims | null {
74
+ findEffectiveDims(sessionID: SessionID): Dims | null {
73
75
  const clients = this.bySession.get(sessionID);
74
76
 
75
77
  if (clients === undefined || clients.size === 0) {
@@ -0,0 +1,43 @@
1
+ import { PROTOCOL_V } from '../protocol/protocol';
2
+ import type { EventMsg } from '../protocol/protocol';
3
+ import type { SessionID } from '../shared/session-id';
4
+ import type { Session, SessionDescriptor, SessionEventKind, SessionManager } from './sessions';
5
+
6
+ /**
7
+ * Builds the wire event for a session-manager notification, or null when the
8
+ * notification carries no descriptor to send: `added` and `state` fold in
9
+ * the current descriptor and skip emitting if the session is gone by the
10
+ * time the descriptor is collected.
11
+ */
12
+ export function buildSessionEvent(
13
+ mgr: SessionManager,
14
+ kind: SessionEventKind,
15
+ s: Session,
16
+ ): EventMsg | null {
17
+ const builders: Record<SessionEventKind, () => EventMsg | null> = {
18
+ added: () => {
19
+ const session = findDescriptor(mgr, s.id);
20
+
21
+ return session === null ? null : { v: PROTOCOL_V, ev: 'session.added', session };
22
+ },
23
+ state: () => {
24
+ const session = findDescriptor(mgr, s.id);
25
+
26
+ return session === null ? null : { v: PROTOCOL_V, ev: 'session.state', session };
27
+ },
28
+ renamed: () => ({
29
+ v: PROTOCOL_V,
30
+ ev: 'session.renamed',
31
+ s: s.id,
32
+ name: s.name,
33
+ namedBy: s.namedBy,
34
+ }),
35
+ removed: () => ({ v: PROTOCOL_V, ev: 'session.removed', s: s.id }),
36
+ };
37
+
38
+ return builders[kind]();
39
+ }
40
+
41
+ function findDescriptor(mgr: SessionManager, id: SessionID): SessionDescriptor | null {
42
+ return mgr.collectDescriptors().find((x) => x.id === id) ?? null;
43
+ }