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