@paradow/ledge 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/{attach-3BKQSEWC.js → attach-JNECAG6L.js} +11 -11
  2. package/dist/cli/{bridge-APNFBPNE.js → bridge-H3ZPGIUY.js} +2 -2
  3. package/dist/cli/{chunk-3HCID6NW.js → chunk-24AHC55Y.js} +3 -3
  4. package/dist/cli/{chunk-3EIRKAX7.js → chunk-2VSEIOGA.js} +4 -4
  5. package/dist/cli/{chunk-TRTS24PF.js → chunk-35BPMR6P.js} +2 -2
  6. package/dist/cli/{chunk-43D6NMYH.js → chunk-6IYPRUQG.js} +1 -1
  7. package/dist/cli/{chunk-ZUYOA55W.js → chunk-AJFC33TH.js} +2 -2
  8. package/dist/cli/{chunk-LOOOHYZR.js → chunk-CDME3WRV.js} +8 -8
  9. package/dist/cli/{chunk-BVBX222F.js → chunk-ERN736NQ.js} +3 -3
  10. package/dist/cli/{chunk-HP3G2TYJ.js → chunk-EX66KH66.js} +730 -646
  11. package/dist/cli/{chunk-7ZAA75HG.js → chunk-HOOHS3MX.js} +1 -1
  12. package/dist/cli/{chunk-FQZ3IX6E.js → chunk-K7UEKSB2.js} +2 -2
  13. package/dist/cli/{chunk-GXFU4XAX.js → chunk-KACRWFN3.js} +1 -1
  14. package/dist/cli/{chunk-WJ6YMZTT.js → chunk-NLL2EQJ2.js} +2 -2
  15. package/dist/cli/{chunk-WP77O5LC.js → chunk-NVSBPIXY.js} +844 -404
  16. package/dist/cli/chunk-PFBZUINN.js +6 -0
  17. package/dist/cli/{chunk-KKNE7BFZ.js → chunk-XR42PIDL.js} +22 -3
  18. package/dist/cli/{chunk-WCS24ICX.js → chunk-ZQON4EV7.js} +2 -2
  19. package/dist/cli/{chunk-Y5DJ5INP.js → chunk-ZZMZDAF6.js} +3 -3
  20. package/dist/cli/{daemon-66EUIZ3J.js → daemon-DVPDCY3V.js} +87 -25
  21. package/dist/cli/{handover-UXISKYPS.js → handover-AZ35QZRK.js} +5 -5
  22. package/dist/cli/{hook-5UAARBWU.js → hook-ERYA6BKY.js} +20 -13
  23. package/dist/cli/index.js +16 -16
  24. package/dist/cli/launch-LAER5DI6.js +34 -0
  25. package/dist/cli/{send-NLVR6PL2.js → send-BFPJBXXH.js} +5 -5
  26. package/dist/cli/{setup-EEIXRAMU.js → setup-47ODKJBM.js} +4 -4
  27. package/dist/cli/{status-2OCYHOWZ.js → status-JPF4NZNI.js} +3 -3
  28. package/dist/cli/{task-AOOWHPMN.js → task-K43WJIUX.js} +5 -5
  29. package/dist/cli/{team-TVNSVYWW.js → team-SZJHGA7D.js} +6 -6
  30. package/dist/cli/{tui-6JY7IS4D.js → tui-AMERXSL7.js} +825 -373
  31. package/dist/cli/{uninstall-UUN7LIIT.js → uninstall-QNWRD27D.js} +4 -4
  32. package/dist/cli/{version-XSEI3V2C.js → version-BFNXT3H4.js} +1 -1
  33. package/dist/cli/who-NYPVBO64.js +11 -0
  34. package/package.json +1 -1
  35. package/dist/cli/chunk-4RD7OJ2J.js +0 -6
  36. package/dist/cli/launch-YQYCDZ7N.js +0 -34
  37. package/dist/cli/who-52MENZTT.js +0 -11
@@ -1,14 +1,20 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-LOOOHYZR.js";
4
- import "./chunk-WP77O5LC.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";
3
+ } from "./chunk-CDME3WRV.js";
11
4
  import {
5
+ imageInfo
6
+ } from "./chunk-NVSBPIXY.js";
7
+ import "./chunk-2VSEIOGA.js";
8
+ import {
9
+ MAX_FRAME
10
+ } from "./chunk-OLLQBK6N.js";
11
+ import "./chunk-XR42PIDL.js";
12
+ import "./chunk-24AHC55Y.js";
13
+ import "./chunk-35BPMR6P.js";
14
+ import "./chunk-KACRWFN3.js";
15
+ import {
16
+ ATTACHMENTS_MAX,
17
+ ATTACHMENT_LOCAL_MAX,
12
18
  CONTEXT_NEAR,
13
19
  CONTEXT_NEAR_MAX,
14
20
  CONTEXT_NEAR_MIN,
@@ -19,6 +25,7 @@ import {
19
25
  MachinesSchema,
20
26
  NOTE_MAX,
21
27
  NOTE_NOT_TYPED,
28
+ PREVIEW_MAX,
22
29
  PROJECT_COLORS,
23
30
  PROTOCOL_PORT,
24
31
  ProjectsSchema,
@@ -32,13 +39,14 @@ import {
32
39
  projectOf,
33
40
  proposalRows,
34
41
  readDaemonToken,
35
- taskClosed
36
- } from "./chunk-HP3G2TYJ.js";
37
- import "./chunk-WJ6YMZTT.js";
38
- import "./chunk-43D6NMYH.js";
42
+ taskClosed,
43
+ transmissions
44
+ } from "./chunk-EX66KH66.js";
45
+ import "./chunk-NLL2EQJ2.js";
46
+ import "./chunk-6IYPRUQG.js";
39
47
  import {
40
48
  ledgeDir
41
- } from "./chunk-7ZAA75HG.js";
49
+ } from "./chunk-HOOHS3MX.js";
42
50
 
43
51
  // src/tui/index.tsx
44
52
  import { render } from "ink";
@@ -480,6 +488,7 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
480
488
  const inspections = /* @__PURE__ */ new Map();
481
489
  const listings = /* @__PURE__ */ new Map();
482
490
  const scans = /* @__PURE__ */ new Map();
491
+ const attachmentAsks = /* @__PURE__ */ new Map();
483
492
  const modelLists = /* @__PURE__ */ new Map();
484
493
  const operations = /* @__PURE__ */ new Map();
485
494
  const closeRows = /* @__PURE__ */ new Map();
@@ -527,7 +536,12 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
527
536
  let listSeq = 0;
528
537
  const local = (id) => id === LEDGE_GIVER ? id : qualify("local", id);
529
538
  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) });
539
+ const taskOut = (t) => ({
540
+ ...t,
541
+ giver: local(t.giver),
542
+ owner: local(t.owner),
543
+ log: t.log.map((e) => ({ ...e, ...e.from ? { from: local(e.from) } : {}, ...e.to ? { to: local(e.to) } : {} }))
544
+ });
531
545
  const putTasks = (list) => {
532
546
  for (const t of list) tasks.set(t.id, taskOut(t));
533
547
  taskList = [...tasks.values()].sort((a, b) => a.at.localeCompare(b.at));
@@ -817,6 +831,13 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
817
831
  }
818
832
  return;
819
833
  }
834
+ if (m.type === "attachment.stored" || m.type === "attachment.data" || m.type === "attachment.error") {
835
+ if (attachmentAsks.get(m.requestId) === machine) {
836
+ attachmentAsks.delete(m.requestId);
837
+ emit2(m);
838
+ }
839
+ return;
840
+ }
820
841
  if (m.type === "repos.found") {
821
842
  if (scans.get(m.requestId) === machine) {
822
843
  scans.delete(m.requestId);
@@ -846,7 +867,7 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
846
867
  fail(req.qid, `${target2?.config.label ?? m.result.machineId} is offline`);
847
868
  return;
848
869
  }
849
- target2.client.send({ type: "chat.ask", qid: req.qid, text: m.result.text, tool: req.tool });
870
+ target2.client.send({ type: "chat.ask", qid: req.qid, text: m.result.text, tool: req.tool, ...req.images ? { images: req.images } : {} });
850
871
  }
851
872
  return;
852
873
  }
@@ -935,6 +956,10 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
935
956
  p.fresh = false;
936
957
  const op = operations.get(c.id), closing = closeRows.has(c.id);
937
958
  if (endOperation(c.id)) emit2({ type: "error", message: `${c.label} is offline`, ...op === "launch" ? { request: "session.launch" } : closing ? { request: "session.close" } : {} });
959
+ for (const [requestId, owner] of attachmentAsks) if (owner === c.id) {
960
+ attachmentAsks.delete(requestId);
961
+ emit2({ type: "attachment.error", requestId, reason: "offline" });
962
+ }
938
963
  answerUnfoundInspections(c.id);
939
964
  answerUnfoundListings(c.id, "offline");
940
965
  answerFailedScans(c.id, "offline");
@@ -1019,7 +1044,7 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1019
1044
  }
1020
1045
  const coordinator = [...peers.values()].find((p) => available(p) && p.snapshot?.daemon?.routing);
1021
1046
  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 };
1047
+ routing = { qid: msg.qid, text: msg.text, tool: msg.tool, ...msg.images ? { images: msg.images } : {}, coordinator: coordinator.config.id };
1023
1048
  coordinator.client.send({ type: "chat.route", qid: msg.qid, text: msg.text, tool: msg.tool, context });
1024
1049
  return;
1025
1050
  }
@@ -1031,10 +1056,11 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1031
1056
  }
1032
1057
  let machine = selected;
1033
1058
  let outgoing = msg;
