@paradow/ledge 0.17.0 → 0.19.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 (40) hide show
  1. package/README.md +7 -1
  2. package/assets/ledge.ico +0 -0
  3. package/dist/cli/{attach-EYIX7FZN.js → attach-3BKQSEWC.js} +12 -7
  4. package/dist/cli/{bridge-6UHCD76N.js → bridge-APNFBPNE.js} +2 -2
  5. package/dist/cli/chunk-3EIRKAX7.js +165 -0
  6. package/dist/cli/chunk-3HCID6NW.js +145 -0
  7. package/dist/cli/{chunk-JQZHXVIZ.js → chunk-43D6NMYH.js} +1 -1
  8. package/dist/cli/chunk-4RD7OJ2J.js +6 -0
  9. package/dist/cli/{chunk-ELLXOV4Q.js → chunk-7ZAA75HG.js} +1 -1
  10. package/dist/cli/{chunk-5ZCHFGCV.js → chunk-BVBX222F.js} +9 -7
  11. package/dist/cli/{chunk-OVJL2QTN.js → chunk-FQZ3IX6E.js} +2 -2
  12. package/dist/cli/{chunk-VVK4I6B5.js → chunk-GXFU4XAX.js} +1 -1
  13. package/dist/cli/chunk-HP3G2TYJ.js +1772 -0
  14. package/dist/cli/{chunk-NC5YAEJB.js → chunk-KKNE7BFZ.js} +55 -3
  15. package/dist/cli/{chunk-36KJUV62.js → chunk-LOOOHYZR.js} +38 -12
  16. package/dist/cli/{chunk-VE4LGNXX.js → chunk-TRTS24PF.js} +23 -5
  17. package/dist/cli/{chunk-Q7YNLP66.js → chunk-WCS24ICX.js} +5 -3
  18. package/dist/cli/{chunk-LAUNHDTX.js → chunk-WJ6YMZTT.js} +2 -2
  19. package/dist/cli/{daemon-QEJLAIBT.js → chunk-WP77O5LC.js} +6045 -7228
  20. package/dist/cli/chunk-Y5DJ5INP.js +60 -0
  21. package/dist/cli/{chunk-AQ25HFBP.js → chunk-ZUYOA55W.js} +5 -4
  22. package/dist/cli/daemon-66EUIZ3J.js +2240 -0
  23. package/dist/cli/handover-UXISKYPS.js +16 -0
  24. package/dist/cli/{hook-AE5HC6AS.js → hook-5UAARBWU.js} +86 -25
  25. package/dist/cli/index.js +30 -16
  26. package/dist/cli/launch-YQYCDZ7N.js +34 -0
  27. package/dist/cli/{send-2OZ3YZRV.js → send-NLVR6PL2.js} +5 -5
  28. package/dist/cli/{setup-2VM4GCDJ.js → setup-EEIXRAMU.js} +4 -4
  29. package/dist/cli/{status-3VJAO6UL.js → status-2OCYHOWZ.js} +3 -3
  30. package/dist/cli/{task-PMKNYP6M.js → task-AOOWHPMN.js} +5 -5
  31. package/dist/cli/team-TVNSVYWW.js +101 -0
  32. package/dist/cli/{tui-DWA2EDC2.js → tui-6JY7IS4D.js} +985 -125
  33. package/dist/cli/{uninstall-AKUWE33D.js → uninstall-UUN7LIIT.js} +4 -4
  34. package/dist/cli/{version-WOGCIZY3.js → version-XSEI3V2C.js} +1 -1
  35. package/dist/cli/who-52MENZTT.js +11 -0
  36. package/package.json +2 -2
  37. package/dist/cli/chunk-QD27W6W5.js +0 -1474
  38. package/dist/cli/chunk-V2H7UD2N.js +0 -6
  39. package/dist/cli/launch-B4G5OFBV.js +0 -27
  40. package/dist/cli/who-37TNDYVA.js +0 -11
@@ -1,11 +1,21 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-36KJUV62.js";
4
- import "./chunk-VVK4I6B5.js";
5
- import "./chunk-VE4LGNXX.js";
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";
6
11
  import {
12
+ CONTEXT_NEAR,
13
+ CONTEXT_NEAR_MAX,
14
+ CONTEXT_NEAR_MIN,
7
15
  ChatTurnSchema,
16
+ ContextPrefsSchema,
8
17
  IslandPrefsSchema,
18
+ LEDGE_GIVER,
9
19
  MachinesSchema,
10
20
  NOTE_MAX,
11
21
  NOTE_NOT_TYPED,
@@ -13,6 +23,7 @@ import {
13
23
  PROTOCOL_PORT,
14
24
  ProjectsSchema,
15
25
  backendOf,
26
+ contextNear,
16
27
  isArchived,
17
28
  offers,
18
29
  parseServerMessage,
@@ -20,13 +31,14 @@ import {
20
31
  pickName,
21
32
  projectOf,
22
33
  proposalRows,
23
- readDaemonToken
24
- } from "./chunk-QD27W6W5.js";
25
- import "./chunk-LAUNHDTX.js";
26
- import "./chunk-JQZHXVIZ.js";
34
+ readDaemonToken,
35
+ taskClosed
36
+ } from "./chunk-HP3G2TYJ.js";
37
+ import "./chunk-WJ6YMZTT.js";
38
+ import "./chunk-43D6NMYH.js";
27
39
  import {
28
40
  ledgeDir
29
- } from "./chunk-ELLXOV4Q.js";
41
+ } from "./chunk-7ZAA75HG.js";
30
42
 
31
43
  // src/tui/index.tsx
32
44
  import { render } from "ink";
@@ -226,7 +238,7 @@ var daemonStore = {
226
238
  return;
227
239
  }
