@zgeoff/atc 0.1.11 → 1.0.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 +54 -15
- package/package.json +7 -2
- package/src/{agent-adapter.ts → agents/agent-adapter.ts} +31 -14
- package/src/{build-cli-command.ts → agents/build-cli-command.ts} +1 -1
- package/src/agents/build-hook-settings.ts +59 -0
- package/src/{claude-adapter.ts → agents/claude-adapter.ts} +41 -80
- package/src/{codex-adapter.ts → agents/codex-adapter.ts} +41 -30
- package/src/agents/gateway-adapter.ts +106 -0
- package/src/{grok-adapter.ts → agents/grok-adapter.ts} +83 -46
- package/src/agents/resolve-agent-home.ts +13 -0
- package/src/agents/truncate-detail.ts +7 -0
- package/src/agents/write-hook-settings.ts +37 -0
- package/src/cli.ts +25 -14
- package/src/{boot-daemon.ts → client/boot-daemon.ts} +8 -7
- package/src/client/build-client-machine.ts +119 -0
- package/src/client/collect-agent-picks.ts +31 -0
- package/src/{daemon-client.ts → client/daemon-client.ts} +4 -4
- package/src/{dirs.ts → client/dirs.ts} +8 -0
- package/src/client/format-overlay-agent-mark.ts +15 -0
- package/src/{index.ts → client/index.ts} +181 -550
- package/src/client/keys.ts +111 -0
- package/src/client/parse-daemon-event.ts +114 -0
- package/src/{pick-tab-target.ts → client/pick-tab-target.ts} +2 -2
- package/src/client/spawn-picker.ts +305 -0
- package/src/client/to-mirror-session.ts +65 -0
- package/src/{ui.ts → client/ui.ts} +13 -8
- package/src/{attach-registry.ts → daemon/attach-registry.ts} +12 -10
- package/src/daemon/build-session-event.ts +43 -0
- package/src/{daemon-connection.ts → daemon/daemon-connection.ts} +194 -108
- package/src/{daemon.ts → daemon/daemon.ts} +142 -301
- package/src/{hooks.ts → daemon/hooks.ts} +6 -4
- package/src/daemon/mint-session-id.ts +12 -0
- package/src/{permission-registry.ts → daemon/permission-registry.ts} +5 -3
- package/src/daemon/restore-fleet.ts +141 -0
- package/src/daemon/run-eject-handoff.ts +33 -0
- package/src/{screen-model.ts → daemon/screen-model.ts} +1 -1
- package/src/daemon/session-runtime.ts +64 -0
- package/src/{sessions.ts → daemon/sessions.ts} +98 -144
- package/src/{start-headless-run.ts → daemon/start-headless-run.ts} +9 -3
- package/src/daemon/start-headless-turn.ts +57 -0
- package/src/hook-report.ts +2 -2
- package/src/mcp-server.ts +33 -44
- package/src/protocol/parse-request-params.ts +31 -0
- package/src/{protocol.ts → protocol/protocol.ts} +1 -1
- package/src/protocol/request-param-schemas.ts +109 -0
- package/src/shared/agent-session-id.ts +8 -0
- package/src/shared/build-optional-boolean.ts +10 -0
- package/src/shared/build-optional-string-array.ts +14 -0
- package/src/shared/build-optional-string.ts +10 -0
- package/src/shared/collect-gateways.ts +113 -0
- package/src/{config.ts → shared/config.ts} +47 -31
- package/src/{get-build.ts → shared/get-build.ts} +21 -6
- package/src/shared/get-record.ts +20 -0
- package/src/shared/session-id.ts +8 -0
- package/src/shared/to-agent-session-id.ts +12 -0
- package/src/shared/to-session-id.ts +11 -0
- package/src/shared/to-shell-arg.ts +7 -0
- package/src/statusline.ts +2 -2
- package/src/store/bun-sqlite-driver.ts +96 -0
- package/src/store/fleet-entry.ts +72 -0
- package/src/store/run-migrations.ts +242 -0
- package/src/store/state-store.ts +226 -0
- package/src/format-overlay-agent-mark.ts +0 -17
- package/src/state-store.ts +0 -227
- /package/src/{normalize-hook-event.ts → agents/normalize-hook-event.ts} +0 -0
- /package/src/{print-codex-hook-file.ts → agents/print-codex-hook-file.ts} +0 -0
- /package/src/{print-grok-hook-file.ts → agents/print-grok-hook-file.ts} +0 -0
- /package/src/{build-leader-chords.ts → client/build-leader-chords.ts} +0 -0
- /package/src/{daemon-error.ts → protocol/daemon-error.ts} +0 -0
- /package/src/{outbound-queue.ts → protocol/outbound-queue.ts} +0 -0
- /package/src/{collect-clean-env.ts → shared/collect-clean-env.ts} +0 -0
- /package/src/{report.ts → shared/report.ts} +0 -0
- /package/src/{reset-input-modes.ts → shared/reset-input-modes.ts} +0 -0
- /package/src/{resolve-repo-root.ts → shared/resolve-repo-root.ts} +0 -0
|
@@ -1,52 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { match } from 'ts-pattern';
|
|
2
|
+
import { createActor } from 'xstate';
|
|
3
|
+
import type { AgentID } from '../agents/agent-adapter';
|
|
4
|
+
import { countSessionStates, sortGroupedSessionViews, sortSessionViews } from '../daemon/sessions';
|
|
5
|
+
import type { EventMsg } from '../protocol/protocol';
|
|
6
|
+
import { loadConfig } from '../shared/config';
|
|
2
7
|
import { bootDaemonClient } from './boot-daemon';
|
|
8
|
+
import { buildClientMachine } from './build-client-machine';
|
|
3
9
|
import { buildLeaderChords } from './build-leader-chords';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
10
|
+
import { findFuzzyScore, formatDir } from './dirs';
|
|
11
|
+
import { KEY, isDown, isUp, planTextEdit } from './keys';
|
|
12
|
+
import { parseDaemonEvent } from './parse-daemon-event';
|
|
6
13
|
import { pickTabTarget } from './pick-tab-target';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import type { SessionState } from './sessions';
|
|
14
|
+
import { SpawnPicker } from './spawn-picker';
|
|
15
|
+
import { toMirrorSession } from './to-mirror-session';
|
|
16
|
+
import type { MirrorSession } from './to-mirror-session';
|
|
11
17
|
import { ansi, cols, drawHelp, drawHome, drawOverlay, drawPicker, drawStatusBar, rows } from './ui';
|
|
12
18
|
|
|
13
|
-
type AgentKind = 'claude' | 'grok' | 'codex';
|
|
14
|
-
|
|
15
|
-
type Mode =
|
|
16
|
-
| 'home'
|
|
17
|
-
| 'attached'
|
|
18
|
-
| 'overlay'
|
|
19
|
-
| 'help'
|
|
20
|
-
| 'picker-agent'
|
|
21
|
-
| 'picker-dir'
|
|
22
|
-
| 'picker-name'
|
|
23
|
-
| 'picker-prompt'
|
|
24
|
-
| 'picker-eject';
|
|
25
|
-
|
|
26
|
-
const AGENT_PICKS = [
|
|
27
|
-
{ agent: 'claude' as const, label: 'Claude' },
|
|
28
|
-
{ agent: 'grok' as const, label: 'Grok' },
|
|
29
|
-
{ agent: 'codex' as const, label: 'Codex' },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
interface MirrorSession {
|
|
33
|
-
id: string;
|
|
34
|
-
name: string;
|
|
35
|
-
cwd: string;
|
|
36
|
-
pinned: boolean;
|
|
37
|
-
lastAttachedAt: number;
|
|
38
|
-
repoRoot: string;
|
|
39
|
-
state: SessionState;
|
|
40
|
-
unread: boolean;
|
|
41
|
-
lastMsg: string;
|
|
42
|
-
createdAt: number;
|
|
43
|
-
kind: 'pty' | 'headless';
|
|
44
|
-
alive: boolean;
|
|
45
|
-
resumable: boolean;
|
|
46
|
-
agent: AgentKind;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let mode: Mode = 'home';
|
|
50
19
|
let overlaySelected = 0;
|
|
51
20
|
let confirmKill = false;
|
|
52
21
|
|
|
@@ -60,16 +29,14 @@ const doneAt = new Map<string, number>();
|
|
|
60
29
|
|
|
61
30
|
let focusedID: string | null = null;
|
|
62
31
|
let fleetCount = 0;
|
|
32
|
+
let lastUsedAgent: AgentID = 'claude';
|
|
33
|
+
|
|
34
|
+
// Read once at start, like the leader key: a newly configured backend gets
|
|
35
|
+
// its overlay letter on the next client run.
|
|
36
|
+
const agentMarks: Readonly<Record<AgentID, string>> = Object.fromEntries(
|
|
37
|
+
loadConfig().gateways.map((g) => [g.id, g.mark]),
|
|
38
|
+
);
|
|
63
39
|
|
|
64
|
-
// picker state carried across the dir -> name -> prompt steps
|
|
65
|
-
let allDirs: string[] = [];
|
|
66
|
-
let pickerInput = '';
|
|
67
|
-
let pickerSelected = 0;
|
|
68
|
-
let spawnDir = '';
|
|
69
|
-
let spawnName = '';
|
|
70
|
-
let spawnResume = false;
|
|
71
|
-
let spawnAgent: AgentKind = 'claude';
|
|
72
|
-
let lastUsedAgent: AgentKind = 'claude';
|
|
73
40
|
const stdout = process.stdout;
|
|
74
41
|
|
|
75
42
|
// Full height: the atc status bar only exists on home/overlay screens;
|
|
@@ -82,6 +49,19 @@ function findFocused(): MirrorSession | null {
|
|
|
82
49
|
return fleet.find((s) => s.id === focusedID) ?? null;
|
|
83
50
|
}
|
|
84
51
|
|
|
52
|
+
function openHome() {
|
|
53
|
+
drawHome(fleetCount, leader.label);
|
|
54
|
+
scheduleStatus();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function openAttached(sessionID: string) {
|
|
58
|
+
focusedID = sessionID;
|
|
59
|
+
|
|
60
|
+
stdout.write(ansi.clear + ansi.showCursor);
|
|
61
|
+
|
|
62
|
+
scheduleStatus();
|
|
63
|
+
}
|
|
64
|
+
|
|
85
65
|
let statusTimer: ReturnType<typeof setTimeout> | null = null;
|
|
86
66
|
|
|
87
67
|
function scheduleStatus() {
|
|
@@ -92,7 +72,7 @@ function scheduleStatus() {
|
|
|
92
72
|
statusTimer = setTimeout(() => {
|
|
93
73
|
statusTimer = null;
|
|
94
74
|
|
|
95
|
-
if (
|
|
75
|
+
if (service.getSnapshot().value !== 'attached') {
|
|
96
76
|
const focused = findFocused();
|
|
97
77
|
const urgent = sortSessionViews(fleet).find((s) => s.state === 'needs_you');
|
|
98
78
|
|
|
@@ -114,10 +94,7 @@ async function sendQuiet(m: string, p?: Readonly<Record<string, unknown>>) {
|
|
|
114
94
|
}
|
|
115
95
|
|
|
116
96
|
async function attach(sessionID: string) {
|
|
117
|
-
|
|
118
|
-
mode = 'attached';
|
|
119
|
-
|
|
120
|
-
stdout.write(ansi.clear + ansi.showCursor);
|
|
97
|
+
service.send({ type: 'ATTACH', sessionID });
|
|
121
98
|
|
|
122
99
|
try {
|
|
123
100
|
await client.sendRequest('session.attach', {
|
|
@@ -126,14 +103,8 @@ async function attach(sessionID: string) {
|
|
|
126
103
|
rows: ptyRows(),
|
|
127
104
|
});
|
|
128
105
|
} catch {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
openOverlay();
|
|
132
|
-
|
|
133
|
-
return;
|
|
106
|
+
service.send({ type: 'OVERLAY' });
|
|
134
107
|
}
|
|
135
|
-
|
|
136
|
-
scheduleStatus();
|
|
137
108
|
}
|
|
138
109
|
|
|
139
110
|
function detachFocused() {
|
|
@@ -146,20 +117,15 @@ function toBase() {
|
|
|
146
117
|
const f = findFocused();
|
|
147
118
|
|
|
148
119
|
if (f !== null && f.alive) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
stdout.write(ansi.clear + ansi.showCursor);
|
|
120
|
+
service.send({ type: 'ATTACH', sessionID: f.id });
|
|
152
121
|
void sendQuiet('session.attach', { session: f.id, cols: cols(), rows: ptyRows() });
|
|
153
122
|
} else {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
drawHome(fleetCount, leader.label);
|
|
123
|
+
service.send({ type: 'HOME' });
|
|
157
124
|
}
|
|
158
|
-
|
|
159
|
-
scheduleStatus();
|
|
160
125
|
}
|
|
161
126
|
|
|
162
127
|
let ejectTarget = '';
|
|
128
|
+
let ejectInput = '';
|
|
163
129
|
|
|
164
130
|
async function yankToHeadless(instruction: string) {
|
|
165
131
|
try {
|
|
@@ -171,7 +137,7 @@ async function yankToHeadless(instruction: string) {
|
|
|
171
137
|
recordActionFailure(ejectTarget, error);
|
|
172
138
|
}
|
|
173
139
|
|
|
174
|
-
|
|
140
|
+
service.send({ type: 'OVERLAY' });
|
|
175
141
|
}
|
|
176
142
|
|
|
177
143
|
async function adoptSession(id: string) {
|
|
@@ -181,7 +147,8 @@ async function adoptSession(id: string) {
|
|
|
181
147
|
await attach(id);
|
|
182
148
|
} catch (error) {
|
|
183
149
|
recordActionFailure(id, error);
|
|
184
|
-
|
|
150
|
+
|
|
151
|
+
service.send({ type: 'OVERLAY' });
|
|
185
152
|
}
|
|
186
153
|
}
|
|
187
154
|
|
|
@@ -196,8 +163,6 @@ function recordActionFailure(id: string, error: unknown) {
|
|
|
196
163
|
}
|
|
197
164
|
|
|
198
165
|
function openHelp() {
|
|
199
|
-
mode = 'help';
|
|
200
|
-
|
|
201
166
|
stdout.write(ansi.clear);
|
|
202
167
|
|
|
203
168
|
drawHelp();
|
|
@@ -207,7 +172,7 @@ function applyHelpKey(buf: Buffer) {
|
|
|
207
172
|
const ch = buf.toString();
|
|
208
173
|
|
|
209
174
|
if (buf[0] === KEY.esc || ch === '?' || ch === 'q' || isLeaderKey(buf)) {
|
|
210
|
-
|
|
175
|
+
service.send({ type: 'OVERLAY' });
|
|
211
176
|
}
|
|
212
177
|
}
|
|
213
178
|
|
|
@@ -236,6 +201,7 @@ function renderOverlay() {
|
|
|
236
201
|
|
|
237
202
|
drawOverlay({
|
|
238
203
|
sessions,
|
|
204
|
+
agentMarks,
|
|
239
205
|
selected: overlaySelected,
|
|
240
206
|
confirmKill,
|
|
241
207
|
filter: overlayFilter,
|
|
@@ -247,7 +213,6 @@ function renderOverlay() {
|
|
|
247
213
|
}
|
|
248
214
|
|
|
249
215
|
function openOverlay() {
|
|
250
|
-
mode = 'overlay';
|
|
251
216
|
confirmKill = false;
|
|
252
217
|
overlayFilter = null;
|
|
253
218
|
|
|
@@ -260,102 +225,50 @@ function openOverlay() {
|
|
|
260
225
|
renderOverlay();
|
|
261
226
|
}
|
|
262
227
|
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
selected: pickerSelected,
|
|
273
|
-
input: pickerInput,
|
|
274
|
-
hint: '↑↓ move · ⏎ select · esc cancel',
|
|
275
|
-
});
|
|
276
|
-
} else if (mode === 'picker-dir') {
|
|
277
|
-
const items = pickMatches(allDirs, pickerInput).map((d) => formatDir(d));
|
|
278
|
-
|
|
279
|
-
pickerSelected = Math.min(pickerSelected, Math.max(0, Math.min(items.length, 10) - 1));
|
|
280
|
-
|
|
281
|
-
drawPicker({
|
|
282
|
-
title: `${verb}: directory`,
|
|
283
|
-
items,
|
|
284
|
-
selected: pickerSelected,
|
|
285
|
-
input: pickerInput,
|
|
286
|
-
hint: 'type to filter · ↑↓ move · ⏎ select · esc cancel',
|
|
287
|
-
});
|
|
288
|
-
} else if (mode === 'picker-name') {
|
|
289
|
-
drawPicker({
|
|
290
|
-
title: `${verb}: name`,
|
|
291
|
-
items: [],
|
|
292
|
-
selected: -1,
|
|
293
|
-
input: pickerInput,
|
|
294
|
-
placeholder: basename(spawnDir),
|
|
295
|
-
hint: `session name for ${formatDir(spawnDir)} · ⏎ accept · esc back`,
|
|
296
|
-
});
|
|
297
|
-
} else if (mode === 'picker-eject') {
|
|
298
|
-
drawPicker({
|
|
299
|
-
title: 'eject: headless instruction',
|
|
300
|
-
items: [],
|
|
301
|
-
selected: -1,
|
|
302
|
-
input: pickerInput,
|
|
303
|
-
placeholder: 'continue the task autonomously',
|
|
304
|
-
hint: 'instruction for the headless run · ⏎ eject · esc back',
|
|
305
|
-
});
|
|
306
|
-
} else {
|
|
307
|
-
drawPicker({
|
|
308
|
-
title: 'spawn: initial prompt',
|
|
309
|
-
items: [],
|
|
310
|
-
selected: -1,
|
|
311
|
-
input: pickerInput,
|
|
312
|
-
placeholder: 'optional — ⏎ to start interactive',
|
|
313
|
-
hint: 'first message for the session · ⏎ spawn · esc back',
|
|
314
|
-
});
|
|
315
|
-
}
|
|
228
|
+
function renderEject() {
|
|
229
|
+
drawPicker({
|
|
230
|
+
title: 'eject: headless instruction',
|
|
231
|
+
items: [],
|
|
232
|
+
selected: -1,
|
|
233
|
+
input: ejectInput,
|
|
234
|
+
placeholder: 'continue the task autonomously',
|
|
235
|
+
hint: 'instruction for the headless run · ⏎ eject · esc back',
|
|
236
|
+
});
|
|
316
237
|
|
|
317
238
|
scheduleStatus();
|
|
318
239
|
}
|
|
319
240
|
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
pickerInput = '';
|
|
324
|
-
|
|
325
|
-
pickerSelected = Math.max(
|
|
326
|
-
0,
|
|
327
|
-
AGENT_PICKS.findIndex((p) => p.agent === lastUsedAgent),
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
mode = 'picker-agent';
|
|
241
|
+
function openEject(sessionID: string) {
|
|
242
|
+
ejectTarget = sessionID;
|
|
243
|
+
ejectInput = '';
|
|
331
244
|
|
|
332
245
|
stdout.write(ansi.clear);
|
|
333
246
|
|
|
334
|
-
|
|
247
|
+
renderEject();
|
|
335
248
|
}
|
|
336
249
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
} catch {}
|
|
349
|
-
|
|
350
|
-
allDirs = await collectDirs(recent);
|
|
250
|
+
const picker = new SpawnPicker<MirrorSession>({
|
|
251
|
+
sendRequest,
|
|
252
|
+
ptyRows,
|
|
253
|
+
isLeaderKey,
|
|
254
|
+
getLastUsedAgent,
|
|
255
|
+
scheduleStatus,
|
|
256
|
+
toBase,
|
|
257
|
+
attach,
|
|
258
|
+
toMirrorSession,
|
|
259
|
+
upsertMirror,
|
|
260
|
+
});
|
|
351
261
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
262
|
+
function openPicker(resume: boolean) {
|
|
263
|
+
picker.open(resume);
|
|
264
|
+
}
|
|
355
265
|
|
|
356
|
-
|
|
266
|
+
function sendRequest(m: string, p?: Readonly<Record<string, unknown>>) {
|
|
267
|
+
return client.sendRequest(m, p);
|
|
268
|
+
}
|
|
357
269
|
|
|
358
|
-
|
|
270
|
+
function getLastUsedAgent(): AgentID {
|
|
271
|
+
return lastUsedAgent;
|
|
359
272
|
}
|
|
360
273
|
|
|
361
274
|
async function restoreFleet() {
|
|
@@ -372,32 +285,6 @@ async function restoreFleet() {
|
|
|
372
285
|
}
|
|
373
286
|
}
|
|
374
287
|
|
|
375
|
-
async function spawnFromPicker(prompt: string) {
|
|
376
|
-
try {
|
|
377
|
-
const ok = await client.sendRequest('session.spawn', {
|
|
378
|
-
cwd: spawnDir,
|
|
379
|
-
name: spawnName,
|
|
380
|
-
prompt,
|
|
381
|
-
cols: cols(),
|
|
382
|
-
rows: ptyRows(),
|
|
383
|
-
...(spawnResume ? { resume: true } : {}),
|
|
384
|
-
agent: spawnAgent,
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
const spawned = toMirrorSession(ok['session']);
|
|
388
|
-
|
|
389
|
-
if (spawned !== null) {
|
|
390
|
-
upsertMirror(spawned);
|
|
391
|
-
|
|
392
|
-
await attach(spawned.id);
|
|
393
|
-
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
} catch {}
|
|
397
|
-
|
|
398
|
-
toBase();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
288
|
function quit(code = 0): never {
|
|
402
289
|
detachFocused();
|
|
403
290
|
|
|
@@ -411,47 +298,6 @@ function quit(code = 0): never {
|
|
|
411
298
|
process.exit(code);
|
|
412
299
|
}
|
|
413
300
|
|
|
414
|
-
const SESSION_STATES: readonly SessionState[] = ['running', 'needs_you', 'done', 'exited'];
|
|
415
|
-
|
|
416
|
-
function toMirrorSession(value: unknown): MirrorSession | null {
|
|
417
|
-
if (!isRecord(value)) {
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const state = SESSION_STATES.find((x) => x === value['state']);
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
typeof value['id'] !== 'string' ||
|
|
425
|
-
typeof value['name'] !== 'string' ||
|
|
426
|
-
typeof value['cwd'] !== 'string' ||
|
|
427
|
-
state === undefined ||
|
|
428
|
-
typeof value['unread'] !== 'boolean' ||
|
|
429
|
-
typeof value['lastMsg'] !== 'string' ||
|
|
430
|
-
typeof value['createdAt'] !== 'number' ||
|
|
431
|
-
typeof value['alive'] !== 'boolean'
|
|
432
|
-
) {
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
id: value['id'],
|
|
438
|
-
name: value['name'],
|
|
439
|
-
cwd: value['cwd'],
|
|
440
|
-
pinned: value['pinned'] === true,
|
|
441
|
-
lastAttachedAt:
|
|
442
|
-
typeof value['lastAttachedAt'] === 'number' ? value['lastAttachedAt'] : value['createdAt'],
|
|
443
|
-
repoRoot: typeof value['repoRoot'] === 'string' ? value['repoRoot'] : value['cwd'],
|
|
444
|
-
state,
|
|
445
|
-
unread: value['unread'],
|
|
446
|
-
lastMsg: value['lastMsg'],
|
|
447
|
-
createdAt: value['createdAt'],
|
|
448
|
-
kind: value['kind'] === 'headless' ? 'headless' : 'pty',
|
|
449
|
-
alive: value['alive'],
|
|
450
|
-
resumable: typeof value['agentSessionID'] === 'string',
|
|
451
|
-
agent: value['agent'] === 'grok' || value['agent'] === 'codex' ? value['agent'] : 'claude',
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
301
|
function upsertMirror(d: Readonly<MirrorSession>) {
|
|
456
302
|
const existing = fleet.find((s) => s.id === d.id);
|
|
457
303
|
|
|
@@ -468,132 +314,92 @@ function upsertMirror(d: Readonly<MirrorSession>) {
|
|
|
468
314
|
existing.kind = d.kind;
|
|
469
315
|
existing.alive = d.alive;
|
|
470
316
|
existing.resumable = d.resumable;
|
|
317
|
+
existing.canEject = d.canEject;
|
|
471
318
|
existing.agent = d.agent;
|
|
472
319
|
}
|
|
473
320
|
}
|
|
474
321
|
|
|
475
322
|
function refreshScreens() {
|
|
476
|
-
|
|
323
|
+
const currentMode = service.getSnapshot().value;
|
|
324
|
+
|
|
325
|
+
if (currentMode === 'overlay') {
|
|
477
326
|
renderOverlay();
|
|
478
327
|
}
|
|
479
328
|
|
|
480
329
|
// Focused session died under us: surface the list instead of a dead screen.
|
|
481
330
|
const f = findFocused();
|
|
482
331
|
|
|
483
|
-
if (
|
|
484
|
-
|
|
332
|
+
if (currentMode === 'attached' && f !== null && f.state === 'exited') {
|
|
333
|
+
service.send({ type: 'OVERLAY' });
|
|
485
334
|
}
|
|
486
335
|
|
|
487
336
|
scheduleStatus();
|
|
488
337
|
}
|
|
489
338
|
|
|
490
|
-
function applyDaemonEvent(
|
|
491
|
-
|
|
492
|
-
case 'session.output': {
|
|
493
|
-
if (mode === 'attached' && e['s'] === focusedID && typeof e['d'] === 'string') {
|
|
494
|
-
stdout.write(e['d']);
|
|
495
|
-
}
|
|
339
|
+
function applyDaemonEvent(raw: EventMsg) {
|
|
340
|
+
const event = parseDaemonEvent(raw);
|
|
496
341
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const d = toMirrorSession(e['session']);
|
|
342
|
+
if (event === null) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
501
345
|
|
|
502
|
-
|
|
503
|
-
|
|
346
|
+
match(event)
|
|
347
|
+
.with({ ev: 'session.output' }, (e) => {
|
|
348
|
+
if (service.getSnapshot().value === 'attached' && e.s === focusedID) {
|
|
349
|
+
stdout.write(e.d);
|
|
504
350
|
}
|
|
505
|
-
|
|
351
|
+
})
|
|
352
|
+
.with({ ev: 'session.added' }, (e) => {
|
|
353
|
+
upsertMirror(e.session);
|
|
506
354
|
refreshScreens();
|
|
355
|
+
})
|
|
356
|
+
.with({ ev: 'session.state' }, (e) => {
|
|
357
|
+
const existing = fleet.find((x) => x.id === e.session.id);
|
|
507
358
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const s = fleet.find((x) => x.id === e['s']);
|
|
512
|
-
|
|
513
|
-
if (s !== undefined) {
|
|
514
|
-
const next = SESSION_STATES.find((x) => x === e['state']);
|
|
515
|
-
|
|
516
|
-
if (next === 'done' && s.state !== 'done') {
|
|
517
|
-
doneAt.set(s.id, Date.now());
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (next !== undefined) {
|
|
521
|
-
s.state = next;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (typeof e['alive'] === 'boolean') {
|
|
525
|
-
s.alive = e['alive'];
|
|
526
|
-
} else if (next === 'exited') {
|
|
527
|
-
s.alive = false;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (e['kind'] === 'pty' || e['kind'] === 'headless') {
|
|
531
|
-
s.kind = e['kind'];
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (typeof e['pinned'] === 'boolean') {
|
|
535
|
-
s.pinned = e['pinned'];
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (typeof e['lastAttachedAt'] === 'number') {
|
|
539
|
-
s.lastAttachedAt = e['lastAttachedAt'];
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (typeof e['unread'] === 'boolean') {
|
|
543
|
-
s.unread = e['unread'];
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (typeof e['lastMsg'] === 'string') {
|
|
547
|
-
s.lastMsg = e['lastMsg'];
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (e['agent'] === 'grok' || e['agent'] === 'codex' || e['agent'] === 'claude') {
|
|
551
|
-
s.agent = e['agent'];
|
|
359
|
+
if (existing !== undefined) {
|
|
360
|
+
if (e.session.state === 'done' && existing.state !== 'done') {
|
|
361
|
+
doneAt.set(e.session.id, Date.now());
|
|
552
362
|
}
|
|
553
363
|
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
lastUsedAgent = s.agent;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
s.resumable = true;
|
|
364
|
+
if (e.session.resumable && !existing.resumable) {
|
|
365
|
+
lastUsedAgent = e.session.agent;
|
|
560
366
|
}
|
|
561
367
|
}
|
|
562
368
|
|
|
369
|
+
upsertMirror(e.session);
|
|
563
370
|
refreshScreens();
|
|
371
|
+
})
|
|
372
|
+
.with({ ev: 'session.renamed' }, (e) => {
|
|
373
|
+
const s = fleet.find((x) => x.id === e.s);
|
|
564
374
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
case 'session.renamed': {
|
|
568
|
-
const s = fleet.find((x) => x.id === e['s']);
|
|
569
|
-
|
|
570
|
-
if (s !== undefined && typeof e['name'] === 'string') {
|
|
571
|
-
s.name = e['name'];
|
|
375
|
+
if (s !== undefined) {
|
|
376
|
+
s.name = e.name;
|
|
572
377
|
}
|
|
573
378
|
|
|
574
379
|
refreshScreens();
|
|
380
|
+
})
|
|
381
|
+
.with({ ev: 'session.removed' }, (e) => {
|
|
382
|
+
fleet = fleet.filter((x) => x.id !== e.s);
|
|
575
383
|
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
case 'session.removed': {
|
|
579
|
-
fleet = fleet.filter((x) => x.id !== e['s']);
|
|
580
|
-
|
|
581
|
-
if (typeof e['s'] === 'string') {
|
|
582
|
-
doneAt.delete(e['s']);
|
|
583
|
-
}
|
|
384
|
+
doneAt.delete(e.s);
|
|
584
385
|
|
|
585
|
-
if (focusedID === e
|
|
386
|
+
if (focusedID === e.s) {
|
|
586
387
|
focusedID = null;
|
|
587
388
|
}
|
|
588
389
|
|
|
589
390
|
refreshScreens();
|
|
590
|
-
|
|
591
|
-
}
|
|
391
|
+
})
|
|
592
392
|
|
|
593
|
-
// Desync recovery arrives as ordinary repaint output; permission
|
|
594
|
-
// events matter to structured clients, not this passthrough TUI.
|
|
595
|
-
|
|
596
|
-
|
|
393
|
+
// Desync recovery arrives as ordinary repaint output; permission and
|
|
394
|
+
// resize events matter to structured clients, not this passthrough TUI.
|
|
395
|
+
.with(
|
|
396
|
+
{ ev: 'session.resized' },
|
|
397
|
+
{ ev: 'session.desync' },
|
|
398
|
+
{ ev: 'permission.requested' },
|
|
399
|
+
{ ev: 'permission.resolved' },
|
|
400
|
+
() => {},
|
|
401
|
+
)
|
|
402
|
+
.exhaustive();
|
|
597
403
|
}
|
|
598
404
|
|
|
599
405
|
// Best-effort clipboard: OSC 52 (works through zellij/tmux/ssh) plus any
|
|
@@ -617,23 +423,6 @@ function copyToClipboard(text: string) {
|
|
|
617
423
|
const leader = loadConfig().leader;
|
|
618
424
|
const leaderChords = buildLeaderChords(leader.code);
|
|
619
425
|
|
|
620
|
-
const KEY = {
|
|
621
|
-
tab: 0x09,
|
|
622
|
-
ctrlC: 0x03,
|
|
623
|
-
ctrlU: 0x15,
|
|
624
|
-
esc: 0x1b,
|
|
625
|
-
enter: 0x0d,
|
|
626
|
-
backspace: 0x7f,
|
|
627
|
-
} as const;
|
|
628
|
-
|
|
629
|
-
function isUp(buf: Buffer): boolean {
|
|
630
|
-
return buf.toString() === '\u001B[A' || buf.toString() === '\u001BOA';
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function isDown(buf: Buffer): boolean {
|
|
634
|
-
return buf.toString() === '\u001B[B' || buf.toString() === '\u001BOB';
|
|
635
|
-
}
|
|
636
|
-
|
|
637
426
|
// A fullscreen session can leave the terminal in an enhanced keyboard
|
|
638
427
|
// encoding, so the leader arrives as a CSI chord instead of its bare byte.
|
|
639
428
|
function isLeaderKey(buf: Buffer): boolean {
|
|
@@ -783,7 +572,7 @@ function applyOverlayKey(buf: Buffer) {
|
|
|
783
572
|
}
|
|
784
573
|
|
|
785
574
|
if (ch === 'n') {
|
|
786
|
-
|
|
575
|
+
service.send({ type: 'SPAWN', resume: false });
|
|
787
576
|
|
|
788
577
|
return;
|
|
789
578
|
}
|
|
@@ -809,7 +598,7 @@ function applyOverlayKey(buf: Buffer) {
|
|
|
809
598
|
}
|
|
810
599
|
|
|
811
600
|
if (ch === '?') {
|
|
812
|
-
|
|
601
|
+
service.send({ type: 'HELP' });
|
|
813
602
|
|
|
814
603
|
return;
|
|
815
604
|
}
|
|
@@ -820,20 +609,8 @@ function applyOverlayKey(buf: Buffer) {
|
|
|
820
609
|
return;
|
|
821
610
|
}
|
|
822
611
|
|
|
823
|
-
if (
|
|
824
|
-
|
|
825
|
-
sel !== undefined &&
|
|
826
|
-
sel.kind === 'pty' &&
|
|
827
|
-
sel.alive &&
|
|
828
|
-
sel.agent === 'claude'
|
|
829
|
-
) {
|
|
830
|
-
ejectTarget = sel.id;
|
|
831
|
-
pickerInput = '';
|
|
832
|
-
mode = 'picker-eject';
|
|
833
|
-
|
|
834
|
-
stdout.write(ansi.clear);
|
|
835
|
-
|
|
836
|
-
renderPicker();
|
|
612
|
+
if (ch === 'H' && sel !== undefined && sel.kind === 'pty' && sel.alive && sel.canEject) {
|
|
613
|
+
service.send({ type: 'EJECT', sessionID: sel.id });
|
|
837
614
|
|
|
838
615
|
return;
|
|
839
616
|
}
|
|
@@ -845,7 +622,7 @@ function applyOverlayKey(buf: Buffer) {
|
|
|
845
622
|
}
|
|
846
623
|
|
|
847
624
|
if (ch === 'r') {
|
|
848
|
-
|
|
625
|
+
service.send({ type: 'SPAWN', resume: true });
|
|
849
626
|
|
|
850
627
|
return;
|
|
851
628
|
}
|
|
@@ -892,96 +669,55 @@ async function yankResume(sessionID: string, eject: boolean) {
|
|
|
892
669
|
void sendQuiet('session.kill', { session: sessionID });
|
|
893
670
|
}
|
|
894
671
|
|
|
895
|
-
if (
|
|
672
|
+
if (service.getSnapshot().value === 'overlay') {
|
|
896
673
|
renderOverlay();
|
|
897
674
|
}
|
|
898
675
|
} catch {}
|
|
899
676
|
}
|
|
900
677
|
|
|
901
|
-
function
|
|
902
|
-
|
|
903
|
-
// trailing newline still submits. Escape sequences stay intact.
|
|
904
|
-
if (buf.length > 1 && buf[0] !== KEY.esc) {
|
|
905
|
-
for (const ch of buf.toString()) {
|
|
906
|
-
applyTextKey(Buffer.from(ch), onSubmit, onCancel);
|
|
678
|
+
function applyEjectKey(buf: Buffer) {
|
|
679
|
+
const edit = planTextEdit(buf, ejectInput, { isLeaderKey, moves: false });
|
|
907
680
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
681
|
+
switch (edit.kind) {
|
|
682
|
+
case 'none':
|
|
683
|
+
case 'move': {
|
|
684
|
+
return;
|
|
911
685
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
if (buf[0] === KEY.esc && buf.length === 1) {
|
|
917
|
-
onCancel();
|
|
918
|
-
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
if (isLeaderKey(buf)) {
|
|
923
|
-
toBase();
|
|
924
|
-
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (buf[0] === KEY.enter) {
|
|
929
|
-
onSubmit();
|
|
930
|
-
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (buf[0] === KEY.backspace) {
|
|
935
|
-
pickerInput = pickerInput.slice(0, -1);
|
|
936
|
-
|
|
937
|
-
renderPicker();
|
|
938
|
-
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (buf[0] === KEY.ctrlU) {
|
|
943
|
-
pickerInput = '';
|
|
944
|
-
|
|
945
|
-
renderPicker();
|
|
946
|
-
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if (mode === 'picker-dir' || mode === 'picker-agent') {
|
|
951
|
-
if (isDown(buf)) {
|
|
952
|
-
pickerSelected++;
|
|
953
|
-
|
|
954
|
-
renderPicker();
|
|
686
|
+
case 'cancel': {
|
|
687
|
+
service.send({ type: 'OVERLAY' });
|
|
955
688
|
|
|
956
689
|
return;
|
|
957
690
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
pickerSelected = Math.max(0, pickerSelected - 1);
|
|
961
|
-
|
|
962
|
-
renderPicker();
|
|
691
|
+
case 'leader': {
|
|
692
|
+
toBase();
|
|
963
693
|
|
|
964
694
|
return;
|
|
965
695
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
let printable = true;
|
|
696
|
+
case 'submit': {
|
|
697
|
+
ejectInput = edit.value;
|
|
698
|
+
void yankToHeadless(ejectInput.trim());
|
|
970
699
|
|
|
971
|
-
|
|
972
|
-
if (c < ' ' || c === '\u007F') {
|
|
973
|
-
printable = false;
|
|
974
|
-
break;
|
|
700
|
+
return;
|
|
975
701
|
}
|
|
976
|
-
|
|
702
|
+
case 'input': {
|
|
703
|
+
ejectInput = edit.value;
|
|
977
704
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
renderPicker();
|
|
705
|
+
renderEject();
|
|
706
|
+
}
|
|
982
707
|
}
|
|
983
708
|
}
|
|
984
709
|
|
|
710
|
+
const service = createActor(
|
|
711
|
+
buildClientMachine({
|
|
712
|
+
openHome,
|
|
713
|
+
openAttached,
|
|
714
|
+
openOverlay,
|
|
715
|
+
openHelp,
|
|
716
|
+
openPicker,
|
|
717
|
+
openEject,
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
|
|
985
721
|
const boot = await bootDaemonClient();
|
|
986
722
|
|
|
987
723
|
let client = boot.client;
|
|
@@ -1050,7 +786,7 @@ async function restartDaemon() {
|
|
|
1050
786
|
await sendQuiet('fleet.restore', { cols: cols(), rows: ptyRows() });
|
|
1051
787
|
await refreshMirror().catch(() => {});
|
|
1052
788
|
|
|
1053
|
-
|
|
789
|
+
service.send({ type: 'OVERLAY' });
|
|
1054
790
|
}
|
|
1055
791
|
|
|
1056
792
|
process.stdin.setRawMode(true);
|
|
@@ -1061,10 +797,10 @@ process.stdin.resume();
|
|
|
1061
797
|
const stdinDecoder = new TextDecoder('utf-8');
|
|
1062
798
|
|
|
1063
799
|
process.stdin.on('data', (buf: Buffer) => {
|
|
1064
|
-
switch (
|
|
800
|
+
switch (service.getSnapshot().value) {
|
|
1065
801
|
case 'attached': {
|
|
1066
802
|
if (isLeaderKey(buf)) {
|
|
1067
|
-
|
|
803
|
+
service.send({ type: 'OVERLAY' });
|
|
1068
804
|
|
|
1069
805
|
return;
|
|
1070
806
|
}
|
|
@@ -1080,7 +816,7 @@ process.stdin.on('data', (buf: Buffer) => {
|
|
|
1080
816
|
}
|
|
1081
817
|
case 'home': {
|
|
1082
818
|
if (isLeaderKey(buf)) {
|
|
1083
|
-
|
|
819
|
+
service.send({ type: 'OVERLAY' });
|
|
1084
820
|
|
|
1085
821
|
return;
|
|
1086
822
|
}
|
|
@@ -1088,13 +824,13 @@ process.stdin.on('data', (buf: Buffer) => {
|
|
|
1088
824
|
const ch = buf.toString();
|
|
1089
825
|
|
|
1090
826
|
if (ch === 'n') {
|
|
1091
|
-
|
|
827
|
+
service.send({ type: 'SPAWN', resume: false });
|
|
1092
828
|
|
|
1093
829
|
return;
|
|
1094
830
|
}
|
|
1095
831
|
|
|
1096
832
|
if (ch === 'r') {
|
|
1097
|
-
|
|
833
|
+
service.send({ type: 'SPAWN', resume: true });
|
|
1098
834
|
|
|
1099
835
|
return;
|
|
1100
836
|
}
|
|
@@ -1121,146 +857,43 @@ process.stdin.on('data', (buf: Buffer) => {
|
|
|
1121
857
|
|
|
1122
858
|
return;
|
|
1123
859
|
}
|
|
1124
|
-
case 'picker
|
|
1125
|
-
|
|
1126
|
-
buf,
|
|
1127
|
-
() => {
|
|
1128
|
-
spawnAgent = AGENT_PICKS[pickerSelected]?.agent ?? 'claude';
|
|
1129
|
-
void openDirPicker();
|
|
1130
|
-
},
|
|
1131
|
-
() => {
|
|
1132
|
-
toBase();
|
|
1133
|
-
},
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
case 'picker-dir': {
|
|
1139
|
-
applyTextKey(
|
|
1140
|
-
buf,
|
|
1141
|
-
() => {
|
|
1142
|
-
const items = pickMatches(allDirs, pickerInput);
|
|
1143
|
-
const raw = pickerInput.trim();
|
|
1144
|
-
let chosen = items[pickerSelected] ?? null;
|
|
1145
|
-
|
|
1146
|
-
if (chosen === null && (raw.startsWith('/') || raw.startsWith('~'))) {
|
|
1147
|
-
chosen = raw.replace(/^~/u, process.env['HOME'] ?? '~');
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
if (chosen === null) {
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
spawnDir = chosen;
|
|
1155
|
-
pickerInput = '';
|
|
1156
|
-
mode = 'picker-name';
|
|
1157
|
-
|
|
1158
|
-
stdout.write(ansi.clear);
|
|
1159
|
-
|
|
1160
|
-
renderPicker();
|
|
1161
|
-
},
|
|
1162
|
-
() => {
|
|
1163
|
-
pickerInput = '';
|
|
1164
|
-
|
|
1165
|
-
pickerSelected = Math.max(
|
|
1166
|
-
0,
|
|
1167
|
-
AGENT_PICKS.findIndex((p) => p.agent === spawnAgent),
|
|
1168
|
-
);
|
|
1169
|
-
|
|
1170
|
-
mode = 'picker-agent';
|
|
1171
|
-
|
|
1172
|
-
stdout.write(ansi.clear);
|
|
1173
|
-
|
|
1174
|
-
renderPicker();
|
|
1175
|
-
},
|
|
1176
|
-
);
|
|
1177
|
-
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
case 'picker-name': {
|
|
1181
|
-
applyTextKey(
|
|
1182
|
-
buf,
|
|
1183
|
-
() => {
|
|
1184
|
-
spawnName = pickerInput.trim();
|
|
1185
|
-
pickerInput = '';
|
|
1186
|
-
|
|
1187
|
-
// Adopt skips the prompt step: claude --resume opens its own
|
|
1188
|
-
// session picker inside the new PTY.
|
|
1189
|
-
if (spawnResume) {
|
|
1190
|
-
void spawnFromPicker('');
|
|
1191
|
-
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
mode = 'picker-prompt';
|
|
1196
|
-
|
|
1197
|
-
stdout.write(ansi.clear);
|
|
1198
|
-
|
|
1199
|
-
renderPicker();
|
|
1200
|
-
},
|
|
1201
|
-
() => {
|
|
1202
|
-
pickerInput = '';
|
|
1203
|
-
mode = 'picker-dir';
|
|
1204
|
-
|
|
1205
|
-
stdout.write(ansi.clear);
|
|
1206
|
-
|
|
1207
|
-
renderPicker();
|
|
1208
|
-
},
|
|
1209
|
-
);
|
|
1210
|
-
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
case 'picker-prompt': {
|
|
1214
|
-
applyTextKey(
|
|
1215
|
-
buf,
|
|
1216
|
-
() => {
|
|
1217
|
-
void spawnFromPicker(pickerInput.trim());
|
|
1218
|
-
},
|
|
1219
|
-
() => {
|
|
1220
|
-
pickerInput = '';
|
|
1221
|
-
mode = 'picker-name';
|
|
1222
|
-
|
|
1223
|
-
stdout.write(ansi.clear);
|
|
1224
|
-
|
|
1225
|
-
renderPicker();
|
|
1226
|
-
},
|
|
1227
|
-
);
|
|
860
|
+
case 'picker': {
|
|
861
|
+
picker.applyKey(buf);
|
|
1228
862
|
|
|
1229
863
|
return;
|
|
1230
864
|
}
|
|
1231
865
|
case 'picker-eject': {
|
|
1232
|
-
|
|
1233
|
-
buf,
|
|
1234
|
-
() => {
|
|
1235
|
-
void yankToHeadless(pickerInput.trim());
|
|
1236
|
-
},
|
|
1237
|
-
() => {
|
|
1238
|
-
openOverlay();
|
|
1239
|
-
},
|
|
1240
|
-
);
|
|
866
|
+
applyEjectKey(buf);
|
|
1241
867
|
}
|
|
1242
868
|
}
|
|
1243
869
|
});
|
|
1244
870
|
|
|
1245
871
|
stdout.on('resize', () => {
|
|
1246
|
-
|
|
872
|
+
const currentMode = service.getSnapshot().value;
|
|
873
|
+
|
|
874
|
+
if (currentMode === 'attached' && focusedID !== null) {
|
|
1247
875
|
void sendQuiet('session.resize', { session: focusedID, cols: cols(), rows: ptyRows() });
|
|
1248
876
|
}
|
|
1249
877
|
|
|
1250
|
-
if (
|
|
878
|
+
if (currentMode === 'home') {
|
|
1251
879
|
drawHome(fleetCount, leader.label);
|
|
1252
880
|
}
|
|
1253
881
|
|
|
1254
|
-
if (
|
|
882
|
+
if (currentMode === 'overlay') {
|
|
1255
883
|
stdout.write(ansi.clear);
|
|
1256
884
|
|
|
1257
885
|
renderOverlay();
|
|
1258
886
|
}
|
|
1259
887
|
|
|
1260
|
-
if (
|
|
888
|
+
if (currentMode === 'picker') {
|
|
889
|
+
stdout.write(ansi.clear);
|
|
890
|
+
picker.render();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (currentMode === 'picker-eject') {
|
|
1261
894
|
stdout.write(ansi.clear);
|
|
1262
895
|
|
|
1263
|
-
|
|
896
|
+
renderEject();
|
|
1264
897
|
}
|
|
1265
898
|
|
|
1266
899
|
scheduleStatus();
|
|
@@ -1282,6 +915,4 @@ process.on('SIGHUP', () => quit());
|
|
|
1282
915
|
|
|
1283
916
|
// ---- start ----
|
|
1284
917
|
stdout.write(ansi.altScreenOn);
|
|
1285
|
-
|
|
1286
|
-
drawHome(fleetCount, leader.label);
|
|
1287
|
-
scheduleStatus();
|
|
918
|
+
service.start();
|