1034
- if ("id" in msg) {
1059
+ if ("id" in msg && msg.id !== void 0) {
1035
1060
  const origin = splitId(msg.id);
1036
1061
  if (!origin) {
1037
- emit2({ type: "error", id: msg.id, message: "Unknown machine", request: msg.type });
1062
+ if (msg.type === "attachment.put" || msg.type === "attachment.get") emit2({ type: "attachment.error", requestId: msg.requestId, reason: "offline" });
1063
+ else emit2({ type: "error", id: msg.id, message: "Unknown machine", request: msg.type });
1038
1064
  return;
1039
1065
  }
1040
1066
  machine = origin.machine;
@@ -1049,6 +1075,18 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1049
1075
  machine = b.machine;
1050
1076
  outgoing = { ...outgoing, qid: b.raw };
1051
1077
  }
1078
+ if (msg.type === "attachment.put" || msg.type === "attachment.get") {
1079
+ if (msg.id === void 0) machine = msg.type === "attachment.get" && msg.machine || LOCAL_MACHINE.id;
1080
+ if (outgoing.type === "attachment.get") {
1081
+ const { machine: _m, ...rest } = outgoing;
1082
+ outgoing = rest;
1083
+ }
1084
+ if (!available(peers.get(machine))) {
1085
+ emit2({ type: "attachment.error", requestId: msg.requestId, reason: "offline" });
1086
+ return;
1087
+ }
1088
+ attachmentAsks.set(msg.requestId, machine);
1089
+ }
1052
1090
  if (msg.type === "document.read") {
1053
1091
  if (configs.length > 1 && !msg.machineId && !msg.sessionId) {
1054
1092
  emit2({ type: "document.error", requestId: msg.requestId, path: msg.path, code: "read_failed" });
@@ -1284,15 +1322,68 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1284
1322
  // ../fleet/src/proposals.ts
1285
1323
  var COLORS = Object.keys(PROJECT_COLORS);
1286
1324
 
1325
+ // ../fleet/src/attachments.ts
1326
+ import { useEffect, useState } from "react";
1327
+ var AttachmentFailure = class extends Error {
1328
+ constructor(reason) {
1329
+ super(`attachment: ${reason}`);
1330
+ this.reason = reason;
1331
+ this.name = "AttachmentFailure";
1332
+ }
1333
+ reason;
1334
+ };
1335
+ var TIMEOUT_MS = 2e4;
1336
+ var counter = 0;
1337
+ var nextRequestId = () => `att-${Date.now().toString(36)}-${(counter++).toString(36)}`;
1338
+ function request(client, build, pick, timeoutMs) {
1339
+ const requestId = nextRequestId();
1340
+ return new Promise((resolve, reject) => {
1341
+ let timer;
1342
+ const off = client.onMessage((m) => {
1343
+ if (!("requestId" in m) || m.requestId !== requestId) return;
1344
+ if (m.type === "attachment.error") {
1345
+ done();
1346
+ reject(new AttachmentFailure(m.reason));
1347
+ return;
1348
+ }
1349
+ const value = pick(m);
1350
+ if (value !== void 0) {
1351
+ done();
1352
+ resolve(value);
1353
+ }
1354
+ });
1355
+ const done = () => {
1356
+ off();
1357
+ if (timer) clearTimeout(timer);
1358
+ };
1359
+ timer = setTimeout(() => {
1360
+ off();
1361
+ reject(new AttachmentFailure("timeout"));
1362
+ }, timeoutMs);
1363
+ client.send(build(requestId));
1364
+ });
1365
+ }
1366
+ function putImage(client, image, route = {}, timeoutMs = TIMEOUT_MS) {
1367
+ return request(client, (requestId) => ({ type: "attachment.put", requestId, image, ...route.id ? { id: route.id } : {} }), (m) => m.type === "attachment.stored" ? m.ref : void 0, timeoutMs);
1368
+ }
1369
+ function getImage(client, attachment, route = {}, preview = false, timeoutMs = TIMEOUT_MS) {
1370
+ return request(
1371
+ client,
1372
+ (requestId) => ({ type: "attachment.get", requestId, attachment, ...preview ? { preview: true } : {}, ...route.id ? { id: route.id } : {}, ...route.machine ? { machine: route.machine } : {} }),
1373
+ (m) => m.type === "attachment.data" ? { mime: m.mime, data: m.data } : void 0,
1374
+ timeoutMs
1375
+ );
1376
+ }
1377
+
1287
1378
  // 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";
1379
+ import { useCallback, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef9, useState as useState11 } from "react";
1380
+ import { useApp, useInput as useInput6, useStdout } from "ink";
1290
1381
 
1291
1382
  // src/tui/actions.ts
1292
- import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";
1383
+ import { useEffect as useEffect3, useMemo, useRef as useRef2 } from "react";
1293
1384
 
1294
1385
  // ../fleet/src/decisions.ts
1295
- import { useEffect, useRef, useState } from "react";
1386
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
1296
1387
  function requestKey(s) {
1297
1388
  const p = s.pending;
1298
1389
  return p ? `${p.kind}
@@ -1307,8 +1398,8 @@ function markOf(s) {
1307
1398
  function useDecisions(o) {
1308
1399
  const { sessions, errors } = o;
1309
1400
  const live = useRef(/* @__PURE__ */ new Map());
1310
- const [inFlight, setInFlight] = useState(() => /* @__PURE__ */ new Set());
1311
- const [landed, setLanded] = useState(null);
1401
+ const [inFlight, setInFlight] = useState2(() => /* @__PURE__ */ new Set());
1402
+ const [landed, setLanded] = useState2(null);
1312
1403
  const latest = useRef({ o });
1313
1404
  latest.current = { o };
1314
1405
  const seen = useRef(errors.at(-1)?.seq ?? 0);
@@ -1329,7 +1420,7 @@ function useDecisions(o) {
1329
1420
  }
1330
1421
  return x;
1331
1422
  };
1332
- useEffect(() => {
1423
+ useEffect2(() => {
1333
1424
  const byId = new Map(sessions.map((s) => [s.id, s]));
1334
1425
  for (const [id, x] of [...live.current]) {
1335
1426
  const s = byId.get(id);
@@ -1345,7 +1436,7 @@ function useDecisions(o) {
1345
1436
  }
1346
1437
  for (const id of [...settled.current.keys()]) if (!byId.has(id)) unsettle(id);
1347
1438
  }, [sessions]);
1348
- useEffect(() => {
1439
+ useEffect2(() => {
1349
1440
  const last = errors.at(-1)?.seq ?? 0;
1350
1441
  if (last < seen.current) seen.current = 0;
1351
1442
  for (const e of errors) {
@@ -1362,12 +1453,12 @@ function useDecisions(o) {
1362
1453
  if (x.text !== void 0) x.onRefused?.(x.text);
1363
1454
  }
1364
1455
  }, [errors]);
1365
- useEffect(() => {
1456
+ useEffect2(() => {
1366
1457
  if (!landed) return;
1367
1458
  const timer = setTimeout(() => setLanded(null), FLASH_MS);
1368
1459
  return () => clearTimeout(timer);
1369
1460
  }, [landed]);
1370
- useEffect(() => () => {
1461
+ useEffect2(() => () => {
1371
1462
  for (const x of [...live.current.values(), ...settled.current.values()]) clearTimeout(x.timer);
1372
1463
  live.current.clear();
1373
1464
  settled.current.clear();
@@ -1405,8 +1496,9 @@ function useDecisions(o) {
1405
1496
  return send(s, "session.answer", { type: "session.answer", id: s.id, requestId: p.requestId, answers: [...answers], ...cancel ? { cancel: true } : {} });
1406
1497
  },
1407
1498
  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 });
1499
+ const attachments = how?.attachments?.length ? how.attachments : void 0;
1500
+ if (s.offline || s.source === "hook_only" || !text.trim() && !attachments || live.current.has(s.id) || s.pending && s.pending.kind !== "question") return false;
1501
+ return send(s, "session.reply", { type: "session.reply", id: s.id, text, ...attachments ? { attachments } : {}, ...how?.steer ? { steer: true } : {} }, { text, onRefused });
1410
1502
  }
1411
1503
  };
1412
1504
  }
@@ -1446,7 +1538,7 @@ function useActions(o) {
1446
1538
  const { t, toast } = o;
1447
1539
  const decisions = useDecisions({ client: o.client, sessions: o.sessions, errors: o.errors, notify: (text) => toast({ text, tone: "error" }), copy: t.decisions });
1448
1540
  const onLand = useRef2(/* @__PURE__ */ new Map());
1449
- useEffect2(() => {
1541
+ useEffect3(() => {
1450
1542
  const id = decisions.landed;
1451
1543
  if (!id) return;
1452
1544
  const text = onLand.current.get(id);
@@ -1479,8 +1571,8 @@ function useActions(o) {
1479
1571
  toast({ text: t.toast.noNote(s.name), tone: "info" });
1480
1572
  }
1481
1573
  },
1482
- reply(s, text, onRefused) {
1483
- const sent = decisions.reply(s, text, onRefused);
1574
+ reply(s, text, onRefused, how) {
1575
+ const sent = decisions.reply(s, text, onRefused, how);
1484
1576
  if (sent) onLand.current.set(s.id, t.toast.reply(s.name));
1485
1577
  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
1578
  return sent;
@@ -1518,8 +1610,127 @@ function runAttached(argv, env = process.env) {
1518
1610
  });
1519
1611
  }
1520
1612
 
1613
+ // src/tui/clipboard.ts
1614
+ import { execFile } from "child_process";
1615
+ import { mkdtemp, readFile, rm } from "fs/promises";
1616
+ import { tmpdir } from "os";
1617
+ import { join } from "path";
1618
+ var CLIPBOARD_TIMEOUT_MS = 1e4;
1619
+ var runFile = (file, args, o) => new Promise((resolve, reject) => {
1620
+ execFile(file, args, { ...o, timeout: CLIPBOARD_TIMEOUT_MS }, (err, stdout) => err ? reject(err) : resolve({ stdout }));
1621
+ });
1622
+ var POWERSHELL = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
1623
+ var MAX_BUFFER = 4 * 1024 * 1024;
1624
+ var LOCAL_MAX_BUFFER = 20 * 1024 * 1024;
1625
+ var SCRIPT = [
1626
+ "Add-Type -AssemblyName System.Windows.Forms,System.Drawing;",
1627
+ "$src = [System.Windows.Forms.Clipboard]::GetImage();",
1628
+ "if ($null -eq $src) { exit 0 }",
1629
+ "$codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' };",
1630
+ "function Encode($long, $qualities, $max) {",
1631
+ "$k = [Math]::Min(1.0, $long / [Math]::Max($src.Width, $src.Height));",
1632
+ "$w = [Math]::Max(1, [int][Math]::Round($src.Width * $k)); $h = [Math]::Max(1, [int][Math]::Round($src.Height * $k));",
1633
+ "$bmp = New-Object System.Drawing.Bitmap($w, $h); $ms = $null;",
1634
+ "try {",
1635
+ "$g = [System.Drawing.Graphics]::FromImage($bmp);",
1636
+ "try { $g.Clear([System.Drawing.Color]::White); $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic; $g.DrawImage($src, 0, 0, $w, $h) } finally { $g.Dispose() }",
1637
+ "foreach ($q in $qualities) {",
1638
+ "if ($null -ne $ms) { $ms.Dispose(); $ms = $null }",
1639
+ "$p = New-Object System.Drawing.Imaging.EncoderParameters(1);",
1640
+ "try {",
1641
+ "$p.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, [long]$q);",
1642
+ "$ms = New-Object System.IO.MemoryStream; $bmp.Save($ms, $codec, $p)",
1643
+ "} finally { $p.Dispose() }",
1644
+ "if ($ms.Length -le $max) { break }",
1645
+ "}",
1646
+ "return @{ w = $w; h = $h; b = [Convert]::ToBase64String($ms.ToArray()) }",
1647
+ "} finally { if ($null -ne $ms) { $ms.Dispose() }; $bmp.Dispose() }",
1648
+ "}",
1649
+ "try {",
1650
+ "$main = Encode 1600 @(85, 70, 55, 40) 400000;",
1651
+ "$prev = Encode 480 @(70) 40000;",
1652
+ "[Console]::Out.Write((ConvertTo-Json -Compress @{ w = $main.w; h = $main.h; main = $main.b; prev = $prev.b }))",
1653
+ "} finally { $src.Dispose() }"
1654
+ ].join(" ");
1655
+ async function fromWindows(run2) {
1656
+ const { stdout } = await run2(POWERSHELL, ["-NoProfile", "-STA", "-Command", SCRIPT], { cwd: "/mnt/c", maxBuffer: MAX_BUFFER, encoding: "buffer" });
1657
+ const line = stdout.toString("utf8").trim();
1658
+ if (!line) return null;
1659
+ const j = JSON.parse(line);
1660
+ const preview = j.prev && Buffer.from(j.prev, "base64").length <= PREVIEW_MAX ? { mime: "image/jpeg", data: j.prev } : void 0;
1661
+ return { image: { mime: "image/jpeg", data: j.main, ...preview ? { preview } : {} }, width: j.w, height: j.h, bytes: Buffer.from(j.main, "base64").length };
1662
+ }
1663
+ function fromBytes(b) {
1664
+ const info = imageInfo(b);
1665
+ return info ? { image: { mime: info.mime, data: b.toString("base64") }, width: info.width, height: info.height, bytes: b.length } : null;
1666
+ }
1667
+ async function fromLinux(run2) {
1668
+ const attempts = [["wl-paste", ["--type", "image/png"]], ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]];
1669
+ for (const [file, args] of attempts) {
1670
+ try {
1671
+ const got = fromBytes((await run2(file, args, { maxBuffer: LOCAL_MAX_BUFFER, encoding: "buffer" })).stdout);
1672
+ if (got) return got;
1673
+ } catch {
1674
+ }
1675
+ }
1676
+ return null;
1677
+ }
1678
+ async function fromMac(run2) {
1679
+ const dir = await mkdtemp(join(tmpdir(), "ledge-clip-"));
1680
+ const file = join(dir, "clip.png");
1681
+ try {
1682
+ const script = `set f to open for access POSIX file "${file}" with write permission
1683
+ set eof f to 0
1684
+ write (the clipboard as \xABclass PNGf\xBB) to f
1685
+ close access f`;
1686
+ await run2("osascript", ["-e", script], { maxBuffer: MAX_BUFFER, encoding: "buffer" });
1687
+ return fromBytes(await readFile(file));
1688
+ } catch {
1689
+ return null;
1690
+ } finally {
1691
+ await rm(dir, { recursive: true, force: true });
1692
+ }
1693
+ }
1694
+ async function readClipboardImage(env = process.env, run2 = runFile, platform = process.platform) {
1695
+ try {
1696
+ if (platform === "darwin") return await fromMac(run2);
1697
+ return env.WSL_DISTRO_NAME ? await fromWindows(run2) : await fromLinux(run2);
1698
+ } catch {
1699
+ return null;
1700
+ }
1701
+ }
1702
+
1703
+ // src/tui/openImage.ts
1704
+ import { spawn as spawn2 } from "child_process";
1705
+ function windowsPath(file, spawnImpl) {
1706
+ return new Promise((resolve, reject) => {
1707
+ const child = spawnImpl("wslpath", ["-w", file], { stdio: ["ignore", "pipe", "ignore"] });
1708
+ let out = "";
1709
+ child.stdout.on("data", (d) => {
1710
+ out += d;
1711
+ });
1712
+ child.on("error", reject);
1713
+ child.on("close", (code) => code === 0 && out.trim() ? resolve(out.trim()) : reject(new Error(`wslpath exited with ${code}`)));
1714
+ });
1715
+ }
1716
+ function launch(spawnImpl, file, args, cwd) {
1717
+ return new Promise((resolve, reject) => {
1718
+ const child = spawnImpl(file, args, { detached: true, stdio: "ignore", ...cwd ? { cwd } : {} });
1719
+ child.once("error", reject);
1720
+ child.once("spawn", () => {
1721
+ child.unref();
1722
+ resolve();
1723
+ });
1724
+ });
1725
+ }
1726
+ async function openImage(file, env = process.env, spawnImpl = spawn2, platform = process.platform) {
1727
+ if (platform === "darwin") return launch(spawnImpl, "open", [file]);
1728
+ if (env.WSL_DISTRO_NAME) return launch(spawnImpl, "/mnt/c/Windows/explorer.exe", [await windowsPath(file, spawnImpl)], "/mnt/c");
1729
+ return launch(spawnImpl, "xdg-open", [file]);
1730
+ }
1731
+
1521
1732
  // src/tui/context.tsx
1522
- import { createContext, useContext, useEffect as useEffect3, useRef as useRef3 } from "react";
1733
+ import { createContext, useContext, useEffect as useEffect4, useRef as useRef3 } from "react";
1523
1734
  var SCREENS = ["overview", "session", "ask", "launch", "settings"];
1524
1735
  var TuiContext = createContext(null);
1525
1736
  var useTui = () => {
@@ -1531,7 +1742,7 @@ function useScreenKeys(handler) {
1531
1742
  const { keys } = useTui();
1532
1743
  const latest = useRef3(handler);
1533
1744
  latest.current = handler;
1534
- useEffect3(() => {
1745
+ useEffect4(() => {
1535
1746
  const fn = (i, k) => latest.current(i, k);
1536
1747
  keys.current = fn;
1537
1748
  return () => {
@@ -1541,7 +1752,7 @@ function useScreenKeys(handler) {
1541
1752
  }
1542
1753
  function useEditing(on) {
1543
1754
  const { setEditing } = useTui();
1544
- useEffect3(() => {
1755
+ useEffect4(() => {
1545
1756
  setEditing(on);
1546
1757
  return () => setEditing(false);
1547
1758
  }, [on, setEditing]);
@@ -1550,6 +1761,15 @@ function useEditing(on) {
1550
1761
  // src/tui/i18n/lang.tsx
1551
1762
  import { createContext as createContext2, useContext as useContext2 } from "react";
1552
1763
 
1764
+ // ../ui/src/images/encode.ts
1765
+ function formatBytes(n, lang) {
1766
+ if (n >= 1e6) {
1767
+ const v = (Math.round(n / 1e5) / 10).toFixed(1);
1768
+ return `${lang === "fr" ? v.replace(".", ",") : v} ${lang === "fr" ? "Mo" : "MB"}`;
1769
+ }
1770
+ return `${Math.round(n / 1e3)} ${lang === "fr" ? "Ko" : "KB"}`;
1771
+ }
1772
+
1553
1773
  // src/tui/i18n/fr.ts
1554
1774
  var plural = (n, one, many) => n > 1 ? many : one;
1555
1775
  var fr = {
@@ -1572,6 +1792,8 @@ var fr = {
1572
1792
  tokenMissing: "pas de jeton : lancez une session avec `ledge claude` ou `ledge daemon`"
1573
1793
  },
1574
1794
  keys: {
1795
+ image: "image",
1796
+ openImage: "ouvrir l'image",
1575
1797
  navigate: "naviguer",
1576
1798
  open: "ouvrir",
1577
1799
  answer: "r\xE9pondre",
@@ -1761,6 +1983,18 @@ var fr = {
1761
1983
  settings: "R\xE9glages",
1762
1984
  screens: "changer d\u2019\xE9cran"
1763
1985
  },
1986
+ images: {
1987
+ capture: "capture",
1988
+ size: (n) => formatBytes(n, "fr"),
1989
+ open: (key) => `${key} ouvrir dans la visionneuse`,
1990
+ tooLarge: "Image trop lourde, m\xEAme r\xE9duite",
1991
+ notImage: "Ce fichier n'est pas une image",
1992
+ failed: "Image non envoy\xE9e",
1993
+ max: "5 images au plus par message",
1994
+ dialog: "R\xE9pondez d'abord \xE0 la question, l'image partira apr\xE8s",
1995
+ none: "Pas d'image dans le presse-papiers",
1996
+ openFailed: (why) => `Image non ouverte : ${why}`
1997
+ },
1764
1998
  toast: {
1765
1999
  handover: (n) => `${n} \xE9crit son bilan : une nouvelle session prendra sa place`,
1766
2000
  handoverRefused: (why) => `Rel\xE8ve refus\xE9e : ${why}`,
@@ -1797,7 +2031,7 @@ var fr = {
1797
2031
  question: (n, text) => `${n} a une question : ${text}`,
1798
2032
  title: (n) => n > 0 ? `ledge \xB7 ${n} t\u2019${plural(n, "attend", "attendent")}` : "ledge"
1799
2033
  },
1800
- cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui s\u2019ouvre dans l\u2019onglet Ledge." }
2034
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui s\u2019ouvre dans l\u2019onglet Ledge.", loadingProfile: "Windows Terminal charge le profil Ledge\u2026" }
1801
2035
  };
1802
2036
 
1803
2037
  // src/tui/i18n/en.ts
@@ -1816,6 +2050,8 @@ var en = {
1816
2050
  tokenMissing: "no token: start a session with `ledge claude` or run `ledge daemon`"
1817
2051
  },
1818
2052
  keys: {
2053
+ image: "image",
2054
+ openImage: "open image",
1819
2055
  navigate: "move",
1820
2056
  open: "open",
1821
2057
  answer: "answer",
@@ -1996,6 +2232,18 @@ var en = {
1996
2232
  },
1997
2233
  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
2234
  help: { title: "Help", global: "Everywhere", overview: "Overview", session: "Session", ask: "Ask Ledge", launch: "New session", settings: "Settings", screens: "switch screen" },
2235
+ images: {
2236
+ capture: "capture",
2237
+ size: (n) => formatBytes(n, "en"),
2238
+ open: (key) => `${key} open in viewer`,
2239
+ tooLarge: "Image too large, even reduced",
2240
+ notImage: "This file is not an image",
2241
+ failed: "Image not sent",
2242
+ max: "5 images per message at most",
2243
+ dialog: "Answer the question first, the image will go after",
2244
+ none: "No image in the clipboard",
2245
+ openFailed: (why) => `Image not opened: ${why}`
2246
+ },
1999
2247
  toast: {
2000
2248
  handover: (n) => `${n} is writing its brief: a fresh session will take its place`,
2001
2249
  handoverRefused: (why) => `Relief refused: ${why}`,
@@ -2032,7 +2280,7 @@ var en = {
2032
2280
  question: (n, text) => `${n} has a question: ${text}`,
2033
2281
  title: (n) => n > 0 ? `ledge \xB7 ${n} waiting` : "ledge"
2034
2282
  },
2035
- cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui opens in the Ledge tab." }
2283
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui opens in the Ledge tab.", loadingProfile: "Windows Terminal is loading the Ledge profile\u2026" }
2036
2284
  };
2037
2285
 
2038
2286
  // src/tui/i18n/lang.tsx
@@ -2112,12 +2360,15 @@ function nextWaiting(yours, selected) {
2112
2360
  }
2113
2361
  function exchanges(sessions, max, tasks = [], ledge = "Ledge") {
2114
2362
  const byId = new Map(sessions.map((s) => [s.id, s]));
2363
+ const bare = (id) => splitId(id)?.id ?? id;
2364
+ const find2 = (id) => byId.get(id) ?? sessions.find((x) => bare(x.id) === bare(id));
2365
+ const nameOf = (id, then) => id === LEDGE_GIVER ? ledge : find2(id)?.name ?? then;
2115
2366
  return [...sessions.flatMap((s) => {
2116
2367
  const m = s.lastMessage;
2117
2368
  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);
2369
+ const peer = m.peer ? find2(m.peer) : void 0;
2370
+ return [{ at: m.at, from: s.name, to: peer?.name ?? m.to, summary: m.summary, kind: "message" }];
2371
+ }), ...tasks.flatMap((x) => transmissions(x).map((tr) => ({ at: tr.at, from: nameOf(tr.from, tr.fromName), to: nameOf(tr.to, tr.toName), summary: x.title, kind: "task" })))].sort((a, b) => b.at.localeCompare(a.at)).slice(0, max);
2121
2372
  }
2122
2373
  var nearContext = (sessions, near) => sessions.filter((s) => s.state !== "exited" && !s.archivedAt && (s.handover ? s.handover.state === "failed" : contextNear(s.context, near)));
2123
2374
  var closedLedgeTasks = (tasks) => tasks.filter((x) => x.giver === LEDGE_GIVER && taskClosed(x) && !x.seen);
@@ -2165,7 +2416,7 @@ var title = (text) => `\x1B]0;${text.replace(/[\u0000-\u001f\u007f]/g, " ")}\x07
2165
2416
  import { useMemo as useMemo2 } from "react";
2166
2417
  import { Box as Box6, Text as Text6, useWindowSize as useWindowSize2 } from "ink";
2167
2418
  import { randomUUID } from "crypto";
2168
- import { useState as useState2 } from "react";
2419
+ import { useState as useState3 } from "react";
2169
2420
 
2170
2421
  // ../ui/src/bots/poseFor.ts
2171
2422
  var FROM_SESSION = {
@@ -2429,8 +2680,8 @@ function Overview() {
2429
2680
  const closed = closedLedgeTasks(tasks);
2430
2681
  const threshold = useContextNear(tui.client);
2431
2682
  const near = useMemo2(() => nearContext(d.sessions, threshold), [d.sessions, threshold]);
2432
- const [tasksShown, setTasksShown] = useState2(true);
2433
- const [joining, setJoining] = useState2(null);
2683
+ const [tasksShown, setTasksShown] = useState3(true);
2684
+ const [joining, setJoining] = useState3(null);
2434
2685
  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
2686
  const seatTeam = selected?.team ? teams?.find((x) => x.id === selected.team && x.state !== "dissolved") : void 0;
2436
2687
  const choices2 = node?.teams.map((x) => x.team).filter((x) => x.state !== "dissolved" && x.id !== selected?.team) ?? [];
@@ -2654,10 +2905,14 @@ function Overview() {
2654
2905
  " ",
2655
2906
  /* @__PURE__ */ jsx7(Text6, { color: C.fg, children: x.from }),
2656
2907
  " ",
2657
- /* @__PURE__ */ jsx7(Text6, { color: C.violet, children: g.arrow }),
2908
+ /* @__PURE__ */ jsx7(Text6, { color: x.kind === "task" ? C.green : C.violet, children: g.arrow }),
2658
2909
  " ",
2659
2910
  /* @__PURE__ */ jsx7(Text6, { color: C.fg, children: x.to }),
2660
2911
  " ",
2912
+ x.kind === "task" ? /* @__PURE__ */ jsxs5(Text6, { color: C.green, children: [
2913
+ g.task,
2914
+ " "
2915
+ ] }) : null,
2661
2916
  /* @__PURE__ */ jsx7(Text6, { color: C.dim, children: oneLine(x.summary) })
2662
2917
  ] }, i));
2663
2918
  })() })