228
240
  if (m.type === "chat.step" || m.type === "chat.delta" || m.type === "chat.done" || m.type === "chat.error") {
229
- const chat = state.chat.map((t) => t.qid !== m.qid ? t : m.type === "chat.step" ? { ...t, steps: [...t.steps, m.step] } : m.type === "chat.done" ? { ...t, answer: m.text, answeredAt: m.at, ...m.proposal ? { proposal: m.proposal } : {}, ...m.proposals ? { proposals: m.proposals } : {}, ...m.relay ? { relay: m.relay } : {}, ...m.closes ? { closes: m.closes } : {}, ...m.projectProposal ? { projectProposal: m.projectProposal } : {} } : m.type === "chat.error" ? { ...t, error: m.message } : t);
241
+ const chat = state.chat.map((t) => t.qid !== m.qid ? t : m.type === "chat.step" ? { ...t, steps: [...t.steps, m.step] } : m.type === "chat.done" ? { ...t, answer: m.text, answeredAt: m.at, ...m.proposal ? { proposal: m.proposal } : {}, ...m.proposals ? { proposals: m.proposals } : {}, ...m.relay ? { relay: m.relay } : {}, ...m.closes ? { closes: m.closes } : {}, ...m.projectProposal ? { projectProposal: m.projectProposal } : {}, ...m.team ? { team: m.team } : {}, ...m.tasks ? { tasks: m.tasks } : {} } : m.type === "chat.error" ? { ...t, error: m.message } : t);
230
242
  const chatLive = m.type === "chat.delta" ? { qid: m.qid, text: (state.chatLive?.qid === m.qid ? state.chatLive.text : "") + m.text } : m.type === "chat.step" ? state.chatLive : null;
231
243
  state = { ...state, chat, chatLive };
232
244
  emit();
@@ -420,6 +432,18 @@ function useMachines(client) {
420
432
  return useSyncExternalStore3(client.fleet?.subscribe ?? noopSubscribe, client.fleet?.machines ?? emptySnapshot, emptySnapshot);
421
433
  }
422
434
  var nullSnapshot = () => null;
435
+ var noTasks = [];
436
+ var emptyTasks = () => noTasks;
437
+ function useTeams(client) {
438
+ return useSyncExternalStore3(client.fleet?.subscribe ?? noopSubscribe, client.fleet?.teams ?? nullSnapshot, nullSnapshot);
439
+ }
440
+ var defaultNear = () => CONTEXT_NEAR;
441
+ function useContextNear(client) {
442
+ return useSyncExternalStore3(client.fleet?.subscribe ?? noopSubscribe, () => client.fleet?.contextNear() ?? CONTEXT_NEAR, defaultNear);
443
+ }
444
+ function useTasks(client) {
445
+ return useSyncExternalStore3(client.fleet?.subscribe ?? noopSubscribe, client.fleet?.tasks ?? emptyTasks, emptyTasks);
446
+ }
423
447
  function useProjects(client) {
424
448
  return useSyncExternalStore3(client.fleet?.subscribe ?? noopSubscribe, client.fleet?.projects ?? nullSnapshot, nullSnapshot);
425
449
  }
@@ -493,8 +517,31 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
493
517
  let source = "local";
494
518
  let projects = null;
495
519
  let island;
520
+ let contextNear2;
496
521
  let mobile;
497
522
  let mobileError;
523
+ let teams = null;
524
+ let tasks = /* @__PURE__ */ new Map();
525
+ let taskList = [];
526
+ const taskLists = /* @__PURE__ */ new Set();
527
+ let listSeq = 0;
528
+ const local = (id) => id === LEDGE_GIVER ? id : qualify("local", id);
529
+ const teamOut = (t) => ({ ...t, seats: t.seats.map((s) => ({ ...s, sessionId: s.sessionId === null ? null : qualify("local", s.sessionId) })) });
530
+ const taskOut = (t) => ({ ...t, giver: local(t.giver), owner: local(t.owner) });
531
+ const putTasks = (list) => {
532
+ for (const t of list) tasks.set(t.id, taskOut(t));
533
+ taskList = [...tasks.values()].sort((a, b) => a.at.localeCompare(b.at));
534
+ listeners.forEach((fn) => fn());
535
+ };
536
+ const askTasks = (p) => {
537
+ const ask = (extra) => {
538
+ const rid = `fleet-tasks-${++listSeq}`;
539
+ taskLists.add(rid);
540
+ p.client.send({ type: "task.cmd", rid, by: LEDGE_GIVER, op: "list", all: true, ...extra });
541
+ };
542
+ ask({});
543
+ for (const t of teams ?? []) if (t.state !== "dissolved") ask({ team: t.id });
544
+ };
498
545
  let routing = null, resetAt = "", lastStatus = "connecting";
499
546
  const emit2 = (m) => messages.forEach((fn) => fn(m));
500
547
  const day = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -528,7 +575,16 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
528
575
  const available = (p) => !!p && p.status === "open" && p.fresh && !!p.snapshot;
529
576
  const status = () => [...peers.values()].some(available) ? "open" : [...peers.values()].some((p) => p.status === "connecting") ? "connecting" : "unreachable";
530
577
  const stale = (p) => !!opts.keepStale && !!p && !available(p) && !!p.snapshot;
531
- const session = (p, s) => ({ ...s, id: qualify(p.config.id, s.id), machineId: p.config.id, machineName: p.config.label, ...stale(p) ? { offline: true } : {} });
578
+ const session = (p, s) => ({
579
+ ...s,
580
+ id: qualify(p.config.id, s.id),
581
+ machineId: p.config.id,
582
+ machineName: p.config.label,
583
+ ...stale(p) ? { offline: true } : {},
584
+ // A relief and the one it relieves are on the same machine (context handover spec §3.3).
585
+ ...s.handover?.to ? { handover: { ...s.handover, to: qualify(p.config.id, s.handover.to) } } : {},
586
+ ...s.relieves ? { relieves: { ...s.relieves, id: qualify(p.config.id, s.relieves.id) } } : {}
587
+ });
532
588
  const stateSessions = (p) => (stale(p) ? p.snapshot.sessions.map((s) => ({ ...s, offline: true })) : p.snapshot?.sessions ?? []).filter((s) => !isArchived(s));
533
589
  function refresh() {
534
590
  state2 = configs.map((c) => {
@@ -633,12 +689,40 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
633
689
  listeners.forEach((fn) => fn());
634
690
  return;
635
691
  }
692
+ if (m.type === "context.prefs") {
693
+ if (machine !== "local") return;
694
+ contextNear2 = m.prefs.near;
695
+ listeners.forEach((fn) => fn());
696
+ return;
697
+ }
636
698
  if (m.type === "island") {
637
699
  if (machine !== "local") return;
638
700
  island = m.prefs;
639
701
  listeners.forEach((fn) => fn());
640
702
  return;
641
703
  }
704
+ if (m.type === "teams" || m.type === "team.updated") {
705
+ if (machine !== "local") return;
706
+ const known2 = new Set((teams ?? []).map((t) => t.id));
707
+ teams = m.type === "teams" ? m.teams.map(teamOut) : [...(teams ?? []).filter((t) => t.id !== m.team.id), teamOut(m.team)].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
708
+ listeners.forEach((fn) => fn());
709
+ if (m.type === "team.updated" && !known2.has(m.team.id) && m.team.state !== "dissolved") {
710
+ const rid = `fleet-tasks-${++listSeq}`;
711
+ taskLists.add(rid);
712
+ p.client.send({ type: "task.cmd", rid, by: LEDGE_GIVER, op: "list", all: true, team: m.team.id });
713
+ }
714
+ if (m.type === "teams" && p.snapshot?.daemon?.teams) askTasks(p);
715
+ return;
716
+ }
717
+ if (m.type === "task.updated") {
718
+ if (machine === "local") putTasks([m.task]);
719
+ return;
720
+ }
721
+ if (m.type === "task.result" && taskLists.has(m.rid)) {
722
+ taskLists.delete(m.rid);
723
+ if (machine === "local" && m.tasks) putTasks(m.tasks);
724
+ return;
725
+ }
642
726
  if (m.type === "mobile") {
643
727
  if (machine !== "local") return;
644
728
  mobile = m.state;
@@ -789,7 +873,7 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
789
873
  const qid = globalQid(machine, m.qid), old = turns2.get(qid);
790
874
  if (!old) return;
791
875
  if (m.type === "chat.done") {
792
- const raw = { ...old, qid: m.qid, answer: m.text, answeredAt: m.at, proposal: m.proposal, proposals: m.proposals, relay: m.relay, closes: m.closes, projectProposal: m.projectProposal };
876
+ const raw = { ...old, qid: m.qid, answer: m.text, answeredAt: m.at, proposal: m.proposal, proposals: m.proposals, relay: m.relay, closes: m.closes, projectProposal: m.projectProposal, team: m.team, tasks: m.tasks };
793
877
  const t = mappedTurn(p, { ...raw, steps: [] });
794
878
  turns2.set(qid, { ...t, steps: old.steps });
795
879
  history();
@@ -835,6 +919,10 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
835
919
  emit2({ ...m, ...m.id ? { id: qualify(machine, m.id) } : {}, message: configs.length > 1 ? `${p.config.label}: ${m.message}` : m.message });
836
920
  return;
837
921
  }
922
+ if (m.type === "session.handover.result") {
923
+ emit2(m);
924
+ return;
925
+ }
838
926
  if (machine === "local") emit2(m);
839
927
  }
840
928
  function add(c) {
@@ -1038,6 +1126,25 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1038
1126
  }
1039
1127
  }
1040
1128
  if (msg.type === "update.daemon") machine = "local";
1129
+ if (msg.type === "roles.list" || msg.type === "team.launch") {
1130
+ if (!("qid" in msg && msg.qid)) machine = msg.machineId ?? "local";
1131
+ const { machineId: _m, ...rest } = outgoing;
1132
+ outgoing = rest;
1133
+ }
1134
+ if (msg.type === "team.join" || msg.type === "team.leave" || msg.type === "team.resume" || msg.type === "team.dissolve" || msg.type === "team.save" || msg.type === "task.cmd" && !msg.qid) machine = "local";
1135
+ if (outgoing.type === "team.join" || outgoing.type === "team.leave") {
1136
+ const o = splitId(outgoing.session);
1137
+ if (o) {
1138
+ machine = o.machine;
1139
+ outgoing = { ...outgoing, session: o.id };
1140
+ }
1141
+ }
1142
+ if (outgoing.type === "task.cmd") {
1143
+ const o = outgoing.to ? splitId(outgoing.to) : null;
1144
+ if (o && !outgoing.to.startsWith("@")) outgoing = { ...outgoing, to: o.id };
1145
+ const b = splitId(outgoing.by);
1146
+ if (b && outgoing.by !== LEDGE_GIVER) outgoing = { ...outgoing, by: b.id };
1147
+ }
1041
1148
  if (msg.type === "snapshot.request") {
1042
1149
  publish();
1043
1150
  history();
@@ -1082,9 +1189,9 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1082
1189
  configure(raw) {
1083
1190
  const next2 = parseMachines(raw);
1084
1191
  if (source === "daemon") {
1085
- const local = peers.get("local").client;
1086
- if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1087
- local.send({ type: "machines.set", machines: toWire(next2) });
1192
+ const local2 = peers.get("local").client;
1193
+ if (local2.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1194
+ local2.send({ type: "machines.set", machines: toWire(next2) });
1088
1195
  return;
1089
1196
  }
1090
1197
  localStorage.setItem(MACHINES_KEY, JSON.stringify(next2));
@@ -1133,30 +1240,40 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1133
1240
  projects: () => projects,
1134
1241
  setProjects(next2) {
1135
1242
  const valid = ProjectsSchema.parse(next2);
1136
- const local = peers.get("local").client;
1137
- if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1243
+ const local2 = peers.get("local").client;
1244
+ if (local2.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1138
1245
  if (projects === null) throw Object.assign(new Error("Update the local Ledge daemon: it does not know projects"), { name: "DaemonOutdated" });
1139
- local.send({ type: "projects.set", projects: valid });
1246
+ local2.send({ type: "projects.set", projects: valid });
1140
1247
  },
1141
1248
  island: () => island,
1142
1249
  setIsland(next2) {
1143
1250
  const valid = IslandPrefsSchema.parse(next2);
1144
- const local = peers.get("local").client;
1145
- if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1251
+ const local2 = peers.get("local").client;
1252
+ if (local2.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1146
1253
  if (island === void 0) throw Object.assign(new Error("Update the local Ledge daemon: it does not know the island settings"), { name: "DaemonOutdated" });
1147
- local.send({ type: "island.set", prefs: valid });
1254
+ local2.send({ type: "island.set", prefs: valid });
1148
1255
  },
1149
1256
  mobile: () => mobile,
1150
1257
  mobileError: () => mobileError,
1258
+ teams: () => teams,
1259
+ tasks: () => taskList,
1260
+ contextNear: () => contextNear2,
1261
+ setContextNear(near) {
1262
+ const valid = ContextPrefsSchema.parse({ near });
1263
+ const local2 = peers.get("local").client;
1264
+ if (local2.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1265
+ if (contextNear2 === void 0) throw Object.assign(new Error("Update the local Ledge daemon: it does not know the context threshold"), { name: "DaemonOutdated" });
1266
+ local2.send({ type: "context.set", prefs: valid });
1267
+ },
1151
1268
  sendMobile(msg) {
1152
- const local = peers.get("local").client;
1153
- if (local.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1269
+ const local2 = peers.get("local").client;
1270
+ if (local2.status() !== "open") throw Object.assign(new Error("The local daemon is not connected"), { name: "DaemonOffline" });
1154
1271
  if (mobile === void 0) throw Object.assign(new Error("Update the local Ledge daemon: it does not know mobile access"), { name: "DaemonOutdated" });
1155
1272
  if (mobileError) {
1156
1273
  mobileError = void 0;
1157
1274
  listeners.forEach((fn) => fn());
1158
1275
  }
1159
- local.send(msg);
1276
+ local2.send(msg);
1160
1277
  }
1161
1278
  }
1162
1279
  };
@@ -1168,7 +1285,7 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1168
1285
  var COLORS = Object.keys(PROJECT_COLORS);
1169
1286
 
1170
1287
  // src/tui/App.tsx
1171
- import { useCallback, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef8, useState as useState8 } from "react";
1288
+ import { useCallback, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef8, useState as useState9 } from "react";
1172
1289
  import { useApp, useInput as useInput5, useStdout } from "ink";
1173
1290
 
1174
1291
  // src/tui/actions.ts
@@ -1484,7 +1601,13 @@ var fr = {
1484
1601
  change: "changer",
1485
1602
  scroll: "d\xE9filer",
1486
1603
  space: "espace",
1487
- redraw: "autre nom"
1604
+ redraw: "autre nom",
1605
+ save: "enregistrer l\u2019\xE9quipe",
1606
+ tasks: "t\xE2ches de l\u2019\xE9quipe",
1607
+ join: "rejoindre une \xE9quipe",
1608
+ resume: "reprendre l\u2019\xE9quipe",
1609
+ teamMode: "session ou \xE9quipe",
1610
+ handover: "passer le relais"
1488
1611
  },
1489
1612
  overview: {
1490
1613
  yours: (n) => `\xC0 toi \xB7 ${n}`,
@@ -1506,7 +1629,23 @@ var fr = {
1506
1629
  allow: "Autoriser",
1507
1630
  deny: "Refuser",
1508
1631
  always: "Toujours",
1509
- denyWithNote: "Refuser en expliquant"
1632
+ denyWithNote: "Refuser en expliquant",
1633
+ common: "table commune",
1634
+ teamLine: (done, given) => `\xE9quipe \xB7 ${done}/${given}`,
1635
+ resting: "au repos \xB7 R pour reprendre",
1636
+ tasks: (team) => `T\xE2ches \xB7 ${team}`,
1637
+ noTask: "Aucune t\xE2che.",
1638
+ ledge: "Ledge",
1639
+ near: (name, pct) => `${name} \xB7 ctx ${pct} % \xB7 H rel\xE8ve`,
1640
+ reliefFailed: (name, why) => `La rel\xE8ve de ${name} a \xE9chou\xE9 : ${why}`,
1641
+ context: (pct) => `ctx ${pct} %`,
1642
+ relief: { briefing: "rel\xE8ve : bilan demand\xE9", launching: "rel\xE8ve : lancement\u2026", done: "relev\xE9e", failed: "rel\xE8ve \xE9chou\xE9e" },
1643
+ relievedBy: (name) => `relev\xE9e par ${name}`,
1644
+ relieves: (name) => `rel\xE8ve de ${name}`,
1645
+ ledgeTask: (owner, title2, state2) => `${owner} a ${state2 === "done" ? "fini" : state2 === "refused" ? "refus\xE9" : "abandonn\xE9"} \xAB ${title2} \xBB \xB7 t\xE2che de Ledge`,
1646
+ joinWhich: (name) => `${name} rejoint quelle \xE9quipe ? (\u2191\u2193 \u21B5, esc)`,
1647
+ noTeamHere: "Aucune \xE9quipe dans ce projet.",
1648
+ notInTeam: "Cette session n\u2019est dans aucune \xE9quipe."
1510
1649
  },
1511
1650
  session: {
1512
1651
  you: "Vous",
@@ -1554,7 +1693,23 @@ var fr = {
1554
1693
  sentAt: "envoy\xE9e",
1555
1694
  closed: "ferm\xE9e",
1556
1695
  offline: "Le daemon local est hors ligne : la question attendra.",
1557
- steps: { sessions: "les sessions", pane: (n) => `le terminal de ${n}`, reply: (n) => `la r\xE9ponse de ${n}`, git: (n) => `l\u2019\xE9tat git de ${n}`, repo: (p) => p }
1696
+ steps: { sessions: "les sessions", pane: (n) => `le terminal de ${n}`, reply: (n) => `la r\xE9ponse de ${n}`, git: (n) => `l\u2019\xE9tat git de ${n}`, repo: (p) => p },
1697
+ operator: (s, m) => `Ledge \xB7 Op\xE9rateur \xB7 voit ${s} ${plural(s, "session", "sessions")}, ${m} ${plural(m, "machine", "machines")}`,
1698
+ teamCard: (name) => `\xC9quipe \xB7 ${name}`,
1699
+ taskCard: "T\xE2ches \xE0 confier",
1700
+ launchTeam: "Lancer l\u2019\xE9quipe",
1701
+ save: "Enregistrer",
1702
+ saved: "enregistr\xE9e",
1703
+ seats: "Si\xE8ges",
1704
+ table: "Table",
1705
+ newTable: (project) => `nouvelle table dans la pi\xE8ce ${project}`,
1706
+ autonomy: { ask: "demande", edits: "modifie sans demander", auto: "autonome" },
1707
+ autoWarning: (roles) => `${roles} travaillera sans demander.`,
1708
+ teamLaunched: (at) => `\u2713 \xC9quipe lanc\xE9e \xE0 ${at}`,
1709
+ give: "Confier",
1710
+ waitTeam: "active une fois l\u2019\xE9quipe lanc\xE9e",
1711
+ given: (owner, id, state2) => `\u2713 Confi\xE9e \xE0 ${owner} \xB7 ${id} \xB7 ${state2}`,
1712
+ taskState: { open: "\xE0 prendre", taken: "prise", done: "finie", refused: "refus\xE9e", dropped: "abandonn\xE9e" }
1558
1713
  },
1559
1714
  launch: {
1560
1715
  title: "Nouvelle session",
@@ -1579,9 +1734,23 @@ var fr = {
1579
1734
  thenAttach: "puis a pour s\u2019attacher \xE0 son terminal",
1580
1735
  missing: { prompt: "\xC9crivez une instruction.", machine: "Aucune machine en ligne.", repos: "Choisissez un d\xE9p\xF4t ou un dossier." },
1581
1736
  launching: "Lancement\u2026",
1582
- launched: (name) => `\u2713 ${name} est lanc\xE9e`
1737
+ launched: (name) => `\u2713 ${name} est lanc\xE9e`,
1738
+ teamTitle: "Nouvelle \xE9quipe",
1739
+ mode: "Mode",
1740
+ session: "session",
1741
+ team: "\xE9quipe",
1742
+ template: "Mod\xE8le",
1743
+ custom: "si\xE8ges choisis un par un",
1744
+ seats: "Si\xE8ges",
1745
+ teamName: "Nom",
1746
+ noRole: "aucun r\xF4le : \xE9cris ~/.ledge/roles/<id>.md",
1747
+ loadingRoles: "lecture des r\xF4les\u2026",
1748
+ teamPreview: (team, project) => `L\u2019\xE9quipe ${team} s\u2019installera \xE0 une nouvelle table de la pi\xE8ce ${project}.`,
1749
+ teamLaunched: (name) => `\u2713 \xC9quipe ${name} lanc\xE9e`,
1750
+ teamFailed: (why) => `\xC9quipe non lanc\xE9e : ${why}`,
1751
+ noSeat: "Choisissez au moins un si\xE8ge."
1583
1752
  },
1584
- settings: { title: "R\xE9glages", language: "Langue", saved: "R\xE9glage enregistr\xE9.", saveFailed: (m) => `R\xE9glage appliqu\xE9, mais non enregistr\xE9 : ${m}` },
1753
+ settings: { title: "R\xE9glages", language: "Langue", context: "Contexte max", contextHint: "au-del\xE0 : passer le relais", contextUnknown: "mettez \xE0 jour le daemon", contextSaved: (pct) => `Contexte max : ${pct} %`, saved: "R\xE9glage enregistr\xE9.", saveFailed: (m) => `R\xE9glage appliqu\xE9, mais non enregistr\xE9 : ${m}` },
1585
1754
  help: {
1586
1755
  title: "Aide",
1587
1756
  global: "Partout",
@@ -1593,6 +1762,8 @@ var fr = {
1593
1762
  screens: "changer d\u2019\xE9cran"
1594
1763
  },
1595
1764
  toast: {
1765
+ handover: (n) => `${n} \xE9crit son bilan : une nouvelle session prendra sa place`,
1766
+ handoverRefused: (why) => `Rel\xE8ve refus\xE9e : ${why}`,
1596
1767
  accept: (n) => `\u2713 ${n} : autoris\xE9`,
1597
1768
  deny: (n) => `\u2713 ${n} : refus\xE9`,
1598
1769
  always: (n) => `\u2713 ${n} : toujours autoris\xE9`,
@@ -1609,7 +1780,10 @@ var fr = {
1609
1780
  noTerminalOmp: (n) => `${n} est pilot\xE9 par Ledge via le protocole d\u2019OMP : pas de terminal \xE0 attacher.`,
1610
1781
  noTerminal: (n) => `Pas de terminal pour ${n}.`,
1611
1782
  attachFailed: (code) => `tmux s\u2019est arr\xEAt\xE9 avec le code ${code}.`,
1612
- modelSwitch: "Ce daemon ne change pas le mod\xE8le d\u2019une session en cours."
1783
+ modelSwitch: "Ce daemon ne change pas le mod\xE8le d\u2019une session en cours.",
1784
+ joined: (n, team) => `${n} rejoint ${team}.`,
1785
+ resumed: (team) => `Reprise de l\u2019\xE9quipe ${team} demand\xE9e.`,
1786
+ saved: (path) => `\xC9quipe enregistr\xE9e : ${path}`
1613
1787
  },
1614
1788
  decisions: {
1615
1789
  failed: (name, message) => `D\xE9cision non transmise \xE0 ${name} : ${message}`,
@@ -1623,7 +1797,7 @@ var fr = {
1623
1797
  question: (n, text) => `${n} a une question : ${text}`,
1624
1798
  title: (n) => n > 0 ? `ledge \xB7 ${n} t\u2019${plural(n, "attend", "attendent")}` : "ledge"
1625
1799
  },
1626
- cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)" }
1800
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui s\u2019ouvre dans l\u2019onglet Ledge." }
1627
1801
  };
1628
1802
 
1629
1803
  // src/tui/i18n/en.ts
@@ -1671,7 +1845,13 @@ var en = {
1671
1845
  change: "change",
1672
1846
  scroll: "scroll",
1673
1847
  space: "space",
1674
- redraw: "another name"
1848
+ redraw: "another name",
1849
+ save: "save the team",
1850
+ tasks: "team tasks",
1851
+ join: "join a team",
1852
+ resume: "resume the team",
1853
+ teamMode: "session or team",
1854
+ handover: "hand over"
1675
1855
  },
1676
1856
  overview: {
1677
1857
  yours: (n) => `Yours \xB7 ${n}`,
@@ -1693,7 +1873,23 @@ var en = {
1693
1873
  allow: "Allow",
1694
1874
  deny: "Deny",
1695
1875
  always: "Always",
1696
- denyWithNote: "Deny with a note"
1876
+ denyWithNote: "Deny with a note",
1877
+ common: "common table",
1878
+ teamLine: (done, given) => `team \xB7 ${done}/${given}`,
1879
+ resting: "resting \xB7 R to resume",
1880
+ tasks: (team) => `Tasks \xB7 ${team}`,
1881
+ noTask: "No task.",
1882
+ ledge: "Ledge",
1883
+ near: (name, pct) => `${name} \xB7 ctx ${pct} % \xB7 H hand over`,
1884
+ reliefFailed: (name, why) => `The relief of ${name} failed: ${why}`,
1885
+ context: (pct) => `ctx ${pct} %`,
1886
+ relief: { briefing: "relief: brief asked", launching: "relief: launching\u2026", done: "relieved", failed: "relief failed" },
1887
+ relievedBy: (name) => `relieved by ${name}`,
1888
+ relieves: (name) => `relieves ${name}`,
1889
+ ledgeTask: (owner, title2, state2) => `${owner} ${state2 === "done" ? "finished" : state2 === "refused" ? "refused" : "dropped"} \u201C${title2}\u201D \xB7 Ledge\u2019s task`,
1890
+ joinWhich: (name) => `${name} joins which team? (\u2191\u2193 \u21B5, esc)`,
1891
+ noTeamHere: "No team in this project.",
1892
+ notInTeam: "This session is in no team."
1697
1893
  },
1698
1894
  session: {
1699
1895
  you: "You",
@@ -1741,7 +1937,23 @@ var en = {
1741
1937
  sentAt: "sent",
1742
1938
  closed: "closed",
1743
1939
  offline: "The local daemon is offline: the question will wait.",
1744
- steps: { sessions: "the sessions", pane: (n) => `${n}'s terminal`, reply: (n) => `${n}'s reply`, git: (n) => `${n}'s git state`, repo: (p) => p }
1940
+ steps: { sessions: "the sessions", pane: (n) => `${n}'s terminal`, reply: (n) => `${n}'s reply`, git: (n) => `${n}'s git state`, repo: (p) => p },
1941
+ operator: (s, m) => `Ledge \xB7 Operator \xB7 sees ${s} ${plural2(s, "session", "sessions")}, ${m} ${plural2(m, "machine", "machines")}`,
1942
+ teamCard: (name) => `Team \xB7 ${name}`,
1943
+ taskCard: "Tasks to give",
1944
+ launchTeam: "Launch the team",
1945
+ save: "Save",
1946
+ saved: "saved",
1947
+ seats: "Seats",
1948
+ table: "Table",
1949
+ newTable: (project) => `new table in the ${project} room`,
1950
+ autonomy: { ask: "asks", edits: "edits without asking", auto: "autonomous" },
1951
+ autoWarning: (roles) => `${roles} will work without asking.`,
1952
+ teamLaunched: (at) => `\u2713 Team launched at ${at}`,
1953
+ give: "Give",
1954
+ waitTeam: "active once the team is launched",
1955
+ given: (owner, id, state2) => `\u2713 Given to ${owner} \xB7 ${id} \xB7 ${state2}`,
1956
+ taskState: { open: "to take", taken: "taken", done: "done", refused: "refused", dropped: "dropped" }
1745
1957
  },
1746
1958
  launch: {
1747
1959
  title: "New session",
@@ -1766,11 +1978,27 @@ var en = {
1766
1978
  thenAttach: "then a to attach to its terminal",
1767
1979
  missing: { prompt: "Write an instruction.", machine: "No machine online.", repos: "Pick a repository or a folder." },
1768
1980
  launching: "Launching\u2026",
1769
- launched: (name) => `\u2713 ${name} is started`
1981
+ launched: (name) => `\u2713 ${name} is started`,
1982
+ teamTitle: "New team",
1983
+ mode: "Mode",
1984
+ session: "session",
1985
+ team: "team",
1986
+ template: "Template",
1987
+ custom: "seats chosen one by one",
1988
+ seats: "Seats",
1989
+ teamName: "Name",
1990
+ noRole: "no role: write ~/.ledge/roles/<id>.md",
1991
+ loadingRoles: "reading the roles\u2026",
1992
+ teamPreview: (team, project) => `Team ${team} will sit at a new table in the ${project} room.`,
1993
+ teamLaunched: (name) => `\u2713 Team ${name} launched`,
1994
+ teamFailed: (why) => `Team not launched: ${why}`,
1995
+ noSeat: "Choose at least one seat."
1770
1996
  },
1771
- settings: { title: "Settings", language: "Language", saved: "Setting saved.", saveFailed: (m) => `Setting applied, but not saved: ${m}` },
1997
+ settings: { title: "Settings", language: "Language", context: "Max context", contextHint: "past it: hand over", contextUnknown: "update the daemon", contextSaved: (pct) => `Max context: ${pct} %`, saved: "Setting saved.", saveFailed: (m) => `Setting applied, but not saved: ${m}` },
1772
1998
  help: { title: "Help", global: "Everywhere", overview: "Overview", session: "Session", ask: "Ask Ledge", launch: "New session", settings: "Settings", screens: "switch screen" },
1773
1999
  toast: {
2000
+ handover: (n) => `${n} is writing its brief: a fresh session will take its place`,
2001
+ handoverRefused: (why) => `Relief refused: ${why}`,
1774
2002
  accept: (n) => `\u2713 ${n}: allowed`,
1775
2003
  deny: (n) => `\u2713 ${n}: denied`,
1776
2004
  always: (n) => `\u2713 ${n}: always allowed`,
@@ -1787,7 +2015,10 @@ var en = {
1787
2015
  noTerminalOmp: (n) => `${n} is driven by Ledge over OMP's protocol: there is no terminal to attach to.`,
1788
2016
  noTerminal: (n) => `No terminal for ${n}.`,
1789
2017
  attachFailed: (code) => `tmux stopped with code ${code}.`,
1790
- modelSwitch: "This daemon cannot change a running session\u2019s model."
2018
+ modelSwitch: "This daemon cannot change a running session\u2019s model.",
2019
+ joined: (n, team) => `${n} joins ${team}.`,
2020
+ resumed: (team) => `Asked to resume team ${team}.`,
2021
+ saved: (path) => `Team saved: ${path}`
1791
2022
  },
1792
2023
  decisions: {
1793
2024
  failed: (name, message) => `Could not send the decision to ${name}: ${message}`,
@@ -1801,7 +2032,7 @@ var en = {
1801
2032
  question: (n, text) => `${n} has a question: ${text}`,
1802
2033
  title: (n) => n > 0 ? `ledge \xB7 ${n} waiting` : "ledge"
1803
2034
  },
1804
- cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)" }
2035
+ cli: { notTty: "ledge tui: needs a terminal (stdin and stdout must be a TTY)", movedToTab: "ledge tui opens in the Ledge tab." }
1805
2036
  };
1806
2037
 
1807
2038
  // src/tui/i18n/lang.tsx
@@ -1816,20 +2047,37 @@ function I18nProvider({ lang, children }) {
1816
2047
  // src/tui/model/overview.ts
1817
2048
  var listed = (sessions) => sessions.filter((s) => s.state !== "exited");
1818
2049
  var machineOf = (s) => s.machineId ?? splitId(s.id)?.machine ?? "local";
1819
- function overview(sessions, projects) {
2050
+ function overview(sessions, projects, teams = [], tasks = []) {
1820
2051
  const live = listed(sessions);
1821
2052
  const yours = live.filter(isPending).sort((a, b) => a.lastActivityAt.localeCompare(b.lastActivityAt) || a.id.localeCompare(b.id));
1822
2053
  const nodes = /* @__PURE__ */ new Map();
1823
2054
  const known2 = new Map((projects?.projects ?? []).map((p) => [p.id, p]));
1824
- for (const p of projects?.projects ?? []) nodes.set(`p:${p.id}`, { key: `p:${p.id}`, label: p.name, color: p.color, branches: [], count: 0 });
2055
+ for (const p of projects?.projects ?? []) nodes.set(`p:${p.id}`, { key: `p:${p.id}`, label: p.name, color: p.color, branches: [], count: 0, teams: [] });
2056
+ const rooms = /* @__PURE__ */ new Map();
2057
+ for (const team of teams) {
2058
+ if (team.state === "dissolved") continue;
2059
+ const pid = projects ? projectOf({ machineId: team.machine, cwd: team.cwd }, projects) ?? team.project : team.project;
2060
+ const node = pid ? nodes.get(`p:${pid}`) : void 0;
2061
+ if (!node) continue;
2062
+ const mine = tasks.filter((x) => x.team === team.id && x.giver === LEDGE_GIVER);
2063
+ const row = { key: `${node.key}|team:${team.id}`, team, sessions: [], done: mine.filter((x) => x.state === "done").length, given: mine.length };
2064
+ node.teams.push(row);
2065
+ rooms.set(team.id, row);
2066
+ }
1825
2067
  for (const s of live) {
1826
2068
  const pid = projects ? projectOf({ machineId: machineOf(s), cwd: s.cwd }, projects) : null;
1827
2069
  const key = pid && known2.has(pid) ? `p:${pid}` : `r:${machineOf(s)}:${s.repoPath ?? s.cwd}`;
1828
2070
  let node = nodes.get(key);
1829
2071
  if (!node) {
1830
- node = { key, label: s.repo ?? s.project, branches: [], count: 0 };
2072
+ node = { key, label: s.repo ?? s.project, branches: [], count: 0, teams: [] };
1831
2073
  nodes.set(key, node);
1832
2074
  }
2075
+ const seat = s.team ? rooms.get(s.team) : void 0;
2076
+ if (seat && node.teams.includes(seat)) {
2077
+ seat.sessions.push(s);
2078
+ node.count += 1;
2079
+ continue;
2080
+ }
1833
2081
  const branch = s.branch ?? "-";
1834
2082
  let row = node.branches.find((b) => b.branch === branch);
1835
2083
  if (!row) {
@@ -1839,8 +2087,8 @@ function overview(sessions, projects) {
1839
2087
  row.sessions.push(s);
1840
2088
  node.count += 1;
1841
2089
  }
1842
- const tree = [...nodes.values()].filter((n) => n.count > 0);
1843
- const order = tree.flatMap((n) => n.branches.flatMap((b) => b.sessions.map((s) => s.id)));
2090
+ const tree = [...nodes.values()].filter((n) => n.count > 0 || n.teams.length > 0);
2091
+ const order = tree.flatMap((n) => [...n.branches.flatMap((b) => b.sessions.map((s) => s.id)), ...n.teams.flatMap((x) => x.sessions.map((s) => s.id))]);
1844
2092
  return { yours, tree, order, machines: new Set(live.map(machineOf)).size };
1845
2093
  }
1846
2094
  var helperCount = (s) => liveHelpers(s).length;
@@ -1862,15 +2110,17 @@ function nextWaiting(yours, selected) {
1862
2110
  const at = yours.findIndex((s) => s.id === selected);
1863
2111
  return yours[(at + 1) % yours.length].id;
1864
2112
  }
1865
- function exchanges(sessions, max) {
2113
+ function exchanges(sessions, max, tasks = [], ledge = "Ledge") {
1866
2114
  const byId = new Map(sessions.map((s) => [s.id, s]));
1867
- return sessions.flatMap((s) => {
2115
+ return [...sessions.flatMap((s) => {
1868
2116
  const m = s.lastMessage;
1869
2117
  if (!m) return [];
1870
2118
  const peer = m.peer ? byId.get(m.peer) ?? sessions.find((x) => splitId(x.id)?.id === m.peer) : void 0;
1871
2119
  return [{ at: m.at, from: s.name, to: peer?.name ?? m.to, summary: m.summary }];
1872
- }).sort((a, b) => b.at.localeCompare(a.at)).slice(0, max);
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);
1873
2121
  }
2122
+ var nearContext = (sessions, near) => sessions.filter((s) => s.state !== "exited" && !s.archivedAt && (s.handover ? s.handover.state === "failed" : contextNear(s.context, near)));
2123
+ var closedLedgeTasks = (tasks) => tasks.filter((x) => x.giver === LEDGE_GIVER && taskClosed(x) && !x.seen);
1874
2124
 
1875
2125
  // src/tui/model/time.ts
1876
2126
  function ago(iso, now, t) {
@@ -1914,6 +2164,8 @@ var title = (text) => `\x1B]0;${text.replace(/[\u0000-\u001f\u007f]/g, " ")}\x07
1914
2164
  // src/tui/screens/Overview.tsx
1915
2165
  import { useMemo as useMemo2 } from "react";
1916
2166
  import { Box as Box6, Text as Text6, useWindowSize as useWindowSize2 } from "ink";
2167
+ import { randomUUID } from "crypto";
2168
+ import { useState as useState2 } from "react";
1917
2169
 
1918
2170
  // ../ui/src/bots/poseFor.ts
1919
2171
  var FROM_SESSION = {
@@ -2042,7 +2294,9 @@ function Button({ label, k, primary, off }) {
2042
2294
  ] });
2043
2295
  }
2044
2296
  function SessionCard({ s, note }) {
2045
- const { t, g } = useTui();
2297
+ const tui = useTui();
2298
+ const { t, g } = tui;
2299
+ const threshold = useContextNear(tui.client);
2046
2300
  const state2 = botState(s);
2047
2301
  const helpers = liveHelpers(s);
2048
2302
  const p = s.pending;
@@ -2059,7 +2313,13 @@ function SessionCard({ s, note }) {
2059
2313
  " ",
2060
2314
  TOOL_NAME[s.tool],
2061
2315
  " \xB7 ",
2062
- modelLabel(s, t.session.defaultModel),
2316
+ modelLabel(s, t.session.defaultModel)
2317
+ ] }),
2318
+ s.context ? /* @__PURE__ */ jsxs3(Text3, { color: contextNear(s.context, threshold) ? C.yellow : C.dim, children: [
2319
+ " \xB7 ",
2320
+ t.overview.context(Math.round(s.context.used / s.context.window * 100))
2321
+ ] }) : null,
2322
+ /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
2063
2323
  " \xB7 ",
2064
2324
  machineLabel(s),
2065
2325
  s.branch ? ` \xB7 ${g.branch} ${s.branch}` : ""
@@ -2072,6 +2332,17 @@ function SessionCard({ s, note }) {
2072
2332
  t.overview.readOnly
2073
2333
  ] }) : null
2074
2334
  ] }),
2335
+ s.handover || s.relieves ? /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
2336
+ s.handover ? /* @__PURE__ */ jsxs3(Text3, { color: s.handover.state === "failed" ? C.red : C.yellow, children: [
2337
+ " \u21C4 ",
2338
+ s.handover.state === "done" && s.handover.toName ? t.overview.relievedBy(s.handover.toName) : t.overview.relief[s.handover.state],
2339
+ s.handover.reason ? ` : ${s.handover.reason}` : ""
2340
+ ] }) : null,
2341
+ s.relieves ? /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
2342
+ " \u21C4 ",
2343
+ t.overview.relieves(s.relieves.name)
2344
+ ] }) : null
2345
+ ] }) : null,
2075
2346
  helpers.length > 0 ? /* @__PURE__ */ jsxs3(Text3, { color: C.violet, wrap: "truncate", children: [
2076
2347
  " ",
2077
2348
  helpers.map((h) => `${g.helper} ${h.kind} \xB7 ${oneLine(h.task)}`).join(" ")
@@ -2151,9 +2422,81 @@ function Overview() {
2151
2422
  const { columns, rows: height } = useWindowSize2();
2152
2423
  const wide = columns >= WIDE;
2153
2424
  const left = wide ? Math.min(64, Math.max(44, Math.floor(columns * 0.42))) : columns;
2154
- const view = useMemo2(() => overview(d.sessions, projects), [d.sessions, projects]);
2425
+ const teams = useTeams(tui.client);
2426
+ const tasks = useTasks(tui.client);
2427
+ const view = useMemo2(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
2155
2428
  const sel = selected?.id ?? null;
2429
+ const closed = closedLedgeTasks(tasks);
2430
+ const threshold = useContextNear(tui.client);
2431
+ const near = useMemo2(() => nearContext(d.sessions, threshold), [d.sessions, threshold]);
2432
+ const [tasksShown, setTasksShown] = useState2(true);
2433
+ const [joining, setJoining] = useState2(null);
2434
+ const node = selected ? view.tree.find((n) => n.branches.some((b) => b.sessions.includes(selected)) || n.teams.some((x) => x.sessions.includes(selected))) : void 0;
2435
+ const seatTeam = selected?.team ? teams?.find((x) => x.id === selected.team && x.state !== "dissolved") : void 0;
2436
+ const choices2 = node?.teams.map((x) => x.team).filter((x) => x.state !== "dissolved" && x.id !== selected?.team) ?? [];
2156
2437
  useScreenKeys((input, key) => {
2438
+ if (joining) {
2439
+ if (key.escape) {
2440
+ setJoining(null);
2441
+ return true;
2442
+ }
2443
+ if (input === "j" || key.downArrow) {
2444
+ setJoining({ index: (joining.index + 1) % Math.max(1, choices2.length) });
2445
+ return true;
2446
+ }
2447
+ if (input === "k" || key.upArrow) {
2448
+ setJoining({ index: (joining.index + choices2.length - 1) % Math.max(1, choices2.length) });
2449
+ return true;
2450
+ }
2451
+ if (key.return && selected && choices2[joining.index]) {
2452
+ const team = choices2[joining.index];
2453
+ const role = selected.role ?? team.seats.find((x) => x.sessionId === null)?.role;
2454
+ setJoining(null);
2455
+ if (!role) {
2456
+ tui.toast({ text: t.overview.noTeamHere, tone: "info" });
2457
+ return true;
2458
+ }
2459
+ tui.client.send({ type: "team.join", team: team.id, session: selected.id, role });
2460
+ tui.toast({ text: t.toast.joined(selected.name, team.name), tone: "ok" });
2461
+ return true;
2462
+ }
2463
+ return true;
2464
+ }
2465
+ if (input === "J" && selected) {
2466
+ if (choices2.length) setJoining({ index: 0 });
2467
+ else tui.toast({ text: t.overview.noTeamHere, tone: "info" });
2468
+ return true;
2469
+ }
2470
+ if (input === "R") {
2471
+ const resting = (seatTeam?.state === "resting" ? seatTeam : void 0) ?? node?.teams.find((x) => x.team.state === "resting")?.team ?? view.tree.flatMap((n) => n.teams).find((x) => x.team.state === "resting")?.team;
2472
+ if (resting) {
2473
+ tui.client.send({ type: "team.resume", team: resting.id });
2474
+ tui.toast({ text: t.toast.resumed(resting.name), tone: "ok" });
2475
+ }
2476
+ return true;
2477
+ }
2478
+ if (input === "T") {
2479
+ setTasksShown((v) => !v);
2480
+ return true;
2481
+ }
2482
+ if (input === "H") {
2483
+ const s = (selected && selected.source === "launcher" && selected.state !== "exited" && selected.tool !== "omp" && (!selected.handover || selected.handover.state === "failed") ? selected : void 0) ?? near.find((x) => x.handover?.state !== "failed") ?? near[0];
2484
+ if (!s) return true;
2485
+ const rid = randomUUID();
2486
+ const off = tui.client.onMessage((m) => {
2487
+ if (m.type === "session.handover.result" && m.rid === rid) {
2488
+ off();
2489
+ tui.toast(m.ok ? { text: t.toast.handover(s.name), tone: "ok" } : { text: t.toast.handoverRefused(m.reason ?? "?"), tone: "info" });
2490
+ }
2491
+ });
2492
+ setTimeout(off, 5e3);
2493
+ tui.client.send({ type: "session.handover", rid, id: s.id });
2494
+ return true;
2495
+ }
2496
+ if (input === "x" && closed[0]) {
2497
+ tui.client.send({ type: "task.cmd", rid: randomUUID(), by: LEDGE_GIVER, op: "seen", task: closed[0].id });
2498
+ return true;
2499
+ }
2157
2500
  if (input === "j" || key.downArrow) {
2158
2501
  select(step(view.order, sel, 1));
2159
2502
  return true;
@@ -2188,17 +2531,25 @@ function Overview() {
2188
2531
  }
2189
2532
  return false;
2190
2533
  });
2191
- const tree = view.tree.flatMap((n) => [
2192
- { kind: "project", key: n.key, label: n.label, color: projectColor(n.color), count: n.count },
2193
- ...n.branches.flatMap((b) => [{ kind: "branch", key: `${n.key}|${b.branch}`, branch: b.branch }, ...b.sessions.map((s) => ({ kind: "session", key: s.id, s }))])
2194
- ]);
2195
- const yoursHeight = Math.max(1, Math.min(view.yours.length, 6));
2196
- const treeHeight = Math.max(3, height - 3 - (yoursHeight + 2) - 3);
2534
+ const tree = view.tree.flatMap((n) => {
2535
+ const teamed = n.teams.length > 0;
2536
+ const common = n.branches.length > 0;
2537
+ return [
2538
+ { kind: "project", key: n.key, label: n.label, color: projectColor(n.color), count: n.count },
2539
+ ...teamed && common ? [{ kind: "common", key: `${n.key}|common` }] : [],
2540
+ ...n.branches.flatMap((b) => [{ kind: "branch", key: `${n.key}|${b.branch}`, branch: b.branch, indent: teamed ? 4 : 2 }, ...b.sessions.map((s) => ({ kind: "session", key: s.id, s, indent: teamed ? 2 : 0 }))]),
2541
+ ...n.teams.flatMap((x) => [{ kind: "team", key: x.key, team: x.team, done: x.done, given: x.given }, ...x.sessions.map((s) => ({ kind: "session", key: s.id, s, indent: 2 }))])
2542
+ ];
2543
+ });
2544
+ const yoursCount = view.yours.length + closed.length + near.length;
2545
+ const yoursHeight = Math.max(1, Math.min(yoursCount, 6));
2546
+ const joinHeight = joining && selected ? Math.max(1, choices2.length) + 2 : 0;
2547
+ const treeHeight = Math.max(3, height - 3 - (yoursHeight + 2) - joinHeight - 3);
2197
2548
  const inner = left - 4, nameW = 7;
2198
2549
  const stateW = Math.max(...Object.values(t.state).map((x) => x.length)) + 1;
2199
2550
  const topicW = Math.max(6, inner - 6 - nameW - stateW - 3);
2200
2551
  const askW = Math.max(6, inner - 2 - nameW - 9 - 7);
2201
- const yoursBox = /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.yours(view.yours.length), color: view.yours.length ? C.red : C.line, titleColor: view.yours.length ? C.red : C.dim, height: yoursHeight + 2, children: view.yours.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.nobodyWaits }) : view.yours.slice(0, yoursHeight).map((s) => {
2552
+ const yoursBox = /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.yours(yoursCount), color: yoursCount ? C.red : C.line, titleColor: yoursCount ? C.red : C.dim, height: yoursHeight + 2, children: yoursCount === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.nobodyWaits }) : [...view.yours.slice(0, yoursHeight).map((s) => {
2202
2553
  const st = botState(s);
2203
2554
  return /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
2204
2555
  /* @__PURE__ */ jsxs5(Text6, { color: STATE_COLOR[st], children: [
@@ -2213,42 +2564,91 @@ function Overview() {
2213
2564
  ago(s.lastActivityAt, now, t.time)
2214
2565
  ] })
2215
2566
  ] }, s.id);
2216
- }) });
2567
+ }), ...closed.slice(0, Math.max(0, yoursHeight - view.yours.length)).map((x) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
2568
+ /* @__PURE__ */ jsxs5(Text6, { color: x.state === "done" ? C.green : C.yellow, children: [
2569
+ x.state === "done" ? g.ok : g.fail,
2570
+ " "
2571
+ ] }),
2572
+ /* @__PURE__ */ jsx7(Text6, { color: C.fg, children: t.overview.ledgeTask(x.ownerName, x.title, x.state) })
2573
+ ] }, x.id)), ...near.slice(0, Math.max(0, yoursHeight - view.yours.length - closed.length)).map((s) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
2574
+ /* @__PURE__ */ jsxs5(Text6, { color: C.yellow, children: [
2575
+ s.handover?.state === "failed" ? g.fail : "\u25D4",
2576
+ " "
2577
+ ] }),
2578
+ /* @__PURE__ */ jsx7(Text6, { color: C.fg, children: s.handover?.state === "failed" ? t.overview.reliefFailed(s.name, s.handover.reason ?? "?") : t.overview.near(`${s.name}${s.role ? ` @${s.role}` : ""}`, Math.round((s.context?.used ?? 0) / (s.context?.window ?? 1) * 100)) })
2579
+ ] }, `near:${s.id}`))] });
2217
2580
  const treeBox = /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.projects, flexGrow: 1, children: tree.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.noSession }) : windowed(tree, sel, treeHeight).map((r) => r.kind === "project" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
2218
2581
  /* @__PURE__ */ jsx7(Text6, { color: r.color, children: g.project }),
2219
2582
  " ",
2220
2583
  /* @__PURE__ */ jsx7(Text6, { color: C.fg, bold: true, children: r.label }),
2221
2584
  " ",
2222
2585
  /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.sessions(r.count) })
2223
- ] }, r.key) : r.kind === "branch" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", color: C.faint, children: [
2586
+ ] }, r.key) : r.kind === "common" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", color: C.faint, children: [
2224
2587
  " ",
2588
+ g.common,
2589
+ " ",
2590
+ t.overview.common
2591
+ ] }, r.key) : r.kind === "team" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
2592
+ /* @__PURE__ */ jsx7(Text6, { children: " " }),
2593
+ /* @__PURE__ */ jsx7(Text6, { color: C.coral, children: g.team }),
2594
+ " ",
2595
+ /* @__PURE__ */ jsx7(Text6, { color: r.team.state === "resting" ? C.dim : C.fg, bold: true, children: r.team.name }),
2596
+ " ",
2597
+ /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: r.team.state === "resting" ? t.overview.resting : t.overview.teamLine(r.done, r.given) })
2598
+ ] }, r.key) : r.kind === "branch" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", color: C.faint, children: [
2599
+ " ".repeat(r.indent),
2225
2600
  g.branch,
2226
2601
  " ",
2227
2602
  r.branch
2228
- ] }, r.key) : /* @__PURE__ */ jsx7(SessionRow, { s: r.s, on: r.s.id === sel, nameW, topicW, stateW }, r.key)) });
2603
+ ] }, r.key) : /* @__PURE__ */ jsx7(SessionRow, { s: r.s, on: r.s.id === sel, nameW, topicW: Math.max(6, topicW - r.indent - (r.s.role ? r.s.role.length + 2 : 0)), stateW, indent: r.indent }, r.key)) });
2229
2604
  const keys = [
2230
2605
  ["j k", t.keys.navigate],
2231
2606
  ["\u21B5", t.keys.open],
2232
2607
  ...view.yours.length ? [["tab", t.keys.next], ["y n", t.keys.answer]] : [],
2233
2608
  ["a", t.keys.attach],
2609
+ ...seatTeam ? [["T", t.keys.tasks]] : [],
2610
+ ...choices2.length ? [["J", t.keys.join]] : [],
2611
+ ...view.tree.some((n) => n.teams.some((x) => x.team.state === "resting")) ? [["R", t.keys.resume]] : [],
2612
+ ...near.length ? [["H", t.keys.handover]] : [],
2234
2613
  ["/", t.keys.ask],
2235
2614
  ["+", t.keys.launch],
2236
2615
  [",", t.keys.settings],
2237
2616
  ["?", t.keys.help],
2238
2617
  ["q", t.keys.quit]
2239
2618
  ];
2619
+ const teamTasks = seatTeam ? tasks.filter((x) => x.team === seatTeam.id && (!taskClosed(x) || x.updatedAt.slice(0, 10) === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10))).slice(-4) : [];
2240
2620
  return /* @__PURE__ */ jsx7(Frame, { crumb: tui.machines.map((m) => `${m.label} ${m.ready ? g.machine : "\u25CB"}`).join(" "), keys, children: /* @__PURE__ */ jsxs5(Box6, { flexGrow: 1, children: [
2241
2621
  /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", width: left, flexShrink: 0, children: [
2242
2622
  yoursBox,
2623
+ joining && selected ? /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.joinWhich(selected.name), color: C.coral, titleColor: C.coral, height: joinHeight, children: choices2.map((x, i) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: i === joining.index ? C.sel : void 0, children: [
2624
+ /* @__PURE__ */ jsx7(Text6, { color: C.coral, children: g.team }),
2625
+ " ",
2626
+ x.name
2627
+ ] }, x.id)) }) : null,
2243
2628
  treeBox
2244
2629
  ] }),
2245
2630
  wide ? /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", flexGrow: 1, children: [
2246
2631
  selected ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
2247
2632
  /* @__PURE__ */ jsx7(TitledBox, { title: `${selected.name} \xB7 ${oneLine(selected.topic ?? selected.title)}`, color: isPending(selected) ? C.red : C.line, titleColor: isPending(selected) ? C.red : C.dim, children: /* @__PURE__ */ jsx7(SessionCard, { s: selected, note: true }) }),
2633
+ seatTeam && tasksShown ? /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.tasks(`${g.team} ${seatTeam.name}`), height: Math.max(3, teamTasks.length + 2), children: teamTasks.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.noTask }) : teamTasks.map((x) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
2634
+ /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: x.id }),
2635
+ " ",
2636
+ /* @__PURE__ */ jsx7(Text6, { color: C.fg, children: x.title }),
2637
+ " ",
2638
+ /* @__PURE__ */ jsxs5(Text6, { color: C.dim, children: [
2639
+ x.giverName,
2640
+ " ",
2641
+ g.arrow,
2642
+ " ",
2643
+ x.ownerName
2644
+ ] }),
2645
+ " ",
2646
+ /* @__PURE__ */ jsx7(Text6, { color: x.state === "done" ? C.green : x.state === "taken" ? C.blue : C.faint, children: t.ask.taskState[x.state] })
2647
+ ] }, x.id)) }) : null,
2248
2648
  /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.terminal(paneName(selected)), flexGrow: 1, children: /* @__PURE__ */ jsx7(Tail, { lines: d.previews.get(selected.id) ?? [], color: C.dim, empty: t.overview.noPreview, emptyColor: C.faint }) })
2249
2649
  ] }) : /* @__PURE__ */ jsx7(TitledBox, { title: t.tabs.session, flexGrow: 1, children: /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.session.noSession }) }),
2250
2650
  /* @__PURE__ */ jsx7(TitledBox, { title: t.overview.exchanges, height: 5, children: (() => {
2251
- const list = exchanges(d.sessions, 3);
2651
+ const list = exchanges(d.sessions, 3, tasks, t.overview.ledge);
2252
2652
  return list.length === 0 ? /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: t.overview.noExchange }) : list.map((x, i) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
2253
2653
  /* @__PURE__ */ jsx7(Text6, { color: C.faint, children: clock(x.at) }),
2254
2654
  " ",
@@ -2264,18 +2664,23 @@ function Overview() {
2264
2664
  ] }) : null
2265
2665
  ] }) });
2266
2666
  }
2267
- function SessionRow({ s, on, nameW, topicW, stateW }) {
2667
+ function SessionRow({ s, on, nameW, topicW, stateW, indent = 0 }) {
2268
2668
  const { t, g } = useTui();
2269
2669
  const st = botState(s);
2270
2670
  const n = helperCount(s);
2271
2671
  return /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: on ? C.sel : void 0, children: [
2272
2672
  /* @__PURE__ */ jsx7(Text6, { color: on ? C.coral : C.faint, children: on ? "\u258C" : " " }),
2273
- /* @__PURE__ */ jsx7(Text6, { children: " " }),
2673
+ /* @__PURE__ */ jsx7(Text6, { children: " ".repeat(2 + indent) }),
2274
2674
  /* @__PURE__ */ jsxs5(Text6, { color: s.offline ? C.faint : STATE_COLOR[st], children: [
2275
2675
  g[st],
2276
2676
  " "
2277
2677
  ] }),
2278
2678
  /* @__PURE__ */ jsx7(Text6, { color: s.offline ? C.dim : C.fg, bold: true, children: fit(s.name, nameW) }),
2679
+ s.role ? /* @__PURE__ */ jsxs5(Text6, { color: C.violet, children: [
2680
+ "@",
2681
+ s.role,
2682
+ " "
2683
+ ] }) : null,
2279
2684
  /* @__PURE__ */ jsxs5(Text6, { color: C.dim, children: [
2280
2685
  fit(oneLine(s.topic ?? s.title), topicW),
2281
2686
  " "
@@ -2293,7 +2698,7 @@ function SessionRow({ s, on, nameW, topicW, stateW }) {
2293
2698
  }
2294
2699
 
2295
2700
  // src/tui/screens/Session.tsx
2296
- import { useEffect as useEffect6, useMemo as useMemo3, useState as useState4 } from "react";
2701
+ import { useEffect as useEffect6, useMemo as useMemo3, useState as useState5 } from "react";
2297
2702
  import { Box as Box9, Text as Text10, useWindowSize as useWindowSize3 } from "ink";
2298
2703
 
2299
2704
  // ../ui/src/thread/turns.ts
@@ -2454,18 +2859,18 @@ function threadLines(items, o) {
2454
2859
  }
2455
2860
 
2456
2861
  // src/tui/ui/ModelPicker.tsx
2457
- import { useState as useState3 } from "react";
2862
+ import { useState as useState4 } from "react";
2458
2863
  import { Box as Box7, Text as Text7, useInput } from "ink";
2459
2864
 
2460
2865
  // ../fleet/src/models.ts
2461
- import { useEffect as useEffect4, useState as useState2 } from "react";
2866
+ import { useEffect as useEffect4, useState as useState3 } from "react";
2462
2867
  var IDLE = { status: "idle", models: [] };
2463
2868
  var LOADING = { status: "loading", models: [] };
2464
2869
  var known = /* @__PURE__ */ new Map();
2465
2870
  var next = 0;
2466
2871
  function useModels(client, machine, tool) {
2467
2872
  const key = machine === null || tool === null ? null : `${machine}|${tool}`;
2468
- const [answer, setAnswer] = useState2(null);
2873
+ const [answer, setAnswer] = useState3(null);
2469
2874
  useEffect4(() => {
2470
2875
  if (key === null || machine === null || tool === null || known.has(key)) return;
2471
2876
  const requestId = `models-${++next}`;
@@ -2489,8 +2894,8 @@ function ModelPicker({ machine, tool, current, onPick, onCancel }) {
2489
2894
  useEditing(true);
2490
2895
  const models = useModels(client, machine, tool);
2491
2896
  const rows = [{ id: null, label: t.launch.defaultModel, efforts: [], defaultEffort: void 0 }, ...models.models.map((m) => ({ id: m.id, label: m.label, efforts: m.efforts ?? [], defaultEffort: m.defaultEffort }))];
2492
- const [at, setAt] = useState3(() => Math.max(0, rows.findIndex((r) => r.id === current.model)));
2493
- const [effort, setEffort] = useState3(current.effort);
2897
+ const [at, setAt] = useState4(() => Math.max(0, rows.findIndex((r) => r.id === current.model)));
2898
+ const [effort, setEffort] = useState4(current.effort);
2494
2899
  const row = rows[Math.min(at, rows.length - 1)];
2495
2900
  useInput((input, key) => {
2496
2901
  if (key.escape) {
@@ -2544,7 +2949,7 @@ function Lines({ lines, up = 0, onHeight }) {
2544
2949
  const h = Math.max(1, height);
2545
2950
  const back = Math.min(up, Math.max(0, lines.length - h));
2546
2951
  const shown = lines.slice(Math.max(0, lines.length - h - back), lines.length - back);
2547
- return /* @__PURE__ */ jsx9(Box8, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.map((l, i) => /* @__PURE__ */ jsx9(Text8, { wrap: "truncate", children: l.map((x, k) => /* @__PURE__ */ jsx9(Text8, { color: x.color, bold: x.bold, children: x.text || (k === 0 ? " " : "") }, k)) }, i)) });
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)) });
2548
2953
  }
2549
2954
 
2550
2955
  // src/tui/ui/TextInput.tsx
@@ -2594,12 +2999,12 @@ function SessionScreen() {
2594
2999
  function SessionView({ s }) {
2595
3000
  const { client, d, t, g, now, actions, attach, toast, machines } = useTui();
2596
3001
  const { columns } = useWindowSize3();
2597
- const [mode, setMode] = useState4("read");
2598
- const [draft, setDraft] = useState4("");
2599
- const [diffs, setDiffs] = useState4(false);
2600
- const [unfolded, setUnfolded] = useState4(false);
2601
- const [terminal, setTerminal] = useState4(false);
2602
- const [up, setUp] = useState4(0);
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);
2603
3008
  useEditing(mode === "reply" || mode === "note");
2604
3009
  const items = d.threads.get(s.id);
2605
3010
  useEffect6(() => {
@@ -2608,7 +3013,7 @@ function SessionView({ s }) {
2608
3013
  const width = Math.max(20, columns - 6);
2609
3014
  const lines = useMemo3(() => items ? threadLines(items, { t, g, now, width, diffs, unfolded }) : [], [items, t, g, now, width, diffs, unfolded]);
2610
3015
  const p = s.pending;
2611
- const [view, setView] = useState4(10);
3016
+ const [view, setView] = useState5(10);
2612
3017
  const maxUp = Math.max(0, lines.length - view);
2613
3018
  const machine = splitId(s.id)?.machine ?? "local";
2614
3019
  const canSwitch = machines.find((m) => m.id === machine)?.daemon?.modelSwitch === true;
@@ -2735,12 +3140,12 @@ function SessionView({ s }) {
2735
3140
  offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx11(Button, { label: t.overview.denyWithNote, k: "r" }) : null
2736
3141
  ] }) })
2737
3142
  ] }) : null,
2738
- /* @__PURE__ */ jsx11(TitledBox, { title: mode === "note" ? t.overview.denyWithNote : t.keys.write, color: mode === "read" ? C.line : C.coral, children: /* @__PURE__ */ jsxs8(Text10, { wrap: "truncate", children: [
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: [
2739
3144
  /* @__PURE__ */ jsxs8(Text10, { color: C.coral, children: [
2740
3145
  g.cursor,
2741
3146
  " "
2742
3147
  ] }),
2743
- /* @__PURE__ */ jsx11(
3148
+ /* @__PURE__ */ jsx11(Box9, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx11(
2744
3149
  TextInput,
2745
3150
  {
2746
3151
  value: draft,
@@ -2750,14 +3155,14 @@ function SessionView({ s }) {
2750
3155
  active: mode === "reply" || mode === "note",
2751
3156
  placeholder: mode === "note" ? t.session.noteFor(s.name) : t.session.replyTo(s.name)
2752
3157
  }
2753
- )
3158
+ ) })
2754
3159
  ] }) })
2755
3160
  ] });
2756
3161
  }
2757
3162
 
2758
3163
  // src/tui/screens/Ask.tsx
2759
- import { randomUUID } from "crypto";
2760
- import { useEffect as useEffect7, useMemo as useMemo4, useRef as useRef7, useState as useState5 } from "react";
3164
+ import { randomUUID as randomUUID2 } from "crypto";
3165
+ import { useEffect as useEffect7, useMemo as useMemo4, useRef as useRef7, useState as useState6 } from "react";
2761
3166
  import { Box as Box10, Text as Text11, useInput as useInput3, useWindowSize as useWindowSize4 } from "ink";
2762
3167
 
2763
3168
  // src/tui/model/chat.ts
@@ -2780,6 +3185,8 @@ function chatLines(turns2, live, o) {
2780
3185
  out.push([{ text: "" }]);
2781
3186
  for (const l of wrap(text, width - 2)) out.push([{ text: " " }, { text: l, color: C.fg }]);
2782
3187
  } else out.push([{ text: ` ${t.ask.reading}`, color: C.faint }]);
3188
+ if (turn.team?.teamId && turn.team.launchedAt) out.push([{ text: ` ${g.team} ${turn.team.name} \xB7 ${t.ask.teamLaunched(clock(turn.team.launchedAt))}`, color: C.green }]);
3189
+ for (const c of turn.tasks ?? []) if (c.taskId) out.push([{ text: ` ${c.title} \xB7 ${t.ask.given(c.ownerName ?? c.toName ?? c.to, c.taskId, t.ask.taskState[c.state ?? "open"])}`, color: C.green }]);
2783
3190
  }
2784
3191
  return out;
2785
3192
  }
@@ -2787,6 +3194,9 @@ function openCards(turns2) {
2787
3194
  const out = [];
2788
3195
  for (const turn of [...turns2].reverse()) {
2789
3196
  if (!turn.answeredAt) continue;
3197
+ if (turn.team && !turn.team.teamId) out.push({ kind: "team", qid: turn.qid });
3198
+ const tasks = (turn.tasks ?? []).map((c, i) => c.taskId ? -1 : i).filter((i) => i >= 0);
3199
+ if (tasks.length) out.push({ kind: "task", qid: turn.qid, rows: tasks });
2790
3200
  const launch = proposalRows(turn).map((p, i) => p.launchedId ? -1 : i).filter((i) => i >= 0);
2791
3201
  if (launch.length) out.push({ kind: "launch", qid: turn.qid, rows: launch });
2792
3202
  if (turn.relay && turn.relay.status !== "sent") out.push({ kind: "relay", qid: turn.qid });
@@ -2802,9 +3212,9 @@ var CARD_MS = 2e4;
2802
3212
  function Ask() {
2803
3213
  const { client, d, machines, t, g, toast, go } = useTui();
2804
3214
  const { columns } = useWindowSize4();
2805
- const [draft, setDraft] = useState5("");
2806
- const [focus, setFocus] = useState5(-1);
2807
- const [busy, setBusy] = useState5(null);
3215
+ const [draft, setDraft] = useState6("");
3216
+ const [focus, setFocus] = useState6(-1);
3217
+ const [busy, setBusy] = useState6(null);
2808
3218
  useEditing(focus === -1);
2809
3219
  const cards = useMemo4(() => openCards(d.chat), [d.chat]);
2810
3220
  useEffect7(() => {
@@ -2828,7 +3238,7 @@ function Ask() {
2828
3238
  toast({ text: t.ask.offline, tone: "info" });
2829
3239
  return;
2830
3240
  }
2831
- const qid = `q-${randomUUID()}`;
3241
+ const qid = `q-${randomUUID2()}`;
2832
3242
  daemonStore.beginTurn({ qid, question: text.trim(), askedAt: (/* @__PURE__ */ new Date()).toISOString(), steps: [] });
2833
3243
  client.send({ type: "chat.ask", qid, text: text.trim() });
2834
3244
  setDraft("");
@@ -2837,8 +3247,22 @@ function Ask() {
2837
3247
  if (busy) return;
2838
3248
  const turn = d.chat.find((x) => x.qid === card.qid);
2839
3249
  if (!turn) return;
3250
+ if (card.kind === "task") {
3251
+ const row = card.rows.find((r) => {
3252
+ const c = turn.tasks[r];
3253
+ return !c.to.startsWith("@") || c.team || turn.team?.teamId;
3254
+ });
3255
+ if (row === void 0) {
3256
+ toast({ text: t.ask.waitTeam, tone: "info" });
3257
+ return;
3258
+ }
3259
+ setBusy({ qid: card.qid, turn, errorSeq: d.errors.at(-1)?.seq ?? 0, at: Date.now() });
3260
+ client.send({ type: "task.cmd", rid: randomUUID2(), by: LEDGE_GIVER, op: "give", qid: card.qid, row });
3261
+ return;
3262
+ }
2840
3263
  setBusy({ qid: card.qid, turn, errorSeq: d.errors.at(-1)?.seq ?? 0, at: Date.now() });
2841
- if (card.kind === "launch") {
3264
+ if (card.kind === "team") client.send({ type: "team.launch", rid: randomUUID2(), qid: card.qid });
3265
+ else if (card.kind === "launch") {
2842
3266
  const first = proposalRows(turn)[card.rows[0]];
2843
3267
  client.send({ type: "session.launch", tool: first.tool, cwd: first.cwd, ...first.prompt ? { prompt: first.prompt } : {}, qid: card.qid, rows: card.rows });
2844
3268
  } else if (card.kind === "relay") client.send({ type: turn.relay?.status === "queued" ? "session.relay.cancel" : "session.relay", qid: card.qid });
@@ -2869,6 +3293,11 @@ function Ask() {
2869
3293
  act(cards[focus]);
2870
3294
  return true;
2871
3295
  }
3296
+ if (input === "s" && cards[focus].kind === "team") {
3297
+ const team = d.chat.find((x) => x.qid === cards[focus].qid)?.team;
3298
+ if (team && !team.template && !team.savedPath) client.send({ type: "team.save", rid: randomUUID2(), qid: cards[focus].qid, name: team.name, seats: team.seats.map((x) => ({ role: x.role, tool: x.tool, ...x.model ? { model: x.model } : {}, ...x.effort ? { effort: x.effort } : {} })) });
3299
+ return true;
3300
+ }
2872
3301
  if (key.escape) {
2873
3302
  setFocus(-1);
2874
3303
  return true;
@@ -2878,26 +3307,84 @@ function Ask() {
2878
3307
  const sessions = listed(d.sessions).length;
2879
3308
  const width = Math.max(20, columns - 6);
2880
3309
  const lines = useMemo4(() => chatLines(d.chat, d.chatLive, { t, g, width }), [d.chat, d.chatLive, t, g, width]);
2881
- return /* @__PURE__ */ jsxs9(Frame, { crumb: `${t.tabs.ask}`, keys: [["\u21B5", t.keys.send], ["tab", t.keys.nextCard], ["ctrl+n", t.keys.newChat], ["ctrl+c", t.keys.stopAnswer], ["esc", t.keys.back]], children: [
2882
- /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.title(sessions, Math.max(1, machines.length)), flexGrow: 1, children: lines.length === 0 ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.empty }) : /* @__PURE__ */ jsx12(Lines, { lines }) }),
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 }) }),
2883
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}`)),
2884
- /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.question, color: focus === -1 ? C.coral : C.line, children: /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3313
+ /* @__PURE__ */ jsx12(TitledBox, { title: t.ask.question, flexShrink: 0, color: focus === -1 ? C.coral : C.line, children: /* @__PURE__ */ jsxs9(Box10, { children: [
2885
3314
  /* @__PURE__ */ jsxs9(Text11, { color: C.coral, children: [
2886
3315
  g.cursor,
2887
3316
  " "
2888
3317
  ] }),
2889
- /* @__PURE__ */ jsx12(TextInput, { value: draft, onChange: setDraft, onSubmit: ask, onEscape: () => {
3318
+ /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx12(TextInput, { value: draft, onChange: setDraft, onSubmit: ask, onEscape: () => {
2890
3319
  if (!draft) go("overview");
2891
- }, active: focus === -1 })
3320
+ }, active: focus === -1 }) })
2892
3321
  ] }) })
2893
3322
  ] });
2894
3323
  }
2895
3324
  function CardBox({ card, turn, focused, busy }) {
2896
3325
  const { t, g } = useTui();
2897
3326
  const color = focused ? C.coral : C.line;
3327
+ if (card.kind === "team") {
3328
+ const team = turn.team;
3329
+ const auto = team.seats.filter((x) => x.autonomy === "auto").map((x) => `@${x.role}`);
3330
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.teamCard(team.name), flexShrink: 0, color, titleColor: C.coral, children: [
3331
+ /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3332
+ /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.folder.padEnd(13) }),
3333
+ /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: team.cwd })
3334
+ ] }),
3335
+ /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3336
+ /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: t.ask.table.padEnd(13) }),
3337
+ g.team,
3338
+ " ",
3339
+ t.ask.newTable(team.projectName ?? team.project)
3340
+ ] }),
3341
+ team.seats.map((x) => /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3342
+ /* @__PURE__ */ jsx12(Text11, { color: C.dim, children: "".padEnd(13) }),
3343
+ /* @__PURE__ */ jsxs9(Text11, { color: C.violet, children: [
3344
+ "@",
3345
+ x.role
3346
+ ] }),
3347
+ " ",
3348
+ x.label,
3349
+ " ",
3350
+ /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: [TOOL_NAME[x.tool], x.model, x.effort].filter(Boolean).join(" \xB7 ") }),
3351
+ " ",
3352
+ /* @__PURE__ */ jsx12(Text11, { color: x.autonomy === "auto" ? C.yellow : C.faint, children: t.ask.autonomy[x.autonomy ?? "ask"] })
3353
+ ] }, x.role)),
3354
+ auto.length ? /* @__PURE__ */ jsx12(Text11, { color: C.yellow, wrap: "truncate", children: t.ask.autoWarning(auto.join(", ")) }) : null,
3355
+ team.error ? /* @__PURE__ */ jsx12(Text11, { color: C.red, wrap: "truncate", children: team.error }) : null,
3356
+ /* @__PURE__ */ jsxs9(Box10, { columnGap: 2, children: [
3357
+ /* @__PURE__ */ jsx12(Button, { label: busy ? `${g.live} ${t.ask.launchTeam}` : t.ask.launchTeam, k: "\u21B5", primary: focused }),
3358
+ !team.template ? team.savedPath ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.saved }) : /* @__PURE__ */ jsx12(Button, { label: t.ask.save, k: "s" }) : null,
3359
+ /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.nothingWithoutYou })
3360
+ ] })
3361
+ ] });
3362
+ }
3363
+ if (card.kind === "task") {
3364
+ const tasks = turn.tasks;
3365
+ const waits = !!turn.team && !turn.team.teamId;
3366
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.taskCard, flexShrink: 0, color, titleColor: C.violet, children: [
3367
+ card.rows.map((r) => {
3368
+ const c = tasks[r];
3369
+ return /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
3370
+ /* @__PURE__ */ jsx12(Text11, { color: C.fg, children: c.title }),
3371
+ " ",
3372
+ /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: c.toName ?? c.to }),
3373
+ c.error ? /* @__PURE__ */ jsxs9(Text11, { color: C.red, children: [
3374
+ " ",
3375
+ c.error
3376
+ ] }) : null
3377
+ ] }, r);
3378
+ }),
3379
+ /* @__PURE__ */ jsxs9(Box10, { columnGap: 2, children: [
3380
+ /* @__PURE__ */ jsx12(Button, { label: busy ? `${g.live} ${t.ask.give}` : t.ask.give, k: "\u21B5", primary: focused && !waits }),
3381
+ waits ? /* @__PURE__ */ jsx12(Text11, { color: C.faint, children: t.ask.waitTeam }) : null
3382
+ ] })
3383
+ ] });
3384
+ }
2898
3385
  if (card.kind === "launch") {
2899
3386
  const rows = proposalRows(turn);
2900
- return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.launchCard, color, titleColor: C.coral, children: [
3387
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.launchCard, flexShrink: 0, color, titleColor: C.coral, children: [
2901
3388
  card.rows.map((r) => {
2902
3389
  const p = rows[r];
2903
3390
  return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
@@ -2927,7 +3414,7 @@ function CardBox({ card, turn, focused, busy }) {
2927
3414
  }
2928
3415
  if (card.kind === "relay") {
2929
3416
  const r = turn.relay;
2930
- return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.relayCard(r.name), color, titleColor: C.violet, children: [
3417
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.relayCard(r.name), flexShrink: 0, color, titleColor: C.violet, children: [
2931
3418
  /* @__PURE__ */ jsx12(Text11, { wrap: "truncate", color: C.fg, children: oneLine(r.text) }),
2932
3419
  r.error ? /* @__PURE__ */ jsx12(Text11, { color: C.red, wrap: "truncate", children: r.error }) : null,
2933
3420
  /* @__PURE__ */ jsxs9(Box10, { columnGap: 2, children: [
@@ -2937,7 +3424,7 @@ function CardBox({ card, turn, focused, busy }) {
2937
3424
  ] });
2938
3425
  }
2939
3426
  const closes = turn.closes ?? [];
2940
- return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.closeCard, color, titleColor: C.red, children: [
3427
+ return /* @__PURE__ */ jsxs9(TitledBox, { title: t.ask.closeCard, flexShrink: 0, color, titleColor: C.red, children: [
2941
3428
  card.rows.map((r) => /* @__PURE__ */ jsxs9(Text11, { wrap: "truncate", children: [
2942
3429
  /* @__PURE__ */ jsx12(Text11, { color: C.fg, children: closes[r].name }),
2943
3430
  " ",
@@ -2952,7 +3439,7 @@ function CardBox({ card, turn, focused, busy }) {
2952
3439
  }
2953
3440
 
2954
3441
  // src/tui/screens/Launch.tsx
2955
- import { useEffect as useEffect8, useMemo as useMemo5, useState as useState6 } from "react";
3442
+ import { useEffect as useEffect8, useMemo as useMemo5, useState as useState7 } from "react";
2956
3443
  import { Box as Box11, Text as Text12, useInput as useInput4 } from "ink";
2957
3444
 
2958
3445
  // ../fleet/src/launch.ts
@@ -3020,19 +3507,24 @@ function launchMessage(f, name) {
3020
3507
  }
3021
3508
 
3022
3509
  // src/tui/screens/Launch.tsx
3023
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3510
+ import { randomUUID as randomUUID3 } from "crypto";
3511
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3024
3512
  var FIELDS = ["prompt", "tool", "model", "effort", "project", "machine", "repos", "folder", "name"];
3025
3513
  var TEXT = /* @__PURE__ */ new Set(["prompt", "folder"]);
3026
3514
  var LAUNCH_MS = 2e4;
3027
3515
  var cycle = (list, cur, by) => list[((Math.max(0, list.indexOf(cur)) + by) % list.length + list.length) % list.length];
3028
3516
  function Launch() {
3517
+ const [team, setTeam] = useState7(false);
3518
+ return team ? /* @__PURE__ */ jsx13(TeamLaunch, { onSession: () => setTeam(false) }) : /* @__PURE__ */ jsx13(SessionLaunch, { onTeam: () => setTeam(true) });
3519
+ }
3520
+ function SessionLaunch({ onTeam }) {
3029
3521
  const { client, d, machines, projects, t, g, toast, go, select } = useTui();
3030
3522
  const list = projects?.projects ?? [];
3031
- const [form, setForm] = useState6(() => launchDefaults(list, machines, {}, "claude"));
3032
- const [field, setField] = useState6("prompt");
3033
- const [folder, setFolder] = useState6("");
3034
- const [repoAt, setRepoAt] = useState6(0);
3035
- const [launching, setLaunching] = useState6(null);
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);
3036
3528
  useEditing(true);
3037
3529
  useEffect8(() => {
3038
3530
  if (form.machine === null) {
@@ -3054,7 +3546,7 @@ function Launch() {
3054
3546
  const withFolder = folder.trim() ? { ...form, repos: MULTI_REPO_TOOLS.has(form.tool) ? [folder.trim(), ...form.repos.filter((r) => r !== folder.trim())] : [folder.trim()] } : form;
3055
3547
  const projectIdOf = (s) => projects ? projectOf(s, projects) : null;
3056
3548
  const drawn = useMemo5(() => previewOf(withFolder, d.sessions, projectIdOf), [form.machine, form.project, d.sessions.length]);
3057
- const [name, setName] = useState6(null);
3549
+ const [name, setName] = useState7(null);
3058
3550
  const shownName = name ?? drawn.name;
3059
3551
  const missing = missingOf(withFolder, machines);
3060
3552
  useEffect8(() => {
@@ -3094,6 +3586,10 @@ function Launch() {
3094
3586
  launch();
3095
3587
  return;
3096
3588
  }
3589
+ if (key.ctrl && input === "t") {
3590
+ onTeam();
3591
+ return;
3592
+ }
3097
3593
  if (key.ctrl && input === "r") {
3098
3594
  setName(pickName([...d.sessions.map((s) => s.name), shownName]));
3099
3595
  return;
@@ -3143,8 +3639,27 @@ function Launch() {
3143
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) }),
3144
3640
  " "
3145
3641
  ] }, i)) });
3146
- return /* @__PURE__ */ jsxs10(Frame, { crumb: t.launch.title, keys: [["tab", t.keys.nextField], ["\u2190 \u2192", t.keys.choose], [t.keys.space, t.keys.tick], ["ctrl+s", t.keys.start], ["ctrl+r", t.keys.redraw], ["esc", t.keys.cancel]], children: [
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: [
3147
3643
  /* @__PURE__ */ jsxs10(TitledBox, { title: t.launch.title, flexGrow: 1, children: [
3644
+ /* @__PURE__ */ jsxs10(Box11, { children: [
3645
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: " " }),
3646
+ /* @__PURE__ */ jsx13(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: t.launch.mode }) }),
3647
+ /* @__PURE__ */ jsxs10(Text12, { children: [
3648
+ /* @__PURE__ */ jsxs10(Text12, { color: C.coral, children: [
3649
+ g.radioOn,
3650
+ " ",
3651
+ t.launch.session
3652
+ ] }),
3653
+ " ",
3654
+ /* @__PURE__ */ jsxs10(Text12, { color: C.dim, children: [
3655
+ g.radioOff,
3656
+ " ",
3657
+ t.launch.team
3658
+ ] }),
3659
+ " ",
3660
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: "ctrl+t" })
3661
+ ] })
3662
+ ] }),
3148
3663
  row("prompt", t.launch.instruction, /* @__PURE__ */ jsx13(TextInput, { value: form.prompt, onChange: (v) => setForm((f) => ({ ...f, prompt: v })), active: field === "prompt" })),
3149
3664
  /* @__PURE__ */ jsx13(Text12, { children: " " }),
3150
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: [
@@ -3198,15 +3713,226 @@ function Launch() {
3198
3713
  ] })
3199
3714
  ] });
3200
3715
  }
3716
+ var TEAM_FIELDS = ["project", "folder", "template", "seats", "name"];
3717
+ var AUTONOMY_COLOR = { ask: C.faint, edits: C.dim, auto: C.yellow };
3718
+ function TeamLaunch({ onSession }) {
3719
+ const { client, machines, projects, t, g, toast, go } = useTui();
3720
+ const list = projects?.projects ?? [];
3721
+ const [projectId, setProjectId] = useState7(list[0]?.id ?? null);
3722
+ const project = list.find((p) => p.id === projectId);
3723
+ const folders = useMemo5(() => {
3724
+ const own = (project?.repos ?? []).filter((r) => r.machine === "local").map((r) => r.path);
3725
+ return own.length ? own : (machines.find((m) => m.id === "local")?.recents ?? []).slice(0, 6).map((r) => r.cwd);
3726
+ }, [project, machines]);
3727
+ const [folderAt, setFolderAt] = useState7(0);
3728
+ const cwd = folders[folderAt] ?? folders[0] ?? null;
3729
+ const [field, setField] = useState7("template");
3730
+ const [defs, setDefs] = useState7(null);
3731
+ const [template, setTemplate] = useState7(null);
3732
+ const [seats, setSeats] = useState7([]);
3733
+ const [seatAt, setSeatAt] = useState7(0);
3734
+ const [name, setName] = useState7("");
3735
+ const [launching, setLaunching] = useState7(false);
3736
+ useEditing(true);
3737
+ useEffect8(() => {
3738
+ if (!cwd) {
3739
+ setDefs({ roles: [], templates: [] });
3740
+ return;
3741
+ }
3742
+ setDefs(null);
3743
+ const requestId = `tui-roles-${randomUUID3()}`;
3744
+ const off = client.onMessage((m) => {
3745
+ if (m.type !== "roles.listing" || m.requestId !== requestId) return;
3746
+ off();
3747
+ setDefs({ roles: m.roles, templates: m.templates });
3748
+ setSeats(m.roles.map((role) => ({ role, tool: role.tool ?? "claude", on: false })));
3749
+ setTemplate(m.templates[0]?.id ?? null);
3750
+ setName((n) => n || m.templates[0]?.name || project?.name || "");
3751
+ });
3752
+ client.send({ type: "roles.list", cwd, requestId });
3753
+ return off;
3754
+ }, [cwd]);
3755
+ const tpl = defs?.templates.find((x) => x.id === template);
3756
+ const chosen = tpl ? tpl.seats.map((x) => {
3757
+ const role = defs.roles.find((r) => r.id === x.role);
3758
+ return { id: x.role, tool: x.tool ?? role?.tool ?? "claude", autonomy: role?.autonomy ?? "ask" };
3759
+ }) : seats.filter((x) => x.on).map((x) => ({ id: x.role.id, tool: x.tool, autonomy: x.role.autonomy ?? "ask" }));
3760
+ const auto = chosen.filter((x) => x.autonomy === "auto").map((x) => `@${x.id}`);
3761
+ const teamName = name.trim() || tpl?.name || project?.name || "team";
3762
+ const launch = () => {
3763
+ if (launching || !cwd) return;
3764
+ if (!chosen.length) {
3765
+ toast({ text: t.launch.noSeat, tone: "info" });
3766
+ return;
3767
+ }
3768
+ const rid = randomUUID3();
3769
+ setLaunching(true);
3770
+ const off = client.onMessage((m) => {
3771
+ if (m.type !== "team.launched" && m.type !== "team.failed" || m.rid !== rid) return;
3772
+ off();
3773
+ setLaunching(false);
3774
+ if (m.type === "team.launched") {
3775
+ toast({ text: t.launch.teamLaunched(m.team.name), tone: "ok" });
3776
+ go("overview");
3777
+ } else toast({ text: t.launch.teamFailed(m.reason), tone: "error" });
3778
+ });
3779
+ client.send({ type: "team.launch", rid, cwd, name: teamName, ...tpl ? { template: tpl.id } : { seats: chosen.map((x) => ({ role: x.id, tool: x.tool })) } });
3780
+ };
3781
+ const tools = ["claude", "codex", "cursor", "omp"];
3782
+ const templates = [...(defs?.templates ?? []).map((x) => x.id), null];
3783
+ useInput4((input, key) => {
3784
+ if (key.escape) {
3785
+ go("overview");
3786
+ return;
3787
+ }
3788
+ if (key.ctrl && input === "s") {
3789
+ launch();
3790
+ return;
3791
+ }
3792
+ if (key.ctrl && input === "t") {
3793
+ onSession();
3794
+ return;
3795
+ }
3796
+ if (key.tab || key.downArrow || key.return) {
3797
+ setField((f) => cycle(TEAM_FIELDS, f, key.shift ? -1 : 1));
3798
+ return;
3799
+ }
3800
+ if (key.upArrow) {
3801
+ setField((f) => cycle(TEAM_FIELDS, f, -1));
3802
+ return;
3803
+ }
3804
+ if (field === "name") return;
3805
+ const by = key.rightArrow || input === "l" ? 1 : key.leftArrow || input === "h" ? -1 : 0;
3806
+ if (field === "project" && by) {
3807
+ const ids = list.map((p) => p.id);
3808
+ if (ids.length) {
3809
+ setProjectId((cur) => cycle(ids, cur ?? ids[0], by));
3810
+ setFolderAt(0);
3811
+ }
3812
+ } else if (field === "folder" && by && folders.length) setFolderAt((i) => (i + by + folders.length) % folders.length);
3813
+ else if (field === "template" && by) {
3814
+ const next2 = cycle(templates, template, by);
3815
+ setTemplate(next2);
3816
+ const x = defs?.templates.find((y) => y.id === next2);
3817
+ if (x) setName(x.name);
3818
+ } else if (field === "seats" && !tpl) {
3819
+ if (input === "j") setSeatAt((i) => seats.length ? (i + 1) % seats.length : 0);
3820
+ else if (input === "k") setSeatAt((i) => seats.length ? (i + seats.length - 1) % seats.length : 0);
3821
+ else if (input === " ") setSeats((xs) => xs.map((x, i) => i === seatAt ? { ...x, on: !x.on } : x));
3822
+ else if (by) setSeats((xs) => xs.map((x, i) => i === seatAt ? { ...x, tool: cycle(tools, x.tool, by) } : x));
3823
+ }
3824
+ });
3825
+ const row = (f, label, value) => /* @__PURE__ */ jsxs10(Box11, { backgroundColor: field === f ? C.sel : void 0, children: [
3826
+ /* @__PURE__ */ jsx13(Text12, { color: field === f ? C.coral : C.faint, children: field === f ? "\u258C" : " " }),
3827
+ /* @__PURE__ */ jsx13(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: label }) }),
3828
+ /* @__PURE__ */ jsx13(Box11, { flexGrow: 1, overflow: "hidden", flexDirection: "column", children: value })
3829
+ ] }, f);
3830
+ return /* @__PURE__ */ jsxs10(Frame, { crumb: t.launch.teamTitle, keys: [["tab", t.keys.nextField], ["\u2190 \u2192", t.keys.choose], [t.keys.space, t.keys.tick], ["ctrl+s", t.keys.start], ["ctrl+t", t.keys.teamMode], ["esc", t.keys.cancel]], children: [
3831
+ /* @__PURE__ */ jsxs10(TitledBox, { title: t.launch.teamTitle, flexGrow: 1, children: [
3832
+ /* @__PURE__ */ jsxs10(Box11, { children: [
3833
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: " " }),
3834
+ /* @__PURE__ */ jsx13(Box11, { width: 14, flexShrink: 0, children: /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: t.launch.mode }) }),
3835
+ /* @__PURE__ */ jsxs10(Text12, { children: [
3836
+ /* @__PURE__ */ jsxs10(Text12, { color: C.dim, children: [
3837
+ g.radioOff,
3838
+ " ",
3839
+ t.launch.session
3840
+ ] }),
3841
+ " ",
3842
+ /* @__PURE__ */ jsxs10(Text12, { color: C.coral, children: [
3843
+ g.radioOn,
3844
+ " ",
3845
+ t.launch.team
3846
+ ] }),
3847
+ " ",
3848
+ /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: "ctrl+t" })
3849
+ ] })
3850
+ ] }),
3851
+ row("project", t.launch.project, /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: list.length === 0 ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.none }) : list.map((p) => /* @__PURE__ */ jsxs10(Text12, { color: p.id === projectId ? C.fg : projectColor(p.color), bold: p.id === projectId, children: [
3852
+ p.id === projectId ? `${g.prev} ${p.name} ${g.next}` : p.name,
3853
+ " "
3854
+ ] }, p.id)) })),
3855
+ row("folder", t.launch.folder, /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: cwd ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
3856
+ /* @__PURE__ */ jsx13(Text12, { color: C.fg, children: cwd }),
3857
+ folders.length > 1 ? /* @__PURE__ */ jsxs10(Text12, { color: C.faint, children: [
3858
+ " (",
3859
+ folderAt + 1,
3860
+ "/",
3861
+ folders.length,
3862
+ ")"
3863
+ ] }) : null
3864
+ ] }) : /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.none }) })),
3865
+ row("template", t.launch.template, defs === null ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.loadingRoles }) : /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: templates.map((x) => /* @__PURE__ */ jsxs10(Text12, { color: x === template ? C.fg : C.dim, bold: x === template, children: [
3866
+ x === template ? `${g.prev} ${x ?? t.launch.custom} ${g.next}` : x ?? t.launch.custom,
3867
+ " "
3868
+ ] }, x ?? "-")) })),
3869
+ row("seats", t.launch.seats, tpl ? /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: chosen.map((x) => /* @__PURE__ */ jsxs10(Text12, { children: [
3870
+ /* @__PURE__ */ jsxs10(Text12, { color: C.violet, children: [
3871
+ "@",
3872
+ x.id
3873
+ ] }),
3874
+ " ",
3875
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: TOOL_NAME[x.tool] }),
3876
+ " "
3877
+ ] }, x.id)) }) : seats.length === 0 ? /* @__PURE__ */ jsx13(Text12, { color: C.faint, children: t.launch.noRole }) : /* @__PURE__ */ jsx13(Fragment4, { children: seats.map((x, i) => /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", backgroundColor: field === "seats" && i === seatAt ? "#3a3532" : void 0, children: [
3878
+ /* @__PURE__ */ jsx13(Text12, { color: x.on ? C.green : C.faint, children: x.on ? g.boxOn : g.boxOff }),
3879
+ " ",
3880
+ /* @__PURE__ */ jsxs10(Text12, { color: C.violet, children: [
3881
+ "@",
3882
+ x.role.id
3883
+ ] }),
3884
+ " ",
3885
+ x.role.label,
3886
+ " ",
3887
+ /* @__PURE__ */ jsxs10(Text12, { color: C.dim, children: [
3888
+ g.prev,
3889
+ " ",
3890
+ TOOL_NAME[x.tool],
3891
+ " ",
3892
+ g.next
3893
+ ] }),
3894
+ " ",
3895
+ /* @__PURE__ */ jsx13(Text12, { color: AUTONOMY_COLOR[x.role.autonomy ?? "ask"], children: t.ask.autonomy[x.role.autonomy ?? "ask"] })
3896
+ ] }, x.role.id)) })),
3897
+ row("name", t.launch.teamName, /* @__PURE__ */ jsx13(TextInput, { value: name, onChange: setName, active: field === "name", placeholder: teamName }))
3898
+ ] }),
3899
+ /* @__PURE__ */ jsxs10(TitledBox, { title: t.launch.preview, color: C.coral, titleColor: C.coral, children: [
3900
+ /* @__PURE__ */ jsxs10(Text12, { wrap: "truncate", children: [
3901
+ /* @__PURE__ */ jsx13(Text12, { color: C.coral, children: g.team }),
3902
+ " ",
3903
+ /* @__PURE__ */ jsx13(Text12, { color: C.fg, bold: true, children: t.launch.teamPreview(teamName, project?.name ?? cwd ?? "-") })
3904
+ ] }),
3905
+ /* @__PURE__ */ jsx13(Text12, { wrap: "truncate", children: chosen.map((x) => /* @__PURE__ */ jsxs10(Text12, { children: [
3906
+ /* @__PURE__ */ jsxs10(Text12, { color: C.violet, children: [
3907
+ "@",
3908
+ x.id
3909
+ ] }),
3910
+ " ",
3911
+ /* @__PURE__ */ jsx13(Text12, { color: C.dim, children: TOOL_NAME[x.tool] }),
3912
+ " ",
3913
+ /* @__PURE__ */ jsx13(Text12, { color: AUTONOMY_COLOR[x.autonomy], children: t.ask.autonomy[x.autonomy] }),
3914
+ " "
3915
+ ] }, x.id)) }),
3916
+ auto.length ? /* @__PURE__ */ jsx13(Text12, { color: C.yellow, wrap: "truncate", children: t.ask.autoWarning(auto.join(", ")) }) : null,
3917
+ /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, columnGap: 2, children: [
3918
+ /* @__PURE__ */ jsx13(Button, { label: launching ? t.launch.launching : t.ask.launchTeam, k: "ctrl+s", primary: true }),
3919
+ /* @__PURE__ */ jsx13(Button, { label: t.launch.cancel, k: "esc" })
3920
+ ] })
3921
+ ] })
3922
+ ] });
3923
+ }
3201
3924
 
