@paradow/ledge 0.16.1 → 0.17.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.
Files changed (32) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/{attach-5H727FCW.js → attach-EYIX7FZN.js} +7 -7
  3. package/dist/cli/{bridge-LXMOQN6Q.js → bridge-6UHCD76N.js} +2 -2
  4. package/dist/cli/{chunk-MQICEVKR.js → chunk-36KJUV62.js} +6 -6
  5. package/dist/cli/chunk-5ZCHFGCV.js +115 -0
  6. package/dist/cli/{chunk-QJVODYJ5.js → chunk-AQ25HFBP.js} +40 -2
  7. package/dist/cli/{chunk-R5462SQM.js → chunk-ELLXOV4Q.js} +2 -1
  8. package/dist/cli/{chunk-BNKPXSUY.js → chunk-JQZHXVIZ.js} +1 -1
  9. package/dist/cli/{chunk-2H5FJH4Q.js → chunk-LAUNHDTX.js} +2 -2
  10. package/dist/cli/{chunk-DBSCO3HI.js → chunk-NC5YAEJB.js} +56 -7
  11. package/dist/cli/{chunk-S4ANPQGA.js → chunk-OVJL2QTN.js} +2 -2
  12. package/dist/cli/{chunk-TB47EHML.js → chunk-Q7YNLP66.js} +2 -2
  13. package/dist/cli/{chunk-Q5WWH7FG.js → chunk-QD27W6W5.js} +681 -630
  14. package/dist/cli/chunk-V2H7UD2N.js +6 -0
  15. package/dist/cli/{chunk-H36JD4E3.js → chunk-VE4LGNXX.js} +2 -2
  16. package/dist/cli/{chunk-DEXDXDO4.js → chunk-VVK4I6B5.js} +1 -1
  17. package/dist/cli/{daemon-DKH63BGV.js → daemon-QEJLAIBT.js} +355 -40
  18. package/dist/cli/{hook-LY2YTK5U.js → hook-AE5HC6AS.js} +18 -15
  19. package/dist/cli/index.js +30 -11
  20. package/dist/cli/{launch-UQEFFPBN.js → launch-B4G5OFBV.js} +7 -7
  21. package/dist/cli/send-2OZ3YZRV.js +18 -0
  22. package/dist/cli/{setup-3N2EOJXS.js → setup-2VM4GCDJ.js} +4 -4
  23. package/dist/cli/{status-ZQSYXILE.js → status-3VJAO6UL.js} +3 -3
  24. package/dist/cli/task-PMKNYP6M.js +18 -0
  25. package/dist/cli/tui-DWA2EDC2.js +3561 -0
  26. package/dist/cli/{uninstall-W2OMKU3B.js → uninstall-AKUWE33D.js} +4 -4
  27. package/dist/cli/{version-6BMBIXHN.js → version-WOGCIZY3.js} +1 -1
  28. package/dist/cli/who-37TNDYVA.js +11 -0
  29. package/package.json +7 -1
  30. package/dist/cli/chunk-J4ICZEGP.js +0 -6
  31. package/dist/cli/send-EY63XQXI.js +0 -14
  32. package/dist/cli/who-VGIMGQNS.js +0 -11