@@ -2698,8 +2953,126 @@ function SessionRow({ s, on, nameW, topicW, stateW, indent = 0 }) {
2698
2953
  }
2699
2954
 
2700
2955
  // 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";
2956
+ import { useEffect as useEffect7, useMemo as useMemo3, useState as useState7 } from "react";
2957
+ import { Box as Box9, Text as Text11, useInput as useInput3, useWindowSize as useWindowSize3 } from "ink";
2958
+
2959
+ // src/tui/images.tsx
2960
+ import { mkdir, writeFile } from "fs/promises";
2961
+ import { tmpdir as tmpdir2 } from "os";
2962
+ import { join as join2 } from "path";
2963
+ import { useRef as useRef6, useState as useState4 } from "react";
2964
+ import { Text as Text7 } from "ink";
2965
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2966
+ var counter2 = 0;
2967
+ var INLINE_BUDGET = MAX_FRAME - 64 * 1024;
2968
+ function useChips(put) {
2969
+ const { t, toast } = useTui();
2970
+ const [items, setItems] = useState4([]);
2971
+ const held = useRef6([]);
2972
+ const commit = (next2) => {
2973
+ held.current = next2;
2974
+ setItems(next2);
2975
+ };
2976
+ const patch = (key, over) => commit(held.current.map((c) => c.key === key ? { ...c, ...over } : c));
2977
+ const reasonOf = (e) => {
2978
+ const reason = e instanceof AttachmentFailure ? e.reason : null;
2979
+ return reason === "too_large" ? t.images.tooLarge : reason === "not_image" ? t.images.notImage : t.images.failed;
2980
+ };
2981
+ return {
2982
+ items,
2983
+ add({ image, width, height, bytes }) {
2984
+ if (held.current.length >= ATTACHMENTS_MAX) {
2985
+ toast({ text: t.images.max, tone: "info" });
2986
+ return;
2987
+ }
2988
+ const key = counter2++;
2989
+ const inline = held.current.reduce((n, c) => n + (c.image?.data.length ?? 0), 0);
2990
+ if (bytes > ATTACHMENT_LOCAL_MAX || !put && inline + image.data.length > INLINE_BUDGET) {
2991
+ commit([...held.current, { key, width, height, state: "refused", reason: t.images.tooLarge }]);
2992
+ return;
2993
+ }
2994
+ if (!put) {
2995
+ commit([...held.current, { key, width, height, state: "ready", image }]);
2996
+ return;
2997
+ }
2998
+ commit([...held.current, { key, width, height, state: "uploading" }]);
2999
+ put(image).then((ref) => patch(key, { state: "ready", id: ref.id }), (e) => patch(key, { state: "refused", reason: reasonOf(e) }));
3000
+ },
3001
+ removeLast() {
3002
+ commit(held.current.slice(0, -1));
3003
+ },
3004
+ ready: items.every((c) => c.state === "ready"),
3005
+ ids: () => held.current.flatMap((c) => c.state === "ready" && c.id ? [c.id] : []),
3006
+ uploads: () => held.current.flatMap((c) => c.state === "ready" && c.image ? [c.image] : []),
3007
+ clear() {
3008
+ const taken = held.current;
3009
+ commit([]);
3010
+ return taken;
3011
+ },
3012
+ putBack(taken) {
3013
+ commit([...taken, ...held.current].slice(0, ATTACHMENTS_MAX));
3014
+ }
3015
+ };
3016
+ }
3017
+ function ChipsLine({ chips }) {
3018
+ const { t } = useTui();
3019
+ if (chips.length === 0) return null;
3020
+ return /* @__PURE__ */ jsx8(Text7, { wrap: "truncate", children: chips.map((c, i) => /* @__PURE__ */ jsxs6(Text7, { children: [
3021
+ i ? " " : "",
3022
+ /* @__PURE__ */ jsxs6(Text7, { color: c.state === "refused" ? C.red : C.dim, children: [
3023
+ "\u25A3 ",
3024
+ t.images.capture,
3025
+ " ",
3026
+ c.width,
3027
+ "\xD7",
3028
+ c.height,
3029
+ " ",
3030
+ c.state === "uploading" ? "\u2026" : "\u2715"
3031
+ ] }),
3032
+ c.reason ? /* @__PURE__ */ jsxs6(Text7, { color: C.red, children: [
3033
+ " ",
3034
+ c.reason
3035
+ ] }) : null
3036
+ ] }, c.key)) });
3037
+ }
3038
+ var EXT = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
3039
+ function usePaste(chips) {
3040
+ const { readImage, toast, t } = useTui();
3041
+ const reading = useRef6(false);
3042
+ return () => {
3043
+ if (reading.current) return;
3044
+ reading.current = true;
3045
+ void readImage().then((got) => {
3046
+ if (got) chips.add(got);
3047
+ else toast({ text: t.images.none, tone: "info" });
3048
+ }, () => toast({ text: t.images.none, tone: "info" })).finally(() => {
3049
+ reading.current = false;
3050
+ });
3051
+ };
3052
+ }
3053
+ function useOpenImage() {
3054
+ const { client, openFile, toast, t } = useTui();
3055
+ const presses = useRef6(0);
3056
+ const last = useRef6("");
3057
+ return (items) => {
3058
+ const signature = items.map((x) => x.ref.id).join(",");
3059
+ if (signature !== last.current) {
3060
+ last.current = signature;
3061
+ presses.current = 0;
3062
+ }
3063
+ const item = items[items.length - 1 - presses.current++ % items.length];
3064
+ if (!item) return;
3065
+ const { ref, route } = item;
3066
+ void (async () => {
3067
+ const { mime, data } = await getImage(client, ref.id, route);
3068
+ const dir = join2(tmpdir2(), "ledge-images");
3069
+ await mkdir(dir, { recursive: true, mode: 448 });
3070
+ const file = join2(dir, `${ref.id}.${EXT[mime]}`);
3071
+ await writeFile(file, Buffer.from(data, "base64"), { mode: 384 });
3072
+ await openFile(file);
3073
+ })().catch((e) => toast({ text: t.images.openFailed(e instanceof AttachmentFailure ? e.reason : e.message), tone: "error" }));
3074
+ };
3075
+ }
2703
3076
 
2704
3077
  // ../ui/src/thread/turns.ts
2705
3078
  var byTime = (a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0;
@@ -2761,6 +3134,9 @@ function target(item) {
2761
3134
  var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2762
3135
 
2763
3136
  // src/tui/model/thread.ts
3137
+ function imageLine(ref, t, key) {
3138
+ return [{ text: ` \u25A3 ${t.images.capture} ${ref.width}\xD7${ref.height} \xB7 ${t.images.size(ref.bytes)} \xB7 ${t.images.open(key)}`, color: C.faint }];
3139
+ }
2764
3140
  var DIFF_MAX = 30;
2765
3141
  function wrap(text, width) {
2766
3142
  const out = [];
@@ -2816,7 +3192,7 @@ function itemLines(it, o) {
2816
3192
  const body = (text, color) => wrap(text, width - 2).map((l) => [{ text: ` ${l}`, color }]);
2817
3193
  switch (it.kind) {
2818
3194
  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)];
3195
+ return [[{ text: t.session.you, color: C.coral, bold: true }, { text: ` ${clock(it.at)}`, color: C.faint }], ...body(it.text, C.fg), ...(it.images ?? []).map((r) => imageLine(r, t, "o"))];
2820
3196
  case "message":
2821
3197
  return body(it.text, C.dim);
2822
3198
  case "reply":
@@ -2833,8 +3209,9 @@ function itemLines(it, o) {
2833
3209
  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
3210
  case "received":
2835
3211
  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 }]];
3212
+ // A task the session handed over (#92): its plain line says to whom; the summary alone is the title.
2836
3213
  case "notice":
2837
- return [[{ text: ` \xB7 ${oneLine(it.summary ?? it.text)}`, color: C.faint }]];
3214
+ return [[{ text: ` \xB7 ${oneLine(it.outgoing ? it.text : it.summary ?? it.text)}`, color: C.faint }]];
2838
3215
  }
2839
3216
  }
2840
3217
  function foldedLine(turn, o) {
@@ -2857,21 +3234,25 @@ function threadLines(items, o) {
2857
3234
  });
2858
3235
  return out;
2859
3236
  }
3237
+ function threadImages(items, unfolded) {
3238
+ const all = turns(items);
3239
+ return (unfolded ? all : all.slice(-1)).flatMap((turn) => turn.items.flatMap((it) => it.kind === "prompt" ? it.images ?? [] : []));
3240
+ }
2860
3241
 
2861
3242
  // src/tui/ui/ModelPicker.tsx
2862
- import { useState as useState4 } from "react";
2863
- import { Box as Box7, Text as Text7, useInput } from "ink";
3243
+ import { useState as useState6 } from "react";
3244
+ import { Box as Box7, Text as Text8, useInput } from "ink";
2864
3245
 