3202
3925
  // src/tui/screens/Settings.tsx
3203
- import { useState as useState7 } from "react";
3926
+ import { useState as useState8 } from "react";
3204
3927
  import { Box as Box12, Text as Text13 } from "ink";
3205
3928
  import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3206
3929
  function Settings() {
3207
- const { t, g, lang, setLang } = useTui();
3208
- const rows = [{ id: "lang", label: t.settings.language }];
3209
- const [at, setAt] = useState7(0);
3930
+ const tui = useTui();
3931
+ const { t, g, lang, setLang } = tui;
3932
+ const near = useContextNear(tui.client);
3933
+ const known2 = tui.client.fleet?.contextNear() !== void 0;
3934
+ const rows = [{ id: "lang", label: t.settings.language }, { id: "context", label: t.settings.context }];
3935
+ const [at, setAt] = useState8(0);
3210
3936
  useScreenKeys((input, key) => {
3211
3937
  if (key.upArrow || input === "k") {
3212
3938
  setAt((i) => (i - 1 + rows.length) % rows.length);
@@ -3219,16 +3945,35 @@ function Settings() {
3219
3945
  const by = key.rightArrow || input === "l" || input === " " || key.return ? 1 : key.leftArrow || input === "h" ? -1 : 0;
3220
3946
  if (!by) return false;
3221
3947
  if (rows[at].id === "lang") setLang(LANGS[(LANGS.indexOf(lang) + by + LANGS.length) % LANGS.length]);
3948
+ else if (known2) {
3949
+ const pct = Math.min(CONTEXT_NEAR_MAX * 100, Math.max(CONTEXT_NEAR_MIN * 100, Math.round(near * 100) + by * 5));
3950
+ try {
3951
+ tui.client.fleet?.setContextNear(pct / 100);
3952
+ tui.toast({ text: t.settings.contextSaved(pct), tone: "ok" });
3953
+ } catch (e) {
3954
+ tui.toast({ text: e.message, tone: "info" });
3955
+ }
3956
+ }
3222
3957
  return true;
3223
3958
  });
3224
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: [
3225
3960
  /* @__PURE__ */ jsx14(Text13, { color: i === at ? C.coral : C.faint, children: i === at ? "\u258C" : " " }),
3226
3961
  /* @__PURE__ */ jsx14(Box12, { width: 16, children: /* @__PURE__ */ jsx14(Text13, { color: C.dim, children: r.label }) }),
3227
- /* @__PURE__ */ jsx14(Text13, { children: LANGS.map((l) => /* @__PURE__ */ jsxs11(Text13, { color: l === lang ? C.fg : C.dim, bold: l === lang, children: [
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: [
3228
3963
  l === lang ? `${g.radioOn} ` : `${g.radioOff} `,
3229
3964
  t.lang[l],
3230
3965
  " "
3231
- ] }, l)) })
3966
+ ] }, l)) }) : /* @__PURE__ */ jsxs11(Text13, { children: [
3967
+ /* @__PURE__ */ jsxs11(Text13, { color: C.fg, bold: true, children: [
3968
+ "\u25C2 ",
3969
+ Math.round(near * 100),
3970
+ " % \u25B8"
3971
+ ] }),
3972
+ /* @__PURE__ */ jsxs11(Text13, { color: C.faint, children: [
3973
+ " ",
3974
+ known2 ? t.settings.contextHint : t.settings.contextUnknown
3975
+ ] })
3976
+ ] })
3232
3977
  ] }, r.id)) }) });