@@ -0,0 +1,3561 @@
1
+ import {
2
+ attachEnv
3
+ } from "./chunk-36KJUV62.js";
4
+ import "./chunk-VVK4I6B5.js";
5
+ import "./chunk-VE4LGNXX.js";
6
+ import {
7
+ ChatTurnSchema,
8
+ IslandPrefsSchema,
9
+ MachinesSchema,
10
+ NOTE_MAX,
11
+ NOTE_NOT_TYPED,
12
+ PROJECT_COLORS,
13
+ PROTOCOL_PORT,
14
+ ProjectsSchema,
15
+ backendOf,
16
+ isArchived,
17
+ offers,
18
+ parseServerMessage,
19
+ pendingKey,
20
+ pickName,
21
+ projectOf,
22
+ proposalRows,
23
+ readDaemonToken
24
+ } from "./chunk-QD27W6W5.js";
25
+ import "./chunk-LAUNHDTX.js";
26
+ import "./chunk-JQZHXVIZ.js";
27
+ import {
28
+ ledgeDir
29
+ } from "./chunk-ELLXOV4Q.js";
30
+
31
+ // src/tui/index.tsx
32
+ import { render } from "ink";
33
+
34
+ // ../fleet/src/token.ts
35
+ import { useSyncExternalStore } from "react";
36
+ var DAEMON_PROTOCOL_PREFIX = "ledge.";
37
+ var READ_TIMEOUT_MS = 1e4;
38
+ function createTokenSource(read) {
39
+ let cached = null, missing = false, inflight = null;
40
+ const fns = /* @__PURE__ */ new Set();
41
+ const setMissing = (m) => {
42
+ if (m !== missing) {
43
+ missing = m;
44
+ fns.forEach((f) => f(m));
45
+ }
46
+ };
47
+ const load = async () => {
48
+ let r, timer;
49
+ const unavailable = { kind: "unavailable" };
50
+ try {
51
+ const got = await Promise.race([read(), new Promise((res) => {
52
+ timer = setTimeout(() => res(unavailable), READ_TIMEOUT_MS);
53
+ })]);
54
+ r = got?.kind === "token" && typeof got.token === "string" || got?.kind === "absent" ? got : unavailable;
55
+ } catch {
56
+ r = unavailable;
57
+ } finally {
58
+ clearTimeout(timer);
59
+ }
60
+ if (r.kind === "token") cached = r.token;
61
+ else if (r.kind === "absent") cached = null;
62
+ setMissing(r.kind === "absent");
63
+ return r;
64
+ };
65
+ return {
66
+ get(fresh) {
67
+ if (!fresh && cached !== null) return Promise.resolve({ kind: "token", token: cached });
68
+ inflight ??= load().finally(() => {
69
+ inflight = null;
70
+ });
71
+ return inflight;
72
+ },
73
+ missing: () => missing,
74
+ onMissing(fn) {
75
+ fns.add(fn);
76
+ return () => {
77
+ fns.delete(fn);
78
+ };
79
+ }
80
+ };
81
+ }
82
+
83
+ // ../fleet/src/client.ts
84
+ function createDaemonClient(opts) {
85
+ const Ws = opts.WebSocketImpl ?? globalThis.WebSocket;
86
+ const min = opts.minBackoffMs ?? 2e3, max = opts.maxBackoffMs ?? 3e4, token = opts.token;
87
+ let ws = null, status = "connecting", backoff = min, timer = null, closed = false;
88
+ let failed = false;
89
+ let attempt = 0;
90
+ const msgFns = /* @__PURE__ */ new Set(), statusFns = /* @__PURE__ */ new Set();
91
+ const setStatus = (s) => {
92
+ if (s !== status) {
93
+ status = s;
94
+ statusFns.forEach((f) => f(s));
95
+ }
96
+ };
97
+ const schedule = () => {
98
+ if (closed) return;
99
+ timer = setTimeout(open, backoff);
100
+ backoff = Math.min(max, backoff * 2);
101
+ };
102
+ function open() {
103
+ if (closed) return;
104
+ setStatus(status === "open" ? "connecting" : status);
105
+ if (!token) {
106
+ dial(new Ws(opts.url));
107
+ return;
108
+ }
109
+ const mine = ++attempt;
110
+ void token.get(failed).then((r) => {
111
+ if (closed || mine !== attempt) return;
112
+ if (r.kind === "token") {
113
+ try {
114
+ dial(new Ws(opts.url, [DAEMON_PROTOCOL_PREFIX + r.token]));
115
+ return;
116
+ } catch {
117
+ }
118
+ }
119
+ failed = true;
120
+ setStatus("unreachable");
121
+ schedule();
122
+ });
123
+ }
124
+ function dial(sock) {
125
+ ws = sock;
126
+ let opened = false;
127
+ sock.onopen = () => {
128
+ if (ws !== sock) return;
129
+ opened = true;
130
+ setStatus("open");
131
+ };
132
+ sock.onmessage = (ev) => {
133
+ if (ws !== sock) return;
134
+ try {
135
+ const m = parseServerMessage(JSON.parse(String(ev.data)));
136
+ if (m.type === "snapshot") {
137
+ backoff = min;
138
+ failed = false;
139
+ }
140
+ msgFns.forEach((f) => f(m));
141
+ } catch {
142
+ }
143
+ };
144
+ sock.onerror = () => {
145
+ };
146
+ sock.onclose = () => {
147
+ if (ws !== sock) return;
148
+ ws = null;
149
+ if (!opened) failed = true;
150
+ setStatus("unreachable");
151
+ schedule();
152
+ };
153
+ }
154
+ return {
155
+ // An explicit connect (a « Reconnect ») says « connecting » at once, even after a drop: the automatic retries stay « unreachable ».
156
+ connect: () => {
157
+ closed = false;
158
+ if (timer) {
159
+ clearTimeout(timer);
160
+ timer = null;
161
+ }
162
+ const old = ws;
163
+ ws = null;
164
+ old?.close();
165
+ setStatus("connecting");
166
+ open();
167
+ },
168
+ close: () => {
169
+ closed = true;
170
+ attempt++;
171
+ if (timer) clearTimeout(timer);
172
+ const old = ws;
173
+ ws = null;
174
+ old?.close();
175
+ setStatus("connecting");
176
+ },
177
+ send: (m) => {
178
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify(m));
179
+ },
180
+ status: () => status,
181
+ onMessage: (f) => {
182
+ msgFns.add(f);
183
+ return () => msgFns.delete(f);
184
+ },
185
+ onStatus: (f) => {
186
+ statusFns.add(f);
187
+ return () => statusFns.delete(f);
188
+ }
189
+ };
190
+ }
191
+
192
+ // ../fleet/src/store.ts
193
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
194
+ var ERRORS_KEPT = 20;
195
+ var empty = () => ({ sessions: [], archived: [], byId: /* @__PURE__ */ new Map(), status: "connecting", previews: /* @__PURE__ */ new Map(), threads: /* @__PURE__ */ new Map(), lastError: null, errors: [], recents: [], launcher: null, lastLaunched: null, lastListing: null, chat: [], chatLive: null, daemon: null, snapshotSeq: 0, lastUpdate: null });
196
+ var archivedOf = (m) => [...m.values()].filter(isArchived).sort((a, b) => (b.archivedAt ?? "").localeCompare(a.archivedAt ?? "") || a.id.localeCompare(b.id));
197
+ var state = empty();
198
+ var subs = /* @__PURE__ */ new Set();
199
+ var emit = () => subs.forEach((f) => f());
200
+ var sorted = (m) => [...m.values()].filter((s) => !isArchived(s)).sort((a, b) => a.startedAt.localeCompare(b.startedAt) || a.id.localeCompare(b.id));
201
+ var daemonStore = {
202
+ getState: () => state,
203
+ subscribe(fn) {
204
+ subs.add(fn);
205
+ return () => {
206
+ subs.delete(fn);
207
+ };
208
+ },
209
+ setStatus(status) {
210
+ state = { ...state, status };
211
+ emit();
212
+ },
213
+ reset() {
214
+ state = empty();
215
+ emit();
216
+ },
217
+ /** The overlay lists its own question at once; the daemon's events fill it in. */
218
+ beginTurn(turn) {
219
+ state = { ...state, chat: [...state.chat.filter((t) => t.qid !== turn.qid), turn], chatLive: null };
220
+ emit();
221
+ },
222
+ apply(m) {
223
+ if (m.type === "chat.history") {
224
+ state = { ...state, chat: m.turns, chatLive: null };
225
+ emit();
226
+ return;
227
+ }
228
+ if (m.type === "chat.step" || m.type === "chat.delta" || m.type === "chat.done" || m.type === "chat.error") {
229
+ const chat = state.chat.map((t) => t.qid !== m.qid ? t : m.type === "chat.step" ? { ...t, steps: [...t.steps, m.step] } : m.type === "chat.done" ? { ...t, answer: m.text, answeredAt: m.at, ...m.proposal ? { proposal: m.proposal } : {}, ...m.proposals ? { proposals: m.proposals } : {}, ...m.relay ? { relay: m.relay } : {}, ...m.closes ? { closes: m.closes } : {}, ...m.projectProposal ? { projectProposal: m.projectProposal } : {} } : m.type === "chat.error" ? { ...t, error: m.message } : t);
230
+ const chatLive = m.type === "chat.delta" ? { qid: m.qid, text: (state.chatLive?.qid === m.qid ? state.chatLive.text : "") + m.text } : m.type === "chat.step" ? state.chatLive : null;
231
+ state = { ...state, chat, chatLive };
232
+ emit();
233
+ return;
234
+ }
235
+ if (m.type === "error") {
236
+ const e = { id: m.id, message: m.message, seq: (state.lastError?.seq ?? 0) + 1, ...m.request ? { request: m.request } : {}, ...m.code ? { code: m.code } : {} };
237
+ state = { ...state, lastError: e, errors: [...state.errors, e].slice(-ERRORS_KEPT) };
238
+ emit();
239
+ return;
240
+ }
241
+ if (m.type === "session.launched") {
242
+ state = { ...state, lastLaunched: { id: m.id, seq: (state.lastLaunched?.seq ?? 0) + 1 } };
243
+ emit();
244
+ return;
245
+ }
246
+ if (m.type === "folder.listing") {
247
+ const { type: _, ...l } = m;
248
+ state = { ...state, lastListing: { ...l, seq: (state.lastListing?.seq ?? 0) + 1 } };
249
+ emit();
250
+ return;
251
+ }
252
+ if (m.type === "update.progress" || m.type === "update.failed") {
253
+ state = { ...state, lastUpdate: { msg: m, seq: (state.lastUpdate?.seq ?? 0) + 1 } };
254
+ emit();
255
+ return;
256
+ }
257
+ if (m.type === "recents") {
258
+ state = { ...state, recents: m.recents };
259
+ emit();
260
+ return;
261
+ }
262
+ const byId = new Map(state.byId);
263
+ const previews = new Map(state.previews);
264
+ const threads = new Map(state.threads);
265
+ let { recents, launcher, daemon, snapshotSeq } = state;
266
+ switch (m.type) {
267
+ case "snapshot":
268
+ byId.clear();
269
+ for (const s of m.sessions) byId.set(s.id, s);
270
+ for (const id of previews.keys()) if (!byId.has(id)) previews.delete(id);
271
+ recents = m.recents ?? [];
272
+ launcher = m.launcher ?? null;
273
+ daemon = m.daemon ?? null;
274
+ snapshotSeq += 1;
275
+ for (const id of [...threads.keys()]) if (!byId.get(id)?.offline) threads.delete(id);
276
+ break;
277
+ case "session.updated":
278
+ byId.set(m.session.id, m.session);
279
+ break;
280
+ case "session.removed":
281
+ byId.delete(m.id);
282
+ previews.delete(m.id);
283
+ threads.delete(m.id);
284
+ break;
285
+ case "session.preview":
286
+ previews.set(m.id, m.lines);
287
+ break;
288
+ case "session.thread":
289
+ threads.set(m.id, m.items);
290
+ break;
291
+ case "session.thread.item": {
292
+ const cur = threads.get(m.id);
293
+ if (!cur) break;
294
+ const at = cur.findIndex((i) => i.id === m.item.id);
295
+ threads.set(m.id, at >= 0 ? cur.map((i, k) => k === at ? m.item : i) : [...cur, m.item]);
296
+ break;
297
+ }
298
+ }
299
+ state = { ...state, byId, previews, threads, sessions: sorted(byId), archived: archivedOf(byId), recents, launcher, daemon, snapshotSeq };
300
+ emit();
301
+ }
302
+ };
303
+ function attachClient(client) {
304
+ const a = client.onMessage((m) => daemonStore.apply(m));
305
+ const b = client.onStatus((s) => daemonStore.setStatus(s));
306
+ return () => {
307
+ a();
308
+ b();
309
+ };
310
+ }
311
+ var useDaemon = () => useSyncExternalStore2(daemonStore.subscribe, daemonStore.getState, daemonStore.getState);
312
+
313
+ // ../fleet/src/machines.ts
314
+ import { useSyncExternalStore as useSyncExternalStore3 } from "react";
315
+ var LOCAL_MACHINE = { id: "local", label: "Local", url: "ws://127.0.0.1:47831" };
316
+ function parseMachines(raw) {
317
+ const list = MachinesSchema.parse(Array.isArray(raw) ? raw.map((m) => {
318
+ const rest = { ...m ?? {} };
319
+ delete rest.url;
320
+ return rest;
321
+ }) : raw);
322
+ return list.map((m, i) => i === 0 ? { ...m, url: LOCAL_MACHINE.url } : m);
323
+ }
324
+ function machineUrl(machine, local) {
325
+ if (!machine.ssh) return local.url;
326
+ const url = new URL(local.url);
327
+ if (machine.ssh) url.pathname = `/ssh/${machine.ssh}`;
328
+ return url.toString();
329
+ }
330
+ function loadMachines(prefix = "ledge") {
331
+ try {
332
+ return parseMachines(JSON.parse(localStorage.getItem(`${prefix}.machines`) ?? "null"));
333
+ } catch {
334
+ return [LOCAL_MACHINE];
335
+ }
336
+ }
337
+ function probeMachine(url, ssh, opts = {}) {
338
+ const Ws = opts.WebSocketImpl ?? globalThis.WebSocket;
339
+ return new Promise((resolve) => {
340
+ let sock, error = "", done = false, opened = false;
341
+ const finish = (r) => {
342
+ if (done) return;
343
+ done = true;
344
+ clearTimeout(timer);
345
+ if (sock) {
346
+ sock.onclose = sock.onmessage = sock.onerror = null;
347
+ sock.close();
348
+ }
349
+ resolve(r);
350
+ };
351
+ const timer = setTimeout(() => finish({ ok: false, error: "SSH: no Ledge snapshot received." }), opts.timeoutMs ?? 75e3);
352
+ const start = (protocols) => {
353
+ if (done) return;
354
+ try {
355
+ const target2 = machineUrl({ id: "probe", label: ssh, ssh }, { ...LOCAL_MACHINE, url });
356
+ sock = protocols ? new Ws(target2, protocols) : new Ws(target2);
357
+ } catch (e) {
358
+ finish({ ok: false, error: protocols ? "The local Ledge daemon could not be reached." : e.message });
359
+ return;
360
+ }
361
+ sock.onmessage = (ev) => {
362
+ let m;
363
+ try {
364
+ m = parseServerMessage(JSON.parse(String(ev.data)));
365
+ } catch {
366
+ return;
367
+ }
368
+ if (m.type === "error") error = m.message;
369
+ else if (m.type === "snapshot") finish(m.sshAlias === ssh ? { ok: true, version: m.daemon?.version, sessions: m.sessions.length } : { ok: false, error: "Update the local Ledge daemon: SSH transport is unavailable" });
370
+ };
371
+ sock.onopen = () => {
372
+ opened = true;
373
+ };
374
+ sock.onerror = () => {
375
+ };
376
+ sock.onclose = () => {
377
+ if (!opened) void opts.token?.get(true);
378
+ finish({ ok: false, error: error || "The local Ledge daemon closed the connection." });
379
+ };
380
+ };
381
+ if (!opts.token) {
382
+ start();
383
+ return;
384
+ }
385
+ void opts.token.get(false).then((r) => {
386
+ if (r.kind === "token") start([DAEMON_PROTOCOL_PREFIX + r.token]);
387
+ else finish({ ok: false, error: r.kind === "absent" ? "The local Ledge daemon token is missing: start or update the daemon." : "The local Ledge daemon token could not be read." });
388
+ });
389
+ });
390
+ }
391
+ var qualify = (machine, id) => `${machine}:${encodeURIComponent(id)}`;
392
+ function splitId(id) {
393
+ const at = id.indexOf(":");
394
+ if (at < 0) return null;
395
+ try {
396
+ return { machine: id.slice(0, at), id: decodeURIComponent(id.slice(at + 1)) };
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+ var quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
402
+ var wslArg = (s) => s !== "" && !/["\u0000-\u001f\u007f-\u009f]/.test(s) && !s.endsWith("\\");
403
+ function terminalCommand(machine, rawId, opts = {}) {
404
+ if (!machine.launcher || !rawId) return null;
405
+ const command = [...machine.launcher, "attach", ...opts.readOnly ? ["--read-only"] : [], rawId];
406
+ const argv = machine.ssh ? ["ssh", "-t", "--", machine.ssh, command.map(quote).join(" ")] : command;
407
+ return argv.every(wslArg) ? argv : null;
408
+ }
409
+ function shellCommand(machine, dir) {
410
+ if (!wslArg(dir)) return null;
411
+ const go = `cd ${quote(dir)} 2>/dev/null || cd; exec`;
412
+ const argv = machine.ssh ? ["ssh", "-t", "--", machine.ssh, `${go} $SHELL -l`] : ["sh", "-c", `${go} $(getent passwd $(id -un) | cut -d: -f7) -l`];
413
+ return argv.every(wslArg) ? argv : null;
414
+ }
415
+ var EMPTY = [];
416
+ var noopSubscribe = () => () => {
417
+ };
418
+ var emptySnapshot = () => EMPTY;
419
+ function useMachines(client) {
420
+ return useSyncExternalStore3(client.fleet?.subscribe ?? noopSubscribe, client.fleet?.machines ?? emptySnapshot, emptySnapshot);
421
+ }
422
+ var nullSnapshot = () => null;
423
+ function useProjects(client) {
424
+ return useSyncExternalStore3(client.fleet?.subscribe ?? noopSubscribe, client.fleet?.projects ?? nullSnapshot, nullSnapshot);
425
+ }
426
+
427
+ // ../fleet/src/changes.ts
428
+ var liveHelpers = (session) => session.helpers.filter((h) => h.state !== "exited" && h.state !== "idle");
429
+ var isPending = (s) => s.state === "waiting_permission" || s.state === "waiting_input";
430
+ var sameRequest = (a, b) => !!a && !!b && a.kind === b.kind && a.text === b.text && a.helperId === b.helperId;
431
+ var isBusy = (s) => s.state === "working" || s.state === "idle" && liveHelpers(s).length > 0;
432
+ function changeOf(was, now) {
433
+ if (!was) return { kind: "joined", id: now.id };
434
+ if (now.state === "exited" && was.state !== "exited") {
435
+ const code = now.exitCode ?? null;
436
+ return code !== null && code !== 0 ? { kind: "failed", id: now.id } : { kind: "left", id: now.id, exitCode: code };
437
+ }
438
+ if (now.state === "error" && was.state !== "error") return { kind: "failed", id: now.id };
439
+ if (now.state === "idle" && isBusy(was) && !isBusy(now)) return { kind: "finished", id: now.id };
440
+ if (isPending(now) && !(isPending(was) && sameRequest(was.pending, now.pending))) return { kind: "asked", id: now.id, ask: now.state === "waiting_permission" ? "approval" : "question" };
441
+ return null;
442
+ }
443
+
444
+ // ../fleet/src/fleet.ts
445
+ var toWire = (list) => list.map(({ id, label, ssh }) => ({ id, label, ...ssh ? { ssh } : {} }));
446
+ var OPERATIONS = /* @__PURE__ */ new Set(["session.launch", "session.relay", "session.relay.cancel", "session.close"]);
447
+ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
448
+ const prefix = opts.prefix ?? "ledge";
449
+ const HISTORY_KEY = `${prefix}.fleet.history`, MACHINES_KEY = `${prefix}.machines`;
450
+ let configs = parseMachines(config), selected = configs[0].id, connected = false, configuring = false, state2 = [];
451
+ const peers = /* @__PURE__ */ new Map();
452
+ const machineMessages = /* @__PURE__ */ new Set();
453
+ const messages = /* @__PURE__ */ new Set(), statuses = /* @__PURE__ */ new Set(), listeners = /* @__PURE__ */ new Set();
454
+ const turns2 = /* @__PURE__ */ new Map(), bindings = /* @__PURE__ */ new Map();
455
+ const documents = /* @__PURE__ */ new Map();
456
+ const inspections = /* @__PURE__ */ new Map();
457
+ const listings = /* @__PURE__ */ new Map();
458
+ const scans = /* @__PURE__ */ new Map();
459
+ const modelLists = /* @__PURE__ */ new Map();
460
+ const operations = /* @__PURE__ */ new Map();
461
+ const closeRows = /* @__PURE__ */ new Map();
462
+ const endOperation = (machine) => {
463
+ closeRows.delete(machine);
464
+ return operations.delete(machine);
465
+ };
466
+ const unfound = /* @__PURE__ */ new Map();
467
+ const answerUnfoundInspections = (machine) => {
468
+ for (const [requestId, owner] of inspections) if (owner === machine) {
469
+ inspections.delete(requestId);
470
+ const repos = unfound.get(requestId) ?? [];
471
+ unfound.delete(requestId);
472
+ emit2({ type: "repos.info", requestId, repos });
473
+ }
474
+ };
475
+ const answerUnfoundListings = (machine, error) => {
476
+ for (const [requestId, l] of listings) if (l.machine === machine) {
477
+ listings.delete(requestId);
478
+ emit2({ type: "folder.listing", requestId, path: l.path, parent: null, entries: [], error });
479
+ }
480
+ };
481
+ const answerFailedScans = (machine, error) => {
482
+ for (const [requestId, owner] of scans) if (owner === machine) {
483
+ scans.delete(requestId);
484
+ emit2({ type: "repos.found", requestId, repos: [], truncated: false, error });
485
+ }
486
+ };
487
+ const answerFailedModels = (machine, error) => {
488
+ for (const [requestId, l] of modelLists) if (l.machine === machine) {
489
+ modelLists.delete(requestId);
490
+ emit2({ type: "models.listing", requestId, tool: l.tool, models: [], error });
491
+ }
492
+ };
493
+ let source = "local";
494
+ let projects = null;
495
+ let island;
496
+ let mobile;
497
+ let mobileError;
498
+ let routing = null, resetAt = "", lastStatus = "connecting";
499
+ const emit2 = (m) => messages.forEach((fn) => fn(m));
500
+ const day = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
501
+ let historyDay = day();
502
+ try {
503
+ const saved = JSON.parse(localStorage.getItem(HISTORY_KEY) ?? "null");
504
+ if (saved?.day === day()) {
505
+ for (const raw of (saved.turns ?? []).slice(-50)) {
506
+ const t = ChatTurnSchema.parse(raw);
507
+ if (!t.answeredAt && !t.error) t.error = "Connection interrupted";
508
+ turns2.set(t.qid, t);
509
+ }
510
+ for (const [qid, b] of saved.bindings ?? []) if (turns2.has(qid) && (opts.machinesFromDaemon || configs.some((c) => c.id === b.machine)) && typeof b.raw === "string") bindings.set(qid, b);
511
+ resetAt = typeof saved.resetAt === "string" ? saved.resetAt : "";
512
+ }
513
+ } catch {
514
+ }
515
+ function history() {
516
+ const list = [...turns2.values()].sort((a, b) => a.askedAt.localeCompare(b.askedAt)).slice(-50);
517
+ const kept = new Set(list.map((t) => t.qid));
518
+ for (const id of turns2.keys()) if (!kept.has(id)) {
519
+ turns2.delete(id);
520
+ bindings.delete(id);
521
+ }
522
+ try {
523
+ localStorage.setItem(HISTORY_KEY, JSON.stringify({ day: historyDay, turns: list, bindings: [...bindings], resetAt }));
524
+ } catch {
525
+ }
526
+ emit2({ type: "chat.history", turns: list });
527
+ }
528
+ const available = (p) => !!p && p.status === "open" && p.fresh && !!p.snapshot;
529
+ const status = () => [...peers.values()].some(available) ? "open" : [...peers.values()].some((p) => p.status === "connecting") ? "connecting" : "unreachable";
530
+ const stale = (p) => !!opts.keepStale && !!p && !available(p) && !!p.snapshot;
531
+ const session = (p, s) => ({ ...s, id: qualify(p.config.id, s.id), machineId: p.config.id, machineName: p.config.label, ...stale(p) ? { offline: true } : {} });
532
+ const stateSessions = (p) => (stale(p) ? p.snapshot.sessions.map((s) => ({ ...s, offline: true })) : p.snapshot?.sessions ?? []).filter((s) => !isArchived(s));
533
+ function refresh() {
534
+ state2 = configs.map((c) => {
535
+ const p = peers.get(c.id);
536
+ return { ...c, status: p.status, ready: available(p), recents: p.snapshot?.recents ?? [], sessions: stateSessions(p), launcher: p.snapshot?.launcher ?? null, version: p.snapshot?.daemon?.version, daemon: p.snapshot?.daemon, error: p.error };
537
+ });
538
+ const next2 = status();
539
+ if (next2 !== lastStatus) {
540
+ lastStatus = next2;
541
+ statuses.forEach((fn) => fn(next2));
542
+ }
543
+ listeners.forEach((fn) => fn());
544
+ }
545
+ function publish() {
546
+ if (configuring) return;
547
+ state2 = configs.map((c) => {
548
+ const p = peers.get(c.id);
549
+ return { ...c, status: p.status, ready: available(p), recents: p.snapshot?.recents ?? [], sessions: stateSessions(p), launcher: p.snapshot?.launcher ?? null, version: p.snapshot?.daemon?.version, daemon: p.snapshot?.daemon, error: p.error };
550
+ });
551
+ const primary = peers.get("local")?.snapshot;
552
+ emit2({ type: "snapshot", sessions: [...peers.values()].filter((p) => available(p) || stale(p)).flatMap((p) => p.snapshot.sessions.map((s) => session(p, s))), recents: available(peers.get(selected)) ? peers.get(selected).snapshot.recents : [], launcher: primary?.launcher, daemon: primary?.daemon });
553
+ for (const p of peers.values()) if (available(p) || stale(p)) for (const [id, lines] of p.previews) emit2({ type: "session.preview", id: qualify(p.config.id, id), lines });
554
+ const next2 = status();
555
+ if (next2 !== lastStatus) {
556
+ lastStatus = next2;
557
+ statuses.forEach((fn) => fn(next2));
558
+ }
559
+ listeners.forEach((fn) => fn());
560
+ }
561
+ function fail(qid, message) {
562
+ const t = turns2.get(qid);
563
+ if (t) turns2.set(qid, { ...t, error: message });
564
+ history();
565
+ emit2({ type: "chat.error", qid, message });
566
+ }
567
+ function globalQid(machine, raw) {
568
+ return [...bindings].find(([, b]) => b.machine === machine && b.raw === raw)?.[0] ?? qualify(machine, raw);
569
+ }
570
+ const launchedOn = (p, x) => ({ ...x, ...x.launchedId ? { launchedId: qualify(p.config.id, x.launchedId) } : {} });
571
+ function mappedTurn(p, raw) {
572
+ const qid = globalQid(p.config.id, raw.qid), old = turns2.get(qid);
573
+ bindings.set(qid, { machine: p.config.id, raw: raw.qid });
574
+ return {
575
+ ...raw,
576
+ qid,
577
+ question: old?.question ?? raw.question,
578
+ machineId: p.config.id,
579
+ machineName: p.config.label,
580
+ steps: raw.steps.map((s) => ({ ...s, ...s.id ? { id: qualify(p.config.id, s.id) } : {} })),
581
+ ...raw.proposal ? { proposal: launchedOn(p, raw.proposal) } : {},
582
+ ...raw.proposals ? { proposals: raw.proposals.map((x) => launchedOn(p, x)) } : {},
583
+ ...raw.relay ? { relay: { ...raw.relay, id: qualify(p.config.id, raw.relay.id) } } : {},
584
+ ...raw.closes ? { closes: raw.closes.map((c) => ({ ...c, id: qualify(p.config.id, c.id) })) } : {}
585
+ };
586
+ }
587
+ function apply(next2) {
588
+ configuring = true;
589
+ for (const [id, p] of peers) if (!next2.some((c) => c.id === id && c.url === p.config.url && c.ssh === p.config.ssh)) {
590
+ p.client.close();
591
+ p.off.forEach((off) => off());
592
+ peers.delete(id);
593
+ }
594
+ configs = next2;
595
+ for (const c of configs) {
596
+ const p = peers.get(c.id);
597
+ if (p) p.config = c;
598
+ else add(c);
599
+ }
600
+ if (!peers.has(selected)) selected = "local";
601
+ const gone = source === "daemon" ? [...bindings].filter(([, b]) => !configs.some((c) => c.id === b.machine)) : [];
602
+ for (const [qid] of gone) bindings.delete(qid);
603
+ configuring = false;
604
+ publish();
605
+ if (gone.length) history();
606
+ }
607
+ function receive(p, m) {
608
+ const machine = p.config.id;
609
+ if (m.type === "machines") {
610
+ if (machine !== "local" || !opts.machinesFromDaemon) return;
611
+ let next2;
612
+ try {
613
+ next2 = parseMachines(m.machines);
614
+ } catch {
615
+ return;
616
+ }
617
+ const mine = next2.length === 1 ? loadMachines(prefix) : [];
618
+ source = "daemon";
619
+ if (next2.length === 1 && mine.length > 1) {
620
+ p.client.send({ type: "machines.set", machines: toWire(mine) });
621
+ return;
622
+ }
623
+ try {
624
+ localStorage.removeItem(MACHINES_KEY);
625
+ } catch {
626
+ }
627
+ apply(next2);
628
+ return;
629
+ }
630
+ if (m.type === "projects") {
631
+ if (machine !== "local") return;
632
+ projects = m.projects;
633
+ listeners.forEach((fn) => fn());
634
+ return;
635
+ }
636
+ if (m.type === "island") {
637
+ if (machine !== "local") return;
638
+ island = m.prefs;
639
+ listeners.forEach((fn) => fn());
640
+ return;
641
+ }
642
+ if (m.type === "mobile") {
643
+ if (machine !== "local") return;
644
+ mobile = m.state;
645
+ mobileError = void 0;
646
+ listeners.forEach((fn) => fn());
647
+ return;
648
+ }
649
+ machineMessages.forEach((fn) => fn(machine, m));
650
+ if (m.type === "update.progress") p.restarting = m.step === "restarting";
651
+ if (m.type === "update.failed") p.restarting = false;
652
+ if (m.type === "snapshot") {
653
+ if (p.config.ssh && m.sshAlias !== p.config.ssh) {
654
+ p.error = "Update the local Ledge daemon: SSH transport is unavailable";
655
+ p.client.close();
656
+ publish();
657
+ return;
658
+ }
659
+ p.snapshot = m;
660
+ p.fresh = true;
661
+ p.error = void 0;
662
+ p.restarting = false;
663
+ p.previews.clear();
664
+ publish();
665
+ return;
666
+ }
667
+ if (m.type === "session.updated") {
668
+ if (p.snapshot) p.snapshot = { ...p.snapshot, sessions: [...p.snapshot.sessions.filter((s) => s.id !== m.session.id), m.session] };
669
+ if (available(p)) emit2({ type: "session.updated", session: session(p, m.session) });
670
+ refresh();
671
+ return;
672
+ }
673
+ if (m.type === "session.removed") {
674
+ if (p.snapshot) p.snapshot = { ...p.snapshot, sessions: p.snapshot.sessions.filter((s) => s.id !== m.id) };
675
+ p.previews.delete(m.id);
676
+ emit2({ ...m, id: qualify(machine, m.id) });
677
+ refresh();
678
+ return;
679
+ }
680
+ if (m.type === "recents") {
681
+ if (p.snapshot) p.snapshot = { ...p.snapshot, recents: m.recents };
682
+ if (machine === selected && available(p)) emit2(m);
683
+ refresh();
684
+ return;
685
+ }
686
+ if (m.type === "session.preview") {
687
+ p.previews.set(m.id, m.lines);
688
+ if (available(p)) emit2({ ...m, id: qualify(machine, m.id) });
689
+ return;
690
+ }
691
+ if (m.type === "session.thread" || m.type === "session.thread.item" || m.type === "session.queue.restored") {
692
+ if (available(p)) emit2({ ...m, id: qualify(machine, m.id) });
693
+ return;
694
+ }
695
+ if (m.type === "session.launched") {
696
+ if (!closeRows.has(machine)) operations.delete(machine);
697
+ emit2({ ...m, id: qualify(machine, m.id) });
698
+ return;
699
+ }
700
+ if (m.type === "session.closed") {
701
+ emit2({ ...m, qid: globalQid(machine, m.qid), id: qualify(machine, m.id) });
702
+ return;
703
+ }
704
+ if (m.type === "folder.listing") {
705
+ if (m.requestId !== void 0 && listings.has(m.requestId)) {
706
+ if (listings.get(m.requestId).machine === machine) {
707
+ listings.delete(m.requestId);
708
+ emit2(m);
709
+ }
710
+ return;
711
+ }
712
+ const creation = m.requestId === void 0 ? [...listings].find(([, l]) => l.machine === machine && l.created !== void 0 && (m.error === void 0 ? m.path === l.created : m.path === l.path)) : void 0;
713
+ if (creation) {
714
+ listings.delete(creation[0]);
715
+ emit2({ ...m, requestId: creation[0] });
716
+ return;
717
+ }
718
+ if (m.requestId === void 0 && machine === selected) emit2(m);
719
+ return;
720
+ }
721
+ if (m.type === "document.content" || m.type === "document.error") {
722
+ if (documents.get(m.requestId) === machine) {
723
+ documents.delete(m.requestId);
724
+ emit2(m);
725
+ }
726
+ return;
727
+ }
728
+ if (m.type === "repos.info") {
729
+ if (inspections.get(m.requestId) === machine) {
730
+ inspections.delete(m.requestId);
731
+ unfound.delete(m.requestId);
732
+ emit2(m);
733
+ }
734
+ return;
735
+ }
736
+ if (m.type === "repos.found") {
737
+ if (scans.get(m.requestId) === machine) {
738
+ scans.delete(m.requestId);
739
+ emit2(m);
740
+ }
741
+ return;
742
+ }
743
+ if (m.type === "models.listing") {
744
+ if (modelLists.get(m.requestId)?.machine === machine) {
745
+ modelLists.delete(m.requestId);
746
+ emit2(m);
747
+ }
748
+ return;
749
+ }
750
+ if (m.type === "chat.routed") {
751
+ if (routing?.qid !== m.qid || routing.coordinator !== machine) return;
752
+ const req = routing;
753
+ routing = null;
754
+ if (m.result.machineId === null) {
755
+ const old = turns2.get(m.qid);
756
+ turns2.set(m.qid, { ...old, answer: m.result.text, answeredAt: (/* @__PURE__ */ new Date()).toISOString(), choices: m.result.choices?.map((id) => ({ id, label: peers.get(id)?.config.label ?? id })) });
757
+ history();
758
+ } else {
759
+ const target2 = peers.get(m.result.machineId);
760
+ bindings.set(req.qid, { machine: m.result.machineId, raw: req.qid });
761
+ if (!available(target2)) {
762
+ fail(req.qid, `${target2?.config.label ?? m.result.machineId} is offline`);
763
+ return;
764
+ }
765
+ target2.client.send({ type: "chat.ask", qid: req.qid, text: m.result.text, tool: req.tool });
766
+ }
767
+ return;
768
+ }
769
+ if (m.type === "chat.history") {
770
+ if (m.turns.some((t) => t.qid === operations.get(machine) && t.relay && (t.relay.status || t.relay.error)) && !closeRows.has(machine)) endOperation(machine);
771
+ const close = closeRows.get(machine), closing = close && m.turns.find((t) => t.qid === close.qid);
772
+ if (close && closing?.closes && close.rows.every((r) => {
773
+ const c = closing.closes[r];
774
+ return !c || c.closedAt || c.error !== void 0 && c.error !== close.errors[r];
775
+ })) endOperation(machine);
776
+ for (const raw of m.turns) if (raw.askedAt >= resetAt && raw.askedAt.slice(0, 10) === historyDay) {
777
+ const t = mappedTurn(p, raw);
778
+ turns2.set(t.qid, t);
779
+ }
780
+ history();
781
+ return;
782
+ }
783
+ if (m.type === "chat.step" || m.type === "chat.delta" || m.type === "chat.done" || m.type === "chat.error") {
784
+ if (routing?.qid === m.qid && routing.coordinator === machine && m.type === "chat.error") {
785
+ routing = null;
786
+ fail(m.qid, m.message);
787
+ return;
788
+ }
789
+ const qid = globalQid(machine, m.qid), old = turns2.get(qid);
790
+ if (!old) return;
791
+ if (m.type === "chat.done") {
792
+ const raw = { ...old, qid: m.qid, answer: m.text, answeredAt: m.at, proposal: m.proposal, proposals: m.proposals, relay: m.relay, closes: m.closes, projectProposal: m.projectProposal };
793
+ const t = mappedTurn(p, { ...raw, steps: [] });
794
+ turns2.set(qid, { ...t, steps: old.steps });
795
+ history();
796
+ } else if (m.type === "chat.error") fail(qid, m.message);
797
+ else if (m.type === "chat.step") {
798
+ const step2 = { ...m.step, ...m.step.id ? { id: qualify(machine, m.step.id) } : {} };
799
+ turns2.set(qid, { ...old, steps: [...old.steps, step2] });
800
+ emit2({ ...m, qid, step: step2 });
801
+ } else emit2({ ...m, qid });
802
+ return;
803
+ }
804
+ if (m.type === "error") {
805
+ if (p.restarting) return;
806
+ if (m.request?.startsWith("mobile.")) {
807
+ if (machine === "local") {
808
+ mobileError = { request: m.request, message: m.message, ...m.code ? { code: m.code } : {} };
809
+ listeners.forEach((fn) => fn());
810
+ }
811
+ return;
812
+ }
813
+ if (!p.fresh) {
814
+ p.error = m.message;
815
+ publish();
816
+ return;
817
+ }
818
+ if (m.request === "repos.inspect") {
819
+ answerUnfoundInspections(machine);
820
+ return;
821
+ }
822
+ if (m.request === "repos.scan") {
823
+ answerFailedScans(machine, "outdated");
824
+ return;
825
+ }
826
+ if (m.request === "models.list") {
827
+ answerFailedModels(machine, "outdated");
828
+ return;
829
+ }
830
+ if ((m.request === "folder.list" || m.request === "folder.create") && [...listings.values()].some((l) => l.machine === machine)) {
831
+ answerUnfoundListings(machine, m.message || "not found");
832
+ return;
833
+ }
834
+ if (m.request !== void 0 && OPERATIONS.has(m.request)) endOperation(machine);
835
+ emit2({ ...m, ...m.id ? { id: qualify(machine, m.id) } : {}, message: configs.length > 1 ? `${p.config.label}: ${m.message}` : m.message });
836
+ return;
837
+ }
838
+ if (machine === "local") emit2(m);
839
+ }
840
+ function add(c) {
841
+ const client = makeClient({ url: machineUrl(c, configs[0]), token: opts.token });
842
+ const p = { config: c, client, status: "connecting", fresh: false, snapshot: null, previews: /* @__PURE__ */ new Map(), off: [] };
843
+ peers.set(c.id, p);
844
+ p.off.push(client.onMessage((m) => receive(p, m)), client.onStatus((s) => {
845
+ p.status = s;
846
+ if (s !== "open") {
847
+ p.fresh = false;
848
+ const op = operations.get(c.id), closing = closeRows.has(c.id);
849
+ if (endOperation(c.id)) emit2({ type: "error", message: `${c.label} is offline`, ...op === "launch" ? { request: "session.launch" } : closing ? { request: "session.close" } : {} });
850
+ answerUnfoundInspections(c.id);
851
+ answerUnfoundListings(c.id, "offline");
852
+ answerFailedScans(c.id, "offline");
853
+ answerFailedModels(c.id, "offline");
854
+ for (const [requestId, owner] of documents) if (owner === c.id) {
855
+ documents.delete(requestId);
856
+ emit2({ type: "document.error", requestId, path: "", code: "read_failed" });
857
+ }
858
+ if (routing?.coordinator === c.id) {
859
+ const qid = routing.qid;
860
+ routing = null;
861
+ fail(qid, `${c.label} is offline`);
862
+ }
863
+ for (const [qid, b] of bindings) {
864
+ const t = turns2.get(qid);
865
+ if (b.machine === c.id && t && !t.answeredAt && !t.error) fail(qid, `${c.label} is offline`);
866
+ }
867
+ }
868
+ publish();
869
+ }));
870
+ if (connected) client.connect();
871
+ }
872
+ for (const c of configs) add(c);
873
+ function inventory() {
874
+ return { machines: state2.map((m) => ({ id: m.id, label: m.label, online: m.ready, local: m.id === "local", sessions: m.sessions.slice(0, 500).map((s) => ({ id: s.id, name: s.name, project: s.repo ?? s.project, cwd: s.cwd, topic: s.topic, state: s.state, readOnly: s.source === "hook_only" })), folders: m.recents.slice(0, 100).map((r) => ({ project: r.project, cwd: r.cwd })) })), history: [...turns2.values()].slice(-30).map((t) => ({ question: t.question.slice(0, 2e4), answer: t.answer?.slice(0, 2e4), machineId: t.machineId })) };
875
+ }
876
+ const api = {
877
+ connect() {
878
+ connected = true;
879
+ peers.forEach((p) => p.client.connect());
880
+ publish();
881
+ history();
882
+ },
883
+ close() {
884
+ connected = false;
885
+ peers.forEach((p) => p.client.close());
886
+ },
887
+ status,
888
+ onMessage(fn) {
889
+ messages.add(fn);
890
+ return () => {
891
+ messages.delete(fn);
892
+ };
893
+ },
894
+ onStatus(fn) {
895
+ statuses.add(fn);
896
+ return () => {
897
+ statuses.delete(fn);
898
+ };
899
+ },
900
+ send(msg) {
901
+ if (msg.type === "chat.reset") {
902
+ if ([...turns2.values()].some((t) => !t.answeredAt && !t.error)) return;
903
+ turns2.clear();
904
+ bindings.clear();
905
+ resetAt = (/* @__PURE__ */ new Date()).toISOString();
906
+ peers.forEach((p) => {
907
+ if (available(p)) p.client.send(msg);
908
+ });
909
+ history();
910
+ return;
911
+ }
912
+ if (msg.type === "chat.ask") {
913
+ if (historyDay !== day()) {
914
+ turns2.clear();
915
+ bindings.clear();
916
+ historyDay = day();
917
+ }
918
+ if (routing || [...turns2.values()].some((t) => !t.answeredAt && !t.error)) {
919
+ emit2({ type: "chat.error", qid: msg.qid, message: "one question at a time" });
920
+ return;
921
+ }
922
+ const context = inventory();
923
+ turns2.set(msg.qid, { qid: msg.qid, question: msg.text, askedAt: (/* @__PURE__ */ new Date()).toISOString(), steps: [] });
924
+ history();
925
+ if (configs.length === 1) {
926
+ const p = peers.get("local");
927
+ if (!available(p)) return fail(msg.qid, "Local is offline");
928
+ bindings.set(msg.qid, { machine: "local", raw: msg.qid });
929
+ p.client.send(msg);
930
+ return;
931
+ }
932
+ const coordinator = [...peers.values()].find((p) => available(p) && p.snapshot?.daemon?.routing);
933
+ if (!coordinator) return fail(msg.qid, "Update a connected daemon to enable multi-machine chat");
934
+ routing = { qid: msg.qid, text: msg.text, tool: msg.tool, coordinator: coordinator.config.id };
935
+ coordinator.client.send({ type: "chat.route", qid: msg.qid, text: msg.text, tool: msg.tool, context });
936
+ return;
937
+ }
938
+ if (msg.type === "chat.cancel" && routing?.qid === msg.qid) {
939
+ peers.get(routing.coordinator)?.client.send(msg);
940
+ routing = null;
941
+ fail(msg.qid, "cancelled");
942
+ return;
943
+ }
944
+ let machine = selected;
945
+ let outgoing = msg;
946
+ if ("id" in msg) {
947
+ const origin = splitId(msg.id);
948
+ if (!origin) {
949
+ emit2({ type: "error", id: msg.id, message: "Unknown machine", request: msg.type });
950
+ return;
951
+ }
952
+ machine = origin.machine;
953
+ outgoing = { ...msg, id: origin.id };
954
+ }
955
+ if ("qid" in msg && msg.qid) {
956
+ const b = bindings.get(msg.qid);
957
+ if (!b) {
958
+ emit2({ type: "error", message: "Unknown conversation destination", request: msg.type });
959
+ return;
960
+ }
961
+ machine = b.machine;
962
+ outgoing = { ...outgoing, qid: b.raw };
963
+ }
964
+ if (msg.type === "document.read") {
965
+ if (configs.length > 1 && !msg.machineId && !msg.sessionId) {
966
+ emit2({ type: "document.error", requestId: msg.requestId, path: msg.path, code: "read_failed" });
967
+ return;
968
+ }
969
+ if (msg.machineId) machine = msg.machineId;
970
+ const origin = msg.sessionId ? splitId(msg.sessionId) : null;
971
+ if (origin) {
972
+ machine = origin.machine;
973
+ outgoing = { ...msg, sessionId: origin.id };
974
+ }
975
+ documents.set(msg.requestId, machine);
976
+ }
977
+ if (msg.type === "session.launch" && msg.machineId) {
978
+ machine = msg.machineId;
979
+ const { machineId: _m, ...rest } = msg;
980
+ outgoing = rest;
981
+ }
982
+ if (msg.type === "repos.inspect") {
983
+ machine = msg.machineId ?? "local";
984
+ const { machineId: _m, ...rest } = msg;
985
+ outgoing = rest;
986
+ const none = msg.paths.map((path) => ({ path, exists: false, branch: null }));
987
+ if (!available(peers.get(machine))) {
988
+ emit2({ type: "repos.info", requestId: msg.requestId, repos: none });
989
+ return;
990
+ }
991
+ inspections.set(msg.requestId, machine);
992
+ unfound.set(msg.requestId, none);
993
+ }
994
+ if (msg.type === "repos.scan") {
995
+ machine = msg.machineId ?? "local";
996
+ const { machineId: _m, ...rest } = msg;
997
+ outgoing = rest;
998
+ if (!available(peers.get(machine))) {
999
+ emit2({ type: "repos.found", requestId: msg.requestId, repos: [], truncated: false, error: "offline" });
1000
+ return;
1001
+ }
1002
+ scans.set(msg.requestId, machine);
1003
+ }
1004
+ if (msg.type === "models.list") {
1005
+ machine = msg.machineId ?? "local";
1006
+ const { machineId: _m, ...rest } = msg;
1007
+ outgoing = rest;
1008
+ if (!available(peers.get(machine))) {
1009
+ emit2({ type: "models.listing", requestId: msg.requestId, tool: msg.tool, models: [], error: "offline" });
1010
+ return;
1011
+ }
1012
+ modelLists.set(msg.requestId, { machine, tool: msg.tool });
1013
+ }
1014
+ if (msg.type === "folder.list") {
1015
+ if (msg.machineId) machine = msg.machineId;
1016
+ else if (msg.requestId !== void 0) machine = "local";
1017
+ const { machineId: _m, ...rest } = msg;
1018
+ outgoing = rest;
1019
+ if (msg.requestId !== void 0) {
1020
+ if (!available(peers.get(machine))) {
1021
+ emit2({ type: "folder.listing", requestId: msg.requestId, path: msg.path || "~", parent: null, entries: [], error: "offline" });
1022
+ return;
1023
+ }
1024
+ listings.set(msg.requestId, { machine, path: msg.path || "~" });
1025
+ }
1026
+ }
1027
+ if (msg.type === "folder.create") {
1028
+ if (msg.machineId) machine = msg.machineId;
1029
+ else if (msg.requestId !== void 0) machine = "local";
1030
+ const { machineId: _m, ...rest } = msg;
1031
+ outgoing = rest;
1032
+ if (msg.requestId !== void 0) {
1033
+ if (!available(peers.get(machine))) {
1034
+ emit2({ type: "folder.listing", requestId: msg.requestId, path: msg.parent, parent: null, entries: [], error: "offline" });
1035
+ return;
1036
+ }
1037
+ listings.set(msg.requestId, { machine, path: msg.parent, created: `${msg.parent === "/" ? "" : msg.parent}/${msg.name.trim()}` });
1038
+ }
1039
+ }
1040
+ if (msg.type === "update.daemon") machine = "local";
1041
+ if (msg.type === "snapshot.request") {
1042
+ publish();
1043
+ history();
1044
+ return;
1045
+ }
1046
+ const target2 = peers.get(machine);
1047
+ if (!available(target2)) {
1048
+ if (msg.type === "document.read") {
1049
+ documents.delete(msg.requestId);
1050
+ emit2({ type: "document.error", requestId: msg.requestId, path: msg.path, code: "read_failed" });
1051
+ } else emit2({ type: "error", ..."id" in msg ? { id: msg.id } : {}, message: `${target2?.config.label ?? machine} is offline`, request: msg.type });
1052
+ return;
1053
+ }
1054
+ if (outgoing.type === "session.launch" || outgoing.type === "session.relay" || outgoing.type === "session.relay.cancel") {
1055
+ closeRows.delete(machine);
1056
+ operations.set(machine, outgoing.qid ?? "launch");
1057
+ }
1058
+ if (outgoing.type === "session.close") {
1059
+ const all = turns2.get(msg.type === "session.close" ? msg.qid : "")?.closes ?? [];
1060
+ if (all.length) {
1061
+ closeRows.set(machine, { qid: outgoing.qid, rows: outgoing.rows ?? all.map((_, i) => i), errors: all.map((c) => c.error) });
1062
+ operations.set(machine, outgoing.qid);
1063
+ }
1064
+ }
1065
+ target2.client.send(outgoing);
1066
+ },
1067
+ fleet: {
1068
+ primary: peers.get("local").client,
1069
+ subscribe(fn) {
1070
+ listeners.add(fn);
1071
+ return () => {
1072
+ listeners.delete(fn);
1073
+ };
1074
+ },
1075
+ machines: () => state2,
1076
+ selected: () => selected,
1077
+ select(id) {
1078
+ if (!peers.has(id)) return;
1079
+ selected = id;
1080
+ publish();
1081
+ },
1082
+ configure(raw) {
1083
+ const next2 = parseMachines(raw);
1084
+ if (source === "daemon") {
1085
+ const local = peers.get("local").client;
1086
+ if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1087
+ local.send({ type: "machines.set", machines: toWire(next2) });
1088
+ return;
1089
+ }
1090
+ localStorage.setItem(MACHINES_KEY, JSON.stringify(next2));
1091
+ apply(next2);
1092
+ },
1093
+ machinesSource: () => source,
1094
+ probe: (ssh) => probeMachine(configs[0].url, ssh, { token: opts.token }),
1095
+ // The last try's error is forgotten: the machine is « connecting » until its next answer (a snapshot, or an error again).
1096
+ retry(id) {
1097
+ const p = peers.get(id);
1098
+ if (!p || !connected) return;
1099
+ p.error = void 0;
1100
+ p.client.connect();
1101
+ publish();
1102
+ },
1103
+ terminal(id, opts2) {
1104
+ const origin = splitId(id);
1105
+ const m = state2.find((x) => x.id === origin?.machine);
1106
+ return origin && m ? terminalCommand(m, origin.id, opts2) : null;
1107
+ },
1108
+ shell(id) {
1109
+ const origin = splitId(id);
1110
+ const m = state2.find((x) => x.id === origin?.machine);
1111
+ const s = m?.sessions.find((x) => x.id === origin?.id);
1112
+ return m && s ? shellCommand(m, s.workdir ?? s.cwd) : null;
1113
+ },
1114
+ turnMachine(qid) {
1115
+ return state2.find((m) => m.id === bindings.get(qid)?.machine);
1116
+ },
1117
+ documentMachine(id) {
1118
+ return splitId(id)?.machine;
1119
+ },
1120
+ localSnapshot: () => peers.get("local")?.snapshot ?? null,
1121
+ sendTo(machine, msg) {
1122
+ const p = peers.get(machine);
1123
+ if (!available(p)) return false;
1124
+ p.client.send(msg);
1125
+ return true;
1126
+ },
1127
+ onMachineMessage(fn) {
1128
+ machineMessages.add(fn);
1129
+ return () => {
1130
+ machineMessages.delete(fn);
1131
+ };
1132
+ },
1133
+ projects: () => projects,
1134
+ setProjects(next2) {
1135
+ const valid = ProjectsSchema.parse(next2);
1136
+ const local = peers.get("local").client;
1137
+ if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1138
+ if (projects === null) throw Object.assign(new Error("Update the local Ledge daemon: it does not know projects"), { name: "DaemonOutdated" });
1139
+ local.send({ type: "projects.set", projects: valid });
1140
+ },
1141
+ island: () => island,
1142
+ setIsland(next2) {
1143
+ const valid = IslandPrefsSchema.parse(next2);
1144
+ const local = peers.get("local").client;
1145
+ if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1146
+ if (island === void 0) throw Object.assign(new Error("Update the local Ledge daemon: it does not know the island settings"), { name: "DaemonOutdated" });
1147
+ local.send({ type: "island.set", prefs: valid });
1148
+ },
1149
+ mobile: () => mobile,
1150
+ mobileError: () => mobileError,
1151
+ sendMobile(msg) {
1152
+ const local = peers.get("local").client;
1153
+ if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1154
+ if (mobile === void 0) throw Object.assign(new Error("Update the local Ledge daemon: it does not know mobile access"), { name: "DaemonOutdated" });
1155
+ if (mobileError) {
1156
+ mobileError = void 0;
1157
+ listeners.forEach((fn) => fn());
1158
+ }
1159
+ local.send(msg);
1160
+ }
1161
+ }
1162
+ };
1163
+ publish();
1164
+ return api;
1165
+ }
1166
+
1167
+ // ../fleet/src/proposals.ts
1168
+ var COLORS = Object.keys(PROJECT_COLORS);
1169
+
1170
+ // src/tui/App.tsx
1171
+ import { useCallback, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef8, useState as useState8 } from "react";
1172
+ import { useApp, useInput as useInput5, useStdout } from "ink";
1173
+
1174
+ // src/tui/actions.ts
1175
+ import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";
1176
+
1177
+ // ../fleet/src/decisions.ts
1178
+ import { useEffect, useRef, useState } from "react";
1179
+ function requestKey(s) {
1180
+ const p = s.pending;
1181
+ return p ? `${p.kind}
1182
+ ${p.text}
1183
+ ${p.helperId ?? ""}` : null;
1184
+ }
1185
+ var UNCONFIRMED_MS = 8e3;
1186
+ var FLASH_MS = 1800;
1187
+ function markOf(s) {
1188
+ return JSON.stringify([s.state, s.lastActivityAt, s.lastReply ?? null, requestKey(s)]);
1189
+ }
1190
+ function useDecisions(o) {
1191
+ const { sessions, errors } = o;
1192
+ const live = useRef(/* @__PURE__ */ new Map());
1193
+ const [inFlight, setInFlight] = useState(() => /* @__PURE__ */ new Set());
1194
+ const [landed, setLanded] = useState(null);
1195
+ const latest = useRef({ o });
1196
+ latest.current = { o };
1197
+ const seen = useRef(errors.at(-1)?.seq ?? 0);
1198
+ const forget = (id) => {
1199
+ const x = live.current.get(id);
1200
+ if (!x) return null;
1201
+ clearTimeout(x.timer);
1202
+ live.current.delete(id);
1203
+ setInFlight(new Set(live.current.keys()));
1204
+ return x;
1205
+ };
1206
+ const settled = useRef(/* @__PURE__ */ new Map());
1207
+ const unsettle = (id) => {
1208
+ const x = settled.current.get(id);
1209
+ if (x) {
1210
+ clearTimeout(x.timer);
1211
+ settled.current.delete(id);
1212
+ }
1213
+ return x;
1214
+ };
1215
+ useEffect(() => {
1216
+ const byId = new Map(sessions.map((s) => [s.id, s]));
1217
+ for (const [id, x] of [...live.current]) {
1218
+ const s = byId.get(id);
1219
+ if (!s) forget(id);
1220
+ else if (!s.offline && (x.message === "session.reply" ? markOf(s) !== x.mark : requestKey(s) !== x.pending)) {
1221
+ forget(id);
1222
+ setLanded({ id });
1223
+ if (x.message === "session.reply" || x.text !== void 0) {
1224
+ unsettle(id);
1225
+ settled.current.set(id, { ...x, timer: setTimeout(() => settled.current.delete(id), UNCONFIRMED_MS) });
1226
+ }
1227
+ }
1228
+ }
1229
+ for (const id of [...settled.current.keys()]) if (!byId.has(id)) unsettle(id);
1230
+ }, [sessions]);
1231
+ useEffect(() => {
1232
+ const last = errors.at(-1)?.seq ?? 0;
1233
+ if (last < seen.current) seen.current = 0;
1234
+ for (const e of errors) {
1235
+ if (e.seq <= seen.current) continue;
1236
+ seen.current = e.seq;
1237
+ if (e.id === void 0 || !e.request) continue;
1238
+ const matches = (x2) => !!x2 && e.request === x2.message && e.seq > x2.errorSeq;
1239
+ const inFlightNow = live.current.get(e.id);
1240
+ const done = settled.current.get(e.id);
1241
+ const x = matches(inFlightNow) ? forget(e.id) : matches(done) && (done.message === "session.reply" || e.code === NOTE_NOT_TYPED) ? unsettle(e.id) : null;
1242
+ if (!x) continue;
1243
+ const { o: cur } = latest.current, tr = cur.copy;
1244
+ cur.notify(e.code === NOTE_NOT_TYPED ? tr.noteNotTyped : x.message === "session.decide" ? tr.failed(x.name, e.message) : tr.replyFailed(x.name, e.message));
1245
+ if (x.text !== void 0) x.onRefused?.(x.text);
1246
+ }
1247
+ }, [errors]);
1248
+ useEffect(() => {
1249
+ if (!landed) return;
1250
+ const timer = setTimeout(() => setLanded(null), FLASH_MS);
1251
+ return () => clearTimeout(timer);
1252
+ }, [landed]);
1253
+ useEffect(() => () => {
1254
+ for (const x of [...live.current.values(), ...settled.current.values()]) clearTimeout(x.timer);
1255
+ live.current.clear();
1256
+ settled.current.clear();
1257
+ }, []);
1258
+ const send = (s, message, payload, extra = {}) => {
1259
+ const { name, id } = s;
1260
+ if (message === "session.reply") unsettle(id);
1261
+ const timer = setTimeout(() => {
1262
+ const x = forget(id);
1263
+ if (!x) return;
1264
+ const { o: cur } = latest.current, tr = cur.copy;
1265
+ cur.notify(message === "session.decide" ? tr.unconfirmed(name) : tr.replyUnconfirmed(name));
1266
+ }, UNCONFIRMED_MS);
1267
+ live.current.set(id, { message, pending: requestKey(s), mark: markOf(s), errorSeq: latest.current.o.errors.at(-1)?.seq ?? 0, name, timer, ...extra });
1268
+ setInFlight(new Set(live.current.keys()));
1269
+ latest.current.o.client.send(payload);
1270
+ return true;
1271
+ };
1272
+ return {
1273
+ busy: (id) => inFlight.has(id),
1274
+ landed: landed?.id ?? null,
1275
+ decide(s, decision, note, onRefused) {
1276
+ const p = s.pending;
1277
+ if (s.offline || s.source === "hook_only" || !p || p.kind === "question" || live.current.has(s.id)) return false;
1278
+ if (decision === "always" && !offers(s.tool, p.kind, "always")) return false;
1279
+ if (note) {
1280
+ if (decision !== "deny" || note.length > NOTE_MAX || !offers(s.tool, p.kind, "note")) return false;
1281
+ }
1282
+ const payload = { type: "session.decide", id: s.id, decision, ...note ? { note } : {}, expect: { kind: p.kind, text: p.text, key: pendingKey(p) } };
1283
+ return send(s, "session.decide", payload, note ? { text: note, onRefused } : {});
1284
+ },
1285
+ answer(s, answers, cancel) {
1286
+ const p = s.pending;
1287
+ if (s.offline || s.source === "hook_only" || !p?.questions || !p.requestId || live.current.has(s.id)) return false;
1288
+ return send(s, "session.answer", { type: "session.answer", id: s.id, requestId: p.requestId, answers: [...answers], ...cancel ? { cancel: true } : {} });
1289
+ },
1290
+ reply(s, text, onRefused, how) {
1291
+ if (s.offline || s.source === "hook_only" || !text.trim() || live.current.has(s.id) || s.pending && s.pending.kind !== "question") return false;
1292
+ return send(s, "session.reply", { type: "session.reply", id: s.id, text, ...how?.steer ? { steer: true } : {} }, { text, onRefused });
1293
+ }
1294
+ };
1295
+ }
1296
+
1297
+ // src/tui/model/pending.ts
1298
+ var isFreeRow = (label) => /^Type something\.?$/.test(label);
1299
+ function answerFor(s, key) {
1300
+ const p = s.pending;
1301
+ const digit = /^[1-9]$/.test(key) ? Number(key) : null;
1302
+ if (key !== "y" && key !== "n" && key !== "A" && digit === null) return null;
1303
+ if (s.offline) return { kind: "refused", why: "offline" };
1304
+ if (s.source === "hook_only") return { kind: "refused", why: "readOnly" };
1305
+ if (!p) return { kind: "refused", why: "notNow" };
1306
+ if (p.kind !== "question") {
1307
+ if (key === "y") return { kind: "decide", decision: "accept" };
1308
+ if (key === "n") return { kind: "decide", decision: "deny" };
1309
+ if (key === "A") return offers(s.tool, p.kind, "always") ? { kind: "decide", decision: "always" } : { kind: "refused", why: "noAlways" };
1310
+ return { kind: "refused", why: "notNow" };
1311
+ }
1312
+ if (digit === null) return { kind: "refused", why: "notNow" };
1313
+ const q = p.questions?.length === 1 ? p.questions[0] : void 0;
1314
+ if (q) {
1315
+ const option2 = q.options[digit - 1];
1316
+ return option2 && !q.multi ? { kind: "select", questionId: q.id, label: option2.label, n: digit } : { kind: "refused", why: "notNow" };
1317
+ }
1318
+ const option = p.options?.[digit - 1];
1319
+ return option !== void 0 && !isFreeRow(option) ? { kind: "option", n: digit } : { kind: "refused", why: "notNow" };
1320
+ }
1321
+ function choices(p) {
1322
+ const q = p.questions?.length === 1 ? p.questions[0] : void 0;
1323
+ const labels = q ? q.options.map((o) => o.label) : p.options ?? [];
1324
+ return labels.map((label, i) => ({ n: i + 1, label })).filter((c) => !isFreeRow(c.label));
1325
+ }
1326
+
1327
+ // src/tui/actions.ts
1328
+ function useActions(o) {
1329
+ const { t, toast } = o;
1330
+ const decisions = useDecisions({ client: o.client, sessions: o.sessions, errors: o.errors, notify: (text) => toast({ text, tone: "error" }), copy: t.decisions });
1331
+ const onLand = useRef2(/* @__PURE__ */ new Map());
1332
+ useEffect2(() => {
1333
+ const id = decisions.landed;
1334
+ if (!id) return;
1335
+ const text = onLand.current.get(id);
1336
+ if (text) {
1337
+ onLand.current.delete(id);
1338
+ toast({ text, tone: "ok" });
1339
+ }
1340
+ }, [decisions.landed]);
1341
+ return useMemo(() => ({
1342
+ key(s, key) {
1343
+ const a = answerFor(s, key);
1344
+ if (!a) return false;
1345
+ if (a.kind === "refused") {
1346
+ toast({ text: t.toast[a.why](s.name), tone: "info" });
1347
+ return true;
1348
+ }
1349
+ if (decisions.busy(s.id)) {
1350
+ toast({ text: t.toast.busy(s.name), tone: "info" });
1351
+ return true;
1352
+ }
1353
+ const sent = a.kind === "decide" ? decisions.decide(s, a.decision) : a.kind === "option" ? decisions.reply(s, String(a.n)) : decisions.answer(s, [{ id: a.questionId, selected: [a.label] }]);
1354
+ if (sent) onLand.current.set(s.id, a.kind === "decide" ? t.toast[a.decision](s.name) : t.toast.option(s.name, a.n));
1355
+ else toast({ text: t.toast.notNow(s.name), tone: "info" });
1356
+ return true;
1357
+ },
1358
+ denyWithNote(s, note, onRefused) {
1359
+ if (decisions.decide(s, "deny", note, onRefused)) onLand.current.set(s.id, t.toast.deny(s.name));
1360
+ else {
1361
+ onRefused(note);
1362
+ toast({ text: t.toast.noNote(s.name), tone: "info" });
1363
+ }
1364
+ },
1365
+ reply(s, text, onRefused) {
1366
+ const sent = decisions.reply(s, text, onRefused);
1367
+ if (sent) onLand.current.set(s.id, t.toast.reply(s.name));
1368
+ else toast({ text: s.offline ? t.toast.offline(s.name) : s.source === "hook_only" ? t.toast.readOnly(s.name) : t.toast.busy(s.name), tone: "info" });
1369
+ return sent;
1370
+ },
1371
+ interrupt(s) {
1372
+ if (s.offline) {
1373
+ toast({ text: t.toast.offline(s.name), tone: "info" });
1374
+ return;
1375
+ }
1376
+ if (s.source === "hook_only") {
1377
+ toast({ text: t.toast.readOnly(s.name), tone: "info" });
1378
+ return;
1379
+ }
1380
+ o.client.send({ type: "session.interrupt", id: s.id });
1381
+ toast({ text: t.toast.interrupted(s.name), tone: "info" });
1382
+ },
1383
+ busy: (id) => decisions.busy(id)
1384
+ }), [decisions, t, toast, o.client]);
1385
+ }
1386
+
1387
+ // src/tui/attach.ts
1388
+ import { spawn } from "child_process";
1389
+ function attachCommand(fleet, s) {
1390
+ if (s.offline) return { why: "offline" };
1391
+ if (backendOf(s) === "omp-rpc") return { why: "omp" };
1392
+ if (!s.tmuxSession) return { why: "none" };
1393
+ const argv = fleet.terminal(s.id);
1394
+ return argv && argv.length > 0 ? { argv } : { why: "none" };
1395
+ }
1396
+ function runAttached(argv, env = process.env) {
1397
+ return new Promise((resolve) => {
1398
+ const child = spawn(argv[0], argv.slice(1), { stdio: "inherit", env: attachEnv(env) });
1399
+ child.on("error", () => resolve(127));
1400
+ child.on("exit", (code) => resolve(code ?? 1));
1401
+ });
1402
+ }
1403
+
1404
+ // src/tui/context.tsx
1405
+ import { createContext, useContext, useEffect as useEffect3, useRef as useRef3 } from "react";
1406
+ var SCREENS = ["overview", "session", "ask", "launch", "settings"];
1407
+ var TuiContext = createContext(null);
1408
+ var useTui = () => {
1409
+ const tui = useContext(TuiContext);
1410
+ if (!tui) throw new Error("useTui outside the TUI");
1411
+ return tui;
1412
+ };
1413
+ function useScreenKeys(handler) {
1414
+ const { keys } = useTui();
1415
+ const latest = useRef3(handler);
1416
+ latest.current = handler;
1417
+ useEffect3(() => {
1418
+ const fn = (i, k) => latest.current(i, k);
1419
+ keys.current = fn;
1420
+ return () => {
1421
+ if (keys.current === fn) keys.current = null;
1422
+ };
1423
+ }, [keys]);
1424
+ }
1425
+ function useEditing(on) {
1426
+ const { setEditing } = useTui();
1427
+ useEffect3(() => {
1428
+ setEditing(on);
1429
+ return () => setEditing(false);
1430
+ }, [on, setEditing]);
1431
+ }
1432
+
1433
+ // src/tui/i18n/lang.tsx
1434
+ import { createContext as createContext2, useContext as useContext2 } from "react";
1435
+
1436
+ // src/tui/i18n/fr.ts
1437
+ var plural = (n, one, many) => n > 1 ? many : one;
1438
+ var fr = {
1439
+ lang: { fr: "Fran\xE7ais", en: "English" },
1440
+ state: { approval: "Votre accord", question: "Une question", starting: "D\xE9marrage", idle: "Disponible", working: "Au travail", done: "Termin\xE9e", error: "Erreur", readonly: "Lecture seule" },
1441
+ time: {
1442
+ now: "\xE0 l\u2019instant",
1443
+ s: (n) => `${n} s`,
1444
+ min: (n) => `${n} min`,
1445
+ h: (n) => `${n} h`,
1446
+ d: (n) => `${n} j`
1447
+ },
1448
+ tabs: { overview: "Vue d\u2019ensemble", session: "Session", ask: "Demander \xE0 Ledge", launch: "Nouvelle session", settings: "R\xE9glages" },
1449
+ bar: {
1450
+ sessions: (n) => `${n} ${plural(n, "session", "sessions")}`,
1451
+ machines: (n) => `${n} ${plural(n, "machine", "machines")}`,
1452
+ waiting: (n) => `${n} t\u2019${plural(n, "attend", "attendent")}`,
1453
+ connecting: "connexion au daemon\u2026",
1454
+ unreachable: "daemon injoignable, nouvel essai\u2026",
1455
+ tokenMissing: "pas de jeton : lancez une session avec `ledge claude` ou `ledge daemon`"
1456
+ },
1457
+ keys: {
1458
+ navigate: "naviguer",
1459
+ open: "ouvrir",
1460
+ answer: "r\xE9pondre",
1461
+ attach: "attacher (tmux)",
1462
+ ask: "demander \xE0 Ledge",
1463
+ launch: "nouvelle session",
1464
+ settings: "r\xE9glages",
1465
+ help: "aide",
1466
+ quit: "quitter",
1467
+ back: "retour",
1468
+ write: "\xE9crire",
1469
+ diffs: "diffs",
1470
+ terminal: "terminal",
1471
+ model: "mod\xE8le",
1472
+ interrupt: "interrompre",
1473
+ next: "suivante en attente",
1474
+ unfold: "d\xE9plier",
1475
+ send: "envoyer / lancer",
1476
+ nextCard: "carte suivante",
1477
+ newChat: "nouvelle conversation",
1478
+ stopAnswer: "arr\xEAter la r\xE9ponse",
1479
+ nextField: "champ suivant",
1480
+ choose: "choisir",
1481
+ tick: "cocher",
1482
+ start: "lancer",
1483
+ cancel: "annuler",
1484
+ change: "changer",
1485
+ scroll: "d\xE9filer",
1486
+ space: "espace",
1487
+ redraw: "autre nom"
1488
+ },
1489
+ overview: {
1490
+ yours: (n) => `\xC0 toi \xB7 ${n}`,
1491
+ projects: "Projets",
1492
+ exchanges: "\xC9changes",
1493
+ terminal: (name) => `Terminal \xB7 ${name}`,
1494
+ nobodyWaits: "Personne ne t\u2019attend.",
1495
+ noSession: "Aucune session. + pour en lancer une.",
1496
+ noPreview: "Pas d\u2019aper\xE7u.",
1497
+ noExchange: "Aucun \xE9change.",
1498
+ sessions: (n) => `${n} ${plural(n, "session", "sessions")}`,
1499
+ outside: "Hors projet",
1500
+ offline: "hors ligne",
1501
+ readOnly: "lecture seule",
1502
+ wantsToRun: "Veut ex\xE9cuter :",
1503
+ asks: "Demande :",
1504
+ plan: "Propose un plan :",
1505
+ diff: "Veut modifier :",
1506
+ allow: "Autoriser",
1507
+ deny: "Refuser",
1508
+ always: "Toujours",
1509
+ denyWithNote: "Refuser en expliquant"
1510
+ },
1511
+ session: {
1512
+ you: "Vous",
1513
+ unfold: "d\xE9plier",
1514
+ fold: "replier",
1515
+ actions: (n) => `${n} ${plural(n, "action", "actions")}`,
1516
+ files: (n) => `${n} ${plural(n, "fichier modifi\xE9", "fichiers modifi\xE9s")}`,
1517
+ since: (label, t) => `${label} \xB7 depuis ${t}`,
1518
+ wantsTo: (name) => `${name} veut ex\xE9cuter :`,
1519
+ sentTo: "envoy\xE9 \xE0",
1520
+ receivedFrom: "re\xE7u de",
1521
+ sent: { sending: "envoi\u2026", delivered: "remis", read: "lu", failed: "\xE9chec" },
1522
+ helperWorking: (n) => `au travail \xB7 ${n} ${plural(n, "action", "actions")}`,
1523
+ helperDone: "termin\xE9",
1524
+ replyTo: (name) => `R\xE9pondre \xE0 ${name}\u2026`,
1525
+ noteFor: (name) => `Expliquer le refus \xE0 ${name}\u2026`,
1526
+ loading: "Chargement du fil\u2026",
1527
+ empty: "Rien encore dans ce fil.",
1528
+ noSession: "Choisissez une session dans la vue d\u2019ensemble.",
1529
+ model: (m) => `mod\xE8le : ${m}`,
1530
+ defaultModel: "par d\xE9faut",
1531
+ notice: "Ledge"
1532
+ },
1533
+ ask: {
1534
+ title: (s, m) => `Ledge \xB7 voit ${s} ${plural(s, "session", "sessions")}, ${m} ${plural(m, "machine", "machines")}`,
1535
+ question: "Question",
1536
+ read: "lu",
1537
+ reading: "Ledge lit\u2026",
1538
+ you: "vous",
1539
+ empty: "Posez une question : qui a besoin de moi, o\xF9 en est une session, que lancer\u2026",
1540
+ launchCard: "Nouvelle session",
1541
+ relayCard: (name) => `Relayer \xE0 ${name}`,
1542
+ closeCard: "Fermer des sessions",
1543
+ launch: "Lancer la session",
1544
+ relay: "Envoyer",
1545
+ cancelRelay: "Retirer de la file",
1546
+ close: "Fermer",
1547
+ ignore: "Ignorer",
1548
+ nothingWithoutYou: "Ledge ne lance rien sans toi.",
1549
+ folder: "Dossier",
1550
+ agent: "Agent",
1551
+ instruction: "Instruction",
1552
+ launched: "lanc\xE9e",
1553
+ queued: "en file",
1554
+ sentAt: "envoy\xE9e",
1555
+ closed: "ferm\xE9e",
1556
+ offline: "Le daemon local est hors ligne : la question attendra.",
1557
+ steps: { sessions: "les sessions", pane: (n) => `le terminal de ${n}`, reply: (n) => `la r\xE9ponse de ${n}`, git: (n) => `l\u2019\xE9tat git de ${n}`, repo: (p) => p }
1558
+ },
1559
+ launch: {
1560
+ title: "Nouvelle session",
1561
+ preview: "Aper\xE7u",
1562
+ instruction: "Instruction",
1563
+ agent: "Agent",
1564
+ model: "Mod\xE8le",
1565
+ effort: "Effort",
1566
+ project: "Projet",
1567
+ machine: "Machine",
1568
+ repos: "D\xE9p\xF4ts",
1569
+ folder: "Dossier",
1570
+ name: "Nom",
1571
+ none: "aucun",
1572
+ defaultModel: "par d\xE9faut",
1573
+ loading: "chargement\u2026",
1574
+ drawn: "(tir\xE9 au sort \xB7 ctrl+r pour un autre)",
1575
+ folderHint: "chemin sur la machine (~ accept\xE9)",
1576
+ joins: (name, table) => `${name} rejoindra ${table}`,
1577
+ launch: "Lancer",
1578
+ cancel: "Annuler",
1579
+ thenAttach: "puis a pour s\u2019attacher \xE0 son terminal",
1580
+ missing: { prompt: "\xC9crivez une instruction.", machine: "Aucune machine en ligne.", repos: "Choisissez un d\xE9p\xF4t ou un dossier." },
1581
+ launching: "Lancement\u2026",
1582
+ launched: (name) => `\u2713 ${name} est lanc\xE9e`
1583
+ },
1584
+ settings: { title: "R\xE9glages", language: "Langue", saved: "R\xE9glage enregistr\xE9.", saveFailed: (m) => `R\xE9glage appliqu\xE9, mais non enregistr\xE9 : ${m}` },
1585
+ help: {
1586
+ title: "Aide",
1587
+ global: "Partout",
1588
+ overview: "Vue d\u2019ensemble",
1589
+ session: "Session",
1590
+ ask: "Demander \xE0 Ledge",
1591
+ launch: "Nouvelle session",
1592
+ settings: "R\xE9glages",
1593
+ screens: "changer d\u2019\xE9cran"
1594
+ },
1595
+ toast: {
1596
+ accept: (n) => `\u2713 ${n} : autoris\xE9`,
1597
+ deny: (n) => `\u2713 ${n} : refus\xE9`,
1598
+ always: (n) => `\u2713 ${n} : toujours autoris\xE9`,
1599
+ option: (n, k) => `\u2713 ${n} : option ${k} envoy\xE9e`,
1600
+ reply: (n) => `\u2713 R\xE9ponse envoy\xE9e \xE0 ${n}`,
1601
+ interrupted: (n) => `${n} : interruption demand\xE9e`,
1602
+ model: (n, m) => `${n} : mod\xE8le ${m}`,
1603
+ notNow: (n) => `Rien \xE0 r\xE9pondre pour ${n}.`,
1604
+ offline: (n) => `${n} est hors ligne.`,
1605
+ readOnly: (n) => `${n} est en lecture seule : r\xE9pondez dans son terminal.`,
1606
+ busy: (n) => `Un envoi pour ${n} est d\xE9j\xE0 en cours.`,
1607
+ noAlways: (n) => `${n} ne propose pas \xAB toujours \xBB ici.`,
1608
+ noNote: (n) => `${n} ne prend pas de note ici.`,
1609
+ noTerminalOmp: (n) => `${n} est pilot\xE9 par Ledge via le protocole d\u2019OMP : pas de terminal \xE0 attacher.`,
1610
+ noTerminal: (n) => `Pas de terminal pour ${n}.`,
1611
+ attachFailed: (code) => `tmux s\u2019est arr\xEAt\xE9 avec le code ${code}.`,
1612
+ modelSwitch: "Ce daemon ne change pas le mod\xE8le d\u2019une session en cours."
1613
+ },
1614
+ decisions: {
1615
+ failed: (name, message) => `D\xE9cision non transmise \xE0 ${name} : ${message}`,
1616
+ unconfirmed: (name) => `Ledge n\u2019a pas vu la d\xE9cision pour ${name} aboutir. Ouvrez la session pour v\xE9rifier.`,
1617
+ replyFailed: (name, message) => `R\xE9ponse non transmise \xE0 ${name} : ${message}`,
1618
+ replyUnconfirmed: (name) => `Ledge n\u2019a pas vu la r\xE9ponse \xE0 ${name} aboutir. V\xE9rifiez le fil.`,
1619
+ noteNotTyped: "Refus\xE9, mais la note n\u2019a pas pu \xEAtre tap\xE9e."
1620
+ },
1621
+ notify: {
1622
+ approval: (n, text) => `${n} attend ton accord : ${text}`,
1623
+ question: (n, text) => `${n} a une question : ${text}`,
1624
+ title: (n) => n > 0 ? `ledge \xB7 ${n} t\u2019${plural(n, "attend", "attendent")}` : "ledge"
1625
+ },
1626
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)" }
1627
+ };
1628
+
1629
+ // src/tui/i18n/en.ts
1630
+ var plural2 = (n, one, many) => n > 1 ? many : one;
1631
+ var en = {
1632
+ lang: { fr: "Fran\xE7ais", en: "English" },
1633
+ state: { approval: "Needs you", question: "A question", starting: "Starting", idle: "Available", working: "Working", done: "Finished", error: "Error", readonly: "Read-only" },
1634
+ time: { now: "just now", s: (n) => `${n}s`, min: (n) => `${n} min`, h: (n) => `${n} h`, d: (n) => `${n} d` },
1635
+ tabs: { overview: "Overview", session: "Session", ask: "Ask Ledge", launch: "New session", settings: "Settings" },
1636
+ bar: {
1637
+ sessions: (n) => `${n} ${plural2(n, "session", "sessions")}`,
1638
+ machines: (n) => `${n} ${plural2(n, "machine", "machines")}`,
1639
+ waiting: (n) => `${n} waiting for you`,
1640
+ connecting: "connecting to the daemon\u2026",
1641
+ unreachable: "daemon unreachable, retrying\u2026",
1642
+ tokenMissing: "no token: start a session with `ledge claude` or run `ledge daemon`"
1643
+ },
1644
+ keys: {
1645
+ navigate: "move",
1646
+ open: "open",
1647
+ answer: "answer",
1648
+ attach: "attach (tmux)",
1649
+ ask: "ask Ledge",
1650
+ launch: "new session",
1651
+ settings: "settings",
1652
+ help: "help",
1653
+ quit: "quit",
1654
+ back: "back",
1655
+ write: "write",
1656
+ diffs: "diffs",
1657
+ terminal: "terminal",
1658
+ model: "model",
1659
+ interrupt: "interrupt",
1660
+ next: "next waiting",
1661
+ unfold: "unfold",
1662
+ send: "send / launch",
1663
+ nextCard: "next card",
1664
+ newChat: "new conversation",
1665
+ stopAnswer: "stop the answer",
1666
+ nextField: "next field",
1667
+ choose: "choose",
1668
+ tick: "tick",
1669
+ start: "launch",
1670
+ cancel: "cancel",
1671
+ change: "change",
1672
+ scroll: "scroll",
1673
+ space: "space",
1674
+ redraw: "another name"
1675
+ },
1676
+ overview: {
1677
+ yours: (n) => `Yours \xB7 ${n}`,
1678
+ projects: "Projects",
1679
+ exchanges: "Messages",
1680
+ terminal: (name) => `Terminal \xB7 ${name}`,
1681
+ nobodyWaits: "Nobody is waiting for you.",
1682
+ noSession: "No session. Press + to start one.",
1683
+ noPreview: "No preview.",
1684
+ noExchange: "No message yet.",
1685
+ sessions: (n) => `${n} ${plural2(n, "session", "sessions")}`,
1686
+ outside: "No project",
1687
+ offline: "offline",
1688
+ readOnly: "read-only",
1689
+ wantsToRun: "Wants to run:",
1690
+ asks: "Asks:",
1691
+ plan: "Proposes a plan:",
1692
+ diff: "Wants to edit:",
1693
+ allow: "Allow",
1694
+ deny: "Deny",
1695
+ always: "Always",
1696
+ denyWithNote: "Deny with a note"
1697
+ },
1698
+ session: {
1699
+ you: "You",
1700
+ unfold: "unfold",
1701
+ fold: "fold",
1702
+ actions: (n) => `${n} ${plural2(n, "action", "actions")}`,
1703
+ files: (n) => `${n} ${plural2(n, "file changed", "files changed")}`,
1704
+ since: (label, t) => `${label} \xB7 for ${t}`,
1705
+ wantsTo: (name) => `${name} wants to run:`,
1706
+ sentTo: "sent to",
1707
+ receivedFrom: "from",
1708
+ sent: { sending: "sending\u2026", delivered: "delivered", read: "read", failed: "failed" },
1709
+ helperWorking: (n) => `working \xB7 ${n} ${plural2(n, "action", "actions")}`,
1710
+ helperDone: "done",
1711
+ replyTo: (name) => `Reply to ${name}\u2026`,
1712
+ noteFor: (name) => `Tell ${name} why\u2026`,
1713
+ loading: "Loading the thread\u2026",
1714
+ empty: "Nothing in this thread yet.",
1715
+ noSession: "Pick a session in the overview.",
1716
+ model: (m) => `model: ${m}`,
1717
+ defaultModel: "default",
1718
+ notice: "Ledge"
1719
+ },
1720
+ ask: {
1721
+ title: (s, m) => `Ledge \xB7 sees ${s} ${plural2(s, "session", "sessions")}, ${m} ${plural2(m, "machine", "machines")}`,
1722
+ question: "Question",
1723
+ read: "read",
1724
+ reading: "Ledge is reading\u2026",
1725
+ you: "you",
1726
+ empty: "Ask anything: who needs me, how a session is doing, what to start\u2026",
1727
+ launchCard: "New session",
1728
+ relayCard: (name) => `Relay to ${name}`,
1729
+ closeCard: "Close sessions",
1730
+ launch: "Launch the session",
1731
+ relay: "Send",
1732
+ cancelRelay: "Take it back",
1733
+ close: "Close",
1734
+ ignore: "Ignore",
1735
+ nothingWithoutYou: "Ledge starts nothing without you.",
1736
+ folder: "Folder",
1737
+ agent: "Agent",
1738
+ instruction: "Instruction",
1739
+ launched: "launched",
1740
+ queued: "queued",
1741
+ sentAt: "sent",
1742
+ closed: "closed",
1743
+ offline: "The local daemon is offline: the question will wait.",
1744
+ steps: { sessions: "the sessions", pane: (n) => `${n}'s terminal`, reply: (n) => `${n}'s reply`, git: (n) => `${n}'s git state`, repo: (p) => p }
1745
+ },
1746
+ launch: {
1747
+ title: "New session",
1748
+ preview: "Preview",
1749
+ instruction: "Instruction",
1750
+ agent: "Agent",
1751
+ model: "Model",
1752
+ effort: "Effort",
1753
+ project: "Project",
1754
+ machine: "Machine",
1755
+ repos: "Repositories",
1756
+ folder: "Folder",
1757
+ name: "Name",
1758
+ none: "none",
1759
+ defaultModel: "default",
1760
+ loading: "loading\u2026",
1761
+ drawn: "(drawn at random \xB7 ctrl+r for another)",
1762
+ folderHint: "path on the machine (~ allowed)",
1763
+ joins: (name, table) => `${name} will join ${table}`,
1764
+ launch: "Launch",
1765
+ cancel: "Cancel",
1766
+ thenAttach: "then a to attach to its terminal",
1767
+ missing: { prompt: "Write an instruction.", machine: "No machine online.", repos: "Pick a repository or a folder." },
1768
+ launching: "Launching\u2026",
1769
+ launched: (name) => `\u2713 ${name} is started`
1770
+ },
1771
+ settings: { title: "Settings", language: "Language", saved: "Setting saved.", saveFailed: (m) => `Setting applied, but not saved: ${m}` },
1772
+ help: { title: "Help", global: "Everywhere", overview: "Overview", session: "Session", ask: "Ask Ledge", launch: "New session", settings: "Settings", screens: "switch screen" },
1773
+ toast: {
1774
+ accept: (n) => `\u2713 ${n}: allowed`,
1775
+ deny: (n) => `\u2713 ${n}: denied`,
1776
+ always: (n) => `\u2713 ${n}: always allowed`,
1777
+ option: (n, k) => `\u2713 ${n}: option ${k} sent`,
1778
+ reply: (n) => `\u2713 Reply sent to ${n}`,
1779
+ interrupted: (n) => `${n}: interrupt asked`,
1780
+ model: (n, m) => `${n}: model ${m}`,
1781
+ notNow: (n) => `Nothing to answer for ${n}.`,
1782
+ offline: (n) => `${n} is offline.`,
1783
+ readOnly: (n) => `${n} is read-only: answer in its terminal.`,
1784
+ busy: (n) => `A send for ${n} is already in flight.`,
1785
+ noAlways: (n) => `${n} does not offer \u201Calways\u201D here.`,
1786
+ noNote: (n) => `${n} takes no note here.`,
1787
+ noTerminalOmp: (n) => `${n} is driven by Ledge over OMP's protocol: there is no terminal to attach to.`,
1788
+ noTerminal: (n) => `No terminal for ${n}.`,
1789
+ attachFailed: (code) => `tmux stopped with code ${code}.`,
1790
+ modelSwitch: "This daemon cannot change a running session\u2019s model."
1791
+ },
1792
+ decisions: {
1793
+ failed: (name, message) => `Could not send the decision to ${name}: ${message}`,
1794
+ unconfirmed: (name) => `Ledge did not see the decision for ${name} land. Open the session to check.`,
1795
+ replyFailed: (name, message) => `Could not send the reply to ${name}: ${message}`,
1796
+ replyUnconfirmed: (name) => `Ledge did not see the reply to ${name} land. Check the thread.`,
1797
+ noteNotTyped: "Refused, but the note could not be typed."
1798
+ },
1799
+ notify: {
1800
+ approval: (n, text) => `${n} needs your approval: ${text}`,
1801
+ question: (n, text) => `${n} has a question: ${text}`,
1802
+ title: (n) => n > 0 ? `ledge \xB7 ${n} waiting` : "ledge"
1803
+ },
1804
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)" }
1805
+ };
1806
+
1807
+ // src/tui/i18n/lang.tsx
1808
+ import { jsx } from "react/jsx-runtime";
1809
+ var LANGS = ["fr", "en"];
1810
+ var MESSAGES = { fr, en };
1811
+ var I18nContext = createContext2({ lang: "fr", t: fr });
1812
+ function I18nProvider({ lang, children }) {
1813
+ return /* @__PURE__ */ jsx(I18nContext.Provider, { value: { lang, t: MESSAGES[lang] }, children });
1814
+ }
1815
+
1816
+ // src/tui/model/overview.ts
1817
+ var listed = (sessions) => sessions.filter((s) => s.state !== "exited");
1818
+ var machineOf = (s) => s.machineId ?? splitId(s.id)?.machine ?? "local";
1819
+ function overview(sessions, projects) {
1820
+ const live = listed(sessions);
1821
+ const yours = live.filter(isPending).sort((a, b) => a.lastActivityAt.localeCompare(b.lastActivityAt) || a.id.localeCompare(b.id));
1822
+ const nodes = /* @__PURE__ */ new Map();
1823
+ const known2 = new Map((projects?.projects ?? []).map((p) => [p.id, p]));
1824
+ for (const p of projects?.projects ?? []) nodes.set(`p:${p.id}`, { key: `p:${p.id}`, label: p.name, color: p.color, branches: [], count: 0 });
1825
+ for (const s of live) {
1826
+ const pid = projects ? projectOf({ machineId: machineOf(s), cwd: s.cwd }, projects) : null;
1827
+ const key = pid && known2.has(pid) ? `p:${pid}` : `r:${machineOf(s)}:${s.repoPath ?? s.cwd}`;
1828
+ let node = nodes.get(key);
1829
+ if (!node) {
1830
+ node = { key, label: s.repo ?? s.project, branches: [], count: 0 };
1831
+ nodes.set(key, node);
1832
+ }
1833
+ const branch = s.branch ?? "-";
1834
+ let row = node.branches.find((b) => b.branch === branch);
1835
+ if (!row) {
1836
+ row = { branch, sessions: [] };
1837
+ node.branches.push(row);
1838
+ }
1839
+ row.sessions.push(s);
1840
+ node.count += 1;
1841
+ }
1842
+ const tree = [...nodes.values()].filter((n) => n.count > 0);
1843
+ const order = tree.flatMap((n) => n.branches.flatMap((b) => b.sessions.map((s) => s.id)));
1844
+ return { yours, tree, order, machines: new Set(live.map(machineOf)).size };
1845
+ }
1846
+ var helperCount = (s) => liveHelpers(s).length;
1847
+ function keepSelection(order, selected, previous = []) {
1848
+ if (selected && order.includes(selected)) return selected;
1849
+ if (selected) {
1850
+ const at = previous.indexOf(selected);
1851
+ for (let i = at; i >= 0 && at >= 0; i--) if (order.includes(previous[i])) return previous[i];
1852
+ }
1853
+ return order[0] ?? null;
1854
+ }
1855
+ function step(order, selected, by) {
1856
+ if (order.length === 0) return null;
1857
+ const at = selected ? order.indexOf(selected) : -1;
1858
+ return order[(at + by + order.length) % order.length];
1859
+ }
1860
+ function nextWaiting(yours, selected) {
1861
+ if (yours.length === 0) return null;
1862
+ const at = yours.findIndex((s) => s.id === selected);
1863
+ return yours[(at + 1) % yours.length].id;
1864
+ }
1865
+ function exchanges(sessions, max) {
1866
+ const byId = new Map(sessions.map((s) => [s.id, s]));
1867
+ return sessions.flatMap((s) => {
1868
+ const m = s.lastMessage;
1869
+ if (!m) return [];
1870
+ const peer = m.peer ? byId.get(m.peer) ?? sessions.find((x) => splitId(x.id)?.id === m.peer) : void 0;
1871
+ return [{ at: m.at, from: s.name, to: peer?.name ?? m.to, summary: m.summary }];
1872
+ }).sort((a, b) => b.at.localeCompare(a.at)).slice(0, max);
1873
+ }
1874
+
1875
+ // src/tui/model/time.ts
1876
+ function ago(iso, now, t) {
1877
+ const s = Math.max(0, Math.floor((now - Date.parse(iso)) / 1e3));
1878
+ if (s < 5) return t.now;
1879
+ if (s < 60) return t.s(s);
1880
+ const m = Math.floor(s / 60);
1881
+ if (m < 60) return t.min(m);
1882
+ const h = Math.floor(m / 60);
1883
+ return h < 24 ? t.h(h) : t.d(Math.floor(h / 24));
1884
+ }
1885
+ var clock = (iso) => {
1886
+ const d = new Date(iso);
1887
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
1888
+ };
1889
+ var oneLine = (s) => s.replace(/\*\*|__|`/g, "").replace(/\s*\n\s*/g, " ").trim();
1890
+
1891
+ // src/tui/notify.ts
1892
+ var NOTIFY_MODES = ["bell", "osc9", "off"];
1893
+ function asked(was, now, t) {
1894
+ const out = [];
1895
+ for (const s of now) {
1896
+ const prev = was.get(s.id);
1897
+ if (!prev) continue;
1898
+ const c = changeOf(prev, s);
1899
+ if (c?.kind !== "asked") continue;
1900
+ const text = oneLine(s.pending?.text ?? "").slice(0, 120);
1901
+ out.push(c.ask === "approval" ? t.approval(s.name, text) : t.question(s.name, text));
1902
+ }
1903
+ return out;
1904
+ }
1905
+ function notification(text, mode, env) {
1906
+ if (mode === "off") return "";
1907
+ if (mode === "bell") return "\x07";
1908
+ const safe = text.replace(/[\u0000-\u001f\u007f]/g, " ");
1909
+ const osc = `\x1B]9;${safe}\x07`;
1910
+ return env.TMUX ? `\x1BPtmux;${osc.replaceAll("\x1B", "\x1B\x1B")}\x1B\\` : osc;
1911
+ }
1912
+ var title = (text) => `\x1B]0;${text.replace(/[\u0000-\u001f\u007f]/g, " ")}\x07`;
1913
+
1914
+ // src/tui/screens/Overview.tsx
1915
+ import { useMemo as useMemo2 } from "react";
1916
+ import { Box as Box6, Text as Text6, useWindowSize as useWindowSize2 } from "ink";
1917
+
1918
+ // ../ui/src/bots/poseFor.ts
1919
+ var FROM_SESSION = {
1920
+ starting: "starting",
1921
+ working: "working",
1922
+ waiting_input: "question",
1923
+ waiting_permission: "approval",
1924
+ idle: "idle",
1925
+ exited: "done",
1926
+ error: "error"
1927
+ };
1928
+ function botState(session) {
1929
+ if (session.state === "idle" && liveHelpers(session).length > 0) return "working";
1930
+ return FROM_SESSION[session.state];
1931
+ }
1932
+
1933
+ // src/tui/model/labels.ts
1934
+ var TOOL_NAME = { claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", omp: "OMP" };
1935
+ var modelLabel = (s, fallback) => [s.model ?? (s.effort ? fallback : ""), s.effort ?? ""].filter(Boolean).join(" \xB7 ") || fallback;
1936
+ var machineLabel = (s) => s.machineName ?? splitId(s.id)?.machine ?? "Local";
1937
+ var paneName = (s) => s.tmuxSession ?? `ledge-${splitId(s.id)?.id ?? s.id}`;
1938
+ function fit(s, n) {
1939
+ const cps = [...s];
1940
+ if (n <= 0) return "";
1941
+ return cps.length > n ? `${cps.slice(0, Math.max(0, n - 1)).join("")}\u2026` : s + " ".repeat(n - cps.length);
1942
+ }
1943
+
1944
+ // src/tui/theme.ts
1945
+ var C = {
1946
+ fg: "#e6e0dc",
1947
+ dim: "#8f8681",
1948
+ faint: "#5f5854",
1949
+ line: "#3a3532",
1950
+ sel: "#2b2725",
1951
+ coral: "#f07a62",
1952
+ blue: "#7fb2ec",
1953
+ red: "#f0806e",
1954
+ yellow: "#e6c46a",
1955
+ green: "#86cf9d",
1956
+ violet: "#b9a2f0"
1957
+ };
1958
+ var STATE_COLOR = {
1959
+ approval: C.red,
1960
+ question: C.yellow,
1961
+ working: C.blue,
1962
+ starting: C.blue,
1963
+ idle: C.green,
1964
+ done: C.faint,
1965
+ error: C.red,
1966
+ readonly: C.dim
1967
+ };
1968
+ var projectColor = (color) => color ? PROJECT_COLORS[color] : C.dim;
1969
+
1970
+ // src/tui/ui/Frame.tsx
1971
+ import { Box as Box2, Text as Text2, useWindowSize } from "ink";
1972
+
1973
+ // src/tui/ui/KeyBar.tsx
1974
+ import { Box, Text } from "ink";
1975
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1976
+ function KeyBar({ keys }) {
1977
+ return /* @__PURE__ */ jsx2(Box, { paddingX: 1, columnGap: 2, flexWrap: "wrap", height: 1, overflow: "hidden", children: keys.map(([k, label]) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
1978
+ /* @__PURE__ */ jsxs(Text, { color: C.fg, backgroundColor: "#2c2926", children: [
1979
+ " ",
1980
+ k,
1981
+ " "
1982
+ ] }),
1983
+ /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
1984
+ " ",
1985
+ label
1986
+ ] })
1987
+ ] }, k)) });
1988
+ }
1989
+
1990
+ // src/tui/ui/Frame.tsx
1991
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1992
+ function Frame({ crumb, keys, children }) {
1993
+ const { d, machines, t, g, lastToast } = useTui();
1994
+ const { rows, columns } = useWindowSize();
1995
+ const live = listed(d.sessions);
1996
+ const waiting = live.filter(isPending).length;
1997
+ const status = d.status === "open" ? null : d.status === "connecting" ? t.bar.connecting : t.bar.unreachable;
1998
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: rows, width: columns, children: [
1999
+ /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, justifyContent: "space-between", height: 1, backgroundColor: "#1f1d1b", children: [
2000
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
2001
+ /* @__PURE__ */ jsxs2(Text2, { color: C.coral, children: [
2002
+ g.logo,
2003
+ " ledge"
2004
+ ] }),
2005
+ /* @__PURE__ */ jsxs2(Text2, { color: C.dim, children: [
2006
+ " ",
2007
+ t.bar.sessions(live.length),
2008
+ " \xB7 ",
2009
+ t.bar.machines(Math.max(1, machines.length))
2010
+ ] }),
2011
+ waiting > 0 ? /* @__PURE__ */ jsxs2(Text2, { color: C.red, children: [
2012
+ " \xB7 ",
2013
+ t.bar.waiting(waiting)
2014
+ ] }) : null,
2015
+ status ? /* @__PURE__ */ jsxs2(Text2, { color: C.yellow, children: [
2016
+ " ",
2017
+ status
2018
+ ] }) : null
2019
+ ] }),
2020
+ /* @__PURE__ */ jsx3(Text2, { wrap: "truncate", color: C.dim, children: crumb })
2021
+ ] }),
2022
+ /* @__PURE__ */ jsx3(Box2, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children }),
2023
+ lastToast ? /* @__PURE__ */ jsx3(Box2, { paddingX: 1, height: 1, children: /* @__PURE__ */ jsx3(Text2, { wrap: "truncate", color: lastToast.tone === "error" ? C.red : lastToast.tone === "ok" ? C.green : C.yellow, children: lastToast.text }) }) : null,
2024
+ /* @__PURE__ */ jsx3(KeyBar, { keys })
2025
+ ] });
2026
+ }
2027
+
2028
+ // src/tui/ui/SessionCard.tsx
2029
+ import { Box as Box3, Text as Text3 } from "ink";
2030
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2031
+ function Button({ label, k, primary, off }) {
2032
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
2033
+ /* @__PURE__ */ jsxs3(Text3, { color: off ? C.faint : primary ? C.fg : C.fg, backgroundColor: off ? void 0 : primary ? "#3a201b" : "#2c2926", children: [
2034
+ " ",
2035
+ label,
2036
+ " "
2037
+ ] }),
2038
+ /* @__PURE__ */ jsxs3(Text3, { color: C.dim, backgroundColor: off ? void 0 : primary ? "#3a201b" : "#2c2926", children: [
2039
+ k,
2040
+ " "
2041
+ ] })
2042
+ ] });
2043
+ }
2044
+ function SessionCard({ s, note }) {
2045
+ const { t, g } = useTui();
2046
+ const state2 = botState(s);
2047
+ const helpers = liveHelpers(s);
2048
+ const p = s.pending;
2049
+ const canAct = !s.offline && s.source !== "hook_only";
2050
+ const verb = !p ? "" : p.kind === "question" ? t.overview.asks : p.kind === "plan" ? t.overview.plan : p.kind === "diff" ? t.overview.diff : t.overview.wantsToRun;
2051
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
2052
+ /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
2053
+ /* @__PURE__ */ jsxs3(Text3, { color: STATE_COLOR[state2], children: [
2054
+ g[state2],
2055
+ " ",
2056
+ t.state[state2]
2057
+ ] }),
2058
+ /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
2059
+ " ",
2060
+ TOOL_NAME[s.tool],
2061
+ " \xB7 ",
2062
+ modelLabel(s, t.session.defaultModel),
2063
+ " \xB7 ",
2064
+ machineLabel(s),
2065
+ s.branch ? ` \xB7 ${g.branch} ${s.branch}` : ""
2066
+ ] }),
2067
+ s.offline ? /* @__PURE__ */ jsxs3(Text3, { color: C.yellow, children: [
2068
+ " ",
2069
+ t.overview.offline
2070
+ ] }) : s.source === "hook_only" ? /* @__PURE__ */ jsxs3(Text3, { color: C.faint, children: [
2071
+ " ",
2072
+ t.overview.readOnly
2073
+ ] }) : null
2074
+ ] }),
2075
+ helpers.length > 0 ? /* @__PURE__ */ jsxs3(Text3, { color: C.violet, wrap: "truncate", children: [
2076
+ " ",
2077
+ helpers.map((h) => `${g.helper} ${h.kind} \xB7 ${oneLine(h.task)}`).join(" ")
2078
+ ] }) : null,
2079
+ p ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
2080
+ /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
2081
+ /* @__PURE__ */ jsx4(Text3, { color: STATE_COLOR[state2], children: verb }),
2082
+ " ",
2083
+ /* @__PURE__ */ jsx4(Text3, { color: C.fg, bold: true, children: oneLine(p.text) }),
2084
+ p.tool ? /* @__PURE__ */ jsxs3(Text3, { color: C.faint, children: [
2085
+ " ",
2086
+ p.tool
2087
+ ] }) : null
2088
+ ] }),
2089
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, columnGap: 2, flexWrap: "wrap", children: p.kind === "question" ? choices(p).slice(0, 9).map((c, i) => /* @__PURE__ */ jsx4(Button, { label: `${c.n} \xB7 ${c.label}`, k: String(c.n), primary: i === 0, off: !canAct }, c.n)) : /* @__PURE__ */ jsxs3(Fragment, { children: [
2090
+ /* @__PURE__ */ jsx4(Button, { label: t.overview.allow, k: "y", primary: true, off: !canAct }),
2091
+ /* @__PURE__ */ jsx4(Button, { label: t.overview.deny, k: "n", off: !canAct }),
2092
+ offers(s.tool, p.kind, "always") ? /* @__PURE__ */ jsx4(Button, { label: t.overview.always, k: "A", off: !canAct }) : null,
2093
+ note && offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx4(Button, { label: t.overview.denyWithNote, k: "r", off: !canAct }) : null
2094
+ ] }) })
2095
+ ] }) : s.lastReply && state2 === "idle" ? /* @__PURE__ */ jsxs3(Text3, { color: C.dim, wrap: "truncate", children: [
2096
+ " ",
2097
+ oneLine(s.lastReply.text)
2098
+ ] }) : null
2099
+ ] });
2100
+ }
2101
+
2102
+ // src/tui/ui/Tail.tsx
2103
+ import { useRef as useRef4 } from "react";
2104
+ import { Box as Box4, Text as Text4, useBoxMetrics } from "ink";
2105
+ import { jsx as jsx5 } from "react/jsx-runtime";
2106
+ function Tail({ lines, color, empty: empty2, emptyColor }) {
2107
+ const ref = useRef4(null);
2108
+ const { height } = useBoxMetrics(ref);
2109
+ let end = lines.length;
2110
+ while (end > 0 && !lines[end - 1].trim()) end--;
2111
+ const shown = end === 0 ? [] : lines.slice(0, end).slice(-Math.max(1, height));
2112
+ return /* @__PURE__ */ jsx5(Box4, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.length === 0 ? /* @__PURE__ */ jsx5(Text4, { color: emptyColor, children: empty2 }) : shown.map((l, i) => /* @__PURE__ */ jsx5(Text4, { color, wrap: "truncate", children: l || " " }, i)) });
2113
+ }
2114
+
2115
+ // src/tui/ui/TitledBox.tsx
2116
+ import { useRef as useRef5 } from "react";
2117
+ import { Box as Box5, Text as Text5, useBoxMetrics as useBoxMetrics2 } from "ink";
2118
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
2119
+ function TitledBox({ title: title2, color = C.line, titleColor = C.dim, children, flexGrow, height, width, flexShrink }) {
2120
+ const ref = useRef5(null);
2121
+ const { width: w } = useBoxMetrics2(ref);
2122
+ const label = ` ${title2.toUpperCase()} `;
2123
+ const room = Math.max(0, w - 3);
2124
+ const shown = [...label].length > room ? `${[...label].slice(0, Math.max(0, room - 2)).join("")}\u2026 ` : label;
2125
+ const rest = Math.max(0, w - 3 - [...shown].length);
2126
+ return /* @__PURE__ */ jsxs4(Box5, { ref, flexDirection: "column", flexGrow, flexShrink, height, width, overflow: "hidden", children: [
2127
+ /* @__PURE__ */ jsxs4(Text5, { wrap: "truncate", children: [
2128
+ /* @__PURE__ */ jsx6(Text5, { color, children: "\u256D\u2500" }),
2129
+ /* @__PURE__ */ jsx6(Text5, { color: titleColor, children: w > 0 ? shown : "" }),
2130
+ /* @__PURE__ */ jsxs4(Text5, { color, children: [
2131
+ "\u2500".repeat(rest),
2132
+ "\u256E"
2133
+ ] })
2134
+ ] }),
2135
+ /* @__PURE__ */ jsx6(Box5, { flexDirection: "column", borderStyle: "round", borderTop: false, borderColor: color, paddingX: 1, flexGrow: 1, overflow: "hidden", children })
2136
+ ] });
2137
+ }
2138
+
2139
+ // src/tui/screens/Overview.tsx
2140
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
2141
+ var WIDE = 100;
2142
+ function windowed(rows, selectedKey, height) {
2143
+ if (rows.length <= height) return [...rows];
2144
+ const at = Math.max(0, rows.findIndex((r) => r.key === selectedKey));
2145
+ const start = Math.min(Math.max(0, at - Math.floor(height / 2)), rows.length - height);
2146
+ return rows.slice(start, start + height);
2147
+ }
2148
+ function Overview() {
2149
+ const tui = useTui();
2150
+ const { d, projects, t, g, now, selected, select, actions, go, attach } = tui;
2151
+ const { columns, rows: height } = useWindowSize2();
2152
+ const wide = columns >= WIDE;
2153
+ const left = wide ? Math.min(64, Math.max(44, Math.floor(columns * 0.42))) : columns;
2154
+ const view = useMemo2(() => overview(d.sessions, projects), [d.sessions, projects]);
2155
+ const sel = selected?.id ?? null;
2156
+ useScreenKeys((input, key) => {
2157
+ if (input === "j" || key.downArrow) {
2158
+ select(step(view.order, sel, 1));
2159
+ return true;
2160
+ }
2161
+ if (input === "k" || key.upArrow) {
2162
+ select(step(view.order, sel, -1));
2163
+ return true;
2164
+ }
2165
+ if (key.tab) {
2166
+ const next2 = nextWaiting(view.yours, sel);
2167
+ if (next2) select(next2);
2168
+ return true;
2169
+ }
2170
+ if (key.return) {
2171
+ if (selected) go("session");
2172
+ return true;
2173
+ }
2174
+ if (!selected) return false;
2175
+ if (input === "a") {
2176
+ attach(selected);
2177
+ return true;
2178
+ }
2179
+ if (input === "r" && selected.pending) {
2180
+ go("session");
2181
+ return true;
2182
+ }
2183
+ if (/^[1-9]$/.test(input) && selected.pending?.kind !== "question") return false;
2184
+ if (input === "y" || input === "n" || input === "A" || /^[1-9]$/.test(input)) return actions.key(selected, input);
2185
+ if (input === "i" && selected.state === "working") {
2186
+ actions.interrupt(selected);
2187
+ return true;
2188
+ }
2189
+ return false;
2190
+ });
2191
+ const tree = view.tree.flatMap((n) => [
2192
+ { kind: "project", key: n.key, label: n.label, color: projectColor(n.color), count: n.count },
2193
+ ...n.branches.flatMap((b) => [{ kind: "branch", key: `${n.key}|${b.branch}`, branch: b.branch }, ...b.sessions.map((s) => ({ kind: "session", key: s.id, s }))])
2194
+ ]);
2195
+ const yoursHeight = Math.max(1, Math.min(view.yours.length, 6));
2196
+ const treeHeight = Math.max(3, height - 3 - (yoursHeight + 2) - 3);
2197
+ const inner = left - 4, nameW = 7;
2198
+ const stateW = Math.max(...Object.values(t.state).map((x) => x.length)) + 1;
2199
+ const topicW = Math.max(6, inner - 6 - nameW - stateW - 3);
2200
+ const askW = Math.max(6, inner - 2 - nameW - 9 - 7);
2201
+ const yoursBox = /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.yours(view.yours.length), color: view.yours.length ? C.red : C.line, titleColor: view.yours.length ? C.red : C.dim, height: yoursHeight + 2, children: view.yours.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.nobodyWaits }) : view.yours.slice(0, yoursHeight).map((s) => {
2202
+ const st = botState(s);
2203
+ return /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
2204
+ /* @__PURE__ */ jsxs5(Text6, { color: STATE_COLOR[st], children: [
2205
+ g[st],
2206
+ " "
2207
+ ] }),
2208
+ /* @__PURE__ */ jsx7(Text6, { color: C.fg, bold: true, children: fit(s.name, nameW) }),
2209
+ /* @__PURE__ */ jsx7(Text6, { color: C.dim, children: fit(s.repo ?? s.project, 9) }),
2210
+ /* @__PURE__ */ jsx7(Text6, { color: STATE_COLOR[st], children: fit(oneLine(s.pending?.text ?? ""), askW) }),
2211
+ /* @__PURE__ */ jsxs5(Text6, { color: C.faint, children: [
2212
+ " ",
2213
+ ago(s.lastActivityAt, now, t.time)
2214
+ ] })
2215
+ ] }, s.id);
2216
+ }) });
2217
+ const treeBox = /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.projects, flexGrow: 1, children: tree.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.noSession }) : windowed(tree, sel, treeHeight).map((r) => r.kind === "project" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
2218
+ /* @__PURE__ */ jsx7(Text6, { color: r.color, children: g.project }),
2219
+ " ",
2220
+ /* @__PURE__ */ jsx7(Text6, { color: C.fg, bold: true, children: r.label }),
2221
+ " ",
2222
+ /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.sessions(r.count) })
2223
+ ] }, r.key) : r.kind === "branch" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", color: C.faint, children: [
2224
+ " ",
2225
+ g.branch,
2226
+ " ",
2227
+ r.branch
2228
+ ] }, r.key) : /* @__PURE__ */ jsx7(SessionRow, { s: r.s, on: r.s.id === sel, nameW, topicW, stateW }, r.key)) });
2229
+ const keys = [
2230
+ ["j k", t.keys.navigate],
2231
+ ["\u21B5", t.keys.open],
2232
+ ...view.yours.length ? [["tab", t.keys.next], ["y n", t.keys.answer]] : [],
2233
+ ["a", t.keys.attach],
2234
+ ["/", t.keys.ask],
2235
+ ["+", t.keys.launch],
2236
+ [",", t.keys.settings],
2237
+ ["?", t.keys.help],
2238
+ ["q", t.keys.quit]
2239
+ ];
2240
+ return /* @__PURE__ */ jsx7(Frame, { crumb: tui.machines.map((m) => `${m.label} ${m.ready ? g.machine : "\u25CB"}`).join(" "), keys, children: /* @__PURE__ */ jsxs5(Box6, { flexGrow: 1, children: [
2241
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", width: left, flexShrink: 0, children: [
2242
+ yoursBox,
2243
+ treeBox
2244
+ ] }),
2245
+ wide ? /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", flexGrow: 1, children: [
2246
+ selected ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
2247
+ /* @__PURE__ */ jsx7(TitledBox, { title: `${selected.name} \xB7 ${oneLine(selected.topic ?? selected.title)}`, color: isPending(selected) ? C.red : C.line, titleColor: isPending(selected) ? C.red : C.dim, children: /* @__PURE__ */ jsx7(SessionCard, { s: selected, note: true }) }),
2248
+ /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.terminal(paneName(selected)), flexGrow: 1, children: /* @__PURE__ */ jsx7(Tail, { lines: d.previews.get(selected.id) ?? [], color: C.dim, empty: t.overview.noPreview, emptyColor: C.faint }) })
2249
+ ] }) : /* @__PURE__ */ jsx7(TitledBox, { title: t.tabs.session, flexGrow: 1, children: /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.session.noSession }) }),
2250
+ /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.exchanges, height: 5, children: (() => {
2251
+ const list = exchanges(d.sessions, 3);
2252
+ return list.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.noExchange }) : list.map((x, i) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
2253
+ /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: clock(x.at) }),
2254
+ " ",
2255
+ /* @__PURE__ */ jsx7(Text6, { color: C.fg, children: x.from }),
2256
+ " ",
2257
+ /* @__PURE__ */ jsx7(Text6, { color: C.violet, children: g.arrow }),
2258
+ " ",
2259
+ /* @__PURE__ */ jsx7(Text6, { color: C.fg, children: x.to }),
2260
+ " ",
2261
+ /* @__PURE__ */ jsx7(Text6, { color: C.dim, children: oneLine(x.summary) })
2262
+ ] }, i));
2263
+ })() })
2264
+ ] }) : null
2265
+ ] }) });
2266
+ }
2267
+ function SessionRow({ s, on, nameW, topicW, stateW }) {
2268
+ const { t, g } = useTui();
2269
+ const st = botState(s);
2270
+ const n = helperCount(s);
2271
+ return /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: on ? C.sel : void 0, children: [
2272
+ /* @__PURE__ */ jsx7(Text6, { color: on ? C.coral : C.faint, children: on ? "\u258C" : " " }),
2273
+ /* @__PURE__ */ jsx7(Text6, { children: " " }),
2274
+ /* @__PURE__ */ jsxs5(Text6, { color: s.offline ? C.faint : STATE_COLOR[st], children: [
2275
+ g[st],
2276
+ " "
2277
+ ] }),
2278
+ /* @__PURE__ */ jsx7(Text6, { color: s.offline ? C.dim : C.fg, bold: true, children: fit(s.name, nameW) }),
2279
+ /* @__PURE__ */ jsxs5(Text6, { color: C.dim, children: [
2280
+ fit(oneLine(s.topic ?? s.title), topicW),
2281
+ " "
2282
+ ] }),
2283
+ /* @__PURE__ */ jsx7(Text6, { color: s.offline ? C.faint : STATE_COLOR[st], children: fit(s.offline ? t.overview.offline : t.state[st], stateW).trimEnd() }),
2284
+ n > 0 ? /* @__PURE__ */ jsxs5(Text6, { color: C.violet, children: [
2285
+ " +",
2286
+ n
2287
+ ] }) : null,
2288
+ s.machineName && s.machineId !== "local" ? /* @__PURE__ */ jsxs5(Text6, { color: C.faint, children: [
2289
+ " \xB7 ",
2290
+ machineLabel(s)
2291
+ ] }) : null
2292
+ ] });
2293
+ }
2294
+
2295
+ // src/tui/screens/Session.tsx
2296
+ import { useEffect as useEffect6, useMemo as useMemo3, useState as useState4 } from "react";
2297
+ import { Box as Box9, Text as Text10, useWindowSize as useWindowSize3 } from "ink";
2298
+
2299
+ // ../ui/src/thread/turns.ts
2300
+ var byTime = (a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0;
2301
+ function withoutEcho(items, reply) {
2302
+ if (!reply) return items;
2303
+ let last = -1;
2304
+ items.forEach((it, i) => {
2305
+ if (it.kind === "message") last = i;
2306
+ });
2307
+ const echo = items[last];
2308
+ return echo?.kind === "message" && echo.text.trim() === reply.text.trim() ? items.filter((_, i) => i !== last) : items;
2309
+ }
2310
+ function turns(items) {
2311
+ const map = /* @__PURE__ */ new Map();
2312
+ for (const it of items) {
2313
+ if (it.helperId) continue;
2314
+ const t = map.get(it.turn) ?? { n: it.turn, items: [] };
2315
+ t.items.push(it);
2316
+ if (it.kind === "prompt") t.prompt = it;
2317
+ if (it.kind === "reply") t.reply = it;
2318
+ map.set(it.turn, t);
2319
+ }
2320
+ return [...map.values()].sort((a, b) => a.n - b.n).map((t) => ({ ...t, items: withoutEcho([...t.items].sort(byTime), t.reply) }));
2321
+ }
2322
+
2323
+ // ../ui/src/thread/tools.ts
2324
+ var TARGET_MAX = 40;
2325
+ var looksLikePath = (s) => !/\s/.test(s) && s.includes("/");
2326
+ function cut(s, max = TARGET_MAX) {
2327
+ if (s.length <= max) return s;
2328
+ const head = s.slice(0, max - 1);
2329
+ const space = head.lastIndexOf(" ");
2330
+ return `${(space > max / 2 ? head.slice(0, space) : head).trimEnd()}\u2026`;
2331
+ }
2332
+ function shortPath(path) {
2333
+ const parts = path.split("/").filter(Boolean);
2334
+ const two = parts.slice(-2).join("/");
2335
+ return two.length <= TARGET_MAX ? two : cut(parts.at(-1) ?? two);
2336
+ }
2337
+ function rawTarget(item) {
2338
+ let s = item.label.trim();
2339
+ const prefix = `${item.tool} \xB7 `;
2340
+ if (s.startsWith(prefix)) s = s.slice(prefix.length);
2341
+ if (s === item.tool) return "";
2342
+ if (s.startsWith("{")) {
2343
+ try {
2344
+ const o = JSON.parse(s);
2345
+ const v = ["pattern", "query", "url", "file_path", "path", "command", "description", "prompt"].map((k) => o[k]).find((x) => typeof x === "string" && x);
2346
+ if (typeof v === "string") s = v;
2347
+ } catch {
2348
+ }
2349
+ }
2350
+ return (s.split("\n")[0] ?? "").trim();
2351
+ }
2352
+ function target(item) {
2353
+ const raw = rawTarget(item);
2354
+ return looksLikePath(raw) ? shortPath(raw) : cut(raw);
2355
+ }
2356
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2357
+
2358
+ // src/tui/model/thread.ts
2359
+ var DIFF_MAX = 30;
2360
+ function wrap(text, width) {
2361
+ const out = [];
2362
+ const w = Math.max(10, width);
2363
+ for (const raw of text.replace(/\r/g, "").split("\n")) {
2364
+ let rest = [...raw];
2365
+ if (rest.length === 0) {
2366
+ out.push("");
2367
+ continue;
2368
+ }
2369
+ while (rest.length > w) {
2370
+ const head = rest.slice(0, w);
2371
+ const space = head.lastIndexOf(" ");
2372
+ const at = space > 0 ? space : w;
2373
+ out.push(rest.slice(0, at).join("").trimEnd());
2374
+ rest = [...rest.slice(at).join("").trimStart()];
2375
+ }
2376
+ out.push(rest.join(""));
2377
+ }
2378
+ return out;
2379
+ }
2380
+ function diffCount(diff) {
2381
+ let add = 0, del = 0;
2382
+ for (const l of (diff ?? "").split("\n")) {
2383
+ if (l.startsWith("+++") || l.startsWith("---")) continue;
2384
+ if (l.startsWith("+")) add++;
2385
+ else if (l.startsWith("-")) del++;
2386
+ }
2387
+ return { add, del };
2388
+ }
2389
+ var seconds = (ms) => `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)} s`;
2390
+ function toolLines(it, o) {
2391
+ const status = it.status === "success" ? { g: o.g.ok, c: C.green } : it.status === "failed" ? { g: o.g.fail, c: C.red } : it.status === "running" ? { g: o.ascii ? o.g.live : SPINNER[Math.floor(o.now / 1e3) % SPINNER.length], c: C.blue } : { g: "\xB7", c: C.faint };
2392
+ const counts = it.files.reduce((a, f) => {
2393
+ const n = diffCount(f.diff);
2394
+ return { add: a.add + n.add, del: a.del + n.del };
2395
+ }, { add: 0, del: 0 });
2396
+ const took = it.durationMs !== void 0 ? seconds(it.durationMs) : it.status === "running" ? seconds(Math.max(0, o.now - Date.parse(it.at))) : "";
2397
+ const line = [{ text: " " }, { text: status.g, color: status.c }, { text: ` ${it.tool.padEnd(6)} `, color: C.dim }, { text: target(it), color: C.fg }];
2398
+ if (counts.add || counts.del) line.push({ text: " " }, { text: `+${counts.add}`, color: C.green }, { text: " " }, { text: `\u2212${counts.del}`, color: C.red });
2399
+ if (took) line.push({ text: ` ${took}`, color: C.faint });
2400
+ const out = [line];
2401
+ if (it.status === "failed" && it.error) out.push([{ text: ` ${oneLine(it.error).slice(0, 200)}`, color: C.red }]);
2402
+ if (o.diffs) for (const f of it.files) {
2403
+ const lines = (f.diff ?? "").split("\n").filter((l) => l && !l.startsWith("+++") && !l.startsWith("---"));
2404
+ for (const l of lines.slice(0, DIFF_MAX)) out.push([{ text: ` ${o.g.bar} `, color: C.faint }, { text: l, color: l.startsWith("+") ? C.green : l.startsWith("-") ? C.red : C.faint }]);
2405
+ if (lines.length > DIFF_MAX) out.push([{ text: ` ${o.g.bar} ${o.g.ellipsis} ${lines.length - DIFF_MAX}`, color: C.faint }]);
2406
+ }
2407
+ return out;
2408
+ }
2409
+ function itemLines(it, o) {
2410
+ const { t, g, width } = o;
2411
+ const body = (text, color) => wrap(text, width - 2).map((l) => [{ text: ` ${l}`, color }]);
2412
+ switch (it.kind) {
2413
+ case "prompt":
2414
+ return [[{ text: t.session.you, color: C.coral, bold: true }, { text: ` ${clock(it.at)}`, color: C.faint }], ...body(it.text, C.fg)];
2415
+ case "message":
2416
+ return body(it.text, C.dim);
2417
+ case "reply":
2418
+ return body(it.text, C.fg);
2419
+ case "tool":
2420
+ return toolLines(it, o);
2421
+ case "permission": {
2422
+ const d = it.decision ? ` ${g.arrow} ${it.decision}` : "";
2423
+ return [[{ text: " " }, { text: g.approval, color: C.red }, { text: ` ${it.tool.padEnd(6)} `, color: C.dim }, { text: oneLine(it.text), color: C.fg }, { text: d, color: C.faint }]];
2424
+ }
2425
+ case "helper":
2426
+ return [[{ text: ` ${g.helper} `, color: C.violet }, { text: it.agentType ?? "", color: C.dim }, { text: ` ${oneLine(it.task)}`, color: C.violet }, { text: ` ${it.state === "working" ? t.state.working : t.session.helperDone}`, color: C.faint }]];
2427
+ case "sent":
2428
+ return [[{ text: ` ${g.message} `, color: C.violet }, { text: `${t.session.sentTo} `, color: C.dim }, { text: it.to, color: C.fg, bold: true }, { text: ` \xAB ${oneLine(it.summary ?? it.text)} \xBB`, color: C.dim }, { text: ` ${t.session.sent[it.status]}`, color: it.status === "failed" ? C.red : C.green }]];
2429
+ case "received":
2430
+ return [[{ text: ` ${g.message} `, color: C.violet }, { text: `${t.session.receivedFrom} `, color: C.dim }, { text: it.from, color: C.fg, bold: true }, { text: ` \xAB ${oneLine(it.text)} \xBB`, color: C.dim }]];
2431
+ case "notice":
2432
+ return [[{ text: ` \xB7 ${oneLine(it.summary ?? it.text)}`, color: C.faint }]];
2433
+ }
2434
+ }
2435
+ function foldedLine(turn, o) {
2436
+ const tools = turn.items.filter((i) => i.kind === "tool");
2437
+ const files = new Set(tools.flatMap((i) => i.status === "success" ? i.files.map((f) => f.path) : []));
2438
+ const parts = [turn.prompt ? oneLine(turn.prompt.text) : "", o.t.session.actions(tools.length), ...files.size ? [o.t.session.files(files.size)] : [], ...turn.reply ? [oneLine(turn.reply.text)] : []].filter(Boolean);
2439
+ return [{ text: `${o.g.folded} ${clock(turn.prompt?.at ?? turn.items[0]?.at ?? (/* @__PURE__ */ new Date(0)).toISOString())} `, color: C.faint }, { text: parts.join(" \xB7 "), color: C.faint }];
2440
+ }
2441
+ function threadLines(items, o) {
2442
+ const all = turns(items);
2443
+ const out = [];
2444
+ all.forEach((turn, i) => {
2445
+ const last = i === all.length - 1;
2446
+ if (!last && !o.unfolded) {
2447
+ out.push(foldedLine(turn, o));
2448
+ return;
2449
+ }
2450
+ if (out.length) out.push([{ text: "" }]);
2451
+ for (const it of turn.items) out.push(...itemLines(it, o));
2452
+ });
2453
+ return out;
2454
+ }
2455
+
2456
+ // src/tui/ui/ModelPicker.tsx
2457
+ import { useState as useState3 } from "react";
2458
+ import { Box as Box7, Text as Text7, useInput } from "ink";
2459
+
2460
+ // ../fleet/src/models.ts
2461
+ import { useEffect as useEffect4, useState as useState2 } from "react";
2462
+ var IDLE = { status: "idle", models: [] };
2463
+ var LOADING = { status: "loading", models: [] };
2464
+ var known = /* @__PURE__ */ new Map();
2465
+ var next = 0;
2466
+ function useModels(client, machine, tool) {
2467
+ const key = machine === null || tool === null ? null : `${machine}|${tool}`;
2468
+ const [answer, setAnswer] = useState2(null);
2469
+ useEffect4(() => {
2470
+ if (key === null || machine === null || tool === null || known.has(key)) return;
2471
+ const requestId = `models-${++next}`;
2472
+ const off = client.onMessage((m) => {
2473
+ if (m.type !== "models.listing" || m.requestId !== requestId) return;
2474
+ const state2 = { status: "ready", models: m.models, ...m.error ? { error: m.error } : {} };
2475
+ if (!m.error) known.set(key, state2);
2476
+ setAnswer({ key, state: state2 });
2477
+ });
2478
+ client.send({ type: "models.list", requestId, tool, machineId: machine });
2479
+ return off;
2480
+ }, [client, key, machine, tool]);
2481
+ if (key === null) return IDLE;
2482
+ return known.get(key) ?? (answer && answer.key === key ? answer.state : LOADING);
2483
+ }
2484
+
2485
+ // src/tui/ui/ModelPicker.tsx
2486
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2487
+ function ModelPicker({ machine, tool, current, onPick, onCancel }) {
2488
+ const { client, t, g } = useTui();
2489
+ useEditing(true);
2490
+ const models = useModels(client, machine, tool);
2491
+ const rows = [{ id: null, label: t.launch.defaultModel, efforts: [], defaultEffort: void 0 }, ...models.models.map((m) => ({ id: m.id, label: m.label, efforts: m.efforts ?? [], defaultEffort: m.defaultEffort }))];
2492
+ const [at, setAt] = useState3(() => Math.max(0, rows.findIndex((r) => r.id === current.model)));
2493
+ const [effort, setEffort] = useState3(current.effort);
2494
+ const row = rows[Math.min(at, rows.length - 1)];
2495
+ useInput((input, key) => {
2496
+ if (key.escape) {
2497
+ onCancel();
2498
+ return;
2499
+ }
2500
+ if (key.upArrow || input === "k") {
2501
+ setAt((i) => (i - 1 + rows.length) % rows.length);
2502
+ setEffort(null);
2503
+ return;
2504
+ }
2505
+ if (key.downArrow || input === "j") {
2506
+ setAt((i) => (i + 1) % rows.length);
2507
+ setEffort(null);
2508
+ return;
2509
+ }
2510
+ if ((key.leftArrow || key.rightArrow) && row.efforts.length) {
2511
+ const cur = Math.max(0, row.efforts.indexOf(effort ?? row.defaultEffort ?? row.efforts[0]));
2512
+ setEffort(row.efforts[(cur + (key.rightArrow ? 1 : -1) + row.efforts.length) % row.efforts.length]);
2513
+ return;
2514
+ }
2515
+ if (key.return) onPick({ model: row.id, effort: row.efforts.length ? effort ?? null : null });
2516
+ });
2517
+ if (models.status !== "ready") return /* @__PURE__ */ jsx8(Text7, { color: C.faint, children: t.launch.loading });
2518
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2519
+ models.error ? /* @__PURE__ */ jsx8(Text7, { color: C.yellow, children: models.error }) : null,
2520
+ rows.map((r, i) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: i === at ? C.sel : void 0, children: [
2521
+ /* @__PURE__ */ jsxs6(Text7, { color: i === at ? C.coral : C.faint, children: [
2522
+ i === at ? g.cursor : " ",
2523
+ " "
2524
+ ] }),
2525
+ /* @__PURE__ */ jsx8(Text7, { color: C.fg, children: r.label }),
2526
+ i === at && r.efforts.length ? /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
2527
+ " ",
2528
+ r.efforts.map((e) => e === (effort ?? r.defaultEffort) ? `${g.prev} ${e} ${g.next}` : e).join(" ")
2529
+ ] }) : null
2530
+ ] }, r.id ?? "-"))
2531
+ ] });
2532
+ }
2533
+
2534
+ // src/tui/ui/Lines.tsx
2535
+ import { useEffect as useEffect5, useRef as useRef6 } from "react";
2536
+ import { Box as Box8, Text as Text8, useBoxMetrics as useBoxMetrics3 } from "ink";
2537
+ import { jsx as jsx9 } from "react/jsx-runtime";
2538
+ function Lines({ lines, up = 0, onHeight }) {
2539
+ const ref = useRef6(null);
2540
+ const { height } = useBoxMetrics3(ref);
2541
+ useEffect5(() => {
2542
+ if (height > 0) onHeight?.(height);
2543
+ }, [height, onHeight]);
2544
+ const h = Math.max(1, height);
2545
+ const back = Math.min(up, Math.max(0, lines.length - h));
2546
+ const shown = lines.slice(Math.max(0, lines.length - h - back), lines.length - back);
2547
+ return /* @__PURE__ */ jsx9(Box8, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.map((l, i) => /* @__PURE__ */ jsx9(Text8, { wrap: "truncate", children: l.map((x, k) => /* @__PURE__ */ jsx9(Text8, { color: x.color, bold: x.bold, children: x.text || (k === 0 ? " " : "") }, k)) }, i)) });
2548
+ }
2549
+
2550
+ // src/tui/ui/TextInput.tsx
2551
+ import { Text as Text9, useInput as useInput2 } from "ink";
2552
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
2553
+ function TextInput({ value, onChange, onSubmit, onEscape, placeholder, active, mask }) {
2554
+ useInput2((input, key) => {
2555
+ if (key.return) {
2556
+ onSubmit?.(value);
2557
+ return;
2558
+ }
2559
+ if (key.escape) {
2560
+ onEscape?.();
2561
+ return;
2562
+ }
2563
+ if (key.backspace || key.delete) {
2564
+ onChange([...value].slice(0, -1).join(""));
2565
+ return;
2566
+ }
2567
+ if (key.ctrl && input === "u") {
2568
+ onChange("");
2569
+ return;
2570
+ }
2571
+ if (key.ctrl || key.meta || key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.pageUp || key.pageDown) return;
2572
+ if (input) onChange(value + input.replace(/\r\n?/g, "\n"));
2573
+ }, { isActive: active });
2574
+ const shown = mask ? "\u2022".repeat(value.length) : value.replace(/\n/g, "\u23CE");
2575
+ if (!shown && !active) return /* @__PURE__ */ jsx10(Text9, { color: C.faint, wrap: "truncate", children: placeholder ?? "" });
2576
+ return /* @__PURE__ */ jsxs7(Text9, { wrap: "truncate-start", children: [
2577
+ shown ? /* @__PURE__ */ jsx10(Text9, { color: C.fg, children: shown }) : null,
2578
+ active ? /* @__PURE__ */ jsx10(Text9, { inverse: true, children: " " }) : null,
2579
+ !shown && placeholder ? /* @__PURE__ */ jsxs7(Text9, { color: C.faint, children: [
2580
+ " ",
2581
+ placeholder
2582
+ ] }) : null
2583
+ ] });
2584
+ }
2585
+
2586
+ // src/tui/screens/Session.tsx
2587
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
2588
+ function SessionScreen() {
2589
+ const tui = useTui();
2590
+ const { selected: s, t } = tui;
2591
+ if (!s) return /* @__PURE__ */ jsx11(Frame, { crumb: "", keys: [["esc", t.keys.back]], children: /* @__PURE__ */ jsx11(Text10, { color: C.faint, children: t.session.noSession }) });
2592
+ return /* @__PURE__ */ jsx11(SessionView, { s }, s.id);
2593
+ }
2594
+ function SessionView({ s }) {
2595
+ const { client, d, t, g, now, actions, attach, toast, machines } = useTui();
2596
+ const { columns } = useWindowSize3();
2597
+ const [mode, setMode] = useState4("read");
2598
+ const [draft, setDraft] = useState4("");
2599
+ const [diffs, setDiffs] = useState4(false);
2600
+ const [unfolded, setUnfolded] = useState4(false);
2601
+ const [terminal, setTerminal] = useState4(false);
2602
+ const [up, setUp] = useState4(0);
2603
+ useEditing(mode === "reply" || mode === "note");
2604
+ const items = d.threads.get(s.id);
2605
+ useEffect6(() => {
2606
+ if (!items && !s.offline) client.send({ type: "session.thread", id: s.id });
2607
+ }, [items === void 0, s.id, d.snapshotSeq]);
2608
+ const width = Math.max(20, columns - 6);
2609
+ const lines = useMemo3(() => items ? threadLines(items, { t, g, now, width, diffs, unfolded }) : [], [items, t, g, now, width, diffs, unfolded]);
2610
+ const p = s.pending;
2611
+ const [view, setView] = useState4(10);
2612
+ const maxUp = Math.max(0, lines.length - view);
2613
+ const machine = splitId(s.id)?.machine ?? "local";
2614
+ const canSwitch = machines.find((m) => m.id === machine)?.daemon?.modelSwitch === true;
2615
+ const send = (text) => {
2616
+ if (!text.trim()) {
2617
+ setMode("read");
2618
+ return;
2619
+ }
2620
+ const back = (x) => {
2621
+ setDraft(x);
2622
+ };
2623
+ if (mode === "note") actions.denyWithNote(s, text.trim(), back);
2624
+ else if (!actions.reply(s, text, back)) return;
2625
+ setDraft("");
2626
+ setMode("read");
2627
+ };
2628
+ useScreenKeys((input, key) => {
2629
+ if (mode === "model") return true;
2630
+ if (mode !== "read") {
2631
+ if (key.ctrl && input === "c") {
2632
+ setMode("read");
2633
+ return true;
2634
+ }
2635
+ return false;
2636
+ }
2637
+ if (key.pageUp) {
2638
+ setUp((u) => Math.min(maxUp, u + view - 1));
2639
+ return true;
2640
+ }
2641
+ if (key.pageDown) {
2642
+ setUp((u) => Math.max(0, u - view + 1));
2643
+ return true;
2644
+ }
2645
+ if (input === "k" || key.upArrow) {
2646
+ setUp((u) => Math.min(maxUp, u + 1));
2647
+ return true;
2648
+ }
2649
+ if (input === "j" || key.downArrow) {
2650
+ setUp((u) => Math.max(0, u - 1));
2651
+ return true;
2652
+ }
2653
+ if (input === "G" || key.end) {
2654
+ setUp(0);
2655
+ return true;
2656
+ }
2657
+ if (input === " ") {
2658
+ setUnfolded((u) => !u);
2659
+ return true;
2660
+ }
2661
+ if (input === "d") {
2662
+ setDiffs((x) => !x);
2663
+ return true;
2664
+ }
2665
+ if (input === "t") {
2666
+ setTerminal((x) => !x);
2667
+ return true;
2668
+ }
2669
+ if (input === "a") {
2670
+ attach(s);
2671
+ return true;
2672
+ }
2673
+ if (input === "i") {
2674
+ actions.interrupt(s);
2675
+ return true;
2676
+ }
2677
+ if (input === "m") {
2678
+ if (!canSwitch) toast({ text: t.toast.modelSwitch, tone: "info" });
2679
+ else setMode("model");
2680
+ return true;
2681
+ }
2682
+ if (input === "r" || key.return) {
2683
+ if (s.offline) {
2684
+ toast({ text: t.toast.offline(s.name), tone: "info" });
2685
+ return true;
2686
+ }
2687
+ if (s.source === "hook_only") {
2688
+ toast({ text: t.toast.readOnly(s.name), tone: "info" });
2689
+ return true;
2690
+ }
2691
+ const note = p && p.kind !== "question" && input === "r";
2692
+ if (note && !offers(s.tool, p.kind, "note")) {
2693
+ toast({ text: t.toast.noNote(s.name), tone: "info" });
2694
+ return true;
2695
+ }
2696
+ if (p && p.kind !== "question" && !note) return true;
2697
+ setMode(note ? "note" : "reply");
2698
+ return true;
2699
+ }
2700
+ if (/^[1-9]$/.test(input) && p?.kind !== "question") return false;
2701
+ if (input === "y" || input === "n" || input === "A" || /^[1-9]$/.test(input)) return actions.key(s, input);
2702
+ return false;
2703
+ });
2704
+ const state2 = botState(s);
2705
+ const title2 = `${s.name} \xB7 ${oneLine(s.topic ?? s.title)} \xB7 ${TOOL_NAME[s.tool]} \xB7 ${modelLabel(s, t.session.defaultModel)}`;
2706
+ const keys = mode === "model" ? [["\u2191 \u2193", t.keys.choose], ["\u2190 \u2192", t.launch.effort], ["\u21B5", t.keys.choose], ["esc", t.keys.cancel]] : mode !== "read" ? [["\u21B5", t.keys.send], ["esc", t.keys.back]] : [["esc", t.keys.back], ...p ? [["y n A", t.keys.answer]] : [], ["r", t.keys.write], ["j k", t.keys.scroll], [t.keys.space, t.keys.unfold], ["d", t.keys.diffs], ["t", t.keys.terminal], ["a", t.keys.attach], ["m", t.keys.model], ["i", t.keys.interrupt]];
2707
+ return /* @__PURE__ */ jsxs8(Frame, { crumb: `${s.repo ?? s.project}${s.branch ? ` \u203A ${g.branch} ${s.branch}` : ""} \u203A ${s.name} \xB7 ${machineLabel(s)}`, keys, children: [
2708
+ /* @__PURE__ */ jsx11(TitledBox, { title: terminal ? t.overview.terminal(paneName(s)) : title2, flexGrow: 1, children: mode === "model" ? /* @__PURE__ */ jsx11(
2709
+ ModelPicker,
2710
+ {
2711
+ machine,
2712
+ tool: s.tool,
2713
+ current: { model: s.model ?? null, effort: s.effort ?? null },
2714
+ onCancel: () => setMode("read"),
2715
+ onPick: (pick) => {
2716
+ client.send({ type: "session.model", id: s.id, model: pick.model, effort: pick.effort });
2717
+ toast({ text: t.toast.model(s.name, pick.model ?? t.session.defaultModel), tone: "ok" });
2718
+ setMode("read");
2719
+ }
2720
+ }
2721
+ ) : terminal ? /* @__PURE__ */ jsx11(Tail, { lines: d.previews.get(s.id) ?? [], color: C.dim, empty: t.overview.noPreview, emptyColor: C.faint }) : !items ? /* @__PURE__ */ jsx11(Text10, { color: C.faint, children: t.session.loading }) : lines.length === 0 ? /* @__PURE__ */ jsx11(Text10, { color: C.faint, children: t.session.empty }) : /* @__PURE__ */ jsx11(Lines, { lines, up, onHeight: setView }) }),
2722
+ p ? /* @__PURE__ */ jsxs8(TitledBox, { title: t.session.since(t.state[state2], ago(s.lastActivityAt, now, t.time)), color: isPending(s) ? C.red : C.line, titleColor: STATE_COLOR[state2], children: [
2723
+ /* @__PURE__ */ jsxs8(Text10, { wrap: "truncate", children: [
2724
+ /* @__PURE__ */ jsx11(Text10, { color: C.fg, children: p.kind === "question" ? oneLine(p.text) : `${t.session.wantsTo(s.name)} ` }),
2725
+ p.kind !== "question" ? /* @__PURE__ */ jsx11(Text10, { color: C.fg, bold: true, children: oneLine(p.text) }) : null,
2726
+ p.tool ? /* @__PURE__ */ jsxs8(Text10, { color: C.faint, children: [
2727
+ " ",
2728
+ p.tool
2729
+ ] }) : null
2730
+ ] }),
2731
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, columnGap: 2, flexWrap: "wrap", children: p.kind === "question" ? choices(p).slice(0, 9).map((c, i) => /* @__PURE__ */ jsx11(Button, { label: `${c.n} \xB7 ${c.label}`, k: String(c.n), primary: i === 0 }, c.n)) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
2732
+ /* @__PURE__ */ jsx11(Button, { label: t.overview.allow, k: "y", primary: true }),
2733
+ /* @__PURE__ */ jsx11(Button, { label: t.overview.deny, k: "n" }),
2734
+ offers(s.tool, p.kind, "always") ? /* @__PURE__ */ jsx11(Button, { label: t.overview.always, k: "A" }) : null,
2735
+ offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx11(Button, { label: t.overview.denyWithNote, k: "r" }) : null
2736
+ ] }) })
2737
+ ] }) : null,
2738
+ /* @__PURE__ */ jsx11(TitledBox, { title: mode === "note" ? t.overview.denyWithNote : t.keys.write, color: mode === "read" ? C.line : C.coral, children: /* @__PURE__ */ jsxs8(Text10, { wrap: "truncate", children: [
2739
+ /* @__PURE__ */ jsxs8(Text10, { color: C.coral, children: [
2740
+ g.cursor,
2741
+ " "
2742
+ ] }),
2743
+ /* @__PURE__ */ jsx11(
2744
+ TextInput,
2745
+ {
2746
+ value: draft,
2747
+ onChange: setDraft,
2748
+ onSubmit: send,
2749
+ onEscape: () => setMode("read"),
2750
+ active: mode === "reply" || mode === "note",
2751
+ placeholder: mode === "note" ? t.session.noteFor(s.name) : t.session.replyTo(s.name)
2752
+ }
2753
+ )
2754
+ ] }) })
2755
+ ] });
2756
+ }
2757
+
2758
+ // src/tui/screens/Ask.tsx
2759
+ import { randomUUID } from "crypto";
2760
+ import { useEffect as useEffect7, useMemo as useMemo4, useRef as useRef7, useState as useState5 } from "react";
2761
+ import { Box as Box10, Text as Text11, useInput as useInput3, useWindowSize as useWindowSize4 } from "ink";
2762
+
2763
+ // src/tui/model/chat.ts
2764
+ function stepLabel(s, t) {
2765
+ const name = s.name ?? s.id ?? "";
2766
+ return s.kind === "sessions" ? t.sessions : s.kind === "repo" ? t.repo(s.path ?? "") : t[s.kind](name);
2767
+ }
2768
+ function chatLines(turns2, live, o) {
2769
+ const { t, g, width } = o;
2770
+ const out = [];
2771
+ for (const turn of turns2) {
2772
+ if (out.length) out.push([{ text: "" }]);
2773
+ for (const l of wrap(turn.question, width - 2)) out.push([{ text: `${g.cursor} `, color: C.coral }, { text: l, color: C.fg, bold: true }]);
2774
+ out.push([{ text: ` ${t.ask.you} \xB7 ${clock(turn.askedAt)}`, color: C.faint }]);
2775
+ const steps = [...new Set(turn.steps.map((s) => stepLabel(s, t.ask.steps)))];
2776
+ if (steps.length) out.push([{ text: ` ${t.ask.read} : ${steps.join(" \xB7 ")}`, color: C.faint }]);
2777
+ const text = turn.answer ?? (live?.qid === turn.qid ? live.text : void 0);
2778
+ if (turn.error) out.push([{ text: ` ${turn.error}`, color: C.red }]);
2779
+ else if (text) {
2780
+ out.push([{ text: "" }]);
2781
+ for (const l of wrap(text, width - 2)) out.push([{ text: " " }, { text: l, color: C.fg }]);
2782
+ } else out.push([{ text: ` ${t.ask.reading}`, color: C.faint }]);
2783
+ }
2784
+ return out;
2785
+ }
2786
+ function openCards(turns2) {
2787
+ const out = [];
2788
+ for (const turn of [...turns2].reverse()) {
2789
+ if (!turn.answeredAt) continue;
2790
+ const launch = proposalRows(turn).map((p, i) => p.launchedId ? -1 : i).filter((i) => i >= 0);
2791
+ if (launch.length) out.push({ kind: "launch", qid: turn.qid, rows: launch });
2792
+ if (turn.relay && turn.relay.status !== "sent") out.push({ kind: "relay", qid: turn.qid });
2793
+ const close = (turn.closes ?? []).map((c, i) => c.closedAt ? -1 : i).filter((i) => i >= 0);
2794
+ if (close.length) out.push({ kind: "close", qid: turn.qid, rows: close });
2795
+ }
2796
+ return out;
2797
+ }
2798
+
2799
+ // src/tui/screens/Ask.tsx
2800
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2801
+ var CARD_MS = 2e4;
2802
+ function Ask() {
2803
+ const { client, d, machines, t, g, toast, go } = useTui();
2804
+ const { columns } = useWindowSize4();
2805
+ const [draft, setDraft] = useState5("");
2806
+ const [focus, setFocus] = useState5(-1);
2807
+ const [busy, setBusy] = useState5(null);
2808
+ useEditing(focus === -1);
2809
+ const cards = useMemo4(() => openCards(d.chat), [d.chat]);
2810
+ useEffect7(() => {
2811
+ if (focus >= cards.length) setFocus(cards.length ? cards.length - 1 : -1);
2812
+ }, [cards.length, focus]);
2813
+ useEffect7(() => {
2814
+ if (!busy) return;
2815
+ const turn = d.chat.find((x) => x.qid === busy.qid);
2816
+ if (turn !== busy.turn || (d.errors.at(-1)?.seq ?? 0) > busy.errorSeq) {
2817
+ setBusy(null);
2818
+ return;
2819
+ }
2820
+ const timer = setTimeout(() => setBusy(null), Math.max(0, busy.at + CARD_MS - Date.now()));
2821
+ return () => clearTimeout(timer);
2822
+ }, [busy, d.chat, d.errors]);
2823
+ const local = machines.find((m) => m.id === "local");
2824
+ const asking = d.chat.find((x) => !x.answeredAt && !x.error);
2825
+ const ask = (text) => {
2826
+ if (!text.trim()) return;
2827
+ if (!local?.ready) {
2828
+ toast({ text: t.ask.offline, tone: "info" });
2829
+ return;
2830
+ }
2831
+ const qid = `q-${randomUUID()}`;
2832
+ daemonStore.beginTurn({ qid, question: text.trim(), askedAt: (/* @__PURE__ */ new Date()).toISOString(), steps: [] });
2833
+ client.send({ type: "chat.ask", qid, text: text.trim() });
2834
+ setDraft("");
2835
+ };
2836
+ const act = (card) => {
2837
+ if (busy) return;
2838
+ const turn = d.chat.find((x) => x.qid === card.qid);
2839
+ if (!turn) return;
2840
+ setBusy({ qid: card.qid, turn, errorSeq: d.errors.at(-1)?.seq ?? 0, at: Date.now() });
2841
+ if (card.kind === "launch") {
2842
+ const first = proposalRows(turn)[card.rows[0]];
2843
+ client.send({ type: "session.launch", tool: first.tool, cwd: first.cwd, ...first.prompt ? { prompt: first.prompt } : {}, qid: card.qid, rows: card.rows });
2844
+ } else if (card.kind === "relay") client.send({ type: turn.relay?.status === "queued" ? "session.relay.cancel" : "session.relay", qid: card.qid });
2845
+ else client.send({ type: "session.close", qid: card.qid, rows: card.rows });
2846
+ };
2847
+ const latest = useRef7({ asking, cards, focus });
2848
+ latest.current = { asking, cards, focus };
2849
+ useInput3((input, key) => {
2850
+ if (key.ctrl && input === "n") {
2851
+ client.send({ type: "chat.reset" });
2852
+ return;
2853
+ }
2854
+ if (key.tab) {
2855
+ const n = latest.current.cards.length;
2856
+ setFocus((f) => n === 0 ? -1 : f + 1 >= n ? -1 : f + 1);
2857
+ }
2858
+ });
2859
+ useScreenKeys((input, key) => {
2860
+ if (key.ctrl && input === "c") {
2861
+ if (latest.current.asking) {
2862
+ client.send({ type: "chat.cancel", qid: latest.current.asking.qid });
2863
+ return true;
2864
+ }
2865
+ return false;
2866
+ }
2867
+ if (focus < 0) return false;
2868
+ if (key.return) {
2869
+ act(cards[focus]);
2870
+ return true;
2871
+ }
2872
+ if (key.escape) {
2873
+ setFocus(-1);
2874
+ return true;
2875
+ }
2876
+ return false;
2877
+ });
2878
+ const sessions = listed(d.sessions).length;
2879
+ const width = Math.max(20, columns - 6);
2880
+ const lines = useMemo4(() => chatLines(d.chat, d.chatLive, { t, g, width }), [d.chat, d.chatLive, t, g, width]);
2881
+ return /* @__PURE__ */ jsxs9(Frame, { crumb: `${t.tabs.ask}`, keys: [["\u21B5", t.keys.send], ["tab", t.keys.nextCard], ["ctrl+n", t.keys.newChat], ["ctrl+c", t.keys.stopAnswer], ["esc", t.keys.back]], children: [
2882
+ /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.title(sessions, Math.max(1, machines.length)), flexGrow: 1, children: lines.length === 0 ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.empty }) : /* @__PURE__ */ jsx12(Lines, { lines }) }),
2883
+ cards.slice(0, 2).map((c, i) => /* @__PURE__ */ jsx12(CardBox, { card: c, turn: d.chat.find((x) => x.qid === c.qid), focused: i === focus, busy: busy?.qid === c.qid }, `${c.kind}:${c.qid}`)),
2884
+ /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.question, color: focus === -1 ? C.coral : C.line, children: /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
2885
+ /* @__PURE__ */ jsxs9(Text11, { color: C.coral, children: [
2886
+ g.cursor,
2887
+ " "
2888
+ ] }),
2889
+ /* @__PURE__ */ jsx12(TextInput, { value: draft, onChange: setDraft, onSubmit: ask, onEscape: () => {
2890
+ if (!draft) go("overview");
2891
+ }, active: focus === -1 })
2892
+ ] }) })
2893
+ ] });
2894
+ }
2895
+ function CardBox({ card, turn, focused, busy }) {
2896
+ const { t, g } = useTui();
2897
+ const color = focused ? C.coral : C.line;
2898
+ if (card.kind === "launch") {
2899
+ const rows = proposalRows(turn);
2900
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.launchCard, color, titleColor: C.coral, children: [
2901
+ card.rows.map((r) => {
2902
+ const p = rows[r];
2903
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
2904
+ /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
2905
+ /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.folder.padEnd(13) }),
2906
+ /* @__PURE__ */ jsx12(Text11, { color: C.fg, children: p.project }),
2907
+ " ",
2908
+ /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: p.cwd })
2909
+ ] }),
2910
+ /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
2911
+ /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.agent.padEnd(13) }),
2912
+ TOOL_NAME[p.tool]
2913
+ ] }),
2914
+ p.prompt ? /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
2915
+ /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.instruction.padEnd(13) }),
2916
+ oneLine(p.prompt)
2917
+ ] }) : null,
2918
+ p.error ? /* @__PURE__ */ jsx12(Text11, { color: C.red, wrap: "truncate", children: p.error }) : null
2919
+ ] }, r);
2920
+ }),
2921
+ /* @__PURE__ */ jsxs9(Box10, { columnGap: 2, children: [
2922
+ /* @__PURE__ */ jsx12(Button, { label: busy ? `${g.live} ${t.ask.launch}` : t.ask.launch, k: "\u21B5", primary: focused }),
2923
+ /* @__PURE__ */ jsx12(Button, { label: t.ask.ignore, k: "esc" }),
2924
+ /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.nothingWithoutYou })
2925
+ ] })
2926
+ ] });
2927
+ }
2928
+ if (card.kind === "relay") {
2929
+ const r = turn.relay;
2930
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.relayCard(r.name), color, titleColor: C.violet, children: [
2931
+ /* @__PURE__ */ jsx12(Text11, { wrap: "truncate", color: C.fg, children: oneLine(r.text) }),
2932
+ r.error ? /* @__PURE__ */ jsx12(Text11, { color: C.red, wrap: "truncate", children: r.error }) : null,
2933
+ /* @__PURE__ */ jsxs9(Box10, { columnGap: 2, children: [
2934
+ /* @__PURE__ */ jsx12(Button, { label: r.status === "queued" ? t.ask.cancelRelay : t.ask.relay, k: "\u21B5", primary: focused }),
2935
+ r.status === "queued" ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.queued }) : null
2936
+ ] })
2937
+ ] });
2938
+ }
2939
+ const closes = turn.closes ?? [];
2940
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.closeCard, color, titleColor: C.red, children: [
2941
+ card.rows.map((r) => /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
2942
+ /* @__PURE__ */ jsx12(Text11, { color: C.fg, children: closes[r].name }),
2943
+ " ",
2944
+ /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: TOOL_NAME[closes[r].tool] }),
2945
+ closes[r].error ? /* @__PURE__ */ jsxs9(Text11, { color: C.red, children: [
2946
+ " ",
2947
+ closes[r].error
2948
+ ] }) : null
2949
+ ] }, r)),
2950
+ /* @__PURE__ */ jsx12(Box10, { columnGap: 2, children: /* @__PURE__ */ jsx12(Button, { label: t.ask.close, k: "\u21B5", primary: focused }) })
2951
+ ] });
2952
+ }
2953
+
2954
+ // src/tui/screens/Launch.tsx
2955
+ import { useEffect as useEffect8, useMemo as useMemo5, useState as useState6 } from "react";
2956
+ import { Box as Box11, Text as Text12, useInput as useInput4 } from "ink";
2957
+
2958
+ // ../fleet/src/launch.ts
2959
+ var MULTI_REPO_TOOLS = /* @__PURE__ */ new Set(["claude", "omp"]);
2960
+ var NO_PICK = { model: null, effort: null };
2961
+ var isUp = (m) => m.ready;
2962
+ var find = (projects, id) => projects.find((p) => p.id === id);
2963
+ var usable = (infos, path) => infos?.get(path)?.exists !== false;
2964
+ function reposOn(project, machine) {
2965
+ return machine === null ? [] : project?.repos.filter((r) => r.machine === machine) ?? [];
2966
+ }
2967
+ function firstRepo(project, machine, infos) {
2968
+ const r = reposOn(project, machine).find((r2) => usable(infos, r2.path));
2969
+ return r ? [r.path] : [];
2970
+ }
2971
+ function launchDefaults(projects, machines, wanted, tool, infos) {
2972
+ const project = (find(projects, wanted.project ?? null) ?? find(projects, wanted.filter ?? null) ?? projects[0])?.id ?? null;
2973
+ const up = machines.filter(isUp);
2974
+ const machine = (up.find((m) => m.id === "local") ?? up[0])?.id ?? null;
2975
+ return { tool, prompt: "", project, machine, repos: firstRepo(find(projects, project), machine, infos), ...NO_PICK, approval: "always-ask" };
2976
+ }
2977
+ function withProject(f, projects, id, infos) {
2978
+ return { ...f, project: id, repos: firstRepo(find(projects, id), f.machine, infos) };
2979
+ }
2980
+ function withMachine(f, projects, machine, infos) {
2981
+ return { ...f, machine, repos: firstRepo(find(projects, f.project), machine, infos) };
2982
+ }
2983
+ function withTool(f, tool, pick = NO_PICK) {
2984
+ return { ...f, tool, repos: MULTI_REPO_TOOLS.has(tool) ? f.repos : f.repos.slice(0, 1), model: pick.model, effort: pick.effort };
2985
+ }
2986
+ function withPick(f, pick) {
2987
+ return { ...f, model: pick.model, effort: pick.effort };
2988
+ }
2989
+ function toggleRepo(f, path) {
2990
+ if (!MULTI_REPO_TOOLS.has(f.tool)) return { ...f, repos: [path] };
2991
+ return { ...f, repos: f.repos.includes(path) ? f.repos.filter((p) => p !== path) : [...f.repos, path] };
2992
+ }
2993
+ function missingOf(f, machines) {
2994
+ if (!f.prompt.trim()) return "prompt";
2995
+ if (f.machine === null || !machines.some((m) => m.id === f.machine && isUp(m))) return "machine";
2996
+ if (f.repos.length === 0) return "repos";
2997
+ return null;
2998
+ }
2999
+ function previewOf(f, sessions, projectIdOf) {
3000
+ const machineOf2 = (s) => splitId(s.id)?.machine ?? s.machineId ?? "local";
3001
+ const taken = sessions.filter((s) => s.state !== "exited" && machineOf2(s) === f.machine).map((s) => s.name);
3002
+ const seated = f.project === null ? 0 : sessions.filter((s) => projectIdOf(s) === f.project).length;
3003
+ return { name: pickName(taken), seat: seated + 1 };
3004
+ }
3005
+ function launchMessage(f, name) {
3006
+ const [cwd = "", ...rest] = f.repos;
3007
+ const { tool } = f;
3008
+ return {
3009
+ type: "session.launch",
3010
+ tool,
3011
+ cwd,
3012
+ ...MULTI_REPO_TOOLS.has(tool) && rest.length ? { dirs: rest } : {},
3013
+ name,
3014
+ prompt: f.prompt.trim(),
3015
+ ...f.model ? { model: f.model } : {},
3016
+ ...f.effort ? { effort: f.effort } : {},
3017
+ ...tool === "omp" && f.approval !== "always-ask" ? { approval: f.approval } : {},
3018
+ ...f.machine ? { machineId: f.machine } : {}
3019
+ };
3020
+ }
3021
+
3022
+ // src/tui/screens/Launch.tsx
3023
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3024
+ var FIELDS = ["prompt", "tool", "model", "effort", "project", "machine", "repos", "folder", "name"];
3025
+ var TEXT = /* @__PURE__ */ new Set(["prompt", "folder"]);
3026
+ var LAUNCH_MS = 2e4;
3027
+ var cycle = (list, cur, by) => list[((Math.max(0, list.indexOf(cur)) + by) % list.length + list.length) % list.length];
3028
+ function Launch() {
3029
+ const { client, d, machines, projects, t, g, toast, go, select } = useTui();
3030
+ const list = projects?.projects ?? [];
3031
+ const [form, setForm] = useState6(() => launchDefaults(list, machines, {}, "claude"));
3032
+ const [field, setField] = useState6("prompt");
3033
+ const [folder, setFolder] = useState6("");
3034
+ const [repoAt, setRepoAt] = useState6(0);
3035
+ const [launching, setLaunching] = useState6(null);
3036
+ useEditing(true);
3037
+ useEffect8(() => {
3038
+ if (form.machine === null) {
3039
+ const up = machines.find((m) => m.ready);
3040
+ if (up) setForm((f) => withMachine(f, list, up.id));
3041
+ }
3042
+ }, [machines]);
3043
+ const machine = machines.find((m) => m.id === form.machine);
3044
+ const tools = ["claude", "codex", "cursor", ...machine?.daemon?.omp ? ["omp"] : []];
3045
+ const models = useModels(client, machine?.daemon?.models ? form.machine : null, form.tool);
3046
+ const modelIds = [null, ...models.models.map((m) => m.id)];
3047
+ const model = models.models.find((m) => m.id === form.model);
3048
+ const efforts = [null, ...model?.efforts ?? []];
3049
+ const project = list.find((p) => p.id === form.project);
3050
+ const repoOptions = useMemo5(() => {
3051
+ const own = reposOn(project, form.machine).map((r) => r.path);
3052
+ return own.length ? own : (machine?.recents ?? []).slice(0, 6).map((r) => r.cwd);
3053
+ }, [project, form.machine, machine?.recents]);
3054
+ const withFolder = folder.trim() ? { ...form, repos: MULTI_REPO_TOOLS.has(form.tool) ? [folder.trim(), ...form.repos.filter((r) => r !== folder.trim())] : [folder.trim()] } : form;
3055
+ const projectIdOf = (s) => projects ? projectOf(s, projects) : null;
3056
+ const drawn = useMemo5(() => previewOf(withFolder, d.sessions, projectIdOf), [form.machine, form.project, d.sessions.length]);
3057
+ const [name, setName] = useState6(null);
3058
+ const shownName = name ?? drawn.name;
3059
+ const missing = missingOf(withFolder, machines);
3060
+ useEffect8(() => {
3061
+ if (!launching) return;
3062
+ if ((d.lastLaunched?.seq ?? 0) > launching.seq) {
3063
+ select(d.lastLaunched.id);
3064
+ toast({ text: t.launch.launched(launching.name), tone: "ok" });
3065
+ setLaunching(null);
3066
+ go("overview");
3067
+ return;
3068
+ }
3069
+ const refused = d.errors.find((e) => e.seq > launching.errorSeq && e.id === void 0);
3070
+ if (refused) {
3071
+ toast({ text: refused.message, tone: "error" });
3072
+ setLaunching(null);
3073
+ return;
3074
+ }
3075
+ const timer = setTimeout(() => setLaunching(null), Math.max(0, launching.at + LAUNCH_MS - Date.now()));
3076
+ return () => clearTimeout(timer);
3077
+ }, [launching, d.lastLaunched, d.errors]);
3078
+ const launch = () => {
3079
+ if (launching) return;
3080
+ if (missing) {
3081
+ toast({ text: t.launch.missing[missing], tone: "info" });
3082
+ setField(missing === "prompt" ? "prompt" : missing === "machine" ? "machine" : "repos");
3083
+ return;
3084
+ }
3085
+ setLaunching({ seq: d.lastLaunched?.seq ?? 0, errorSeq: d.errors.at(-1)?.seq ?? 0, at: Date.now(), name: shownName });
3086
+ client.send(launchMessage(withFolder, shownName));
3087
+ };
3088
+ useInput4((input, key) => {
3089
+ if (key.escape) {
3090
+ go("overview");
3091
+ return;
3092
+ }
3093
+ if (key.ctrl && input === "s") {
3094
+ launch();
3095
+ return;
3096
+ }
3097
+ if (key.ctrl && input === "r") {
3098
+ setName(pickName([...d.sessions.map((s) => s.name), shownName]));
3099
+ return;
3100
+ }
3101
+ if (key.tab || key.downArrow || key.return && field !== "name") {
3102
+ setField((f) => cycle(FIELDS, f, key.shift ? -1 : 1));
3103
+ return;
3104
+ }
3105
+ if (key.upArrow) {
3106
+ setField((f) => cycle(FIELDS, f, -1));
3107
+ return;
3108
+ }
3109
+ if (key.return) {
3110
+ launch();
3111
+ return;
3112
+ }
3113
+ if (TEXT.has(field)) return;
3114
+ const by = key.rightArrow || input === "l" ? 1 : key.leftArrow || input === "h" ? -1 : 0;
3115
+ if (field === "repos") {
3116
+ if (by) setRepoAt((i) => repoOptions.length ? (i + by + repoOptions.length) % repoOptions.length : 0);
3117
+ else if (input === " " && repoOptions[repoAt]) setForm((f) => toggleRepo(f, repoOptions[repoAt]));
3118
+ return;
3119
+ }
3120
+ if (!by) return;
3121
+ if (field === "tool") setForm((f) => withTool(f, cycle(tools, f.tool, by)));
3122
+ else if (field === "model") setForm((f) => withPick(f, { model: cycle(modelIds, f.model, by), effort: null }));
3123
+ else if (field === "effort") setForm((f) => withPick(f, { model: f.model, effort: cycle(efforts, f.effort, by) }));
3124
+ else if (field === "project") {
3125
+ const ids = [...list.map((p) => p.id), null];
3126
+ setForm((f) => {
3127
+ const next2 = cycle(ids, f.project, by);
3128
+ return next2 === null ? { ...f, project: null, repos: [] } : withProject(f, list, next2);
3129
+ });
3130
+ setRepoAt(0);
3131
+ } else if (field === "machine") {
3132
+ const up = machines.filter((m) => m.ready).map((m) => m.id);
3133
+ if (up.length) setForm((f) => withMachine(f, list, cycle(up, f.machine ?? up[0], by)));
3134
+ setRepoAt(0);
3135
+ }
3136
+ });
3137
+ const row = (f, label, value) => /* @__PURE__ */ jsxs10(Box11, { backgroundColor: field === f ? C.sel : void 0, children: [
3138
+ /* @__PURE__ */ jsx13(Text12, { color: field === f ? C.coral : C.faint, children: field === f ? "\u258C" : " " }),
3139
+ /* @__PURE__ */ jsx13(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: label }) }),
3140
+ /* @__PURE__ */ jsx13(Box11, { flexGrow: 1, overflow: "hidden", children: value })
3141
+ ] }, f);
3142
+ const choice = (items, cur, label, color) => /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: items.map((x, i) => /* @__PURE__ */ jsxs10(Text12, { children: [
3143
+ x === cur ? /* @__PURE__ */ jsx13(Text12, { color: C.fg, bold: true, children: `${g.prev} ${label(x)} ${g.next}` }) : /* @__PURE__ */ jsx13(Text12, { color: color?.(x) ?? C.dim, children: label(x) }),
3144
+ " "
3145
+ ] }, i)) });
3146
+ return /* @__PURE__ */ jsxs10(Frame, { crumb: t.launch.title, keys: [["tab", t.keys.nextField], ["\u2190 \u2192", t.keys.choose], [t.keys.space, t.keys.tick], ["ctrl+s", t.keys.start], ["ctrl+r", t.keys.redraw], ["esc", t.keys.cancel]], children: [
3147
+ /* @__PURE__ */ jsxs10(TitledBox, { title: t.launch.title, flexGrow: 1, children: [
3148
+ row("prompt", t.launch.instruction, /* @__PURE__ */ jsx13(TextInput, { value: form.prompt, onChange: (v) => setForm((f) => ({ ...f, prompt: v })), active: field === "prompt" })),
3149
+ /* @__PURE__ */ jsx13(Text12, { children: " " }),
3150
+ row("tool", t.launch.agent, /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: tools.map((x) => /* @__PURE__ */ jsxs10(Text12, { color: x === form.tool ? C.coral : C.dim, children: [
3151
+ x === form.tool ? g.radioOn : g.radioOff,
3152
+ " ",
3153
+ TOOL_NAME[x],
3154
+ " "
3155
+ ] }, x)) })),
3156
+ row("model", t.launch.model, models.status === "loading" ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.loading }) : choice(modelIds, form.model, (x) => x === null ? t.launch.defaultModel : models.models.find((m) => m.id === x)?.label ?? x)),
3157
+ row("effort", t.launch.effort, efforts.length > 1 ? choice(efforts, form.effort, (x) => x ?? t.launch.defaultModel) : /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.defaultModel })),
3158
+ row("project", t.launch.project, choice([...list.map((p) => p.id), null], form.project, (x) => x === null ? t.launch.none : list.find((p) => p.id === x).name, (x) => projectColor(list.find((p) => p.id === x)?.color))),
3159
+ row("machine", t.launch.machine, /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: machines.map((m) => /* @__PURE__ */ jsxs10(Text12, { color: m.id === form.machine ? C.fg : C.dim, bold: m.id === form.machine, children: [
3160
+ /* @__PURE__ */ jsx13(Text12, { color: m.ready ? C.green : C.faint, children: g.machine }),
3161
+ " ",
3162
+ m.label,
3163
+ " "
3164
+ ] }, m.id)) })),
3165
+ row("repos", t.launch.repos, repoOptions.length === 0 ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.none }) : /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: repoOptions.map((r, i) => /* @__PURE__ */ jsxs10(Text12, { backgroundColor: field === "repos" && i === repoAt ? "#3a3532" : void 0, color: form.repos.includes(r) ? C.fg : C.dim, children: [
3166
+ /* @__PURE__ */ jsx13(Text12, { color: form.repos.includes(r) ? C.green : C.faint, children: form.repos.includes(r) ? g.boxOn : g.boxOff }),
3167
+ " ",
3168
+ r.split("/").filter(Boolean).at(-1) ?? r,
3169
+ " ",
3170
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: r }),
3171
+ " "
3172
+ ] }, r)) })),
3173
+ row("folder", t.launch.folder, /* @__PURE__ */ jsx13(TextInput, { value: folder, onChange: setFolder, active: field === "folder", placeholder: t.launch.folderHint })),
3174
+ row("name", t.launch.name, /* @__PURE__ */ jsxs10(Text12, { children: [
3175
+ shownName,
3176
+ " ",
3177
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.drawn })
3178
+ ] }))
3179
+ ] }),
3180
+ /* @__PURE__ */ jsxs10(TitledBox, { title: t.launch.preview, color: C.coral, titleColor: C.coral, children: [
3181
+ /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3182
+ /* @__PURE__ */ jsx13(Text12, { color: C.fg, bold: true, children: project ? t.launch.joins(shownName, project.name) : shownName }),
3183
+ /* @__PURE__ */ jsxs10(Text12, { color: C.faint, children: [
3184
+ " \xB7 ",
3185
+ machine?.label ?? "-",
3186
+ " \xB7 ",
3187
+ TOOL_NAME[form.tool],
3188
+ " \xB7 ",
3189
+ form.model ?? t.launch.defaultModel,
3190
+ form.effort ? ` \xB7 ${form.effort}` : ""
3191
+ ] })
3192
+ ] }),
3193
+ /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, columnGap: 2, children: [
3194
+ /* @__PURE__ */ jsx13(Button, { label: launching ? t.launch.launching : t.launch.launch, k: "ctrl+s", primary: true }),
3195
+ /* @__PURE__ */ jsx13(Button, { label: t.launch.cancel, k: "esc" }),
3196
+ missing ? /* @__PURE__ */ jsx13(Text12, { color: C.yellow, children: t.launch.missing[missing] }) : /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.thenAttach })
3197
+ ] })
3198
+ ] })
3199
+ ] });
3200
+ }
3201
+
3202
+ // src/tui/screens/Settings.tsx
3203
+ import { useState as useState7 } from "react";
3204
+ import { Box as Box12, Text as Text13 } from "ink";
3205
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3206
+ function Settings() {
3207
+ const { t, g, lang, setLang } = useTui();
3208
+ const rows = [{ id: "lang", label: t.settings.language }];
3209
+ const [at, setAt] = useState7(0);
3210
+ useScreenKeys((input, key) => {
3211
+ if (key.upArrow || input === "k") {
3212
+ setAt((i) => (i - 1 + rows.length) % rows.length);
3213
+ return true;
3214
+ }
3215
+ if (key.downArrow || input === "j") {
3216
+ setAt((i) => (i + 1) % rows.length);
3217
+ return true;
3218
+ }
3219
+ const by = key.rightArrow || input === "l" || input === " " || key.return ? 1 : key.leftArrow || input === "h" ? -1 : 0;
3220
+ if (!by) return false;
3221
+ if (rows[at].id === "lang") setLang(LANGS[(LANGS.indexOf(lang) + by + LANGS.length) % LANGS.length]);
3222
+ return true;
3223
+ });
3224
+ return /* @__PURE__ */ jsx14(Frame, { crumb: t.settings.title, keys: [["\u2191 \u2193", t.keys.navigate], ["\u2190 \u2192", t.keys.change], ["esc", t.keys.back]], children: /* @__PURE__ */ jsx14(TitledBox, { title: t.settings.title, flexGrow: 1, children: rows.map((r, i) => /* @__PURE__ */ jsxs11(Box12, { backgroundColor: i === at ? C.sel : void 0, children: [
3225
+ /* @__PURE__ */ jsx14(Text13, { color: i === at ? C.coral : C.faint, children: i === at ? "\u258C" : " " }),
3226
+ /* @__PURE__ */ jsx14(Box12, { width: 16, children: /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: r.label }) }),
3227
+ /* @__PURE__ */ jsx14(Text13, { children: LANGS.map((l) => /* @__PURE__ */ jsxs11(Text13, { color: l === lang ? C.fg : C.dim, bold: l === lang, children: [
3228
+ l === lang ? `${g.radioOn} ` : `${g.radioOff} `,
3229
+ t.lang[l],
3230
+ " "
3231
+ ] }, l)) })
3232
+ ] }, r.id)) }) });
3233
+ }
3234
+
3235
+ // src/tui/screens/Help.tsx
3236
+ import { Box as Box13, Text as Text14 } from "ink";
3237
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3238
+ function Help() {
3239
+ const { t } = useTui();
3240
+ const k = t.keys;
3241
+ const groups = [
3242
+ [t.help.global, [["1\u20265", t.help.screens], ["/", k.ask], ["+", k.launch], [",", k.settings], ["?", k.help], ["esc", k.back], ["q", k.quit]]],
3243
+ [t.help.overview, [["j k \u2191 \u2193", k.navigate], ["tab", k.next], ["\u21B5", k.open], ["y n A", k.answer], ["1\u20269", k.answer], ["r", t.overview.denyWithNote], ["a", k.attach], ["i", k.interrupt]]],
3244
+ [t.help.session, [["y n A 1\u20269", k.answer], ["r \u21B5", k.write], ["j k PgUp PgDn", k.scroll], [t.keys.space, k.unfold], ["d", k.diffs], ["t", k.terminal], ["a", k.attach], ["m", k.model], ["i", k.interrupt]]],
3245
+ [t.help.ask, [["\u21B5", k.send], ["tab", k.nextCard], ["ctrl+n", k.newChat], ["ctrl+c", k.stopAnswer]]],
3246
+ [t.help.launch, [["tab \u2191 \u2193", k.nextField], ["\u2190 \u2192", k.choose], [t.keys.space, k.tick], ["ctrl+r", t.keys.redraw], ["ctrl+s", k.start], ["esc", k.cancel]]],
3247
+ [t.help.settings, [["\u2191 \u2193", k.navigate], ["\u2190 \u2192", k.change]]]
3248
+ ];
3249
+ return /* @__PURE__ */ jsx15(Frame, { crumb: t.help.title, keys: [["esc", k.back]], children: /* @__PURE__ */ jsx15(TitledBox, { title: t.help.title, flexGrow: 1, children: /* @__PURE__ */ jsx15(Box13, { flexWrap: "wrap", columnGap: 4, children: groups.map(([title2, keys]) => /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", width: 40, marginBottom: 1, children: [
3250
+ /* @__PURE__ */ jsx15(Text14, { color: C.coral, bold: true, children: title2 }),
3251
+ keys.map(([key, label]) => /* @__PURE__ */ jsxs12(Text14, { wrap: "truncate", children: [
3252
+ /* @__PURE__ */ jsx15(Text14, { color: C.fg, children: key.padEnd(16) }),
3253
+ /* @__PURE__ */ jsx15(Text14, { color: C.dim, children: label })
3254
+ ] }, key + label))
3255
+ ] }, title2)) }) }) });
3256
+ }
3257
+
3258
+ // src/tui/App.tsx
3259
+ import { jsx as jsx16 } from "react/jsx-runtime";
3260
+ var TOAST_MS = 5e3;
3261
+ function App(props) {
3262
+ const [lang, setLangState] = useState8(props.lang);
3263
+ return /* @__PURE__ */ jsx16(I18nProvider, { lang, children: /* @__PURE__ */ jsx16(Shell, { ...props, lang, setLang: setLangState }) });
3264
+ }
3265
+ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs, env, run }) {
3266
+ const { exit, suspendTerminal } = useApp();
3267
+ const { stdout } = useStdout();
3268
+ const write = useCallback((seq) => {
3269
+ if (seq) stdout.write(seq);
3270
+ }, [stdout]);
3271
+ const d = useDaemon();
3272
+ const machines = useMachines(client);
3273
+ const projects = useProjects(client);
3274
+ const fleet = client.fleet;
3275
+ const t = MESSAGES[lang];
3276
+ const [screen, setScreen] = useState8("overview");
3277
+ const [back, setBack] = useState8("overview");
3278
+ const [selectedId, setSelectedId] = useState8(null);
3279
+ const [lastToast, setToast] = useState8(null);
3280
+ const [editing, setEditingState] = useState8(false);
3281
+ const [now, setNow] = useState8(() => Date.now());
3282
+ const keys = useRef8(null);
3283
+ useEffect9(() => {
3284
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
3285
+ return () => clearInterval(timer);
3286
+ }, []);
3287
+ useEffect9(() => {
3288
+ if (!lastToast) return;
3289
+ const timer = setTimeout(() => setToast(null), TOAST_MS);
3290
+ return () => clearTimeout(timer);
3291
+ }, [lastToast]);
3292
+ const toast = useCallback((x) => setToast(x), []);
3293
+ const setEditing = useCallback((on) => setEditingState(on), []);
3294
+ const view = useMemo6(() => overview(d.sessions, projects), [d.sessions, projects]);
3295
+ const order = useRef8([]);
3296
+ useEffect9(() => {
3297
+ setSelectedId((cur) => cur === null && view.yours[0] ? view.yours[0].id : keepSelection(view.order, cur, order.current));
3298
+ order.current = view.order;
3299
+ }, [view]);
3300
+ const selected = (selectedId ? d.byId.get(selectedId) : void 0) ?? null;
3301
+ const seen = useRef8(null);
3302
+ useEffect9(() => {
3303
+ if (seen.current) for (const text of asked(seen.current, d.sessions, t.notify)) write(notification(text, notify, env));
3304
+ seen.current = d.byId;
3305
+ write(title(t.notify.title(listed(d.sessions).filter(isPending).length)));
3306
+ }, [d.byId]);
3307
+ const actions = useActions({ client, sessions: d.sessions, errors: d.errors, t, toast });
3308
+ const go = useCallback((next2) => {
3309
+ setScreen((cur) => {
3310
+ if (next2 === "help" || next2 === "settings") setBack(cur === "help" || cur === "settings" ? "overview" : cur);
3311
+ return next2;
3312
+ });
3313
+ }, []);
3314
+ const setLang = useCallback((next2) => {
3315
+ setLangState(next2);
3316
+ try {
3317
+ saveLang(next2);
3318
+ toast({ text: MESSAGES[next2].settings.saved, tone: "ok" });
3319
+ } catch (e) {
3320
+ toast({ text: MESSAGES[next2].settings.saveFailed(e.message), tone: "error" });
3321
+ }
3322
+ }, [saveLang, setLangState, toast]);
3323
+ const attach = useCallback((s) => {
3324
+ const cmd = attachCommand(fleet, s);
3325
+ if ("why" in cmd) {
3326
+ toast({ text: cmd.why === "offline" ? t.toast.offline(s.name) : cmd.why === "omp" ? t.toast.noTerminalOmp(s.name) : t.toast.noTerminal(s.name), tone: "info" });
3327
+ return;
3328
+ }
3329
+ void suspendTerminal(async () => {
3330
+ const code = await (run ?? runAttached)(cmd.argv);
3331
+ if (code !== 0) toast({ text: t.toast.attachFailed(code), tone: "error" });
3332
+ });
3333
+ }, [fleet, suspendTerminal, run, t, toast]);
3334
+ const tui = {
3335
+ client,
3336
+ fleet,
3337
+ d,
3338
+ machines,
3339
+ projects,
3340
+ t,
3341
+ lang,
3342
+ setLang,
3343
+ g: glyphs,
3344
+ now,
3345
+ screen,
3346
+ go,
3347
+ selected,
3348
+ select: setSelectedId,
3349
+ toast,
3350
+ lastToast,
3351
+ actions,
3352
+ setEditing,
3353
+ attach,
3354
+ keys
3355
+ };
3356
+ useInput5((input, key) => {
3357
+ if (key.ctrl && input === "c") {
3358
+ if (keys.current?.(input, key)) return;
3359
+ exit();
3360
+ return;
3361
+ }
3362
+ if (editing) return;
3363
+ if (keys.current?.(input, key)) return;
3364
+ if (input === "q") {
3365
+ exit();
3366
+ return;
3367
+ }
3368
+ if (key.escape) {
3369
+ go(screen === "help" || screen === "settings" ? back : "overview");
3370
+ return;
3371
+ }
3372
+ if (input === "?") {
3373
+ go("help");
3374
+ return;
3375
+ }
3376
+ if (input === ",") {
3377
+ go("settings");
3378
+ return;
3379
+ }
3380
+ if (input === "/") {
3381
+ go("ask");
3382
+ return;
3383
+ }
3384
+ if (input === "+") {
3385
+ go("launch");
3386
+ return;
3387
+ }
3388
+ if (/^[1-5]$/.test(input)) {
3389
+ go(SCREENS[Number(input) - 1]);
3390
+ return;
3391
+ }
3392
+ });
3393
+ const Body = { overview: Overview, session: SessionScreen, ask: Ask, launch: Launch, settings: Settings, help: Help }[screen];
3394
+ return /* @__PURE__ */ jsx16(TuiContext.Provider, { value: tui, children: /* @__PURE__ */ jsx16(Body, {}) });
3395
+ }
3396
+
3397
+ // src/tui/fleet.ts
3398
+ import WebSocket from "ws";
3399
+ async function readToken() {
3400
+ try {
3401
+ const token = readDaemonToken();
3402
+ return token ? { kind: "token", token } : { kind: "absent" };
3403
+ } catch {
3404
+ return { kind: "unavailable" };
3405
+ }
3406
+ }
3407
+ function createTuiFleet(opts = {}) {
3408
+ const local = LOCAL_MACHINE.url, port = opts.port ?? PROTOCOL_PORT;
3409
+ const backoff = { ...opts.minBackoffMs ? { minBackoffMs: opts.minBackoffMs } : {}, ...opts.maxBackoffMs ? { maxBackoffMs: opts.maxBackoffMs } : {} };
3410
+ return createFleetClient(
3411
+ [LOCAL_MACHINE],
3412
+ (c) => createDaemonClient({ url: c.url.replace(local, `ws://127.0.0.1:${port}`), WebSocketImpl: WebSocket, token: c.token, ...backoff }),
3413
+ { prefix: "tui", keepStale: true, machinesFromDaemon: true, ...opts.token === null ? {} : { token: opts.token ?? createTokenSource(readToken) } }
3414
+ );
3415
+ }
3416
+
3417
+ // src/tui/glyphs.ts
3418
+ var UNICODE = {
3419
+ approval: "\u25C6",
3420
+ question: "?",
3421
+ working: "\u25CF",
3422
+ idle: "\u25CB",
3423
+ starting: "\u25CC",
3424
+ done: "\xB7",
3425
+ error: "\u2717",
3426
+ readonly: "\u25CB",
3427
+ branch: "\u2387",
3428
+ helper: "\u21B3",
3429
+ message: "\u2709",
3430
+ arrow: "\u2192",
3431
+ ok: "\u2713",
3432
+ fail: "\u2717",
3433
+ live: "\u25CC",
3434
+ folded: "\u25B8",
3435
+ unfolded: "\u25BE",
3436
+ project: "\u25A0",
3437
+ machine: "\u25CF",
3438
+ logo: "\u25D6",
3439
+ cursor: "\u276F",
3440
+ radioOn: "\u25C9",
3441
+ radioOff: "\u25CB",
3442
+ boxOn: "\u2713",
3443
+ boxOff: "\u2610",
3444
+ prev: "\u25C2",
3445
+ next: "\u25B8",
3446
+ ellipsis: "\u2026",
3447
+ bar: "\u2502"
3448
+ };
3449
+ var ASCII = {
3450
+ approval: "!",
3451
+ question: "?",
3452
+ working: "*",
3453
+ idle: "o",
3454
+ starting: ".",
3455
+ done: "-",
3456
+ error: "x",
3457
+ readonly: "o",
3458
+ branch: "git:",
3459
+ helper: ">",
3460
+ message: "@",
3461
+ arrow: "->",
3462
+ ok: "v",
3463
+ fail: "x",
3464
+ live: "~",
3465
+ folded: "+",
3466
+ unfolded: "-",
3467
+ project: "#",
3468
+ machine: "*",
3469
+ logo: "(",
3470
+ cursor: ">",
3471
+ radioOn: "(*)",
3472
+ radioOff: "( )",
3473
+ boxOn: "[x]",
3474
+ boxOff: "[ ]",
3475
+ prev: "<",
3476
+ next: ">",
3477
+ ellipsis: "...",
3478
+ bar: "|"
3479
+ };
3480
+ function glyphsFor(env) {
3481
+ if (env.LEDGE_TUI_ASCII === "1") return ASCII;
3482
+ const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
3483
+ return locale && !/utf-?8/i.test(locale) && locale !== "C.UTF-8" ? ASCII : UNICODE;
3484
+ }
3485
+
3486
+ // src/tui/settings.ts
3487
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
3488
+ import { join } from "path";
3489
+ import { z } from "zod";
3490
+ var SettingsSchema = z.object({ lang: z.enum(["fr", "en"]).optional() }).passthrough();
3491
+ var settingsPath = () => join(ledgeDir(), "tui.json");
3492
+ function defaultLang(env) {
3493
+ const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || "";
3494
+ return /^fr/i.test(locale) ? "fr" : "en";
3495
+ }
3496
+ function loadSettings() {
3497
+ try {
3498
+ return SettingsSchema.parse(JSON.parse(readFileSync(settingsPath(), "utf8")));
3499
+ } catch {
3500
+ return {};
3501
+ }
3502
+ }
3503
+ function saveSettings(settings) {
3504
+ const path = settingsPath(), tmp = `${path}.${process.pid}.tmp`;
3505
+ mkdirSync(ledgeDir(), { recursive: true });
3506
+ writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}
3507
+ `, { mode: 384 });
3508
+ renameSync(tmp, path);
3509
+ }
3510
+
3511
+ // src/tui/index.tsx
3512
+ import { jsx as jsx17 } from "react/jsx-runtime";
3513
+ var TUI_USAGE = "ledge tui [--notify bell|osc9|off]";
3514
+ function tuiArgs(args) {
3515
+ let notify = "bell";
3516
+ for (let i = 0; i < args.length; i++) {
3517
+ const a = args[i];
3518
+ const value = a.startsWith("--notify=") ? a.slice("--notify=".length) : a === "--notify" ? args[++i] : void 0;
3519
+ if (value === void 0 || !NOTIFY_MODES.includes(value)) return null;
3520
+ notify = value;
3521
+ }
3522
+ return { notify };
3523
+ }
3524
+ async function runTui(args) {
3525
+ const parsed = tuiArgs(args);
3526
+ if (!parsed) {
3527
+ console.error(`usage: ${TUI_USAGE}`);
3528
+ return 2;
3529
+ }
3530
+ const settings = loadSettings();
3531
+ const lang = settings.lang ?? defaultLang(process.env);
3532
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3533
+ console.error(MESSAGES[lang].cli.notTty);
3534
+ return 2;
3535
+ }
3536
+ const client = createTuiFleet();
3537
+ const detach = attachClient(client);
3538
+ client.connect();
3539
+ const app = render(
3540
+ /* @__PURE__ */ jsx17(App, { client, lang, saveLang: (next2) => saveSettings({ ...loadSettings(), lang: next2 }), notify: parsed.notify, glyphs: glyphsFor(process.env), env: process.env }),
3541
+ { alternateScreen: true, exitOnCtrlC: false, maxFps: 30 }
3542
+ );
3543
+ const stop = () => app.unmount();
3544
+ process.once("SIGTERM", stop);
3545
+ process.once("SIGHUP", stop);
3546
+ try {
3547
+ await app.waitUntilExit();
3548
+ } finally {
3549
+ process.off("SIGTERM", stop);
3550
+ process.off("SIGHUP", stop);
3551
+ detach();
3552
+ client.close();
3553
+ process.stdout.write("\x1B]0;\x07");
3554
+ }
3555
+ return 0;
3556
+ }
3557
+ export {
3558
+ TUI_USAGE,
3559
+ runTui,
3560
+ tuiArgs
3561
+ };