2865
3246
  // ../fleet/src/models.ts
2866
- import { useEffect as useEffect4, useState as useState3 } from "react";
3247
+ import { useEffect as useEffect5, useState as useState5 } from "react";
2867
3248
  var IDLE = { status: "idle", models: [] };
2868
3249
  var LOADING = { status: "loading", models: [] };
2869
3250
  var known = /* @__PURE__ */ new Map();
2870
3251
  var next = 0;
2871
3252
  function useModels(client, machine, tool) {
2872
3253
  const key = machine === null || tool === null ? null : `${machine}|${tool}`;
2873
- const [answer, setAnswer] = useState3(null);
2874
- useEffect4(() => {
3254
+ const [answer, setAnswer] = useState5(null);
3255
+ useEffect5(() => {
2875
3256
  if (key === null || machine === null || tool === null || known.has(key)) return;
2876
3257
  const requestId = `models-${++next}`;
2877
3258
  const off = client.onMessage((m) => {
@@ -2888,14 +3269,14 @@ function useModels(client, machine, tool) {
2888
3269
  }
2889
3270
 
2890
3271
  // src/tui/ui/ModelPicker.tsx
2891
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
3272
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2892
3273
  function ModelPicker({ machine, tool, current, onPick, onCancel }) {
2893
3274
  const { client, t, g } = useTui();
2894
3275
  useEditing(true);
2895
3276
  const models = useModels(client, machine, tool);
2896
3277
  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);
3278
+ const [at, setAt] = useState6(() => Math.max(0, rows.findIndex((r) => r.id === current.model)));
3279
+ const [effort, setEffort] = useState6(current.effort);
2899
3280
  const row = rows[Math.min(at, rows.length - 1)];
2900
3281
  useInput((input, key) => {
2901
3282
  if (key.escape) {
@@ -2919,16 +3300,16 @@ function ModelPicker({ machine, tool, current, onPick, onCancel }) {
2919
3300
  }
2920
3301
  if (key.return) onPick({ model: row.id, effort: row.efforts.length ? effort ?? null : null });
2921
3302
  });
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: [
3303
+ if (models.status !== "ready") return /* @__PURE__ */ jsx9(Text8, { color: C.faint, children: t.launch.loading });
3304
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3305
+ models.error ? /* @__PURE__ */ jsx9(Text8, { color: C.yellow, children: models.error }) : null,
3306
+ rows.map((r, i) => /* @__PURE__ */ jsxs7(Text8, { wrap: "truncate", backgroundColor: i === at ? C.sel : void 0, children: [
3307
+ /* @__PURE__ */ jsxs7(Text8, { color: i === at ? C.coral : C.faint, children: [
2927
3308
  i === at ? g.cursor : " ",
2928
3309
  " "
2929
3310
  ] }),
2930
- /* @__PURE__ */ jsx8(Text7, { color: C.fg, children: r.label }),
2931
- i === at && r.efforts.length ? /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
3311
+ /* @__PURE__ */ jsx9(Text8, { color: C.fg, children: r.label }),
3312
+ i === at && r.efforts.length ? /* @__PURE__ */ jsxs7(Text8, { color: C.dim, children: [
2932
3313
  " ",
2933
3314
  r.efforts.map((e) => e === (effort ?? r.defaultEffort) ? `${g.prev} ${e} ${g.next}` : e).join(" ")
2934
3315
  ] }) : null
@@ -2937,24 +3318,24 @@ function ModelPicker({ machine, tool, current, onPick, onCancel }) {
2937
3318
  }
2938
3319
 
2939
3320
  // 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";
3321
+ import { useEffect as useEffect6, useRef as useRef7 } from "react";
3322
+ import { Box as Box8, Text as Text9, useBoxMetrics as useBoxMetrics3 } from "ink";
3323
+ import { jsx as jsx10 } from "react/jsx-runtime";
2943
3324
  function Lines({ lines, up = 0, onHeight }) {
2944
- const ref = useRef6(null);
3325
+ const ref = useRef7(null);
2945
3326
  const { height } = useBoxMetrics3(ref);
2946
- useEffect5(() => {
3327
+ useEffect6(() => {
2947
3328
  if (height > 0) onHeight?.(height);
2948
3329
  }, [height, onHeight]);
2949
3330
  const h = Math.max(1, height);
2950
3331
  const back = Math.min(up, Math.max(0, lines.length - h));
2951
3332
  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)) });
3333
+ return /* @__PURE__ */ jsx10(Box8, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.map((l, i) => /* @__PURE__ */ jsx10(Box8, { flexShrink: 0, children: /* @__PURE__ */ jsx10(Text9, { wrap: "truncate", children: l.map((x, k) => /* @__PURE__ */ jsx10(Text9, { color: x.color, bold: x.bold, children: x.text || (k === 0 ? " " : "") }, k)) }) }, i)) });
2953
3334
  }
2954
3335
 
2955
3336
  // 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";
3337
+ import { Text as Text10, useInput as useInput2 } from "ink";
3338
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
2958
3339
  function TextInput({ value, onChange, onSubmit, onEscape, placeholder, active, mask }) {
2959
3340
  useInput2((input, key) => {
2960
3341
  if (key.return) {
@@ -2977,11 +3358,11 @@ function TextInput({ value, onChange, onSubmit, onEscape, placeholder, active, m
2977
3358
  if (input) onChange(value + input.replace(/\r\n?/g, "\n"));
2978
3359
  }, { isActive: active });
2979
3360
  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: [
3361
+ if (!shown && !active) return /* @__PURE__ */ jsx11(Text10, { color: C.faint, wrap: "truncate", children: placeholder ?? "" });
3362
+ return /* @__PURE__ */ jsxs8(Text10, { wrap: "truncate-start", children: [
3363
+ shown ? /* @__PURE__ */ jsx11(Text10, { color: C.fg, children: shown }) : null,
3364
+ active ? /* @__PURE__ */ jsx11(Text10, { inverse: true, children: " " }) : null,
3365
+ !shown && placeholder ? /* @__PURE__ */ jsxs8(Text10, { color: C.faint, children: [
2985
3366
  " ",
2986
3367
  placeholder
2987
3368
  ] }) : null
@@ -2989,47 +3370,71 @@ function TextInput({ value, onChange, onSubmit, onEscape, placeholder, active, m
2989
3370
  }
2990
3371
 
2991
3372
  // src/tui/screens/Session.tsx
2992
- import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
3373
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2993
3374
  function SessionScreen() {
2994
3375
  const tui = useTui();
2995
3376
  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);
3377
+ if (!s) return /* @__PURE__ */ jsx12(Frame, { crumb: "", keys: [["esc", t.keys.back]], children: /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.session.noSession }) });
3378
+ return /* @__PURE__ */ jsx12(SessionView, { s }, s.id);
2998
3379
  }
2999
3380
  function SessionView({ s }) {
3000
3381
  const { client, d, t, g, now, actions, attach, toast, machines } = useTui();
3001
3382
  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);
3383
+ const [mode, setMode] = useState7("read");
3384
+ const [draft, setDraft] = useState7("");
3385
+ const [diffs, setDiffs] = useState7(false);
3386
+ const [unfolded, setUnfolded] = useState7(false);
3387
+ const [terminal, setTerminal] = useState7(false);
3388
+ const [up, setUp] = useState7(0);
3008
3389
  useEditing(mode === "reply" || mode === "note");
3009
3390
  const items = d.threads.get(s.id);
3010
- useEffect6(() => {
3391
+ useEffect7(() => {
3011
3392
  if (!items && !s.offline) client.send({ type: "session.thread", id: s.id });
3012
3393
  }, [items === void 0, s.id, d.snapshotSeq]);
3013
3394
  const width = Math.max(20, columns - 6);
3014
3395
  const lines = useMemo3(() => items ? threadLines(items, { t, g, now, width, diffs, unfolded }) : [], [items, t, g, now, width, diffs, unfolded]);
3015
3396
  const p = s.pending;
3016
- const [view, setView] = useState5(10);
3397
+ const [view, setView] = useState7(10);
3017
3398
  const maxUp = Math.max(0, lines.length - view);
3399
+ const images = useMemo3(() => (items ? threadImages(items, unfolded) : []).map((ref) => ({ ref, route: { id: s.id } })), [items, unfolded, s.id]);
3400
+ const chips = useChips((image) => putImage(client, image, { id: s.id }));
3401
+ const paste = usePaste(chips);
3402
+ const openImage2 = useOpenImage();
3018
3403
  const machine = splitId(s.id)?.machine ?? "local";
3019
3404
  const canSwitch = machines.find((m) => m.id === machine)?.daemon?.modelSwitch === true;
3020
3405
  const send = (text) => {
3021
- if (!text.trim()) {
3022
- setMode("read");
3023
- return;
3406
+ if (mode === "note") {
3407
+ if (!text.trim()) {
3408
+ setMode("read");
3409
+ return;
3410
+ }
3411
+ actions.denyWithNote(s, text.trim(), (x) => setDraft(x));
3412
+ } else {
3413
+ if (!text.trim() && chips.items.length === 0) {
3414
+ setMode("read");
3415
+ return;
3416
+ }
3417
+ if (chips.items.length && !chips.ready) return;
3418
+ if (chips.items.length && p?.kind === "question") {
3419
+ toast({ text: t.images.dialog, tone: "info" });
3420
+ return;
3421
+ }
3422
+ const taken = chips.clear();
3423
+ if (!actions.reply(s, text, (x) => {
3424
+ setDraft(x);
3425
+ chips.putBack(taken);
3426
+ }, { attachments: taken.flatMap((c) => c.id ? [c.id] : []) })) {
3427
+ chips.putBack(taken);
3428
+ return;
3429
+ }
3024
3430
  }
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
3431
  setDraft("");
3031
3432
  setMode("read");
3032
3433
  };
3434
+ useInput3((input, key) => {
3435
+ if (key.meta && input === "v") paste();
3436
+ else if ((key.backspace || key.delete) && draft === "") chips.removeLast();
3437
+ }, { isActive: mode === "reply" });
3033
3438
  useScreenKeys((input, key) => {
3034
3439
  if (mode === "model") return true;
3035
3440
  if (mode !== "read") {
@@ -3071,6 +3476,10 @@ function SessionView({ s }) {
3071
3476
  setTerminal((x) => !x);
3072
3477
  return true;
3073
3478
  }
3479
+ if (input === "o" && images.length) {
3480
+ openImage2(images);
3481
+ return true;
3482
+ }
3074
3483
  if (input === "a") {
3075
3484
  attach(s);
3076
3485
  return true;
@@ -3108,9 +3517,9 @@ function SessionView({ s }) {
3108
3517
  });
3109
3518
  const state2 = botState(s);
3110
3519
  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(
3520
+ 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], ...mode === "reply" ? [["alt+v", t.keys.image]] : [], ["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], ...images.length ? [["o", t.keys.openImage]] : []];
3521
+ return /* @__PURE__ */ jsxs9(Frame, { crumb: `${s.repo ?? s.project}${s.branch ? ` \u203A ${g.branch} ${s.branch}` : ""} \u203A ${s.name} \xB7 ${machineLabel(s)}`, keys, children: [
3522
+ /* @__PURE__ */ jsx12(TitledBox, { title: terminal ? t.overview.terminal(paneName(s)) : title2, flexGrow: 1, children: mode === "model" ? /* @__PURE__ */ jsx12(
3114
3523
  ModelPicker,
3115
3524
  {
3116
3525
  machine,
@@ -3123,47 +3532,50 @@ function SessionView({ s }) {
3123
3532
  setMode("read");
3124
3533
  }
3125
3534
  }
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: [
3535
+ ) : terminal ? /* @__PURE__ */ jsx12(Tail, { lines: d.previews.get(s.id) ?? [], color: C.dim, empty: t.overview.noPreview, emptyColor: C.faint }) : !items ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.session.loading }) : lines.length === 0 ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.session.empty }) : /* @__PURE__ */ jsx12(Lines, { lines, up, onHeight: setView }) }),
3536
+ p ? /* @__PURE__ */ jsxs9(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: [
3537
+ /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3538
+ /* @__PURE__ */ jsx12(Text11, { color: C.fg, children: p.kind === "question" ? oneLine(p.text) : `${t.session.wantsTo(s.name)} ` }),
3539
+ p.kind !== "question" ? /* @__PURE__ */ jsx12(Text11, { color: C.fg, bold: true, children: oneLine(p.text) }) : null,
3540
+ p.tool ? /* @__PURE__ */ jsxs9(Text11, { color: C.faint, children: [
3132
3541
  " ",
3133
3542
  p.tool
3134
3543
  ] }) : null
3135
3544
  ] }),
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
3545
+ /* @__PURE__ */ jsx12(Box9, { marginTop: 1, columnGap: 2, flexWrap: "wrap", children: p.kind === "question" ? choices(p).slice(0, 9).map((c, i) => /* @__PURE__ */ jsx12(Button, { label: `${c.n} \xB7 ${c.label}`, k: String(c.n), primary: i === 0 }, c.n)) : /* @__PURE__ */ jsxs9(Fragment3, { children: [
3546
+ /* @__PURE__ */ jsx12(Button, { label: t.overview.allow, k: "y", primary: true }),
3547
+ /* @__PURE__ */ jsx12(Button, { label: t.overview.deny, k: "n" }),
3548
+ offers(s.tool, p.kind, "always") ? /* @__PURE__ */ jsx12(Button, { label: t.overview.always, k: "A" }) : null,
3549
+ offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx12(Button, { label: t.overview.denyWithNote, k: "r" }) : null
3141
3550
  ] }) })
3142
3551
  ] }) : null,
3143
- /* @__PURE__ */ jsx11(TitledBox, { title: mode === "note" ? t.overview.denyWithNote : t.keys.write, color: mode === "read" ? C.line : C.coral, children: /* @__PURE__ */ jsxs8(Box9, { children: [
3144
- /* @__PURE__ */ jsxs8(Text10, { color: C.coral, children: [
3145
- g.cursor,
3146
- " "
3147
- ] }),
3148
- /* @__PURE__ */ jsx11(Box9, { flexGrow: 1, overflow: "hidden", children: /* @__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
- ] }) })
3552
+ /* @__PURE__ */ jsxs9(TitledBox, { title: mode === "note" ? t.overview.denyWithNote : t.keys.write, color: mode === "read" ? C.line : C.coral, children: [
3553
+ /* @__PURE__ */ jsx12(ChipsLine, { chips: chips.items }),
3554
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3555
+ /* @__PURE__ */ jsxs9(Text11, { color: C.coral, children: [
3556
+ g.cursor,
3557
+ " "
3558
+ ] }),
3559
+ /* @__PURE__ */ jsx12(Box9, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx12(
3560
+ TextInput,
3561
+ {
3562
+ value: draft,
3563
+ onChange: setDraft,
3564
+ onSubmit: send,
3565
+ onEscape: () => setMode("read"),
3566
+ active: mode === "reply" || mode === "note",
3567
+ placeholder: mode === "note" ? t.session.noteFor(s.name) : t.session.replyTo(s.name)
3568
+ }
3569
+ ) })
3570
+ ] })
3571
+ ] })
3160
3572
  ] });
3161
3573
  }
3162
3574
 
3163
3575
  // src/tui/screens/Ask.tsx
3164
3576
  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";
3577
+ import { useEffect as useEffect8, useMemo as useMemo4, useRef as useRef8, useState as useState8 } from "react";
3578
+ import { Box as Box10, Text as Text12, useInput as useInput4, useWindowSize as useWindowSize4 } from "ink";
3167
3579
 
3168
3580
  // src/tui/model/chat.ts
3169
3581
  function stepLabel(s, t) {
@@ -3177,6 +3589,7 @@ function chatLines(turns2, live, o) {
3177
3589
  if (out.length) out.push([{ text: "" }]);
3178
3590
  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
3591
  out.push([{ text: ` ${t.ask.you} \xB7 ${clock(turn.askedAt)}`, color: C.faint }]);
3592
+ for (const a of turn.attachments ?? []) out.push(imageLine(a, t, "alt+o"));
3180
3593
  const steps = [...new Set(turn.steps.map((s) => stepLabel(s, t.ask.steps)))];
3181
3594
  if (steps.length) out.push([{ text: ` ${t.ask.read} : ${steps.join(" \xB7 ")}`, color: C.faint }]);
3182
3595
  const text = turn.answer ?? (live?.qid === turn.qid ? live.text : void 0);
@@ -3197,8 +3610,8 @@ function openCards(turns2) {
3197
3610
  if (turn.team && !turn.team.teamId) out.push({ kind: "team", qid: turn.qid });
3198
3611
  const tasks = (turn.tasks ?? []).map((c, i) => c.taskId ? -1 : i).filter((i) => i >= 0);
3199
3612
  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 });
3613
+ const launch2 = proposalRows(turn).map((p, i) => p.launchedId ? -1 : i).filter((i) => i >= 0);
3614
+ if (launch2.length) out.push({ kind: "launch", qid: turn.qid, rows: launch2 });
3202
3615
  if (turn.relay && turn.relay.status !== "sent") out.push({ kind: "relay", qid: turn.qid });
3203
3616
  const close = (turn.closes ?? []).map((c, i) => c.closedAt ? -1 : i).filter((i) => i >= 0);
3204
3617
  if (close.length) out.push({ kind: "close", qid: turn.qid, rows: close });
@@ -3207,20 +3620,34 @@ function openCards(turns2) {
3207
3620
  }
3208
3621
 
3209
3622
  // src/tui/screens/Ask.tsx
3210
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
3623
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3211
3624
  var CARD_MS = 2e4;
3212
3625
  function Ask() {
3213
3626
  const { client, d, machines, t, g, toast, go } = useTui();
3214
3627
  const { columns } = useWindowSize4();
3215
- const [draft, setDraft] = useState6("");
3216
- const [focus, setFocus] = useState6(-1);
3217
- const [busy, setBusy] = useState6(null);
3628
+ const [draft, setDraft] = useState8("");
3629
+ const [focus, setFocus] = useState8(-1);
3630
+ const chips = useChips(null);
3631
+ const paste = usePaste(chips);
3632
+ const openImage2 = useOpenImage();
3633
+ const [busy, setBusy] = useState8(null);
3218
3634
  useEditing(focus === -1);
3635
+ const sentAsk = useRef8(null);
3636
+ useEffect8(() => {
3637
+ const s = sentAsk.current;
3638
+ const turn = s && d.chat.find((x) => x.qid === s.qid);
3639
+ if (!s || !turn) return;
3640
+ if (turn.error) {
3641
+ sentAsk.current = null;
3642
+ chips.putBack(s.taken);
3643
+ setDraft((x) => x || s.text);
3644
+ } else if (turn.answeredAt) sentAsk.current = null;
3645
+ }, [d.chat]);
3219
3646
  const cards = useMemo4(() => openCards(d.chat), [d.chat]);
3220
- useEffect7(() => {
3647
+ useEffect8(() => {
3221
3648
  if (focus >= cards.length) setFocus(cards.length ? cards.length - 1 : -1);
3222
3649
  }, [cards.length, focus]);
3223
- useEffect7(() => {
3650
+ useEffect8(() => {
3224
3651
  if (!busy) return;
3225
3652
  const turn = d.chat.find((x) => x.qid === busy.qid);
3226
3653
  if (turn !== busy.turn || (d.errors.at(-1)?.seq ?? 0) > busy.errorSeq) {
@@ -3233,14 +3660,16 @@ function Ask() {
3233
3660
  const local = machines.find((m) => m.id === "local");
3234
3661
  const asking = d.chat.find((x) => !x.answeredAt && !x.error);
3235
3662
  const ask = (text) => {
3236
- if (!text.trim()) return;
3663
+ const images = chips.uploads();
3664
+ if (!text.trim() && images.length === 0) return;
3237
3665
  if (!local?.ready) {
3238
3666
  toast({ text: t.ask.offline, tone: "info" });
3239
3667
  return;
3240
3668
  }
3241
3669
  const qid = `q-${randomUUID2()}`;
3242
3670
  daemonStore.beginTurn({ qid, question: text.trim(), askedAt: (/* @__PURE__ */ new Date()).toISOString(), steps: [] });
3243
- client.send({ type: "chat.ask", qid, text: text.trim() });
3671
+ client.send({ type: "chat.ask", qid, text: text.trim(), ...images.length ? { images } : {} });
3672
+ sentAsk.current = { qid, taken: chips.clear(), text: text.trim() };
3244
3673
  setDraft("");
3245
3674
  };
3246
3675
  const act = (card) => {
@@ -3268,13 +3697,26 @@ function Ask() {
3268
3697
  } else if (card.kind === "relay") client.send({ type: turn.relay?.status === "queued" ? "session.relay.cancel" : "session.relay", qid: card.qid });
3269
3698
  else client.send({ type: "session.close", qid: card.qid, rows: card.rows });
3270
3699
  };
3271
- const latest = useRef7({ asking, cards, focus });
3700
+ const latest = useRef8({ asking, cards, focus });
3272
3701
  latest.current = { asking, cards, focus };
3273
- useInput3((input, key) => {
3702
+ const turnImages = useMemo4(() => d.chat.flatMap((x) => (x.attachments ?? []).map((ref) => ({ ref, route: { machine: x.machineId } }))), [d.chat]);
3703
+ useInput4((input, key) => {
3274
3704
  if (key.ctrl && input === "n") {
3275
3705
  client.send({ type: "chat.reset" });
3276
3706
  return;
3277
3707
  }
3708
+ if (key.meta && input === "v" && latest.current.focus === -1) {
3709
+ paste();
3710
+ return;
3711
+ }
3712
+ if ((key.backspace || key.delete) && latest.current.focus === -1 && draft === "") {
3713
+ chips.removeLast();
3714
+ return;
3715
+ }
3716
+ if (key.meta && input === "o" && turnImages.length) {
3717
+ openImage2(turnImages);
3718
+ return;
3719
+ }
3278
3720
  if (key.tab) {
3279
3721
  const n = latest.current.cards.length;
3280
3722
  setFocus((f) => n === 0 ? -1 : f + 1 >= n ? -1 : f + 1);
@@ -3307,18 +3749,21 @@ function Ask() {
3307
3749
  const sessions = listed(d.sessions).length;
3308
3750
  const width = Math.max(20, columns - 6);
3309
3751
  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(Box10, { children: [
3314
- /* @__PURE__ */ jsxs9(Text11, { color: C.coral, children: [
3315
- g.cursor,
3316
- " "
3317
- ] }),
3318
- /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx12(TextInput, { value: draft, onChange: setDraft, onSubmit: ask, onEscape: () => {
3319
- if (!draft) go("overview");
3320
- }, active: focus === -1 }) })
3321
- ] }) })
3752
+ return /* @__PURE__ */ jsxs10(Frame, { crumb: `${t.tabs.ask}`, keys: [["\u21B5", t.keys.send], ["alt+v", t.keys.image], ...turnImages.length ? [["alt+o", t.keys.openImage]] : [], ["tab", t.keys.nextCard], ["s", t.keys.save], ["ctrl+n", t.keys.newChat], ["ctrl+c", t.keys.stopAnswer], ["esc", t.keys.back]], children: [
3753
+ /* @__PURE__ */ jsx13(TitledBox, { title: t.ask.operator(sessions, Math.max(1, machines.length)), flexGrow: 1, children: lines.length === 0 ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.ask.empty }) : /* @__PURE__ */ jsx13(Lines, { lines }) }),
3754
+ cards.slice(0, 2).map((c, i) => /* @__PURE__ */ jsx13(CardBox, { card: c, turn: d.chat.find((x) => x.qid === c.qid), focused: i === focus, busy: busy?.qid === c.qid }, `${c.kind}:${c.qid}`)),
3755
+ /* @__PURE__ */ jsxs10(TitledBox, { title: t.ask.question, flexShrink: 0, color: focus === -1 ? C.coral : C.line, children: [
3756
+ /* @__PURE__ */ jsx13(ChipsLine, { chips: chips.items }),
3757
+ /* @__PURE__ */ jsxs10(Box10, { children: [
3758
+ /* @__PURE__ */ jsxs10(Text12, { color: C.coral, children: [
3759
+ g.cursor,
3760
+ " "
3761
+ ] }),
3762
+ /* @__PURE__ */ jsx13(Box10, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx13(TextInput, { value: draft, onChange: setDraft, onSubmit: ask, onEscape: () => {
3763
+ if (!draft) go("overview");
3764
+ }, active: focus === -1 }) })
3765
+ ] })
3766
+ ] })
3322
3767
  ] });
3323
3768
  }
3324
3769
  function CardBox({ card, turn, focused, busy }) {
@@ -3327,120 +3772,120 @@ function CardBox({ card, turn, focused, busy }) {
3327
3772
  if (card.kind === "team") {
3328
3773
  const team = turn.team;
3329
3774
  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 })
3775
+ return /* @__PURE__ */ jsxs10(TitledBox, { title: t.ask.teamCard(team.name), flexShrink: 0, color, titleColor: C.coral, children: [
3776
+ /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3777
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: t.ask.folder.padEnd(13) }),
3778
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: team.cwd })
3334
3779
  ] }),
3335
- /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3336
- /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.table.padEnd(13) }),
3780
+ /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3781
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: t.ask.table.padEnd(13) }),
3337
3782
  g.team,
3338
3783
  " ",
3339
3784
  t.ask.newTable(team.projectName ?? team.project)
3340
3785
  ] }),
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: [
3786
+ team.seats.map((x) => /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3787
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: "".padEnd(13) }),
3788
+ /* @__PURE__ */ jsxs10(Text12, { color: C.violet, children: [
3344
3789
  "@",
3345
3790
  x.role
3346
3791
  ] }),
3347
3792
  " ",
3348
3793
  x.label,
3349
3794
  " ",
3350
- /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: [TOOL_NAME[x.tool], x.model, x.effort].filter(Boolean).join(" \xB7 ") }),
3795
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: [TOOL_NAME[x.tool], x.model, x.effort].filter(Boolean).join(" \xB7 ") }),
3351
3796
  " ",
3352
- /* @__PURE__ */ jsx12(Text11, { color: x.autonomy === "auto" ? C.yellow : C.faint, children: t.ask.autonomy[x.autonomy ?? "ask"] })
3797
+ /* @__PURE__ */ jsx13(Text12, { color: x.autonomy === "auto" ? C.yellow : C.faint, children: t.ask.autonomy[x.autonomy ?? "ask"] })
3353
3798
  ] }, 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 })
3799
+ auto.length ? /* @__PURE__ */ jsx13(Text12, { color: C.yellow, wrap: "truncate", children: t.ask.autoWarning(auto.join(", ")) }) : null,
3800
+ team.error ? /* @__PURE__ */ jsx13(Text12, { color: C.red, wrap: "truncate", children: team.error }) : null,
3801
+ /* @__PURE__ */ jsxs10(Box10, { columnGap: 2, children: [
3802
+ /* @__PURE__ */ jsx13(Button, { label: busy ? `${g.live} ${t.ask.launchTeam}` : t.ask.launchTeam, k: "\u21B5", primary: focused }),
3803
+ !team.template ? team.savedPath ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.ask.saved }) : /* @__PURE__ */ jsx13(Button, { label: t.ask.save, k: "s" }) : null,
3804
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.ask.nothingWithoutYou })
3360
3805
  ] })
3361
3806
  ] });
3362
3807
  }