3233
3978
  }
3234
3979
 
@@ -3259,10 +4004,10 @@ function Help() {
3259
4004
  import { jsx as jsx16 } from "react/jsx-runtime";
3260
4005
  var TOAST_MS = 5e3;
3261
4006
  function App(props) {
3262
- const [lang, setLangState] = useState8(props.lang);
4007
+ const [lang, setLangState] = useState9(props.lang);
3263
4008
  return /* @__PURE__ */ jsx16(I18nProvider, { lang, children: /* @__PURE__ */ jsx16(Shell, { ...props, lang, setLang: setLangState }) });
3264
4009
  }
3265
- function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs, env, run }) {
4010
+ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs, env, run: run2 }) {
3266
4011
  const { exit, suspendTerminal } = useApp();
3267
4012
  const { stdout } = useStdout();
3268
4013
  const write = useCallback((seq) => {
@@ -3271,14 +4016,16 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
3271
4016
  const d = useDaemon();
3272
4017
  const machines = useMachines(client);
3273
4018
  const projects = useProjects(client);
4019
+ const teams = useTeams(client);
4020
+ const tasks = useTasks(client);
3274
4021
  const fleet = client.fleet;
3275
4022
  const t = MESSAGES[lang];
3276
- const [screen, setScreen] = useState8("overview");
3277
- const [back, setBack] = useState8("overview");
3278
- const [selectedId, setSelectedId] = useState8(null);
3279
- const [lastToast, setToast] = useState8(null);
3280
- const [editing, setEditingState] = useState8(false);
3281
- const [now, setNow] = useState8(() => Date.now());
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());
3282
4029
  const keys = useRef8(null);
3283
4030
  useEffect9(() => {
3284
4031
  const timer = setInterval(() => setNow(Date.now()), 1e3);
@@ -3291,7 +4038,7 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
3291
4038
  }, [lastToast]);
3292
4039
  const toast = useCallback((x) => setToast(x), []);
3293
4040
  const setEditing = useCallback((on) => setEditingState(on), []);
3294
- const view = useMemo6(() => overview(d.sessions, projects), [d.sessions, projects]);
4041
+ const view = useMemo6(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
3295
4042
  const order = useRef8([]);
3296
4043
  useEffect9(() => {
3297
4044
  setSelectedId((cur) => cur === null && view.yours[0] ? view.yours[0].id : keepSelection(view.order, cur, order.current));
@@ -3327,10 +4074,10 @@ function Shell({ client, lang, setLang: setLangState, saveLang, notify, glyphs,
3327
4074
  return;
3328
4075
  }
3329
4076
  void suspendTerminal(async () => {
3330
- const code = await (run ?? runAttached)(cmd.argv);
4077
+ const code = await (run2 ?? runAttached)(cmd.argv);
3331
4078
  if (code !== 0) toast({ text: t.toast.attachFailed(code), tone: "error" });
3332
4079
  });
3333
- }, [fleet, suspendTerminal, run, t, toast]);
4080
+ }, [fleet, suspendTerminal, run2, t, toast]);
3334
4081
  const tui = {
3335
4082
  client,
3336
4083
  fleet,
@@ -3444,7 +4191,10 @@ var UNICODE = {
3444
4191
  prev: "\u25C2",
3445
4192
  next: "\u25B8",
3446
4193
  ellipsis: "\u2026",
3447
- bar: "\u2502"
4194
+ bar: "\u2502",
4195
+ /** A team's table and the common table (roles spec §3.9). */
4196
+ team: "\u25A3",
4197
+ common: "\u25AD"
3448
4198
  };
3449
4199
  var ASCII = {
3450
4200
  approval: "!",
@@ -3475,7 +4225,9 @@ var ASCII = {
3475
4225
  prev: "<",
3476
4226
  next: ">",
3477
4227
  ellipsis: "...",
3478
- bar: "|"
4228
+ bar: "|",
4229
+ team: "[T]",
4230
+ common: "[ ]"
3479
4231
  };
3480
4232
  function glyphsFor(env) {
3481
4233
  if (env.LEDGE_TUI_ASCII === "1") return ASCII;
@@ -3508,18 +4260,121 @@ function saveSettings(settings) {
3508
4260
  renameSync(tmp, path);
3509
4261
  }
3510
4262
 
4263
+ // 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";
4267
+ import { fileURLToPath } from "url";
4268
+ import { promisify } from "util";
4269
+ var PROFILE = "Ledge";
4270
+ var ICON = fileURLToPath(new URL("../../assets/ledge.ico", import.meta.url));
4271
+ var SETTINGS = [
4272
+ "Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json",
4273
+ "Packages/Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe/LocalState/settings.json",
4274
+ "Microsoft/Windows Terminal/settings.json"
4275
+ ];
4276
+ var fragmentDir = (localAppData) => join2(localAppData, "Microsoft", "Windows Terminal", "Fragments", PROFILE);
4277
+ var PROFILE_ID = "{d10fee13-0978-595f-a851-b60ec80f11c9}";
4278
+ function movesToLedgeTab(env, here, script) {
4279
+ return !here && !!env.WT_SESSION && !!env.WSL_DISTRO_NAME && !env.TMUX && env.WT_PROFILE_ID !== PROFILE_ID && !script.endsWith(".ts");
4280
+ }
4281
+ function winArg(s) {
4282
+ if (!s || /["\u0000-\u001f]/.test(s) || s.endsWith("\\")) return null;
4283
+ return /\s/.test(s) ? `"${s}"` : s;
4284
+ }
4285
+ function profileCommandline(distro, launcher) {
4286
+ const args = ["wsl.exe", "-d", distro, "--cd", "~", "--exec", "/bin/sh", launcher].map(winArg);
4287
+ return args.every((a) => a !== null) ? args.join(" ") : null;
4288
+ }
4289
+ var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
4290
+ function launcherScript(node, script, ledgeHome) {
4291
+ const home = ledgeHome ? `export LEDGE_HOME=${shQuote(ledgeHome)}
4292
+ ` : "";
4293
+ return `#!/bin/sh
4294
+ # Written by \`ledge tui\`: the Ledge profile of Windows Terminal runs it.
4295
+ ${home}exec ${shQuote(node)} ${shQuote(script)} tui --here
4296
+ `;
4297
+ }
4298
+ function fragment(commandline, iconWin) {
4299
+ return `${JSON.stringify({ profiles: [{ name: PROFILE, commandline, icon: iconWin }] }, null, 2)}
4300
+ `;
4301
+ }
4302
+ async function profileLoaded(localAppData) {
4303
+ for (const rel of SETTINGS) {
4304
+ const text = await readFile(join2(localAppData, rel), "utf8").catch(() => "");
4305
+ if (new RegExp(`"source"\\s*:\\s*"${PROFILE}"`).test(text)) return true;
4306
+ }
4307
+ return false;
4308
+ }
4309
+ async function writeIfChanged(path, content) {
4310
+ const current = await readFile(path).catch(() => null);
4311
+ if (current && current.equals(Buffer.from(content))) return false;
4312
+ await writeFile(path, content);
4313
+ return true;
4314
+ }
4315
+ async function openInLedgeTab(o) {
4316
+ const commandline = profileCommandline(o.distro, o.launcher);
4317
+ if (!commandline) return false;
4318
+ await mkdir(dirname(o.launcher), { recursive: true });
4319
+ await writeIfChanged(o.launcher, launcherScript(o.node, o.script, o.ledgeHome));
4320
+ const dir = fragmentDir(o.localAppData.linux);
4321
+ await mkdir(dir, { recursive: true });
4322
+ await writeIfChanged(join2(dir, "ledge.ico"), await readFile(o.icon ?? ICON));
4323
+ 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)) {
4327
+ if (Date.now() > deadline) return false;
4328
+ await new Promise((r) => setTimeout(r, 100));
4329
+ }
4330
+ await o.wt(["-w", "0", "new-tab", "-p", PROFILE]);
4331
+ return true;
4332
+ }
4333
+ var run = promisify(execFile);
4334
+ async function openTuiInLedgeTab(env) {
4335
+ try {
4336
+ const cmd = "/mnt/c/Windows/System32/cmd.exe";
4337
+ const win = (await run(cmd, ["/d", "/c", "echo %LOCALAPPDATA%"], { cwd: "/mnt/c", timeout: 5e3 })).stdout.trim();
4338
+ if (!/^[A-Za-z]:\\/.test(win)) return false;
4339
+ const linux = (await run("wslpath", ["-u", win], { timeout: 5e3 })).stdout.trim();
4340
+ return await openInLedgeTab({
4341
+ localAppData: { win, linux },
4342
+ distro: env.WSL_DISTRO_NAME,
4343
+ node: await realpath(process.execPath),
4344
+ script: await realpath(process.argv[1]),
4345
+ launcher: join2(ledgeDir(), "tui.sh"),
4346
+ ledgeHome: env.LEDGE_HOME,
4347
+ wt: (args) => new Promise((resolve, reject) => {
4348
+ const child = spawn2(join2(linux, "Microsoft", "WindowsApps", "wt.exe"), args, { cwd: "/mnt/c", stdio: "ignore", detached: true });
4349
+ child.once("error", reject);
4350
+ child.once("spawn", () => {
4351
+ child.unref();
4352
+ resolve();
4353
+ });
4354
+ })
4355
+ });
4356
+ } catch {
4357
+ return false;
4358
+ }
4359
+ }
4360
+
3511
4361
  // src/tui/index.tsx
3512
4362
  import { jsx as jsx17 } from "react/jsx-runtime";
3513
- var TUI_USAGE = "ledge tui [--notify bell|osc9|off]";
4363
+ var TUI_USAGE = "ledge tui [--notify bell|osc9|off] [--here]";
3514
4364
  function tuiArgs(args) {
3515
4365
  let notify = "bell";
4366
+ let here = false;
3516
4367
  for (let i = 0; i < args.length; i++) {
3517
4368
  const a = args[i];
4369
+ if (a === "--here") {
4370
+ here = true;
4371
+ continue;
4372
+ }
3518
4373
  const value = a.startsWith("--notify=") ? a.slice("--notify=".length) : a === "--notify" ? args[++i] : void 0;
3519
4374
  if (value === void 0 || !NOTIFY_MODES.includes(value)) return null;
3520
4375
  notify = value;
3521
4376
  }
3522
- return { notify };
4377
+ return { notify, here };
3523
4378
  }
3524
4379
  async function runTui(args) {
3525
4380
  const parsed = tuiArgs(args);
@@ -3533,12 +4388,17 @@ async function runTui(args) {
3533
4388
  console.error(MESSAGES[lang].cli.notTty);
3534
4389
  return 2;
3535
4390
  }
4391
+ if (movesToLedgeTab(process.env, parsed.here, process.argv[1] ?? "") && await openTuiInLedgeTab(process.env)) {
4392
+ console.log(MESSAGES[lang].cli.movedToTab);
4393
+ return 0;
4394
+ }
3536
4395
  const client = createTuiFleet();
3537
4396
  const detach = attachClient(client);
3538
4397
  client.connect();
3539
4398
  const app = render(
3540
4399
  /* @__PURE__ */ jsx17(App, { client, lang, saveLang: (next2) => saveSettings({ ...loadSettings(), lang: next2 }), notify: parsed.notify, glyphs: glyphsFor(process.env), env: process.env }),
3541
- { alternateScreen: true, exitOnCtrlC: false, maxFps: 30 }
4400
+ // Incremental: a key rewrites the lines it changed, not the whole screen erased then drawn again (the flicker).
4401
+ { alternateScreen: true, exitOnCtrlC: false, maxFps: 30, incrementalRendering: true }
3542
4402
  );
3543
4403
  const stop = () => app.unmount();
3544
4404
  process.once("SIGTERM", stop);