3363
3808
  if (card.kind === "task") {
3364
3809
  const tasks = turn.tasks;
3365
3810
  const waits = !!turn.team && !turn.team.teamId;
3366
- return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.taskCard, flexShrink: 0, color, titleColor: C.violet, children: [
3811
+ return /* @__PURE__ */ jsxs10(TitledBox, { title: t.ask.taskCard, flexShrink: 0, color, titleColor: C.violet, children: [
3367
3812
  card.rows.map((r) => {
3368
3813
  const c = tasks[r];
3369
- return /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3370
- /* @__PURE__ */ jsx12(Text11, { color: C.fg, children: c.title }),
3814
+ return /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3815
+ /* @__PURE__ */ jsx13(Text12, { color: C.fg, children: c.title }),
3371
3816
  " ",
3372
- /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: c.toName ?? c.to }),
3373
- c.error ? /* @__PURE__ */ jsxs9(Text11, { color: C.red, children: [
3817
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: c.toName ?? c.to }),
3818
+ c.error ? /* @__PURE__ */ jsxs10(Text12, { color: C.red, children: [
3374
3819
  " ",
3375
3820
  c.error
3376
3821
  ] }) : null
3377
3822
  ] }, r);
3378
3823
  }),
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
3824
+ /* @__PURE__ */ jsxs10(Box10, { columnGap: 2, children: [
3825
+ /* @__PURE__ */ jsx13(Button, { label: busy ? `${g.live} ${t.ask.give}` : t.ask.give, k: "\u21B5", primary: focused && !waits }),
3826
+ waits ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.ask.waitTeam }) : null
3382
3827
  ] })
3383
3828
  ] });
3384
3829
  }
3385
3830
  if (card.kind === "launch") {
3386
3831
  const rows = proposalRows(turn);
3387
- return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.launchCard, flexShrink: 0, color, titleColor: C.coral, children: [
3832
+ return /* @__PURE__ */ jsxs10(TitledBox, { title: t.ask.launchCard, flexShrink: 0, color, titleColor: C.coral, children: [
3388
3833
  card.rows.map((r) => {
3389
3834
  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 }),
3835
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
3836
+ /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3837
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: t.ask.folder.padEnd(13) }),
3838
+ /* @__PURE__ */ jsx13(Text12, { color: C.fg, children: p.project }),
3394
3839
  " ",
3395
- /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: p.cwd })
3840
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: p.cwd })
3396
3841
  ] }),
3397
- /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3398
- /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.agent.padEnd(13) }),
3842
+ /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3843
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: t.ask.agent.padEnd(13) }),
3399
3844
  TOOL_NAME[p.tool]
3400
3845
  ] }),
3401
- p.prompt ? /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3402
- /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.instruction.padEnd(13) }),
3846
+ p.prompt ? /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3847
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: t.ask.instruction.padEnd(13) }),
3403
3848
  oneLine(p.prompt)
3404
3849
  ] }) : null,
3405
- p.error ? /* @__PURE__ */ jsx12(Text11, { color: C.red, wrap: "truncate", children: p.error }) : null
3850
+ p.error ? /* @__PURE__ */ jsx13(Text12, { color: C.red, wrap: "truncate", children: p.error }) : null
3406
3851
  ] }, r);
3407
3852
  }),
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 })
3853
+ /* @__PURE__ */ jsxs10(Box10, { columnGap: 2, children: [
3854
+ /* @__PURE__ */ jsx13(Button, { label: busy ? `${g.live} ${t.ask.launch}` : t.ask.launch, k: "\u21B5", primary: focused }),
3855
+ /* @__PURE__ */ jsx13(Button, { label: t.ask.ignore, k: "esc" }),
3856
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.ask.nothingWithoutYou })
3412
3857
  ] })
3413
3858
  ] });
3414
3859
  }
3415
3860
  if (card.kind === "relay") {
3416
3861
  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
3862
+ return /* @__PURE__ */ jsxs10(TitledBox, { title: t.ask.relayCard(r.name), flexShrink: 0, color, titleColor: C.violet, children: [
3863
+ /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", color: C.fg, children: oneLine(r.text) }),
3864
+ r.error ? /* @__PURE__ */ jsx13(Text12, { color: C.red, wrap: "truncate", children: r.error }) : null,
3865
+ /* @__PURE__ */ jsxs10(Box10, { columnGap: 2, children: [
3866
+ /* @__PURE__ */ jsx13(Button, { label: r.status === "queued" ? t.ask.cancelRelay : t.ask.relay, k: "\u21B5", primary: focused }),
3867
+ r.status === "queued" ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.ask.queued }) : null
3423
3868
  ] })
3424
3869
  ] });
3425
3870
  }
3426
3871
  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 }),
3872
+ return /* @__PURE__ */ jsxs10(TitledBox, { title: t.ask.closeCard, flexShrink: 0, color, titleColor: C.red, children: [
3873
+ card.rows.map((r) => /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3874
+ /* @__PURE__ */ jsx13(Text12, { color: C.fg, children: closes[r].name }),
3430
3875
  " ",
3431
- /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: TOOL_NAME[closes[r].tool] }),
3432
- closes[r].error ? /* @__PURE__ */ jsxs9(Text11, { color: C.red, children: [
3876
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: TOOL_NAME[closes[r].tool] }),
3877
+ closes[r].error ? /* @__PURE__ */ jsxs10(Text12, { color: C.red, children: [
3433
3878
  " ",
3434
3879
  closes[r].error
3435
3880
  ] }) : null
3436
3881
  ] }, r)),
3437
- /* @__PURE__ */ jsx12(Box10, { columnGap: 2, children: /* @__PURE__ */ jsx12(Button, { label: t.ask.close, k: "\u21B5", primary: focused }) })
3882
+ /* @__PURE__ */ jsx13(Box10, { columnGap: 2, children: /* @__PURE__ */ jsx13(Button, { label: t.ask.close, k: "\u21B5", primary: focused }) })
3438
3883
  ] });
3439
3884
  }
3440
3885
 
3441
3886
  // 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";
3887
+ import { useEffect as useEffect9, useMemo as useMemo5, useState as useState9 } from "react";
3888
+ import { Box as Box11, Text as Text13, useInput as useInput5 } from "ink";
3444
3889
 
3445
3890
  // ../fleet/src/launch.ts
3446
3891
  var MULTI_REPO_TOOLS = /* @__PURE__ */ new Set(["claude", "omp"]);
@@ -3508,25 +3953,25 @@ function launchMessage(f, name) {
3508
3953
 
3509
3954
  // src/tui/screens/Launch.tsx
3510
3955
  import { randomUUID as randomUUID3 } from "crypto";
3511
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3956
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3512
3957
  var FIELDS = ["prompt", "tool", "model", "effort", "project", "machine", "repos", "folder", "name"];
3513
3958
  var TEXT = /* @__PURE__ */ new Set(["prompt", "folder"]);
3514
3959
  var LAUNCH_MS = 2e4;
3515
3960
  var cycle = (list, cur, by) => list[((Math.max(0, list.indexOf(cur)) + by) % list.length + list.length) % list.length];
3516
3961
  function Launch() {
3517
- const [team, setTeam] = useState7(false);
3518
- return team ? /* @__PURE__ */ jsx13(TeamLaunch, { onSession: () => setTeam(false) }) : /* @__PURE__ */ jsx13(SessionLaunch, { onTeam: () => setTeam(true) });
3962
+ const [team, setTeam] = useState9(false);
3963
+ return team ? /* @__PURE__ */ jsx14(TeamLaunch, { onSession: () => setTeam(false) }) : /* @__PURE__ */ jsx14(SessionLaunch, { onTeam: () => setTeam(true) });
3519
3964
  }
3520
3965
  function SessionLaunch({ onTeam }) {
3521
3966
  const { client, d, machines, projects, t, g, toast, go, select } = useTui();
3522
3967
  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);
3968
+ const [form, setForm] = useState9(() => launchDefaults(list, machines, {}, "claude"));
3969
+ const [field, setField] = useState9("prompt");
3970
+ const [folder, setFolder] = useState9("");
3971
+ const [repoAt, setRepoAt] = useState9(0);
3972
+ const [launching, setLaunching] = useState9(null);
3528
3973
  useEditing(true);
3529
- useEffect8(() => {
3974
+ useEffect9(() => {
3530
3975
  if (form.machine === null) {
3531
3976
  const up = machines.find((m) => m.ready);
3532
3977
  if (up) setForm((f) => withMachine(f, list, up.id));
@@ -3546,10 +3991,10 @@ function SessionLaunch({ onTeam }) {
3546
3991
  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
3992
  const projectIdOf = (s) => projects ? projectOf(s, projects) : null;
3548
3993
  const drawn = useMemo5(() => previewOf(withFolder, d.sessions, projectIdOf), [form.machine, form.project, d.sessions.length]);
3549
- const [name, setName] = useState7(null);
3994
+ const [name, setName] = useState9(null);
3550
3995
  const shownName = name ?? drawn.name;
3551
3996
  const missing = missingOf(withFolder, machines);
3552
- useEffect8(() => {
3997
+ useEffect9(() => {
3553
3998
  if (!launching) return;
3554
3999
  if ((d.lastLaunched?.seq ?? 0) > launching.seq) {
3555
4000
  select(d.lastLaunched.id);
@@ -3567,7 +4012,7 @@ function SessionLaunch({ onTeam }) {
3567
4012
  const timer = setTimeout(() => setLaunching(null), Math.max(0, launching.at + LAUNCH_MS - Date.now()));
3568
4013
  return () => clearTimeout(timer);
3569
4014
  }, [launching, d.lastLaunched, d.errors]);
3570
- const launch = () => {
4015
+ const launch2 = () => {
3571
4016
  if (launching) return;
3572
4017
  if (missing) {
3573
4018
  toast({ text: t.launch.missing[missing], tone: "info" });
@@ -3577,13 +4022,13 @@ function SessionLaunch({ onTeam }) {
3577
4022
  setLaunching({ seq: d.lastLaunched?.seq ?? 0, errorSeq: d.errors.at(-1)?.seq ?? 0, at: Date.now(), name: shownName });
3578
4023
  client.send(launchMessage(withFolder, shownName));
3579
4024
  };
3580
- useInput4((input, key) => {
4025
+ useInput5((input, key) => {
3581
4026
  if (key.escape) {
3582
4027
  go("overview");
3583
4028
  return;
3584
4029
  }
3585
4030
  if (key.ctrl && input === "s") {
3586
- launch();
4031
+ launch2();
3587
4032
  return;
3588
4033
  }
3589
4034
  if (key.ctrl && input === "t") {
@@ -3603,7 +4048,7 @@ function SessionLaunch({ onTeam }) {
3603
4048
  return;
3604
4049
  }
3605
4050
  if (key.return) {
3606
- launch();
4051
+ launch2();
3607
4052
  return;
3608
4053
  }
3609
4054
  if (TEXT.has(field)) return;
@@ -3630,72 +4075,72 @@ function SessionLaunch({ onTeam }) {
3630
4075
  setRepoAt(0);
3631
4076
  }
3632
4077
  });
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 })
4078
+ const row = (f, label, value) => /* @__PURE__ */ jsxs11(Box11, { backgroundColor: field === f ? C.sel : void 0, children: [
4079
+ /* @__PURE__ */ jsx14(Text13, { color: field === f ? C.coral : C.faint, children: field === f ? "\u258C" : " " }),
4080
+ /* @__PURE__ */ jsx14(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: label }) }),
4081
+ /* @__PURE__ */ jsx14(Box11, { flexGrow: 1, overflow: "hidden", children: value })
3637
4082
  ] }, 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) }),
4083
+ const choice = (items, cur, label, color) => /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: items.map((x, i) => /* @__PURE__ */ jsxs11(Text13, { children: [
4084
+ x === cur ? /* @__PURE__ */ jsx14(Text13, { color: C.fg, bold: true, children: `${g.prev} ${label(x)} ${g.next}` }) : /* @__PURE__ */ jsx14(Text13, { color: color?.(x) ?? C.dim, children: label(x) }),
3640
4085
  " "
3641
4086
  ] }, 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: [
4087
+ return /* @__PURE__ */ jsxs11(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: [
4088
+ /* @__PURE__ */ jsxs11(TitledBox, { title: t.launch.title, flexGrow: 1, children: [
4089
+ /* @__PURE__ */ jsxs11(Box11, { children: [
4090
+ /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: " " }),
4091
+ /* @__PURE__ */ jsx14(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: t.launch.mode }) }),
4092
+ /* @__PURE__ */ jsxs11(Text13, { children: [
4093
+ /* @__PURE__ */ jsxs11(Text13, { color: C.coral, children: [
3649
4094
  g.radioOn,
3650
4095
  " ",
3651
4096
  t.launch.session
3652
4097
  ] }),
3653
4098
  " ",
3654
- /* @__PURE__ */ jsxs10(Text12, { color: C.dim, children: [
4099
+ /* @__PURE__ */ jsxs11(Text13, { color: C.dim, children: [
3655
4100
  g.radioOff,
3656
4101
  " ",
3657
4102
  t.launch.team
3658
4103
  ] }),
3659
4104
  " ",
3660
- /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: "ctrl+t" })
4105
+ /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: "ctrl+t" })
3661
4106
  ] })
3662
4107
  ] }),
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: [
4108
+ row("prompt", t.launch.instruction, /* @__PURE__ */ jsx14(TextInput, { value: form.prompt, onChange: (v) => setForm((f) => ({ ...f, prompt: v })), active: field === "prompt" })),
4109
+ /* @__PURE__ */ jsx14(Text13, { children: " " }),
4110
+ row("tool", t.launch.agent, /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: tools.map((x) => /* @__PURE__ */ jsxs11(Text13, { color: x === form.tool ? C.coral : C.dim, children: [
3666
4111
  x === form.tool ? g.radioOn : g.radioOff,
3667
4112
  " ",
3668
4113
  TOOL_NAME[x],
3669
4114
  " "
3670
4115
  ] }, 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 })),
4116
+ row("model", t.launch.model, models.status === "loading" ? /* @__PURE__ */ jsx14(Text13, { 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)),
4117
+ row("effort", t.launch.effort, efforts.length > 1 ? choice(efforts, form.effort, (x) => x ?? t.launch.defaultModel) : /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.defaultModel })),
3673
4118
  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 }),
4119
+ row("machine", t.launch.machine, /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: machines.map((m) => /* @__PURE__ */ jsxs11(Text13, { color: m.id === form.machine ? C.fg : C.dim, bold: m.id === form.machine, children: [
4120
+ /* @__PURE__ */ jsx14(Text13, { color: m.ready ? C.green : C.faint, children: g.machine }),
3676
4121
  " ",
3677
4122
  m.label,
3678
4123
  " "
3679
4124
  ] }, 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 }),
4125
+ row("repos", t.launch.repos, repoOptions.length === 0 ? /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.none }) : /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: repoOptions.map((r, i) => /* @__PURE__ */ jsxs11(Text13, { backgroundColor: field === "repos" && i === repoAt ? "#3a3532" : void 0, color: form.repos.includes(r) ? C.fg : C.dim, children: [
4126
+ /* @__PURE__ */ jsx14(Text13, { color: form.repos.includes(r) ? C.green : C.faint, children: form.repos.includes(r) ? g.boxOn : g.boxOff }),
3682
4127
  " ",
3683
4128
  r.split("/").filter(Boolean).at(-1) ?? r,
3684
4129
  " ",
3685
- /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: r }),
4130
+ /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: r }),
3686
4131
  " "
3687
4132
  ] }, 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: [
4133
+ row("folder", t.launch.folder, /* @__PURE__ */ jsx14(TextInput, { value: folder, onChange: setFolder, active: field === "folder", placeholder: t.launch.folderHint })),
4134
+ row("name", t.launch.name, /* @__PURE__ */ jsxs11(Text13, { children: [
3690
4135
  shownName,
3691
4136
  " ",
3692
- /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.drawn })
4137
+ /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.drawn })
3693
4138
  ] }))
3694
4139
  ] }),
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: [
4140
+ /* @__PURE__ */ jsxs11(TitledBox, { title: t.launch.preview, color: C.coral, titleColor: C.coral, children: [
4141
+ /* @__PURE__ */ jsxs11(Text13, { wrap: "truncate", children: [
4142
+ /* @__PURE__ */ jsx14(Text13, { color: C.fg, bold: true, children: project ? t.launch.joins(shownName, project.name) : shownName }),
4143
+ /* @__PURE__ */ jsxs11(Text13, { color: C.faint, children: [
3699
4144
  " \xB7 ",
3700
4145
  machine?.label ?? "-",
3701
4146
  " \xB7 ",
@@ -3705,10 +4150,10 @@ function SessionLaunch({ onTeam }) {
3705
4150
  form.effort ? ` \xB7 ${form.effort}` : ""
3706
4151
  ] })
3707
4152
  ] }),
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 })
4153
+ /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, columnGap: 2, children: [
4154
+ /* @__PURE__ */ jsx14(Button, { label: launching ? t.launch.launching : t.launch.launch, k: "ctrl+s", primary: true }),
4155
+ /* @__PURE__ */ jsx14(Button, { label: t.launch.cancel, k: "esc" }),
4156
+ missing ? /* @__PURE__ */ jsx14(Text13, { color: C.yellow, children: t.launch.missing[missing] }) : /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.thenAttach })
3712
4157
  ] })
3713
4158
  ] })
3714
4159
  ] });
@@ -3718,23 +4163,23 @@ var AUTONOMY_COLOR = { ask: C.faint, edits: C.dim, auto: C.yellow };
3718
4163
  function TeamLaunch({ onSession }) {
3719
4164
  const { client, machines, projects, t, g, toast, go } = useTui();
3720
4165
  const list = projects?.projects ?? [];
3721
- const [projectId, setProjectId] = useState7(list[0]?.id ?? null);
4166
+ const [projectId, setProjectId] = useState9(list[0]?.id ?? null);
3722
4167
  const project = list.find((p) => p.id === projectId);
3723
4168
  const folders = useMemo5(() => {
3724
4169
  const own = (project?.repos ?? []).filter((r) => r.machine === "local").map((r) => r.path);
3725
4170
  return own.length ? own : (machines.find((m) => m.id === "local")?.recents ?? []).slice(0, 6).map((r) => r.cwd);
3726
4171
  }, [project, machines]);
3727
- const [folderAt, setFolderAt] = useState7(0);
4172
+ const [folderAt, setFolderAt] = useState9(0);
3728
4173
  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);
4174
+ const [field, setField] = useState9("template");
4175
+ const [defs, setDefs] = useState9(null);
4176
+ const [template, setTemplate] = useState9(null);
4177
+ const [seats, setSeats] = useState9([]);
4178
+ const [seatAt, setSeatAt] = useState9(0);
4179
+ const [name, setName] = useState9("");
4180
+ const [launching, setLaunching] = useState9(false);
3736
4181
  useEditing(true);
3737
- useEffect8(() => {
4182
+ useEffect9(() => {
3738
4183
  if (!cwd) {
3739
4184
  setDefs({ roles: [], templates: [] });
3740
4185
  return;
@@ -3759,7 +4204,7 @@ function TeamLaunch({ onSession }) {
3759
4204
  }) : seats.filter((x) => x.on).map((x) => ({ id: x.role.id, tool: x.tool, autonomy: x.role.autonomy ?? "ask" }));
3760
4205
  const auto = chosen.filter((x) => x.autonomy === "auto").map((x) => `@${x.id}`);
3761
4206
  const teamName = name.trim() || tpl?.name || project?.name || "team";
3762
- const launch = () => {
4207
+ const launch2 = () => {
3763
4208
  if (launching || !cwd) return;
3764
4209
  if (!chosen.length) {
3765
4210
  toast({ text: t.launch.noSeat, tone: "info" });
@@ -3780,13 +4225,13 @@ function TeamLaunch({ onSession }) {
3780
4225
  };
3781
4226
  const tools = ["claude", "codex", "cursor", "omp"];
3782
4227
  const templates = [...(defs?.templates ?? []).map((x) => x.id), null];
3783
- useInput4((input, key) => {
4228
+ useInput5((input, key) => {
3784
4229
  if (key.escape) {
3785
4230
  go("overview");
3786
4231
  return;
3787
4232
  }
3788
4233
  if (key.ctrl && input === "s") {
3789
- launch();
4234
+ launch2();
3790
4235
  return;
3791
4236
  }
3792
4237
  if (key.ctrl && input === "t") {
@@ -3822,69 +4267,69 @@ function TeamLaunch({ onSession }) {
3822
4267
  else if (by) setSeats((xs) => xs.map((x, i) => i === seatAt ? { ...x, tool: cycle(tools, x.tool, by) } : x));
3823
4268
  }
3824
4269
  });
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 })
4270
+ const row = (f, label, value) => /* @__PURE__ */ jsxs11(Box11, { backgroundColor: field === f ? C.sel : void 0, children: [
4271
+ /* @__PURE__ */ jsx14(Text13, { color: field === f ? C.coral : C.faint, children: field === f ? "\u258C" : " " }),
4272
+ /* @__PURE__ */ jsx14(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: label }) }),
4273
+ /* @__PURE__ */ jsx14(Box11, { flexGrow: 1, overflow: "hidden", flexDirection: "column", children: value })
3829
4274
  ] }, 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: [
4275
+ return /* @__PURE__ */ jsxs11(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: [
4276
+ /* @__PURE__ */ jsxs11(TitledBox, { title: t.launch.teamTitle, flexGrow: 1, children: [
4277
+ /* @__PURE__ */ jsxs11(Box11, { children: [
4278
+ /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: " " }),
4279
+ /* @__PURE__ */ jsx14(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: t.launch.mode }) }),
4280
+ /* @__PURE__ */ jsxs11(Text13, { children: [
4281
+ /* @__PURE__ */ jsxs11(Text13, { color: C.dim, children: [
3837
4282
  g.radioOff,
3838
4283
  " ",
3839
4284
  t.launch.session
3840
4285
  ] }),
3841
4286
  " ",
3842
- /* @__PURE__ */ jsxs10(Text12, { color: C.coral, children: [
4287
+ /* @__PURE__ */ jsxs11(Text13, { color: C.coral, children: [
3843
4288
  g.radioOn,
3844
4289
  " ",
3845
4290
  t.launch.team
3846
4291
  ] }),
3847
4292
  " ",
3848
- /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: "ctrl+t" })
4293
+ /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: "ctrl+t" })
3849
4294
  ] })
3850
4295
  ] }),
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: [
4296
+ row("project", t.launch.project, /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: list.length === 0 ? /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.none }) : list.map((p) => /* @__PURE__ */ jsxs11(Text13, { color: p.id === projectId ? C.fg : projectColor(p.color), bold: p.id === projectId, children: [
3852
4297
  p.id === projectId ? `${g.prev} ${p.name} ${g.next}` : p.name,
3853
4298
  " "
3854
4299
  ] }, 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: [
4300
+ row("folder", t.launch.folder, /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: cwd ? /* @__PURE__ */ jsxs11(Fragment4, { children: [
4301
+ /* @__PURE__ */ jsx14(Text13, { color: C.fg, children: cwd }),
4302
+ folders.length > 1 ? /* @__PURE__ */ jsxs11(Text13, { color: C.faint, children: [
3858
4303
  " (",
3859
4304
  folderAt + 1,
3860
4305
  "/",
3861
4306
  folders.length,
3862
4307
  ")"
3863
4308
  ] }) : 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: [
4309
+ ] }) : /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.none }) })),
4310
+ row("template", t.launch.template, defs === null ? /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.loadingRoles }) : /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: templates.map((x) => /* @__PURE__ */ jsxs11(Text13, { color: x === template ? C.fg : C.dim, bold: x === template, children: [
3866
4311
  x === template ? `${g.prev} ${x ?? t.launch.custom} ${g.next}` : x ?? t.launch.custom,
3867
4312
  " "
3868
4313
  ] }, 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: [
4314
+ row("seats", t.launch.seats, tpl ? /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: chosen.map((x) => /* @__PURE__ */ jsxs11(Text13, { children: [
4315
+ /* @__PURE__ */ jsxs11(Text13, { color: C.violet, children: [
3871
4316
  "@",
3872
4317
  x.id
3873
4318
  ] }),
3874
4319
  " ",
3875
- /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: TOOL_NAME[x.tool] }),
4320
+ /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: TOOL_NAME[x.tool] }),
3876
4321
  " "
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 }),
4322
+ ] }, x.id)) }) : seats.length === 0 ? /* @__PURE__ */ jsx14(Text13, { color: C.faint, children: t.launch.noRole }) : /* @__PURE__ */ jsx14(Fragment4, { children: seats.map((x, i) => /* @__PURE__ */ jsxs11(Text13, { wrap: "truncate", backgroundColor: field === "seats" && i === seatAt ? "#3a3532" : void 0, children: [
4323
+ /* @__PURE__ */ jsx14(Text13, { color: x.on ? C.green : C.faint, children: x.on ? g.boxOn : g.boxOff }),
3879
4324
  " ",
3880
- /* @__PURE__ */ jsxs10(Text12, { color: C.violet, children: [
4325
+ /* @__PURE__ */ jsxs11(Text13, { color: C.violet, children: [
3881
4326
  "@",
3882
4327
  x.role.id
3883
4328
  ] }),
3884
4329
  " ",
3885
4330
  x.role.label,
3886
4331
  " ",
3887
- /* @__PURE__ */ jsxs10(Text12, { color: C.dim, children: [
4332
+ /* @__PURE__ */ jsxs11(Text13, { color: C.dim, children: [
3888
4333
  g.prev,
3889
4334
  " ",
3890
4335
  TOOL_NAME[x.tool],
@@ -3892,47 +4337,47 @@ function TeamLaunch({ onSession }) {
3892
4337
  g.next
3893
4338
  ] }),
3894
4339
  " ",
3895
- /* @__PURE__ */ jsx13(Text12, { color: AUTONOMY_COLOR[x.role.autonomy ?? "ask"], children: t.ask.autonomy[x.role.autonomy ?? "ask"] })
4340
+ /* @__PURE__ */ jsx14(Text13, { color: AUTONOMY_COLOR[x.role.autonomy ?? "ask"], children: t.ask.autonomy[x.role.autonomy ?? "ask"] })
3896
4341
  ] }, x.role.id)) })),
3897
- row("name", t.launch.teamName, /* @__PURE__ */ jsx13(TextInput, { value: name, onChange: setName, active: field === "name", placeholder: teamName }))
4342
+ row("name", t.launch.teamName, /* @__PURE__ */ jsx14(TextInput, { value: name, onChange: setName, active: field === "name", placeholder: teamName }))
3898
4343
  ] }),
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 }),
4344
+ /* @__PURE__ */ jsxs11(TitledBox, { title: t.launch.preview, color: C.coral, titleColor: C.coral, children: [
4345
+ /* @__PURE__ */ jsxs11(Text13, { wrap: "truncate", children: [
4346
+ /* @__PURE__ */ jsx14(Text13, { color: C.coral, children: g.team }),
3902
4347
  " ",
3903
- /* @__PURE__ */ jsx13(Text12, { color: C.fg, bold: true, children: t.launch.teamPreview(teamName, project?.name ?? cwd ?? "-") })
4348
+ /* @__PURE__ */ jsx14(Text13, { color: C.fg, bold: true, children: t.launch.teamPreview(teamName, project?.name ?? cwd ?? "-") })
3904
4349
  ] }),
3905
- /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: chosen.map((x) => /* @__PURE__ */ jsxs10(Text12, { children: [
3906
- /* @__PURE__ */ jsxs10(Text12, { color: C.violet, children: [
4350
+ /* @__PURE__ */ jsx14(Text13, { wrap: "truncate", children: chosen.map((x) => /* @__PURE__ */ jsxs11(Text13, { children: [
4351
+ /* @__PURE__ */ jsxs11(Text13, { color: C.violet, children: [
3907
4352
  "@",
3908
4353
  x.id
3909
4354
  ] }),
3910
4355
  " ",
3911
- /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: TOOL_NAME[x.tool] }),
4356
+ /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: TOOL_NAME[x.tool] }),
3912
4357
  " ",
3913
- /* @__PURE__ */ jsx13(Text12, { color: AUTONOMY_COLOR[x.autonomy], children: t.ask.autonomy[x.autonomy] }),
4358
+ /* @__PURE__ */ jsx14(Text13, { color: AUTONOMY_COLOR[x.autonomy], children: t.ask.autonomy[x.autonomy] }),
3914
4359
  " "
3915
4360
  ] }, 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" })
4361
+ auto.length ? /* @__PURE__ */ jsx14(Text13, { color: C.yellow, wrap: "truncate", children: t.ask.autoWarning(auto.join(", ")) }) : null,
4362
+ /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, columnGap: 2, children: [
4363
+ /* @__PURE__ */ jsx14(Button, { label: launching ? t.launch.launching : t.ask.launchTeam, k: "ctrl+s", primary: true }),
4364
+ /* @__PURE__ */ jsx14(Button, { label: t.launch.cancel, k: "esc" })
3920
4365
  ] })
3921
4366
  ] })
3922
4367
  ] });
3923
4368
  }
3924
4369
 
3925
4370
  // 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";
4371
+ import { useState as useState10 } from "react";
4372
+ import { Box as Box12, Text as Text14 } from "ink";
4373
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3929
4374
  function Settings() {
3930
4375
  const tui = useTui();
3931
4376
  const { t, g, lang, setLang } = tui;
3932
4377
  const near = useContextNear(tui.client);
3933
4378
  const known2 = tui.client.fleet?.contextNear() !== void 0;
3934
4379
  const rows = [{ id: "lang", label: t.settings.language }, { id: "context", label: t.settings.context }];
3935
- const [at, setAt] = useState8(0);
4380
+ const [at, setAt] = useState10(0);
3936
4381
  useScreenKeys((input, key) => {
3937
4382
  if (key.upArrow || input === "k") {
3938
4383
  setAt((i) => (i - 1 + rows.length) % rows.length);
@@ -3956,20 +4401,20 @@ function Settings() {
3956
4401
  }
3957
4402
  return true;
3958
4403
  });
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: [
4404
+ return /* @__PURE__ */ jsx15(Frame, { crumb: t.settings.title, keys: [["\u2191 \u2193", t.keys.navigate], ["\u2190 \u2192", t.keys.change], ["esc", t.keys.back]], children: /* @__PURE__ */ jsx15(TitledBox, { title: t.settings.title, flexGrow: 1, children: rows.map((r, i) => /* @__PURE__ */ jsxs12(Box12, { backgroundColor: i === at ? C.sel : void 0, children: [
4405
+ /* @__PURE__ */ jsx15(Text14, { color: i === at ? C.coral : C.faint, children: i === at ? "\u258C" : " " }),
4406
+ /* @__PURE__ */ jsx15(Box12, { width: 16, children: /* @__PURE__ */ jsx15(Text14, { color: C.dim, children: r.label }) }),
4407
+ r.id === "lang" ? /* @__PURE__ */ jsx15(Text14, { children: LANGS.map((l) => /* @__PURE__ */ jsxs12(Text14, { color: l === lang ? C.fg : C.dim, bold: l === lang, children: [
3963
4408
  l === lang ? `${g.radioOn} ` : `${g.radioOff} `,
3964
4409
  t.lang[l],
3965
4410
  " "
3966
- ] }, l)) }) : /* @__PURE__ */ jsxs11(Text13, { children: [
3967
- /* @__PURE__ */ jsxs11(Text13, { color: C.fg, bold: true, children: [
4411
+ ] }, l)) }) : /* @__PURE__ */ jsxs12(Text14, { children: [
4412
+ /* @__PURE__ */ jsxs12(Text14, { color: C.fg, bold: true, children: [
3968
4413
  "\u25C2 ",
3969
4414
  Math.round(near * 100),
3970
4415
  " % \u25B8"
3971
4416
  ] }),
3972
- /* @__PURE__ */ jsxs11(Text13, { color: C.faint, children: [
4417
+ /* @__PURE__ */ jsxs12(Text14, { color: C.faint, children: [
3973
4418
  " ",
3974
4419
  known2 ? t.settings.contextHint : t.settings.contextUnknown
3975
4420
  ] })
@@ -3978,8 +4423,8 @@ function Settings() {
3978
4423
  }
3979
4424
 
3980
4425
  // 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";
4426
+ import { Box as Box13, Text as Text15 } from "ink";
4427
+ import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
3983
4428
  function Help() {
3984
4429
  const { t } = useTui();
3985
4430
  const k = t.keys;
@@ -3991,23 +4436,23 @@ function Help() {
3991
4436
  [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
4437
  [t.help.settings, [["\u2191 \u2193", k.navigate], ["\u2190 \u2192", k.change]]]
3993
4438
  ];
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 })
4439
+ return /* @__PURE__ */ jsx16(Frame, { crumb: t.help.title, keys: [["esc", k.back]], children: /* @__PURE__ */ jsx16(TitledBox, { title: t.help.title, flexGrow: 1, children: /* @__PURE__ */ jsx16(Box13, { flexWrap: "wrap", columnGap: 4, children: groups.map(([title2, keys]) => /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", width: 40, marginBottom: 1, children: [
4440
+ /* @__PURE__ */ jsx16(Text15, { color: C.coral, bold: true, children: title2 }),
4441
+ keys.map(([key, label]) => /* @__PURE__ */ jsxs13(Text15, { wrap: "truncate", children: [
4442
+ /* @__PURE__ */ jsx16(Text15, { color: C.fg, children: key.padEnd(16) }),
4443
+ /* @__PURE__ */ jsx16(Text15, { color: C.dim, children: label })
3999
4444
  ] }, key + label))
4000
4445
  ] }, title2)) }) }) });
4001
4446
  }
4002
4447
 
4003
4448
  // src/tui/App.tsx
4004
- import { jsx as jsx16 } from "react/jsx-runtime";
4449
+ import { jsx as jsx17 } from "react/jsx-runtime";
4005
4450
  var TOAST_MS = 5e3;
4006
4451
  function App(props) {
4007
- const [lang, setLangState] = useState9(props.lang);
4008
- return /* @__PURE__ */ jsx16(I18nProvider, { lang, children: /* @__PURE__ */ jsx16(Shell, { ...props, lang, setLang: setLangState }) });
4452
+ const [lang, setLangState] = useState11(props.lang);
4453
+ return /* @__PURE__ */ jsx17(I18nProvider, { lang, children: /* @__PURE__ */ jsx17(Shell, { ...props, lang, setLang: setLangState }) });
4009
4454
  }
4010
- function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs, env, run: run2 }) {
4455
+ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs, env, run: run2, readImage, openFile }) {
4011
4456
  const { exit, suspendTerminal } = useApp();
4012
4457
  const { stdout } = useStdout();
4013
4458
  const write = useCallback((seq) => {
@@ -4020,18 +4465,18 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
4020
4465
  const tasks = useTasks(client);
4021
4466
  const fleet = client.fleet;
4022
4467
  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(() => {
4468
+ const [screen, setScreen] = useState11("overview");
4469
+ const [back, setBack] = useState11("overview");
4470
+ const [selectedId, setSelectedId] = useState11(null);
4471
+ const [lastToast, setToast] = useState11(null);
4472
+ const [editing, setEditingState] = useState11(false);
4473
+ const [now, setNow] = useState11(() => Date.now());
4474
+ const keys = useRef9(null);
4475
+ useEffect10(() => {
4031
4476
  const timer = setInterval(() => setNow(Date.now()), 1e3);
4032
4477
  return () => clearInterval(timer);
4033
4478
  }, []);
4034
- useEffect9(() => {
4479
+ useEffect10(() => {
4035
4480
  if (!lastToast) return;
4036
4481
  const timer = setTimeout(() => setToast(null), TOAST_MS);
4037
4482
  return () => clearTimeout(timer);
@@ -4039,14 +4484,14 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
4039
4484
  const toast = useCallback((x) => setToast(x), []);
4040
4485
  const setEditing = useCallback((on) => setEditingState(on), []);
4041
4486
  const view = useMemo6(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
4042
- const order = useRef8([]);
4043
- useEffect9(() => {
4487
+ const order = useRef9([]);
4488
+ useEffect10(() => {
4044
4489
  setSelectedId((cur) => cur === null && view.yours[0] ? view.yours[0].id : keepSelection(view.order, cur, order.current));
4045
4490
  order.current = view.order;
4046
4491
  }, [view]);
4047
4492
  const selected = (selectedId ? d.byId.get(selectedId) : void 0) ?? null;
4048
- const seen = useRef8(null);
4049
- useEffect9(() => {
4493
+ const seen = useRef9(null);
4494
+ useEffect10(() => {
4050
4495
  if (seen.current) for (const text of asked(seen.current, d.sessions, t.notify)) write(notification(text, notify, env));
4051
4496
  seen.current = d.byId;
4052
4497
  write(title(t.notify.title(listed(d.sessions).filter(isPending).length)));
@@ -4098,9 +4543,11 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
4098
4543
  actions,
4099
4544
  setEditing,
4100
4545
  attach,
4101
- keys
4546
+ keys,
4547
+ readImage: readImage ?? (() => readClipboardImage(env)),
4548
+ openFile: openFile ?? ((file) => openImage(file, env))
4102
4549
  };
4103
- useInput5((input, key) => {
4550
+ useInput6((input, key) => {
4104
4551
  if (key.ctrl && input === "c") {
4105
4552
  if (keys.current?.(input, key)) return;
4106
4553
  exit();
@@ -4138,7 +4585,7 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
4138
4585
  }
4139
4586
  });
4140
4587
  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, {}) });
4588
+ return /* @__PURE__ */ jsx17(TuiContext.Provider, { value: tui, children: /* @__PURE__ */ jsx17(Body, {}) });
4142
4589
  }
4143
4590
 
4144
4591
  // src/tui/fleet.ts
@@ -4194,7 +4641,9 @@ var UNICODE = {
4194
4641
  bar: "\u2502",
4195
4642
  /** A team's table and the common table (roles spec §3.9). */
4196
4643
  team: "\u25A3",
4197
- common: "\u25AD"
4644
+ common: "\u25AD",
4645
+ /** A task handed between seats in « Échanges » (#92). */
4646
+ task: "\u25A4"
4198
4647
  };
4199
4648
  var ASCII = {
4200
4649
  approval: "!",
@@ -4227,7 +4676,8 @@ var ASCII = {
4227
4676
  ellipsis: "...",
4228
4677
  bar: "|",
4229
4678
  team: "[T]",
4230
- common: "[ ]"
4679
+ common: "[ ]",
4680
+ task: "[=]"
4231
4681
  };
4232
4682
  function glyphsFor(env) {
4233
4683
  if (env.LEDGE_TUI_ASCII === "1") return ASCII;
@@ -4237,10 +4687,10 @@ function glyphsFor(env) {
4237
4687
 
4238
4688
  // src/tui/settings.ts
4239
4689
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
4240
- import { join } from "path";
4690
+ import { join as join3 } from "path";
4241
4691
  import { z } from "zod";
4242
4692
  var SettingsSchema = z.object({ lang: z.enum(["fr", "en"]).optional() }).passthrough();
4243
- var settingsPath = () => join(ledgeDir(), "tui.json");
4693
+ var settingsPath = () => join3(ledgeDir(), "tui.json");
4244
4694
  function defaultLang(env) {
4245
4695
  const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || "";
4246
4696
  return /^fr/i.test(locale) ? "fr" : "en";
@@ -4261,9 +4711,9 @@ function saveSettings(settings) {
4261
4711
  }
4262
4712
 
4263
4713
  // src/tui/terminalProfile.ts
4264
- import { execFile, spawn as spawn2 } from "child_process";
4265
- import { mkdir, readFile, realpath, writeFile } from "fs/promises";
4266
- import { dirname, join as join2 } from "path";
4714
+ import { execFile as execFile2, spawn as spawn3 } from "child_process";
4715
+ import { mkdir as mkdir2, readFile as readFile2, realpath, writeFile as writeFile2 } from "fs/promises";
4716
+ import { dirname, join as join4 } from "path";
4267
4717
  import { fileURLToPath } from "url";
4268
4718
  import { promisify } from "util";
4269
4719
  var PROFILE = "Ledge";
@@ -4273,7 +4723,7 @@ var SETTINGS = [
4273
4723
  "Packages/Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe/LocalState/settings.json",
4274
4724
  "Microsoft/Windows Terminal/settings.json"
4275
4725
  ];
4276
- var fragmentDir = (localAppData) => join2(localAppData, "Microsoft", "Windows Terminal", "Fragments", PROFILE);
4726
+ var fragmentDir = (localAppData) => join4(localAppData, "Microsoft", "Windows Terminal", "Fragments", PROFILE);
4277
4727
  var PROFILE_ID = "{d10fee13-0978-595f-a851-b60ec80f11c9}";
4278
4728
  function movesToLedgeTab(env, here, script) {
4279
4729
  return !here && !!env.WT_SESSION && !!env.WSL_DISTRO_NAME && !env.TMUX && env.WT_PROFILE_ID !== PROFILE_ID && !script.endsWith(".ts");
@@ -4301,37 +4751,38 @@ function fragment(commandline, iconWin) {
4301
4751
  }
4302
4752
  async function profileLoaded(localAppData) {
4303
4753
  for (const rel of SETTINGS) {
4304
- const text = await readFile(join2(localAppData, rel), "utf8").catch(() => "");
4754
+ const text = await readFile2(join4(localAppData, rel), "utf8").catch(() => "");
4305
4755
  if (new RegExp(`"source"\\s*:\\s*"${PROFILE}"`).test(text)) return true;
4306
4756
  }
4307
4757
  return false;
4308
4758
  }
4309
4759
  async function writeIfChanged(path, content) {
4310
- const current = await readFile(path).catch(() => null);
4760
+ const current = await readFile2(path).catch(() => null);
4311
4761
  if (current && current.equals(Buffer.from(content))) return false;
4312
- await writeFile(path, content);
4762
+ await writeFile2(path, content);
4313
4763
  return true;
4314
4764
  }
4315
4765
  async function openInLedgeTab(o) {
4316
4766
  const commandline = profileCommandline(o.distro, o.launcher);
4317
4767
  if (!commandline) return false;
4318
- await mkdir(dirname(o.launcher), { recursive: true });
4768
+ await mkdir2(dirname(o.launcher), { recursive: true });
4319
4769
  await writeIfChanged(o.launcher, launcherScript(o.node, o.script, o.ledgeHome));
4320
4770
  const dir = fragmentDir(o.localAppData.linux);
4321
- await mkdir(dir, { recursive: true });
4322
- await writeIfChanged(join2(dir, "ledge.ico"), await readFile(o.icon ?? ICON));
4771
+ await mkdir2(dir, { recursive: true });
4772
+ await writeIfChanged(join4(dir, "ledge.ico"), await readFile2(o.icon ?? ICON));
4323
4773
  const iconWin = `${o.localAppData.win}\\Microsoft\\Windows Terminal\\Fragments\\${PROFILE}\\ledge.ico`;
4324
- await writeIfChanged(join2(dir, "ledge.json"), fragment(commandline, iconWin));
4325
- const deadline = Date.now() + (o.waitMs ?? 3e3);
4326
- while (!await profileLoaded(o.localAppData.linux)) {
4774
+ await writeIfChanged(join4(dir, "ledge.json"), fragment(commandline, iconWin));
4775
+ const deadline = Date.now() + (o.waitMs ?? 15e3);
4776
+ for (let first = true; !await profileLoaded(o.localAppData.linux); first = false) {
4327
4777
  if (Date.now() > deadline) return false;
4778
+ if (first) o.onWait?.();
4328
4779
  await new Promise((r) => setTimeout(r, 100));
4329
4780
  }
4330
4781
  await o.wt(["-w", "0", "new-tab", "-p", PROFILE]);
4331
4782
  return true;
4332
4783
  }
4333
- var run = promisify(execFile);
4334
- async function openTuiInLedgeTab(env) {
4784
+ var run = promisify(execFile2);
4785
+ async function openTuiInLedgeTab(env, onWait) {
4335
4786
  try {
4336
4787
  const cmd = "/mnt/c/Windows/System32/cmd.exe";
4337
4788
  const win = (await run(cmd, ["/d", "/c", "echo %LOCALAPPDATA%"], { cwd: "/mnt/c", timeout: 5e3 })).stdout.trim();
@@ -4342,10 +4793,11 @@ async function openTuiInLedgeTab(env) {
4342
4793
  distro: env.WSL_DISTRO_NAME,
4343
4794
  node: await realpath(process.execPath),
4344
4795
  script: await realpath(process.argv[1]),
4345
- launcher: join2(ledgeDir(), "tui.sh"),
4796
+ launcher: join4(ledgeDir(), "tui.sh"),
4346
4797
  ledgeHome: env.LEDGE_HOME,
4798
+ onWait,
4347
4799
  wt: (args) => new Promise((resolve, reject) => {
4348
- const child = spawn2(join2(linux, "Microsoft", "WindowsApps", "wt.exe"), args, { cwd: "/mnt/c", stdio: "ignore", detached: true });
4800
+ const child = spawn3(join4(linux, "Microsoft", "WindowsApps", "wt.exe"), args, { cwd: "/mnt/c", stdio: "ignore", detached: true });
4349
4801
  child.once("error", reject);
4350
4802
  child.once("spawn", () => {
4351
4803
  child.unref();
@@ -4359,7 +4811,7 @@ async function openTuiInLedgeTab(env) {
4359
4811
  }
4360
4812
 
4361
4813
  // src/tui/index.tsx
4362
- import { jsx as jsx17 } from "react/jsx-runtime";
4814
+ import { jsx as jsx18 } from "react/jsx-runtime";
4363
4815
  var TUI_USAGE = "ledge tui [--notify bell|osc9|off] [--here]";
4364
4816
  function tuiArgs(args) {
4365
4817
  let notify = "bell";
@@ -4388,7 +4840,7 @@ async function runTui(args) {
4388
4840
  console.error(MESSAGES[lang].cli.notTty);
4389
4841
  return 2;
4390
4842
  }
4391
- if (movesToLedgeTab(process.env, parsed.here, process.argv[1] ?? "") && await openTuiInLedgeTab(process.env)) {
4843
+ if (movesToLedgeTab(process.env, parsed.here, process.argv[1] ?? "") && await openTuiInLedgeTab(process.env, () => console.log(MESSAGES[lang].cli.loadingProfile))) {
4392
4844
  console.log(MESSAGES[lang].cli.movedToTab);
4393
4845
  return 0;
4394
4846
  }
@@ -4396,7 +4848,7 @@ async function runTui(args) {
4396
4848
  const detach = attachClient(client);
4397
4849
  client.connect();
4398
4850
  const app = render(
4399
- /* @__PURE__ */ jsx17(App, { client, lang, saveLang: (next2) => saveSettings({ ...loadSettings(), lang: next2 }), notify: parsed.notify, glyphs: glyphsFor(process.env), env: process.env }),
4851
+ /* @__PURE__ */ jsx18(App, { client, lang, saveLang: (next2) => saveSettings({ ...loadSettings(), lang: next2 }), notify: parsed.notify, glyphs: glyphsFor(process.env), env: process.env }),
4400
4852
  // Incremental: a key rewrites the lines it changed, not the whole screen erased then drawn again (the flicker).
4401
4853
  { alternateScreen: true, exitOnCtrlC: false, maxFps: 30, incrementalRendering: true }
4402
4854
  );