@paradow/ledge 0.29.0 → 0.31.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 (33) hide show
  1. package/dist/cli/{action-MAPPYLUM.js → action-CFYJM2A2.js} +4 -4
  2. package/dist/cli/{attach-OC64CLB2.js → attach-23PTEIAA.js} +9 -8
  3. package/dist/cli/{bridge-TWX4V37E.js → bridge-UJKWEKA5.js} +1 -1
  4. package/dist/cli/{chunk-RFFTWAAF.js → chunk-6YBIZZQG.js} +1 -1
  5. package/dist/cli/{chunk-TREGH6WM.js → chunk-GMG3ROBU.js} +504 -302
  6. package/dist/cli/{chunk-MZVMW356.js → chunk-HEYHVOD6.js} +2 -2
  7. package/dist/cli/{chunk-XZYQMKKO.js → chunk-HNC5Q5PV.js} +3 -3
  8. package/dist/cli/{chunk-WCBXS3EU.js → chunk-HUY4CFMX.js} +107 -15
  9. package/dist/cli/{chunk-73YNZ3BN.js → chunk-MIPE2FWL.js} +2 -1
  10. package/dist/cli/{chunk-7S7ZLGIY.js → chunk-MYIBYAZC.js} +36 -8
  11. package/dist/cli/{chunk-WIUJNSAZ.js → chunk-NXNIRIGI.js} +5 -5
  12. package/dist/cli/{chunk-353WRRGG.js → chunk-QXH2L4UH.js} +3 -3
  13. package/dist/cli/{chunk-UWDE365N.js → chunk-TCJG3EHB.js} +47 -3
  14. package/dist/cli/{chunk-K3SDHHZF.js → chunk-TLFKLMBN.js} +1 -1
  15. package/dist/cli/{chunk-TNYBWWRO.js → chunk-WF27BASF.js} +2 -2
  16. package/dist/cli/chunk-WY5WSK2D.js +6 -0
  17. package/dist/cli/{chunk-ETGOWBUL.js → chunk-YI4V7YVW.js} +3 -3
  18. package/dist/cli/{daemon-ZTZ7VMYA.js → daemon-WW2P7RDV.js} +7 -7
  19. package/dist/cli/{decide-4BIYS3Y2.js → decide-HLRD37PW.js} +8 -6
  20. package/dist/cli/{handover-IS7BJYGZ.js → handover-56M4LIOP.js} +4 -4
  21. package/dist/cli/{hook-3VKVD4IP.js → hook-UJVKYSQ5.js} +34 -13
  22. package/dist/cli/index.js +16 -16
  23. package/dist/cli/{launch-XAPN2M3Q.js → launch-GIPQDPAC.js} +9 -8
  24. package/dist/cli/{send-ZLF2ZXMJ.js → send-HWZHE65N.js} +4 -4
  25. package/dist/cli/{status-NONO6E2S.js → status-V6ITP7VT.js} +2 -2
  26. package/dist/cli/{task-47MVWDCY.js → task-N53TM34B.js} +4 -4
  27. package/dist/cli/{team-EJ4YIXJH.js → team-CZFZ24P6.js} +6 -6
  28. package/dist/cli/{tui-CAP4O4YQ.js → tui-K32C34XI.js} +1274 -1176
  29. package/dist/cli/{version-EHBPJO7K.js → version-QIFJVKAQ.js} +1 -1
  30. package/dist/cli/{who-FHAXYCM2.js → who-NIOLHHB3.js} +3 -3
  31. package/dist/ledge-guide.md +268 -0
  32. package/package.json +1 -1
  33. package/dist/cli/chunk-JZEWCO6X.js +0 -6
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-WIUJNSAZ.js";
3
+ } from "./chunk-NXNIRIGI.js";
4
4
  import {
5
5
  imageInfo
6
- } from "./chunk-TREGH6WM.js";
6
+ } from "./chunk-GMG3ROBU.js";
7
7
  import {
8
8
  MAX_FRAME
9
9
  } from "./chunk-OLLQBK6N.js";
10
- import "./chunk-WCBXS3EU.js";
11
- import "./chunk-UWDE365N.js";
12
- import "./chunk-73YNZ3BN.js";
13
- import "./chunk-K3SDHHZF.js";
14
- import "./chunk-RFFTWAAF.js";
10
+ import "./chunk-HUY4CFMX.js";
11
+ import "./chunk-TCJG3EHB.js";
12
+ import "./chunk-MIPE2FWL.js";
13
+ import "./chunk-TLFKLMBN.js";
14
+ import "./chunk-6YBIZZQG.js";
15
15
  import {
16
16
  ACTION_ERROR_CODES,
17
17
  ATTACHMENTS_MAX,
@@ -35,6 +35,7 @@ import {
35
35
  contextNear,
36
36
  isArchived,
37
37
  offers,
38
+ openDecision,
38
39
  parseServerMessage,
39
40
  pendingKey,
40
41
  pickName,
@@ -43,7 +44,8 @@ import {
43
44
  readDaemonToken,
44
45
  taskClosed,
45
46
  transmissions
46
- } from "./chunk-7S7ZLGIY.js";
47
+ } from "./chunk-MYIBYAZC.js";
48
+ import "./chunk-WY5WSK2D.js";
47
49
  import "./chunk-NLL2EQJ2.js";
48
50
  import "./chunk-6IYPRUQG.js";
49
51
  import {
@@ -1303,7 +1305,7 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1303
1305
  const { machineId: _m, ...rest } = outgoing;
1304
1306
  outgoing = rest;
1305
1307
  }
1306
- 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";
1308
+ 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 || msg.type === "action.run") machine = "local";
1307
1309
  if (outgoing.type === "team.join" || outgoing.type === "team.leave") {
1308
1310
  const o = splitId(outgoing.session);
1309
1311
  if (o) {
@@ -1510,7 +1512,7 @@ function getImage(client, attachment, route = {}, preview = false, timeoutMs = T
1510
1512
  }
1511
1513
 
1512
1514
  // src/tui/App.tsx
1513
- import { useCallback, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef13, useState as useState14 } from "react";
1515
+ import { useCallback, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef13, useState as useState14 } from "react";
1514
1516
  import { useApp, useInput as useInput7, useStdout, useWindowSize as useWindowSize2 } from "ink";
1515
1517
 
1516
1518
  // src/tui/actions.ts
@@ -1935,7 +1937,8 @@ var fr = {
1935
1937
  "team.dissolve": (v) => need(v, ["team"], (w) => `Dissoudre l\u2019\xE9quipe ${b(w.team)}`),
1936
1938
  "team.save": (v) => need(v, ["team", "name"], (w) => v.replaces ? `Remplacer le mod\xE8le \xAB ${w.name} \xBB par ${b(w.team)}` : `Enregistrer ${b(w.team)} comme mod\xE8le \xAB ${w.name} \xBB`),
1937
1939
  "task.cancel": (v) => need(v, ["task", "owner"], (w) => `Annuler la t\xE2che \xAB ${short(w.task)} \xBB de ${b(w.owner)}`),
1938
- "task.pass": (v) => need(v, ["task", "owner", "session"], (w) => `Passer la t\xE2che \xAB ${short(w.task)} \xBB de ${b(w.owner)} \xE0 ${b(w.session)}`)
1940
+ "task.pass": (v) => need(v, ["task", "owner", "session"], (w) => `Passer la t\xE2che \xAB ${short(w.task)} \xBB de ${b(w.owner)} \xE0 ${b(w.session)}`),
1941
+ "task.answer": (v) => need(v, ["task", "session"], (w) => `R\xE9pondre \xE0 la d\xE9cision \xAB ${short(w.task)} \xBB de ${b(w.session)}`)
1939
1942
  },
1940
1943
  why: (code3, action, v) => {
1941
1944
  const s = v.session ? b(v.session) : void 0, t = v.team ? b(v.team) : void 0, k = v.task ? `\xAB ${short(v.task)} \xBB` : void 0;
@@ -1985,6 +1988,8 @@ var fr = {
1985
1988
  return k ? `la t\xE2che ${k} est d\xE9j\xE0 close.` : gone;
1986
1989
  case "holder":
1987
1990
  return s && k ? `${s} tient d\xE9j\xE0 la t\xE2che ${k}.` : gone;
1991
+ case "not_decision":
1992
+ return k ? `la t\xE2che ${k} n\u2019est pas une d\xE9cision qui t\u2019est pos\xE9e.` : gone;
1988
1993
  case "changed":
1989
1994
  return "la situation a chang\xE9 depuis ma r\xE9ponse : redemande-moi.";
1990
1995
  case "failed":
@@ -2011,7 +2016,8 @@ var en = {
2011
2016
  "team.dissolve": (v) => need(v, ["team"], (w) => `Dissolve the team ${b(w.team)}`),
2012
2017
  "team.save": (v) => need(v, ["team", "name"], (w) => v.replaces ? `Replace the template \u201C${w.name}\u201D with ${b(w.team)}` : `Save ${b(w.team)} as the template \u201C${w.name}\u201D`),
2013
2018
  "task.cancel": (v) => need(v, ["task", "owner"], (w) => `Cancel ${b(w.owner)}\u2019s task \u201C${short(w.task)}\u201D`),
2014
- "task.pass": (v) => need(v, ["task", "owner", "session"], (w) => `Pass ${b(w.owner)}\u2019s task \u201C${short(w.task)}\u201D to ${b(w.session)}`)
2019
+ "task.pass": (v) => need(v, ["task", "owner", "session"], (w) => `Pass ${b(w.owner)}\u2019s task \u201C${short(w.task)}\u201D to ${b(w.session)}`),
2020
+ "task.answer": (v) => need(v, ["task", "session"], (w) => `Answer ${b(w.session)}\u2019s decision \u201C${short(w.task)}\u201D`)
2015
2021
  },
2016
2022
  why: (code3, action, v) => {
2017
2023
  const s = v.session ? b(v.session) : void 0, t = v.team ? b(v.team) : void 0, k = v.task ? `\u201C${short(v.task)}\u201D` : void 0;
@@ -2061,6 +2067,8 @@ var en = {
2061
2067
  return k ? `the task ${k} is already closed.` : gone;
2062
2068
  case "holder":
2063
2069
  return s && k ? `${s} already holds the task ${k}.` : gone;
2070
+ case "not_decision":
2071
+ return k ? `the task ${k} is not a decision put to you.` : gone;
2064
2072
  case "changed":
2065
2073
  return "things have changed since my answer: ask me again.";
2066
2074
  case "failed":
@@ -2095,7 +2103,8 @@ var fr2 = {
2095
2103
  "team.dissolve": "Dissoudre",
2096
2104
  "team.save": "Enregistrer",
2097
2105
  "task.cancel": "Abandonner",
2098
- "task.pass": "Passer le relais"
2106
+ "task.pass": "Passer le relais",
2107
+ "task.answer": "R\xE9pondre"
2099
2108
  },
2100
2109
  apply: "Appliquer",
2101
2110
  deny: "Refuser",
@@ -2161,7 +2170,8 @@ var en2 = {
2161
2170
  "team.dissolve": "Dissolve",
2162
2171
  "team.save": "Save",
2163
2172
  "task.cancel": "Drop",
2164
- "task.pass": "Hand over"
2173
+ "task.pass": "Hand over",
2174
+ "task.answer": "Answer"
2165
2175
  },
2166
2176
  apply: "Apply",
2167
2177
  deny: "Deny",
@@ -2270,6 +2280,9 @@ var fr3 = {
2270
2280
  resume: "reprendre l\u2019\xE9quipe",
2271
2281
  teamMode: "session ou \xE9quipe",
2272
2282
  handover: "passer le relais",
2283
+ decide: "r\xE9pondre \xE0 la d\xE9cision",
2284
+ sendAnswer: "envoyer la r\xE9ponse",
2285
+ edit: "modifier",
2273
2286
  sendOnly: "envoyer",
2274
2287
  newLine: "nouvelle ligne",
2275
2288
  keepAndLeave: "garder et sortir",
@@ -2336,6 +2349,10 @@ var fr3 = {
2336
2349
  relievedBy: (name) => `relev\xE9e par ${name}`,
2337
2350
  relieves: (name) => `rel\xE8ve de ${name}`,
2338
2351
  ledgeTask: (owner, title2, state2) => `${owner} a ${state2 === "done" ? "fini" : state2 === "refused" ? "refus\xE9" : "abandonn\xE9"} \xAB ${title2} \xBB \xB7 t\xE2che de Ledge`,
2352
+ decision: (giver, title2) => `${giver} te demande : ${title2}`,
2353
+ decisionTitle: (giver, title2) => `R\xE9pondre \xE0 ${giver} \xB7 ${title2}`,
2354
+ decisionHint: "Ta r\xE9ponse, telle que la session la lira\u2026",
2355
+ confirmAnswer: (giver) => `Envoyer \xE0 ${giver} ? Une r\xE9ponse ne se reprend pas`,
2339
2356
  joinWhich: (name) => `${name} rejoint quelle \xE9quipe ? (\u2191\u2193 \u21B5, esc)`,
2340
2357
  noTeamHere: "Aucune \xE9quipe dans ce projet.",
2341
2358
  notInTeam: "Cette session n\u2019est dans aucune \xE9quipe."
@@ -2555,6 +2572,7 @@ var fr3 = {
2555
2572
  openFailed: (why) => `Image non ouverte : ${why}`
2556
2573
  },
2557
2574
  toast: {
2575
+ answerFailed: (why) => `R\xE9ponse non envoy\xE9e : ${why}`,
2558
2576
  handover: (n) => `${n} \xE9crit son bilan : une nouvelle session prendra sa place`,
2559
2577
  handoverRefused: (why) => `Rel\xE8ve refus\xE9e : ${why}`,
2560
2578
  accept: (n) => `\u2713 ${n} : autoris\xE9`,
@@ -2657,6 +2675,9 @@ var en3 = {
2657
2675
  resume: "resume the team",
2658
2676
  teamMode: "session or team",
2659
2677
  handover: "hand over",
2678
+ decide: "answer the decision",
2679
+ sendAnswer: "send the answer",
2680
+ edit: "edit",
2660
2681
  sendOnly: "send",
2661
2682
  newLine: "new line",
2662
2683
  keepAndLeave: "keep and leave",
@@ -2723,6 +2744,10 @@ var en3 = {
2723
2744
  relievedBy: (name) => `relieved by ${name}`,
2724
2745
  relieves: (name) => `relieves ${name}`,
2725
2746
  ledgeTask: (owner, title2, state2) => `${owner} ${state2 === "done" ? "finished" : state2 === "refused" ? "refused" : "dropped"} \u201C${title2}\u201D \xB7 Ledge\u2019s task`,
2747
+ decision: (giver, title2) => `${giver} asks you: ${title2}`,
2748
+ decisionTitle: (giver, title2) => `Answer ${giver} \xB7 ${title2}`,
2749
+ decisionHint: "Your answer, as the session will read it\u2026",
2750
+ confirmAnswer: (giver) => `Send to ${giver}? An answer cannot be taken back`,
2726
2751
  joinWhich: (name) => `${name} joins which team? (\u2191\u2193 \u21B5, esc)`,
2727
2752
  noTeamHere: "No team in this project.",
2728
2753
  notInTeam: "This session is in no team."
@@ -2940,6 +2965,7 @@ var en3 = {
2940
2965
  openFailed: (why) => `Image not opened: ${why}`
2941
2966
  },
2942
2967
  toast: {
2968
+ answerFailed: (why) => `Answer not sent: ${why}`,
2943
2969
  handover: (n) => `${n} is writing its brief: a fresh session will take its place`,
2944
2970
  handoverRefused: (why) => `Relief refused: ${why}`,
2945
2971
  accept: (n) => `\u2713 ${n}: allowed`,
@@ -3078,6 +3104,7 @@ function exchanges(sessions, max, tasks = [], ledge = "Ledge") {
3078
3104
  }
3079
3105
  var nearContext = (sessions, near) => sessions.filter((s) => s.state !== "exited" && !s.archivedAt && (s.handover ? s.handover.state === "failed" : contextNear(s.context, near)));
3080
3106
  var closedLedgeTasks = (tasks) => tasks.filter((x) => x.giver === LEDGE_GIVER && taskClosed(x) && !x.seen);
3107
+ var openDecisions = (tasks) => tasks.filter(openDecision);
3081
3108
 
3082
3109
  // src/tui/model/time.ts
3083
3110
  function ago(iso, now2, t) {
@@ -3126,8 +3153,8 @@ function notification(text5, mode, env) {
3126
3153
  var title = (text5) => `\x1B]0;${text5.replace(/[\u0000-\u001f\u007f]/g, " ")}\x07`;
3127
3154
 
3128
3155
  // src/tui/screens/Overview.tsx
3129
- import { useMemo as useMemo2 } from "react";
3130
- import { Box as Box6, Text as Text6 } from "ink";
3156
+ import { useEffect as useEffect6, useMemo as useMemo2 } from "react";
3157
+ import { Box as Box8, Text as Text7 } from "ink";
3131
3158
  import { randomUUID } from "crypto";
3132
3159
  import { useState as useState3 } from "react";
3133
3160
 
@@ -3146,49 +3173,48 @@ function botState(session) {
3146
3173
  return FROM_SESSION[session.state];
3147
3174
  }
3148
3175
 
3149
- // src/tui/model/labels.ts
3150
- var TOOL_NAME = { claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", omp: "OMP" };
3151
- var modelLabel = (s, fallback) => [s.model ?? (s.effort ? fallback : ""), s.effort ?? ""].filter(Boolean).join(" \xB7 ") || fallback;
3152
- var machineLabel = (s) => s.machineName ?? splitId(s.id)?.machine ?? "Local";
3153
- var paneName = (s) => s.tmuxSession ?? `ledge-${splitId(s.id)?.id ?? s.id}`;
3154
- function fit(s, n) {
3155
- const cps = [...s];
3156
- if (n <= 0) return "";
3157
- return cps.length > n ? `${cps.slice(0, Math.max(0, n - 1)).join("")}\u2026` : s + " ".repeat(n - cps.length);
3176
+ // ../ui/src/thread/turns.ts
3177
+ var OPEN_TURNS = 3;
3178
+ var byTime = (a, b2) => a.at < b2.at ? -1 : a.at > b2.at ? 1 : 0;
3179
+ function withoutEcho(items, reply) {
3180
+ if (!reply) return items;
3181
+ let last = -1;
3182
+ items.forEach((it, i) => {
3183
+ if (it.kind === "message") last = i;
3184
+ });
3185
+ const echo = items[last];
3186
+ return echo?.kind === "message" && echo.text.trim() === reply.text.trim() ? items.filter((_, i) => i !== last) : items;
3158
3187
  }
3159
-
3160
- // src/tui/ui/now.ts
3161
- import { useSyncExternalStore as useSyncExternalStore4 } from "react";
3162
- var listeners = /* @__PURE__ */ new Set();
3163
- var now = Date.now();
3164
- var timer = null;
3165
- function subscribe(fn) {
3166
- listeners.add(fn);
3167
- if (!timer) {
3168
- now = Date.now();
3169
- timer = setInterval(() => {
3170
- now = Date.now();
3171
- for (const l of listeners) l();
3172
- }, 1e3);
3173
- timer.unref?.();
3188
+ function turns(items) {
3189
+ const map3 = /* @__PURE__ */ new Map();
3190
+ for (const it of items) {
3191
+ if (it.helperId) continue;
3192
+ const t = map3.get(it.turn) ?? { n: it.turn, items: [] };
3193
+ t.items.push(it);
3194
+ if (it.kind === "prompt") t.prompt = it;
3195
+ if (it.kind === "reply") t.reply = it;
3196
+ map3.set(it.turn, t);
3174
3197
  }
3175
- return () => {
3176
- listeners.delete(fn);
3177
- if (listeners.size === 0 && timer) {
3178
- clearInterval(timer);
3179
- timer = null;
3180
- }
3181
- };
3198
+ return [...map3.values()].sort((a, b2) => a.n - b2.n).map((t) => ({ ...t, items: withoutEcho([...t.items].sort(byTime), t.reply) }));
3182
3199
  }
3183
- var useNow = () => useSyncExternalStore4(subscribe, () => now);
3184
3200
 
3185
- // src/tui/ui/Ago.tsx
3186
- import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
3187
- function Ago({ iso }) {
3188
- const now2 = useNow();
3189
- const { t } = useI18n();
3190
- return /* @__PURE__ */ jsx2(Fragment, { children: ago(iso, now2, t.time) });
3201
+ // ../ui/src/thread/tools.ts
3202
+ function rawTarget(item) {
3203
+ let s = item.label.trim();
3204
+ const prefix = `${item.tool} \xB7 `;
3205
+ if (s.startsWith(prefix)) s = s.slice(prefix.length);
3206
+ if (s === item.tool) return "";
3207
+ if (s.startsWith("{")) {
3208
+ try {
3209
+ const o = JSON.parse(s);
3210
+ const v = ["pattern", "query", "url", "file_path", "path", "command", "description", "prompt"].map((k) => o[k]).find((x) => typeof x === "string" && x);
3211
+ if (typeof v === "string") s = v;
3212
+ } catch {
3213
+ }
3214
+ }
3215
+ return (s.split("\n")[0] ?? "").trim();
3191
3216
  }
3217
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3192
3218
 
3193
3219
  // src/tui/theme.ts
3194
3220
  var THEMES = ["auto", "dark", "light"];
@@ -3253,9 +3279,6 @@ function setTone(next2) {
3253
3279
  }
3254
3280
  var projectColor = (color2) => color2 ? PROJECT_COLORS[color2] : C.dim;
3255
3281
 
3256
- // src/tui/ui/Frame.tsx
3257
- import { Box as Box2, Text as Text2 } from "ink";
3258
-
3259
3282
  // src/tui/model/cells.ts
3260
3283
  import stringWidth from "string-width";
3261
3284
  var graphemes = new Intl.Segmenter(void 0, { granularity: "grapheme" });
@@ -3332,753 +3355,63 @@ function row(prefix, body, meta, width, o = {}) {
3332
3355
  });
3333
3356
  }
3334
3357
 
3335
- // src/tui/ui/KeyBar.tsx
3336
- import { Box, Text } from "ink";
3337
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
3338
- var itemCells = ([k, label]) => cells(` ${k} `) + cells(` ${label}`);
3339
- function fitKeys(keys, width, help) {
3340
- const list3 = keys.filter((k) => k[0] !== help?.[0]);
3341
- const fits = (l) => 1 + l.reduce((n, x, i) => n + itemCells(x) + (i ? 2 : 0), 0) <= width - 1;
3342
- const tail = help ? [help] : [];
3343
- while (list3.length && !fits([...list3, ...tail])) list3.pop();
3344
- return [...list3, ...tail];
3345
- }
3346
- function KeyBar({ keys }) {
3347
- return /* @__PURE__ */ jsx3(Box, { paddingX: 1, columnGap: 2, height: 1, overflow: "hidden", children: keys.map(([k, label]) => /* @__PURE__ */ jsx3(Box, { flexShrink: 0, children: /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
3348
- /* @__PURE__ */ jsxs(Text, { color: C.fg, backgroundColor: C.key, children: [
3349
- " ",
3350
- k,
3351
- " "
3352
- ] }),
3353
- /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
3354
- " ",
3355
- label
3356
- ] })
3357
- ] }) }, k)) });
3358
+ // ../../node_modules/.pnpm/ccount@2.0.1/node_modules/ccount/index.js
3359
+ function ccount(value, character) {
3360
+ const source2 = String(value);
3361
+ if (typeof character !== "string") {
3362
+ throw new TypeError("Expected character");
3363
+ }
3364
+ let count = 0;
3365
+ let index2 = source2.indexOf(character);
3366
+ while (index2 !== -1) {
3367
+ count++;
3368
+ index2 = source2.indexOf(character, index2 + character.length);
3369
+ }
3370
+ return count;
3358
3371
  }
3359
3372
 
3360
- // src/tui/ui/Frame.tsx
3361
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
3362
- var MIN = { columns: 60, rows: 16 };
3363
- function Frame({ crumb, crumbShort, keys, kept, children }) {
3364
- const { d, machines, t, g, lastToast, editing } = useTui();
3365
- const { rows, columns } = useScreenSize();
3366
- const live = listed(d.sessions);
3367
- const pending = live.filter(isPending);
3368
- const waiting = pending.length;
3369
- const status = d.status === "open" ? null : d.status === "connecting" ? t.bar.connecting : t.bar.unreachable;
3370
- if (columns < MIN.columns || rows < MIN.rows) {
3371
- const why = [...pending[0] ? [t.small.waits(pending[0].name, pending[0].pending?.kind === "question")] : [], ...kept ? [t.small.kept] : []].join(" \xB7 ");
3372
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: rows, width: columns, alignItems: "center", justifyContent: "center", children: [
3373
- /* @__PURE__ */ jsxs2(Text2, { color: C.coral, bold: true, children: [
3374
- g.logo,
3375
- " ledge"
3376
- ] }),
3377
- /* @__PURE__ */ jsx4(Text2, { color: C.fg, children: t.small.tooSmall(columns, rows) }),
3378
- /* @__PURE__ */ jsx4(Text2, { color: C.dim, children: t.small.atLeast(MIN.columns, MIN.rows) }),
3379
- why ? /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { color: C.red, children: why }) }) : null
3380
- ] });
3381
- }
3382
- const left2 = cells(` ${g.logo} ledge ${t.bar.sessions(live.length)} \xB7 ${t.bar.machines(Math.max(1, machines.length))}`) + (waiting > 0 ? cells(` \xB7 ${t.bar.waiting(waiting)}`) : 0) + (status ? cells(` ${status}`) : 0);
3383
- const room = columns - left2 - 2;
3384
- const shown = cells(crumb) <= room ? crumb : crumbShort && cells(crumbShort) <= room ? crumbShort : "";
3385
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: rows, width: columns, children: [
3386
- /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, justifyContent: "space-between", height: 1, backgroundColor: C.bar, children: [
3387
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
3388
- /* @__PURE__ */ jsxs2(Text2, { color: C.coral, children: [
3389
- g.logo,
3390
- " ledge"
3391
- ] }),
3392
- /* @__PURE__ */ jsxs2(Text2, { color: C.dim, children: [
3393
- " ",
3394
- t.bar.sessions(live.length),
3395
- " \xB7 ",
3396
- t.bar.machines(Math.max(1, machines.length))
3397
- ] }),
3398
- waiting > 0 ? /* @__PURE__ */ jsxs2(Text2, { color: C.red, children: [
3399
- " \xB7 ",
3400
- t.bar.waiting(waiting)
3401
- ] }) : null,
3402
- status ? /* @__PURE__ */ jsxs2(Text2, { color: C.yellow, children: [
3403
- " ",
3404
- status
3405
- ] }) : null
3406
- ] }),
3407
- shown ? /* @__PURE__ */ jsx4(Text2, { wrap: "truncate", color: C.dim, children: shown }) : null
3408
- ] }),
3409
- /* @__PURE__ */ jsx4(Box2, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children }),
3410
- lastToast ? /* @__PURE__ */ jsx4(Box2, { paddingX: 1, height: 1, children: /* @__PURE__ */ jsx4(Text2, { wrap: "truncate", color: lastToast.tone === "error" ? C.red : lastToast.tone === "ok" ? C.green : lastToast.tone === "quiet" ? C.dim : C.yellow, children: lastToast.text }) }) : null,
3411
- /* @__PURE__ */ jsx4(KeyBar, { keys: fitKeys(keys, columns, editing ? null : ["?", t.keys.help]) })
3412
- ] });
3373
+ // ../../node_modules/.pnpm/devlop@1.1.0/node_modules/devlop/lib/default.js
3374
+ function ok() {
3413
3375
  }
3414
3376
 
3415
- // src/tui/ui/SessionCard.tsx
3416
- import { Box as Box3, Text as Text3 } from "ink";
3417
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
3418
- function Button({ label, k, primary, danger, off }) {
3419
- const bg = off ? void 0 : danger ? C.danger : primary ? C.primary : C.key;
3420
- return /* @__PURE__ */ jsxs3(Text3, { children: [
3421
- /* @__PURE__ */ jsxs3(Text3, { color: off ? C.faint : C.fg, backgroundColor: bg, children: [
3422
- " ",
3423
- label,
3424
- " "
3425
- ] }),
3426
- /* @__PURE__ */ jsxs3(Text3, { color: C.dim, backgroundColor: bg, children: [
3427
- k,
3428
- " "
3429
- ] })
3430
- ] });
3377
+ // ../../node_modules/.pnpm/micromark-util-character@2.1.1/node_modules/micromark-util-character/index.js
3378
+ var asciiAlpha = regexCheck(/[A-Za-z]/);
3379
+ var asciiAlphanumeric = regexCheck(/[\dA-Za-z]/);
3380
+ var asciiAtext = regexCheck(/[#-'*+\--9=?A-Z^-~]/);
3381
+ function asciiControl(code3) {
3382
+ return (
3383
+ // Special whitespace codes (which have negative values), C0 and Control
3384
+ // character DEL
3385
+ code3 !== null && (code3 < 32 || code3 === 127)
3386
+ );
3431
3387
  }
3432
- function SessionCard({ s, note }) {
3433
- const tui = useTui();
3434
- const { t, g } = tui;
3435
- const threshold = useContextNear(tui.client);
3436
- const state2 = botState(s);
3437
- const helpers = liveHelpers(s);
3438
- const p = s.pending;
3439
- const canAct = !s.offline && s.source !== "hook_only";
3440
- const verb = !p ? "" : p.kind === "question" ? t.overview.asks : p.kind === "plan" ? t.overview.plan : p.kind === "diff" ? t.overview.diff : t.overview.wantsToRun;
3441
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
3442
- /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
3443
- /* @__PURE__ */ jsxs3(Text3, { color: STATE_COLOR[state2], children: [
3444
- g[state2],
3445
- " ",
3446
- t.state[state2]
3447
- ] }),
3448
- /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
3449
- " ",
3450
- TOOL_NAME[s.tool],
3451
- " \xB7 ",
3452
- modelLabel(s, t.session.defaultModel)
3453
- ] }),
3454
- s.context ? /* @__PURE__ */ jsxs3(Text3, { color: contextNear(s.context, threshold) ? C.yellow : C.dim, children: [
3455
- " \xB7 ",
3456
- t.overview.context(Math.round(s.context.used / s.context.window * 100))
3457
- ] }) : null,
3458
- /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
3459
- " \xB7 ",
3460
- machineLabel(s),
3461
- s.branch ? ` \xB7 ${g.branch} ${s.branch}` : ""
3462
- ] }),
3463
- s.offline ? /* @__PURE__ */ jsxs3(Text3, { color: C.yellow, children: [
3464
- " ",
3465
- t.overview.offline
3466
- ] }) : s.source === "hook_only" ? /* @__PURE__ */ jsxs3(Text3, { color: C.faint, children: [
3467
- " ",
3468
- t.overview.readOnly
3469
- ] }) : null
3470
- ] }),
3471
- s.handover || s.relieves ? /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
3472
- s.handover ? /* @__PURE__ */ jsxs3(Text3, { color: s.handover.state === "failed" ? C.red : C.yellow, children: [
3473
- " \u21C4 ",
3474
- s.handover.state === "done" && s.handover.toName ? t.overview.relievedBy(s.handover.toName) : t.overview.relief[s.handover.state],
3475
- s.handover.reason ? ` : ${s.handover.reason}` : ""
3476
- ] }) : null,
3477
- s.relieves ? /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
3478
- " \u21C4 ",
3479
- t.overview.relieves(s.relieves.name)
3480
- ] }) : null
3481
- ] }) : null,
3482
- helpers.length > 0 ? /* @__PURE__ */ jsxs3(Text3, { color: C.violet, wrap: "truncate", children: [
3483
- " ",
3484
- helpers.map((h) => `${g.helper} ${h.kind} \xB7 ${oneLine(h.task)}`).join(" ")
3485
- ] }) : null,
3486
- p ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
3487
- /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
3488
- /* @__PURE__ */ jsx5(Text3, { color: STATE_COLOR[state2], children: verb }),
3489
- " ",
3490
- /* @__PURE__ */ jsx5(Text3, { color: C.fg, bold: true, children: oneLine(p.text) }),
3491
- p.tool ? /* @__PURE__ */ jsxs3(Text3, { color: C.faint, children: [
3492
- " ",
3493
- p.tool
3494
- ] }) : null
3495
- ] }),
3496
- /* @__PURE__ */ jsx5(Box3, { marginTop: 1, columnGap: 2, flexWrap: "wrap", children: p.kind === "question" ? choices(p).slice(0, 9).map((c, i) => /* @__PURE__ */ jsx5(Button, { label: `${c.n} \xB7 ${c.label}`, k: String(c.n), primary: i === 0, off: !canAct }, c.n)) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
3497
- /* @__PURE__ */ jsx5(Button, { label: t.overview.allow, k: "y", primary: true, off: !canAct }),
3498
- /* @__PURE__ */ jsx5(Button, { label: t.overview.deny, k: "n", off: !canAct }),
3499
- offers(s.tool, p.kind, "always") ? /* @__PURE__ */ jsx5(Button, { label: t.overview.always, k: "A", off: !canAct }) : null,
3500
- note && offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx5(Button, { label: t.overview.denyWithNote, k: "r", off: !canAct }) : null
3501
- ] }) })
3502
- ] }) : s.lastReply && state2 === "idle" ? /* @__PURE__ */ jsxs3(Text3, { color: C.dim, wrap: "truncate", children: [
3503
- " ",
3504
- oneLine(s.lastReply.text)
3505
- ] }) : null
3506
- ] });
3388
+ var asciiDigit = regexCheck(/\d/);
3389
+ var asciiHexDigit = regexCheck(/[\dA-Fa-f]/);
3390
+ var asciiPunctuation = regexCheck(/[!-/:-@[-`{-~]/);
3391
+ function markdownLineEnding(code3) {
3392
+ return code3 !== null && code3 < -2;
3393
+ }
3394
+ function markdownLineEndingOrSpace(code3) {
3395
+ return code3 !== null && (code3 < 0 || code3 === 32);
3396
+ }
3397
+ function markdownSpace(code3) {
3398
+ return code3 === -2 || code3 === -1 || code3 === 32;
3399
+ }
3400
+ var unicodePunctuation = regexCheck(new RegExp("\\p{P}|\\p{S}", "u"));
3401
+ var unicodeWhitespace = regexCheck(/\s/);
3402
+ function regexCheck(regex) {
3403
+ return check;
3404
+ function check(code3) {
3405
+ return code3 !== null && code3 > -1 && regex.test(String.fromCharCode(code3));
3406
+ }
3507
3407
  }
3508
3408
 
3509
- // src/tui/ui/Tail.tsx
3510
- import { useRef as useRef4 } from "react";
3511
- import { Box as Box4, Text as Text4, useBoxMetrics } from "ink";
3512
- import { jsx as jsx6 } from "react/jsx-runtime";
3513
- function Tail({ lines, color: color2, empty: empty3, emptyColor }) {
3514
- const ref = useRef4(null);
3515
- const { height } = useBoxMetrics(ref);
3516
- let end2 = lines.length;
3517
- while (end2 > 0 && !lines[end2 - 1].trim()) end2--;
3518
- const shown = end2 === 0 ? [] : lines.slice(0, end2).slice(-Math.max(1, height));
3519
- return /* @__PURE__ */ jsx6(Box4, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.length === 0 ? /* @__PURE__ */ jsx6(Text4, { color: emptyColor, children: empty3 }) : shown.map((l, i) => /* @__PURE__ */ jsx6(Text4, { color: color2, wrap: "truncate", children: l || " " }, i)) });
3520
- }
3521
-
3522
- // src/tui/ui/TitledBox.tsx
3523
- import { useRef as useRef5 } from "react";
3524
- import { Box as Box5, Text as Text5, useBoxMetrics as useBoxMetrics2 } from "ink";
3525
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
3526
- function TitledBox({ title: title2, mark, color: color2 = C.line, titleColor = C.dim, titleBold, double, foot, footColor = C.faint, children, flexGrow, height, width, flexShrink, minHeight }) {
3527
- const b2 = double ? { tl: "\u2554", h: "\u2550", tr: "\u2557", bl: "\u255A", br: "\u255D" } : { tl: "\u256D", h: "\u2500", tr: "\u256E", bl: "\u2570", br: "\u256F" };
3528
- const ref = useRef5(null);
3529
- const { width: w } = useBoxMetrics2(ref);
3530
- const text5 = (l) => l.map((s) => s.text).join("");
3531
- const tail = mark ? [{ text: ` ${mark.text.toUpperCase()}`, color: mark.color }] : [];
3532
- const room = Math.max(0, w - 4 - (cells(text5(tail)) < w - 8 ? cells(text5(tail)) : 0));
3533
- const head = [{ text: ` ${title2.toUpperCase()}`, color: titleColor }];
3534
- const shown = cells(text5([...head, ...tail])) > w - 4 ? [...cut(head, room), ...cells(text5(tail)) < w - 8 ? tail : [], { text: " ", color: titleColor }] : [...head, ...tail, { text: " ", color: titleColor }];
3535
- const rest = Math.max(0, w - 3 - cells(text5(shown)));
3536
- const f = foot ? text5(cut([{ text: foot }], Math.max(0, w - 6))) : "";
3537
- return /* @__PURE__ */ jsxs4(Box5, { ref, flexDirection: "column", flexGrow, flexShrink, height, width, minHeight, overflow: "hidden", children: [
3538
- /* @__PURE__ */ jsxs4(Text5, { wrap: "truncate", children: [
3539
- /* @__PURE__ */ jsxs4(Text5, { color: color2, children: [
3540
- b2.tl,
3541
- b2.h
3542
- ] }),
3543
- w > 0 ? shown.map((s, i) => /* @__PURE__ */ jsx7(Text5, { color: s.color, bold: titleBold, children: s.text }, i)) : null,
3544
- /* @__PURE__ */ jsxs4(Text5, { color: color2, children: [
3545
- b2.h.repeat(rest),
3546
- b2.tr
3547
- ] })
3548
- ] }),
3549
- /* @__PURE__ */ jsx7(Box5, { flexDirection: "column", borderStyle: double ? "double" : "round", borderTop: false, borderBottom: !f, borderColor: color2, paddingX: 1, flexGrow: 1, overflow: "hidden", children }),
3550
- f ? /* @__PURE__ */ jsxs4(Text5, { wrap: "truncate", children: [
3551
- /* @__PURE__ */ jsxs4(Text5, { color: color2, children: [
3552
- b2.bl,
3553
- b2.h.repeat(Math.max(1, w - 5 - cells(f))),
3554
- " "
3555
- ] }),
3556
- /* @__PURE__ */ jsx7(Text5, { color: footColor, children: f }),
3557
- /* @__PURE__ */ jsxs4(Text5, { color: color2, children: [
3558
- " ",
3559
- b2.h,
3560
- b2.br
3561
- ] })
3562
- ] }) : null
3563
- ] });
3564
- }
3565
-
3566
- // src/tui/screens/Overview.tsx
3567
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
3568
- var WIDE = 100;
3569
- function windowed(rows, selectedKey, height) {
3570
- if (rows.length <= height) return [...rows];
3571
- const at = Math.max(0, rows.findIndex((r) => r.key === selectedKey));
3572
- const start = Math.min(Math.max(0, at - Math.floor(height / 2)), rows.length - height);
3573
- return rows.slice(start, start + height);
3574
- }
3575
- function Overview() {
3576
- const tui = useTui();
3577
- const { d, projects, t, g, selected, select, actions, go, attach } = tui;
3578
- const { columns, rows: height } = useScreenSize();
3579
- const wide = columns >= WIDE;
3580
- const left2 = wide ? Math.min(64, Math.max(44, Math.floor(columns * 0.42))) : columns;
3581
- const teams = useTeams(tui.client);
3582
- const tasks = useTasks(tui.client);
3583
- const view = useMemo2(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
3584
- const sel = selected?.id ?? null;
3585
- const closed = closedLedgeTasks(tasks);
3586
- const threshold = useContextNear(tui.client);
3587
- const near = useMemo2(() => nearContext(d.sessions, threshold), [d.sessions, threshold]);
3588
- const [tasksShown, setTasksShown] = useState3(true);
3589
- const [joining, setJoining] = useState3(null);
3590
- const node2 = selected ? view.tree.find((n) => n.branches.some((b2) => b2.sessions.includes(selected)) || n.teams.some((x) => x.sessions.includes(selected))) : void 0;
3591
- const seatTeam = selected?.team ? teams?.find((x) => x.id === selected.team && x.state !== "dissolved") : void 0;
3592
- const choices2 = node2?.teams.map((x) => x.team).filter((x) => x.state !== "dissolved" && x.id !== selected?.team) ?? [];
3593
- useScreenKeys((input, key) => {
3594
- if (joining) {
3595
- if (key.escape) {
3596
- setJoining(null);
3597
- return true;
3598
- }
3599
- if (input === "j" || key.downArrow) {
3600
- setJoining({ index: (joining.index + 1) % Math.max(1, choices2.length) });
3601
- return true;
3602
- }
3603
- if (input === "k" || key.upArrow) {
3604
- setJoining({ index: (joining.index + choices2.length - 1) % Math.max(1, choices2.length) });
3605
- return true;
3606
- }
3607
- if (key.return && selected && choices2[joining.index]) {
3608
- const team = choices2[joining.index];
3609
- const role = selected.role ?? team.seats.find((x) => x.sessionId === null)?.role;
3610
- setJoining(null);
3611
- if (!role) {
3612
- tui.toast({ text: t.overview.noTeamHere, tone: "info" });
3613
- return true;
3614
- }
3615
- tui.client.send({ type: "team.join", team: team.id, session: selected.id, role });
3616
- tui.toast({ text: t.toast.joined(selected.name, team.name), tone: "ok" });
3617
- return true;
3618
- }
3619
- return true;
3620
- }
3621
- if (input === "J" && selected) {
3622
- if (choices2.length) setJoining({ index: 0 });
3623
- else tui.toast({ text: t.overview.noTeamHere, tone: "info" });
3624
- return true;
3625
- }
3626
- if (input === "R") {
3627
- const resting = (seatTeam?.state === "resting" ? seatTeam : void 0) ?? node2?.teams.find((x) => x.team.state === "resting")?.team ?? view.tree.flatMap((n) => n.teams).find((x) => x.team.state === "resting")?.team;
3628
- if (resting) {
3629
- tui.client.send({ type: "team.resume", team: resting.id });
3630
- tui.toast({ text: t.toast.resumed(resting.name), tone: "ok" });
3631
- }
3632
- return true;
3633
- }
3634
- if (input === "T") {
3635
- setTasksShown((v) => !v);
3636
- return true;
3637
- }
3638
- if (input === "H") {
3639
- 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];
3640
- if (!s) return true;
3641
- const rid = randomUUID();
3642
- const off = tui.client.onMessage((m) => {
3643
- if (m.type === "session.handover.result" && m.rid === rid) {
3644
- off();
3645
- tui.toast(m.ok ? { text: t.toast.handover(s.name), tone: "ok" } : { text: t.toast.handoverRefused(m.reason ?? "?"), tone: "info" });
3646
- }
3647
- });
3648
- setTimeout(off, 5e3);
3649
- tui.client.send({ type: "session.handover", rid, id: s.id });
3650
- return true;
3651
- }
3652
- if (input === "x" && closed[0]) {
3653
- tui.client.send({ type: "task.cmd", rid: randomUUID(), by: LEDGE_GIVER, op: "seen", task: closed[0].id });
3654
- return true;
3655
- }
3656
- if (input === "j" || key.downArrow) {
3657
- select(step(view.order, sel, 1));
3658
- return true;
3659
- }
3660
- if (input === "k" || key.upArrow) {
3661
- select(step(view.order, sel, -1));
3662
- return true;
3663
- }
3664
- if (key.tab) {
3665
- const next2 = nextWaiting(view.yours, sel);
3666
- if (next2) select(next2);
3667
- return true;
3668
- }
3669
- if (key.return) {
3670
- if (selected) go("session");
3671
- return true;
3672
- }
3673
- if (!selected) return false;
3674
- if (input === "a") {
3675
- attach(selected);
3676
- return true;
3677
- }
3678
- if (input === "r" && selected.pending) {
3679
- go("session");
3680
- return true;
3681
- }
3682
- if (/^[1-9]$/.test(input) && selected.pending?.kind !== "question") return false;
3683
- if (input === "y" || input === "n" || input === "A" || /^[1-9]$/.test(input)) return actions.key(selected, input);
3684
- if (input === "i" && selected.state === "working") {
3685
- actions.interrupt(selected);
3686
- return true;
3687
- }
3688
- return false;
3689
- });
3690
- const tree2 = view.tree.flatMap((n) => {
3691
- const teamed = n.teams.length > 0;
3692
- const common = n.branches.length > 0;
3693
- return [
3694
- { kind: "project", key: n.key, label: n.label, color: projectColor(n.color), count: n.count },
3695
- ...teamed && common ? [{ kind: "common", key: `${n.key}|common` }] : [],
3696
- ...n.branches.flatMap((b2) => [{ kind: "branch", key: `${n.key}|${b2.branch}`, branch: b2.branch, indent: teamed ? 4 : 2 }, ...b2.sessions.map((s) => ({ kind: "session", key: s.id, s, indent: teamed ? 2 : 0 }))]),
3697
- ...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 }))])
3698
- ];
3699
- });
3700
- const yoursCount = view.yours.length + closed.length + near.length;
3701
- const yoursHeight = Math.max(1, Math.min(yoursCount, 6));
3702
- const joinHeight = joining && selected ? Math.max(1, choices2.length) + 2 : 0;
3703
- const treeHeight = Math.max(3, height - 3 - (yoursHeight + 2) - joinHeight - 3);
3704
- const inner = left2 - 4, nameW = 7;
3705
- const stateW = Math.max(...Object.values(t.state).map((x) => x.length)) + 1;
3706
- const topicW = Math.max(6, inner - 6 - nameW - stateW - 3);
3707
- const askW = Math.max(6, inner - 2 - nameW - 9 - 7);
3708
- const yoursBox = /* @__PURE__ */ jsx8(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__ */ jsx8(Text6, { color: C.faint, children: t.overview.nobodyWaits }) : [...view.yours.slice(0, yoursHeight).map((s) => {
3709
- const st = botState(s);
3710
- return /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
3711
- /* @__PURE__ */ jsxs5(Text6, { color: STATE_COLOR[st], children: [
3712
- g[st],
3713
- " "
3714
- ] }),
3715
- /* @__PURE__ */ jsx8(Text6, { color: C.fg, bold: true, children: fit(s.name, nameW) }),
3716
- /* @__PURE__ */ jsx8(Text6, { color: C.dim, children: fit(s.repo ?? s.project, 9) }),
3717
- /* @__PURE__ */ jsx8(Text6, { color: STATE_COLOR[st], children: fit(oneLine(s.pending?.text ?? ""), askW) }),
3718
- /* @__PURE__ */ jsxs5(Text6, { color: C.faint, children: [
3719
- " ",
3720
- /* @__PURE__ */ jsx8(Ago, { iso: s.lastActivityAt })
3721
- ] })
3722
- ] }, s.id);
3723
- }), ...closed.slice(0, Math.max(0, yoursHeight - view.yours.length)).map((x) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
3724
- /* @__PURE__ */ jsxs5(Text6, { color: x.state === "done" ? C.green : C.yellow, children: [
3725
- x.state === "done" ? g.ok : g.fail,
3726
- " "
3727
- ] }),
3728
- /* @__PURE__ */ jsx8(Text6, { color: C.fg, children: t.overview.ledgeTask(x.ownerName, x.title, x.state) })
3729
- ] }, 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: [
3730
- /* @__PURE__ */ jsxs5(Text6, { color: C.yellow, children: [
3731
- s.handover?.state === "failed" ? g.fail : "\u25D4",
3732
- " "
3733
- ] }),
3734
- /* @__PURE__ */ jsx8(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)) })
3735
- ] }, `near:${s.id}`))] });
3736
- const treeBox = /* @__PURE__ */ jsx8(TitledBox, { title: t.overview.projects, flexGrow: 1, children: tree2.length === 0 ? /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: t.overview.noSession }) : windowed(tree2, sel, treeHeight).map((r) => r.kind === "project" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
3737
- /* @__PURE__ */ jsx8(Text6, { color: r.color, children: g.project }),
3738
- " ",
3739
- /* @__PURE__ */ jsx8(Text6, { color: C.fg, bold: true, children: r.label }),
3740
- " ",
3741
- /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: t.overview.sessions(r.count) })
3742
- ] }, r.key) : r.kind === "common" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", color: C.faint, children: [
3743
- " ",
3744
- g.common,
3745
- " ",
3746
- t.overview.common
3747
- ] }, r.key) : r.kind === "team" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
3748
- /* @__PURE__ */ jsx8(Text6, { children: " " }),
3749
- /* @__PURE__ */ jsx8(Text6, { color: C.coral, children: g.team }),
3750
- " ",
3751
- /* @__PURE__ */ jsx8(Text6, { color: r.team.state === "resting" ? C.dim : C.fg, bold: true, children: r.team.name }),
3752
- " ",
3753
- /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: r.team.state === "resting" ? t.overview.resting : t.overview.teamLine(r.done, r.given) })
3754
- ] }, r.key) : r.kind === "branch" ? /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", color: C.faint, children: [
3755
- " ".repeat(r.indent),
3756
- g.branch,
3757
- " ",
3758
- r.branch
3759
- ] }, r.key) : /* @__PURE__ */ jsx8(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)) });
3760
- const keys = [
3761
- ["j k", t.keys.navigate],
3762
- ["\u21B5", t.keys.open],
3763
- ...view.yours.length ? [["tab", t.keys.next], ["y n", t.keys.answer]] : [],
3764
- ["a", t.keys.attach],
3765
- ...seatTeam ? [["T", t.keys.tasks]] : [],
3766
- ...choices2.length ? [["J", t.keys.join]] : [],
3767
- ...view.tree.some((n) => n.teams.some((x) => x.team.state === "resting")) ? [["R", t.keys.resume]] : [],
3768
- ...near.length ? [["H", t.keys.handover]] : [],
3769
- ["/", t.keys.ask],
3770
- ["+", t.keys.launch],
3771
- [",", t.keys.settings],
3772
- ["?", t.keys.help],
3773
- ["q", t.keys.quit]
3774
- ];
3775
- 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) : [];
3776
- return /* @__PURE__ */ jsx8(Frame, { crumb: tui.machines.map((m) => `${m.label} ${m.ready ? g.machine : "\u25CB"}`).join(" "), keys, children: /* @__PURE__ */ jsxs5(Box6, { flexGrow: 1, children: [
3777
- /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", width: left2, flexShrink: 0, children: [
3778
- yoursBox,
3779
- joining && selected ? /* @__PURE__ */ jsx8(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: [
3780
- /* @__PURE__ */ jsx8(Text6, { color: C.coral, children: g.team }),
3781
- " ",
3782
- x.name
3783
- ] }, x.id)) }) : null,
3784
- treeBox
3785
- ] }),
3786
- wide ? /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", flexGrow: 1, children: [
3787
- selected ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
3788
- /* @__PURE__ */ jsx8(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__ */ jsx8(SessionCard, { s: selected, note: true }) }),
3789
- seatTeam && tasksShown ? /* @__PURE__ */ jsx8(TitledBox, { title: t.overview.tasks(`${g.team} ${seatTeam.name}`), height: Math.max(3, teamTasks.length + 2), children: teamTasks.length === 0 ? /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: t.overview.noTask }) : teamTasks.map((x) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
3790
- /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: x.id }),
3791
- " ",
3792
- /* @__PURE__ */ jsx8(Text6, { color: C.fg, children: x.title }),
3793
- " ",
3794
- /* @__PURE__ */ jsxs5(Text6, { color: C.dim, children: [
3795
- x.giverName,
3796
- " ",
3797
- g.arrow,
3798
- " ",
3799
- x.ownerName
3800
- ] }),
3801
- " ",
3802
- /* @__PURE__ */ jsx8(Text6, { color: x.state === "done" ? C.green : x.state === "taken" ? C.blue : C.faint, children: t.ask.taskState[x.state] })
3803
- ] }, x.id)) }) : null,
3804
- /* @__PURE__ */ jsx8(TitledBox, { title: t.overview.terminal(paneName(selected)), flexGrow: 1, children: /* @__PURE__ */ jsx8(Tail, { lines: d.previews.get(selected.id) ?? [], color: C.dim, empty: t.overview.noPreview, emptyColor: C.faint }) })
3805
- ] }) : /* @__PURE__ */ jsx8(TitledBox, { title: t.tabs.session, flexGrow: 1, children: /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: t.session.noSession }) }),
3806
- /* @__PURE__ */ jsx8(TitledBox, { title: t.overview.exchanges, height: 5, children: (() => {
3807
- const list3 = exchanges(d.sessions, 3, tasks, t.overview.ledge);
3808
- return list3.length === 0 ? /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: t.overview.noExchange }) : list3.map((x, i) => /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", children: [
3809
- /* @__PURE__ */ jsx8(Text6, { color: C.faint, children: clock(x.at) }),
3810
- " ",
3811
- /* @__PURE__ */ jsx8(Text6, { color: C.fg, children: x.from }),
3812
- " ",
3813
- /* @__PURE__ */ jsx8(Text6, { color: x.kind === "task" ? C.green : C.violet, children: g.arrow }),
3814
- " ",
3815
- /* @__PURE__ */ jsx8(Text6, { color: C.fg, children: x.to }),
3816
- " ",
3817
- x.kind === "task" ? /* @__PURE__ */ jsxs5(Text6, { color: C.green, children: [
3818
- g.task,
3819
- " "
3820
- ] }) : null,
3821
- /* @__PURE__ */ jsx8(Text6, { color: C.dim, children: oneLine(x.summary) })
3822
- ] }, i));
3823
- })() })
3824
- ] }) : null
3825
- ] }) });
3826
- }
3827
- function SessionRow({ s, on, nameW, topicW, stateW, indent: indent2 = 0 }) {
3828
- const { t, g } = useTui();
3829
- const st = botState(s);
3830
- const n = helperCount(s);
3831
- return /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate", backgroundColor: on ? C.sel : void 0, children: [
3832
- /* @__PURE__ */ jsx8(Text6, { color: on ? C.coral : C.faint, children: on ? "\u258C" : " " }),
3833
- /* @__PURE__ */ jsx8(Text6, { children: " ".repeat(2 + indent2) }),
3834
- /* @__PURE__ */ jsxs5(Text6, { color: s.offline ? C.faint : STATE_COLOR[st], children: [
3835
- g[st],
3836
- " "
3837
- ] }),
3838
- /* @__PURE__ */ jsx8(Text6, { color: s.offline ? C.dim : C.fg, bold: true, children: fit(s.name, nameW) }),
3839
- s.role ? /* @__PURE__ */ jsxs5(Text6, { color: C.violet, children: [
3840
- "@",
3841
- s.role,
3842
- " "
3843
- ] }) : null,
3844
- /* @__PURE__ */ jsxs5(Text6, { color: C.dim, children: [
3845
- fit(oneLine(s.topic ?? s.title), topicW),
3846
- " "
3847
- ] }),
3848
- /* @__PURE__ */ jsx8(Text6, { color: s.offline ? C.faint : STATE_COLOR[st], children: fit(s.offline ? t.overview.offline : t.state[st], stateW).trimEnd() }),
3849
- n > 0 ? /* @__PURE__ */ jsxs5(Text6, { color: C.violet, children: [
3850
- " +",
3851
- n
3852
- ] }) : null,
3853
- s.machineName && s.machineId !== "local" ? /* @__PURE__ */ jsxs5(Text6, { color: C.faint, children: [
3854
- " \xB7 ",
3855
- machineLabel(s)
3856
- ] }) : null
3857
- ] });
3858
- }
3859
-
3860
- // src/tui/screens/Session.tsx
3861
- import { useEffect as useEffect7, useMemo as useMemo3, useState as useState7 } from "react";
3862
- import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
3863
-
3864
- // src/tui/images.tsx
3865
- import { mkdir, writeFile } from "fs/promises";
3866
- import { tmpdir as tmpdir2 } from "os";
3867
- import { join as join3 } from "path";
3868
- import { useRef as useRef6, useState as useState4 } from "react";
3869
- import { Text as Text7 } from "ink";
3870
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
3871
- var counter2 = 0;
3872
- var INLINE_BUDGET = MAX_FRAME - 64 * 1024;
3873
- function useChips(put) {
3874
- const { t, toast } = useTui();
3875
- const [items, setItems] = useState4([]);
3876
- const held = useRef6([]);
3877
- const commit = (next2) => {
3878
- held.current = next2;
3879
- setItems(next2);
3880
- };
3881
- const patch = (key, over) => commit(held.current.map((c) => c.key === key ? { ...c, ...over } : c));
3882
- const reasonOf = (e) => {
3883
- const reason = e instanceof AttachmentFailure ? e.reason : null;
3884
- return reason === "too_large" ? t.images.tooLarge : reason === "not_image" ? t.images.notImage : t.images.failed;
3885
- };
3886
- return {
3887
- items,
3888
- add({ image: image2, width, height, bytes }) {
3889
- if (held.current.length >= ATTACHMENTS_MAX) {
3890
- toast({ text: t.images.max, tone: "info" });
3891
- return;
3892
- }
3893
- const key = counter2++;
3894
- const inline2 = held.current.reduce((n, c) => n + (c.image?.data.length ?? 0), 0);
3895
- if (bytes > ATTACHMENT_LOCAL_MAX || !put && inline2 + image2.data.length > INLINE_BUDGET) {
3896
- commit([...held.current, { key, width, height, state: "refused", reason: t.images.tooLarge }]);
3897
- return;
3898
- }
3899
- if (!put) {
3900
- commit([...held.current, { key, width, height, state: "ready", image: image2 }]);
3901
- return;
3902
- }
3903
- commit([...held.current, { key, width, height, state: "uploading" }]);
3904
- put(image2).then((ref) => patch(key, { state: "ready", id: ref.id }), (e) => patch(key, { state: "refused", reason: reasonOf(e) }));
3905
- },
3906
- removeLast() {
3907
- commit(held.current.slice(0, -1));
3908
- },
3909
- ready: items.every((c) => c.state === "ready"),
3910
- ids: () => held.current.flatMap((c) => c.state === "ready" && c.id ? [c.id] : []),
3911
- uploads: () => held.current.flatMap((c) => c.state === "ready" && c.image ? [c.image] : []),
3912
- clear() {
3913
- const taken = held.current;
3914
- commit([]);
3915
- return taken;
3916
- },
3917
- putBack(taken) {
3918
- commit([...taken, ...held.current].slice(0, ATTACHMENTS_MAX));
3919
- }
3920
- };
3921
- }
3922
- function ChipsLine({ chips }) {
3923
- const { t } = useTui();
3924
- if (chips.length === 0) return null;
3925
- return /* @__PURE__ */ jsx9(Text7, { wrap: "truncate", children: chips.map((c, i) => /* @__PURE__ */ jsxs6(Text7, { children: [
3926
- i ? " " : "",
3927
- /* @__PURE__ */ jsxs6(Text7, { color: c.state === "refused" ? C.red : C.dim, children: [
3928
- "\u25A3 ",
3929
- t.images.capture,
3930
- " ",
3931
- c.width,
3932
- "\xD7",
3933
- c.height,
3934
- " ",
3935
- c.state === "uploading" ? "\u2026" : "\u2715"
3936
- ] }),
3937
- c.reason ? /* @__PURE__ */ jsxs6(Text7, { color: C.red, children: [
3938
- " ",
3939
- c.reason
3940
- ] }) : null
3941
- ] }, c.key)) });
3942
- }
3943
- var EXT = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
3944
- function usePaste(chips) {
3945
- const { readImage, toast, t } = useTui();
3946
- const reading = useRef6(false);
3947
- return () => {
3948
- if (reading.current) return;
3949
- reading.current = true;
3950
- void readImage().then((got) => {
3951
- if (got) chips.add(got);
3952
- else toast({ text: t.images.none, tone: "info" });
3953
- }, () => toast({ text: t.images.none, tone: "info" })).finally(() => {
3954
- reading.current = false;
3955
- });
3956
- };
3957
- }
3958
- function useOpenImage() {
3959
- const { client, openFile, toast, t } = useTui();
3960
- const presses = useRef6(0);
3961
- const last = useRef6("");
3962
- return (items) => {
3963
- const signature = items.map((x) => x.ref.id).join(",");
3964
- if (signature !== last.current) {
3965
- last.current = signature;
3966
- presses.current = 0;
3967
- }
3968
- const item = items[items.length - 1 - presses.current++ % items.length];
3969
- if (!item) return;
3970
- const { ref, route } = item;
3971
- void (async () => {
3972
- const { mime, data } = await getImage(client, ref.id, route);
3973
- const dir = join3(tmpdir2(), "ledge-images");
3974
- await mkdir(dir, { recursive: true, mode: 448 });
3975
- const file = join3(dir, `${ref.id}.${EXT[mime]}`);
3976
- await writeFile(file, Buffer.from(data, "base64"), { mode: 384 });
3977
- await openFile(file);
3978
- })().catch((e) => toast({ text: t.images.openFailed(e instanceof AttachmentFailure ? e.reason : e.message), tone: "error" }));
3979
- };
3980
- }
3981
-
3982
- // ../ui/src/thread/turns.ts
3983
- var OPEN_TURNS = 3;
3984
- var byTime = (a, b2) => a.at < b2.at ? -1 : a.at > b2.at ? 1 : 0;
3985
- function withoutEcho(items, reply) {
3986
- if (!reply) return items;
3987
- let last = -1;
3988
- items.forEach((it, i) => {
3989
- if (it.kind === "message") last = i;
3990
- });
3991
- const echo = items[last];
3992
- return echo?.kind === "message" && echo.text.trim() === reply.text.trim() ? items.filter((_, i) => i !== last) : items;
3993
- }
3994
- function turns(items) {
3995
- const map3 = /* @__PURE__ */ new Map();
3996
- for (const it of items) {
3997
- if (it.helperId) continue;
3998
- const t = map3.get(it.turn) ?? { n: it.turn, items: [] };
3999
- t.items.push(it);
4000
- if (it.kind === "prompt") t.prompt = it;
4001
- if (it.kind === "reply") t.reply = it;
4002
- map3.set(it.turn, t);
4003
- }
4004
- return [...map3.values()].sort((a, b2) => a.n - b2.n).map((t) => ({ ...t, items: withoutEcho([...t.items].sort(byTime), t.reply) }));
4005
- }
4006
-
4007
- // ../ui/src/thread/tools.ts
4008
- function rawTarget(item) {
4009
- let s = item.label.trim();
4010
- const prefix = `${item.tool} \xB7 `;
4011
- if (s.startsWith(prefix)) s = s.slice(prefix.length);
4012
- if (s === item.tool) return "";
4013
- if (s.startsWith("{")) {
4014
- try {
4015
- const o = JSON.parse(s);
4016
- const v = ["pattern", "query", "url", "file_path", "path", "command", "description", "prompt"].map((k) => o[k]).find((x) => typeof x === "string" && x);
4017
- if (typeof v === "string") s = v;
4018
- } catch {
4019
- }
4020
- }
4021
- return (s.split("\n")[0] ?? "").trim();
4022
- }
4023
- var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4024
-
4025
- // ../../node_modules/.pnpm/ccount@2.0.1/node_modules/ccount/index.js
4026
- function ccount(value, character) {
4027
- const source2 = String(value);
4028
- if (typeof character !== "string") {
4029
- throw new TypeError("Expected character");
4030
- }
4031
- let count = 0;
4032
- let index2 = source2.indexOf(character);
4033
- while (index2 !== -1) {
4034
- count++;
4035
- index2 = source2.indexOf(character, index2 + character.length);
4036
- }
4037
- return count;
4038
- }
4039
-
4040
- // ../../node_modules/.pnpm/devlop@1.1.0/node_modules/devlop/lib/default.js
4041
- function ok() {
4042
- }
4043
-
4044
- // ../../node_modules/.pnpm/micromark-util-character@2.1.1/node_modules/micromark-util-character/index.js
4045
- var asciiAlpha = regexCheck(/[A-Za-z]/);
4046
- var asciiAlphanumeric = regexCheck(/[\dA-Za-z]/);
4047
- var asciiAtext = regexCheck(/[#-'*+\--9=?A-Z^-~]/);
4048
- function asciiControl(code3) {
4049
- return (
4050
- // Special whitespace codes (which have negative values), C0 and Control
4051
- // character DEL
4052
- code3 !== null && (code3 < 32 || code3 === 127)
4053
- );
4054
- }
4055
- var asciiDigit = regexCheck(/\d/);
4056
- var asciiHexDigit = regexCheck(/[\dA-Fa-f]/);
4057
- var asciiPunctuation = regexCheck(/[!-/:-@[-`{-~]/);
4058
- function markdownLineEnding(code3) {
4059
- return code3 !== null && code3 < -2;
4060
- }
4061
- function markdownLineEndingOrSpace(code3) {
4062
- return code3 !== null && (code3 < 0 || code3 === 32);
4063
- }
4064
- function markdownSpace(code3) {
4065
- return code3 === -2 || code3 === -1 || code3 === 32;
4066
- }
4067
- var unicodePunctuation = regexCheck(new RegExp("\\p{P}|\\p{S}", "u"));
4068
- var unicodeWhitespace = regexCheck(/\s/);
4069
- function regexCheck(regex) {
4070
- return check;
4071
- function check(code3) {
4072
- return code3 !== null && code3 > -1 && regex.test(String.fromCharCode(code3));
4073
- }
4074
- }
4075
-
4076
- // ../../node_modules/.pnpm/escape-string-regexp@5.0.0/node_modules/escape-string-regexp/index.js
4077
- function escapeStringRegexp(string3) {
4078
- if (typeof string3 !== "string") {
4079
- throw new TypeError("Expected a string");
4080
- }
4081
- return string3.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
3409
+ // ../../node_modules/.pnpm/escape-string-regexp@5.0.0/node_modules/escape-string-regexp/index.js
3410
+ function escapeStringRegexp(string3) {
3411
+ if (typeof string3 !== "string") {
3412
+ throw new TypeError("Expected a string");
3413
+ }
3414
+ return string3.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
4082
3415
  }
4083
3416
 
4084
3417
  // ../../node_modules/.pnpm/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.js
@@ -15927,6 +15260,9 @@ function imageLine(ref, t, key) {
15927
15260
  var DIFF_MAX = 30;
15928
15261
  var CAP = { tool: 3, error: 3, message: 3, notice: 2, permission: 3, helper: 2 };
15929
15262
  var LIVE_PAD = 9;
15263
+ function wrap2(text5, width) {
15264
+ return wrapSegs([{ text: text5.replace(/\r/g, "") }], Math.max(10, width)).map((l) => l.map((s) => s.text).join(""));
15265
+ }
15930
15266
  function diffCount(diff) {
15931
15267
  let add = 0, del2 = 0;
15932
15268
  for (const l of (diff ?? "").split("\n")) {
@@ -16018,399 +15354,1160 @@ function foldedLine(turn, o) {
16018
15354
  const parts = [turn.prompt ? oneLine(turn.prompt.text) : "", o.t.session.actions(tools.length), ...files.size ? [o.t.session.files(files.size)] : [], ...turn.reply ? [oneLine(turn.reply.text)] : []].filter(Boolean);
16019
15355
  return [{ text: `${o.g.folded} ${clock(turn.prompt?.at ?? turn.items[0]?.at ?? (/* @__PURE__ */ new Date(0)).toISOString())} `, color: C.faint }, { text: parts.join(" \xB7 "), color: C.faint }];
16020
15356
  }
16021
- function threadView(items, o) {
16022
- const all2 = turns(items);
16023
- const lines = [], keys = [], turnOf = /* @__PURE__ */ new Map();
16024
- const opts = { ...o, cap: (n) => o.unfolded ? Infinity : n };
16025
- const push2 = (key, l) => {
16026
- for (const x of l) {
16027
- lines.push(x);
16028
- keys.push(key);
15357
+ function threadView(items, o) {
15358
+ const all2 = turns(items);
15359
+ const lines = [], keys = [], turnOf = /* @__PURE__ */ new Map();
15360
+ const opts = { ...o, cap: (n) => o.unfolded ? Infinity : n };
15361
+ const push2 = (key, l) => {
15362
+ for (const x of l) {
15363
+ lines.push(x);
15364
+ keys.push(key);
15365
+ }
15366
+ };
15367
+ all2.forEach((turn, i) => {
15368
+ const recent = i >= all2.length - OPEN_TURNS;
15369
+ const first = turn.prompt?.id ?? turn.items[0]?.id ?? String(i);
15370
+ for (const it of turn.items) turnOf.set(it.id, first);
15371
+ if (!recent && !o.unfolded) {
15372
+ push2(first, [cut(foldedLine(turn, o), o.width)]);
15373
+ return;
15374
+ }
15375
+ if (lines.length) push2(first, [[{ text: "" }]]);
15376
+ for (const it of turn.items) push2(it.id, itemLines(it, opts));
15377
+ });
15378
+ return { lines, keys, turns: turnOf };
15379
+ }
15380
+ function anchorLine(v, anchor) {
15381
+ const at = v.keys.indexOf(anchor.key);
15382
+ if (at >= 0) return at + anchor.offset;
15383
+ const turn = v.turns.get(anchor.key);
15384
+ return turn === void 0 ? -1 : v.keys.indexOf(turn);
15385
+ }
15386
+ function threadImages(items, unfolded) {
15387
+ const all2 = turns(items);
15388
+ return (unfolded ? all2 : all2.slice(-OPEN_TURNS)).flatMap((turn) => turn.items.flatMap((it) => it.kind === "prompt" ? it.images ?? [] : []));
15389
+ }
15390
+
15391
+ // src/tui/model/labels.ts
15392
+ var TOOL_NAME = { claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", omp: "OMP" };
15393
+ var modelLabel = (s, fallback) => [s.model ?? (s.effort ? fallback : ""), s.effort ?? ""].filter(Boolean).join(" \xB7 ") || fallback;
15394
+ var machineLabel = (s) => s.machineName ?? splitId(s.id)?.machine ?? "Local";
15395
+ var paneName = (s) => s.tmuxSession ?? `ledge-${splitId(s.id)?.id ?? s.id}`;
15396
+ function fit(s, n) {
15397
+ const cps = [...s];
15398
+ if (n <= 0) return "";
15399
+ return cps.length > n ? `${cps.slice(0, Math.max(0, n - 1)).join("")}\u2026` : s + " ".repeat(n - cps.length);
15400
+ }
15401
+
15402
+ // src/tui/ui/now.ts
15403
+ import { useSyncExternalStore as useSyncExternalStore4 } from "react";
15404
+ var listeners = /* @__PURE__ */ new Set();
15405
+ var now = Date.now();
15406
+ var timer = null;
15407
+ function subscribe(fn) {
15408
+ listeners.add(fn);
15409
+ if (!timer) {
15410
+ now = Date.now();
15411
+ timer = setInterval(() => {
15412
+ now = Date.now();
15413
+ for (const l of listeners) l();
15414
+ }, 1e3);
15415
+ timer.unref?.();
15416
+ }
15417
+ return () => {
15418
+ listeners.delete(fn);
15419
+ if (listeners.size === 0 && timer) {
15420
+ clearInterval(timer);
15421
+ timer = null;
15422
+ }
15423
+ };
15424
+ }
15425
+ var useNow = () => useSyncExternalStore4(subscribe, () => now);
15426
+
15427
+ // src/tui/ui/Ago.tsx
15428
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
15429
+ function Ago({ iso }) {
15430
+ const now2 = useNow();
15431
+ const { t } = useI18n();
15432
+ return /* @__PURE__ */ jsx2(Fragment, { children: ago(iso, now2, t.time) });
15433
+ }
15434
+
15435
+ // src/tui/ui/Field.tsx
15436
+ import { useReducer, useRef as useRef6 } from "react";
15437
+ import { Box as Box3, usePaste, useInput } from "ink";
15438
+
15439
+ // src/tui/model/field.ts
15440
+ var graphemes2 = new Intl.Segmenter(void 0, { granularity: "grapheme" });
15441
+ var stops = (text5) => [...Array.from(graphemes2.segment(text5), (g) => g.index), text5.length];
15442
+ var newlines = (s) => s.replace(/\r\n?/g, "\n");
15443
+ var insert = (f, s) => {
15444
+ const add = newlines(s);
15445
+ return { text: f.text.slice(0, f.cursor) + add + f.text.slice(f.cursor), cursor: f.cursor + add.length };
15446
+ };
15447
+ var before = (f) => {
15448
+ const a = stops(f.text);
15449
+ let i = a.length - 1;
15450
+ while (i > 0 && a[i] >= f.cursor) i--;
15451
+ return a[i] < f.cursor ? a[i] : 0;
15452
+ };
15453
+ var after = (f) => stops(f.text).find((x) => x > f.cursor) ?? f.text.length;
15454
+ var left = (f) => ({ ...f, cursor: before(f) });
15455
+ var right = (f) => ({ ...f, cursor: after(f) });
15456
+ var back = (f) => {
15457
+ const to = before(f);
15458
+ return { text: f.text.slice(0, to) + f.text.slice(f.cursor), cursor: to };
15459
+ };
15460
+ var del = (f) => ({ text: f.text.slice(0, f.cursor) + f.text.slice(after(f)), cursor: f.cursor });
15461
+ function wordStart(f) {
15462
+ let i = f.cursor;
15463
+ while (i > 0 && /\s/.test(f.text[i - 1])) i--;
15464
+ while (i > 0 && !/\s/.test(f.text[i - 1])) i--;
15465
+ return i;
15466
+ }
15467
+ var wordLeft = (f) => ({ ...f, cursor: wordStart(f) });
15468
+ function wordRight(f) {
15469
+ let i = f.cursor;
15470
+ while (i < f.text.length && /\s/.test(f.text[i])) i++;
15471
+ while (i < f.text.length && !/\s/.test(f.text[i])) i++;
15472
+ return { ...f, cursor: i };
15473
+ }
15474
+ var killWord = (f) => {
15475
+ const to = wordStart(f);
15476
+ return { text: f.text.slice(0, to) + f.text.slice(f.cursor), cursor: to };
15477
+ };
15478
+ var home = (f) => ({ ...f, cursor: f.text.lastIndexOf("\n", f.cursor - 1) + 1 });
15479
+ var end = (f) => {
15480
+ const n = f.text.indexOf("\n", f.cursor);
15481
+ return { ...f, cursor: n < 0 ? f.text.length : n };
15482
+ };
15483
+ function layout(text5, width) {
15484
+ const w = Math.max(2, width - 1);
15485
+ const a = stops(text5), rows = [];
15486
+ let start = 0;
15487
+ for (let i = 0; i < a.length; i++) {
15488
+ const at = a[i];
15489
+ if (at < text5.length && text5[at] !== "\n") continue;
15490
+ if (start === i) rows.push({ a: at, b: at, last: true });
15491
+ for (let s = start; s < i; ) {
15492
+ let n = 0, j = s, space = -1;
15493
+ while (j < i) {
15494
+ const c = cells(text5.slice(a[j], a[j + 1]));
15495
+ if (n + c > w) break;
15496
+ if (text5[a[j]] === " ") space = j;
15497
+ n += c;
15498
+ j++;
15499
+ }
15500
+ if (j === s) j = s + 1;
15501
+ const e = j < i && space > s ? space + 1 : j;
15502
+ rows.push({ a: a[s], b: a[e], last: e >= i });
15503
+ s = e;
15504
+ }
15505
+ start = i + 1;
15506
+ }
15507
+ return rows;
15508
+ }
15509
+ var rowOf = (rows, cursor) => {
15510
+ const k = rows.findIndex((r) => cursor >= r.a && (cursor < r.b || cursor === r.b && r.last));
15511
+ return k < 0 ? rows.length - 1 : k;
15512
+ };
15513
+ function vertical(f, width, by) {
15514
+ const rows = layout(f.text, width), k = rowOf(rows, f.cursor), to = rows[k + by];
15515
+ if (!to) return null;
15516
+ const col = cells(f.text.slice(rows[k].a, f.cursor));
15517
+ let cursor = to.a;
15518
+ for (const x of stops(f.text)) {
15519
+ if (x <= to.a) continue;
15520
+ if (x > to.b || x === to.b && !to.last || cells(f.text.slice(to.a, x)) > col) break;
15521
+ cursor = x;
15522
+ }
15523
+ return { ...f, cursor };
15524
+ }
15525
+ function fieldLines(text5, cursor, width, o) {
15526
+ const indent2 = { text: " ".repeat(cells(o.prefix.text)) };
15527
+ const inv = (t) => ({ text: t, color: o.color, inverse: true });
15528
+ if (!text5) return [[o.prefix, ...cursor !== null ? [inv(" ")] : [], ...o.placeholder ? [o.placeholder] : []]];
15529
+ const rows = layout(text5, width - cells(o.prefix.text));
15530
+ return rows.map((r, k) => {
15531
+ const line = [k === 0 ? o.prefix : indent2];
15532
+ const at = cursor !== null && cursor >= r.a && cursor < r.b ? cursor : -1;
15533
+ if (at < 0) line.push({ text: text5.slice(r.a, r.b), color: o.color });
15534
+ else {
15535
+ const next2 = after({ text: text5, cursor: at });
15536
+ if (at > r.a) line.push({ text: text5.slice(r.a, at), color: o.color });
15537
+ line.push(inv(text5.slice(at, Math.min(next2, r.b))));
15538
+ if (next2 < r.b) line.push({ text: text5.slice(next2, r.b), color: o.color });
15539
+ }
15540
+ if (cursor === r.b && r.last) line.push(inv(" "));
15541
+ return line.filter((s) => s.text !== "" || s === line[0]);
15542
+ });
15543
+ }
15544
+ var scrollTop = (top, row2, total, max) => Math.max(0, Math.min(Math.max(0, total - max), row2 < top ? row2 : row2 >= top + max ? row2 - max + 1 : top));
15545
+ var lineAt = (f) => ({
15546
+ line: f.text.slice(0, f.cursor).split("\n").length,
15547
+ lines: f.text.split("\n").length
15548
+ });
15549
+
15550
+ // src/tui/ui/Lines.tsx
15551
+ import { memo, useEffect as useEffect5, useRef as useRef4 } from "react";
15552
+ import { Box, Text, useBoxMetrics } from "ink";
15553
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
15554
+ function Live({ seg }) {
15555
+ const now2 = useNow();
15556
+ const { t } = useI18n();
15557
+ const l = seg.live;
15558
+ return /* @__PURE__ */ jsx3(Text, { color: seg.color, bold: seg.bold, children: "spin" in l ? spin(now2) : took(Math.max(0, now2 - l.since), t.time.decimal).padStart(l.pad) });
15559
+ }
15560
+ var linked = (text5, url) => `\x1B]8;;${url.replace(/[\x00-\x1f\x7f]/g, "")}\x07${text5}\x1B]8;;\x07`;
15561
+ var LineText = memo(function LineText2({ line }) {
15562
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
15563
+ line.map((x, k) => x.live ? /* @__PURE__ */ jsx3(Live, { seg: x }, k) : /* @__PURE__ */ jsx3(Text, { color: x.color, backgroundColor: x.bg, bold: x.bold, italic: x.italic, underline: x.underline, strikethrough: x.strike, inverse: x.inverse, children: x.link && x.text ? linked(x.text, x.link) : x.text || (k === 0 ? " " : "") }, k)),
15564
+ line.length === 0 ? " " : null
15565
+ ] });
15566
+ });
15567
+ function Lines({ lines, up = 0, onHeight }) {
15568
+ const ref = useRef4(null);
15569
+ const { height } = useBoxMetrics(ref);
15570
+ useEffect5(() => {
15571
+ if (height > 0) onHeight?.(height);
15572
+ }, [height, onHeight]);
15573
+ const h = Math.max(1, height);
15574
+ const back2 = Math.min(up, Math.max(0, lines.length - h));
15575
+ const shown = lines.slice(Math.max(0, lines.length - h - back2), lines.length - back2);
15576
+ return /* @__PURE__ */ jsx3(Box, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.map((l, i) => /* @__PURE__ */ jsx3(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx3(LineText, { line: l }) }, i)) });
15577
+ }
15578
+
15579
+ // src/tui/ui/TitledBox.tsx
15580
+ import { useRef as useRef5 } from "react";
15581
+ import { Box as Box2, Text as Text2, useBoxMetrics as useBoxMetrics2 } from "ink";
15582
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
15583
+ function TitledBox({ title: title2, mark, color: color2 = C.line, titleColor = C.dim, titleBold, double, foot, footColor = C.faint, children, flexGrow, height, width, flexShrink, minHeight }) {
15584
+ const b2 = double ? { tl: "\u2554", h: "\u2550", tr: "\u2557", bl: "\u255A", br: "\u255D" } : { tl: "\u256D", h: "\u2500", tr: "\u256E", bl: "\u2570", br: "\u256F" };
15585
+ const ref = useRef5(null);
15586
+ const { width: w } = useBoxMetrics2(ref);
15587
+ const text5 = (l) => l.map((s) => s.text).join("");
15588
+ const tail = mark ? [{ text: ` ${mark.text.toUpperCase()}`, color: mark.color }] : [];
15589
+ const room = Math.max(0, w - 4 - (cells(text5(tail)) < w - 8 ? cells(text5(tail)) : 0));
15590
+ const head = [{ text: ` ${title2.toUpperCase()}`, color: titleColor }];
15591
+ const shown = cells(text5([...head, ...tail])) > w - 4 ? [...cut(head, room), ...cells(text5(tail)) < w - 8 ? tail : [], { text: " ", color: titleColor }] : [...head, ...tail, { text: " ", color: titleColor }];
15592
+ const rest = Math.max(0, w - 3 - cells(text5(shown)));
15593
+ const f = foot ? text5(cut([{ text: foot }], Math.max(0, w - 6))) : "";
15594
+ return /* @__PURE__ */ jsxs2(Box2, { ref, flexDirection: "column", flexGrow, flexShrink, height, width, minHeight, overflow: "hidden", children: [
15595
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
15596
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15597
+ b2.tl,
15598
+ b2.h
15599
+ ] }),
15600
+ w > 0 ? shown.map((s, i) => /* @__PURE__ */ jsx4(Text2, { color: s.color, bold: titleBold, children: s.text }, i)) : null,
15601
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15602
+ b2.h.repeat(rest),
15603
+ b2.tr
15604
+ ] })
15605
+ ] }),
15606
+ /* @__PURE__ */ jsx4(Box2, { flexDirection: "column", borderStyle: double ? "double" : "round", borderTop: false, borderBottom: !f, borderColor: color2, paddingX: 1, flexGrow: 1, overflow: "hidden", children }),
15607
+ f ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
15608
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15609
+ b2.bl,
15610
+ b2.h.repeat(Math.max(1, w - 5 - cells(f))),
15611
+ " "
15612
+ ] }),
15613
+ /* @__PURE__ */ jsx4(Text2, { color: footColor, children: f }),
15614
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15615
+ " ",
15616
+ b2.h,
15617
+ b2.br
15618
+ ] })
15619
+ ] }) : null
15620
+ ] });
15621
+ }
15622
+
15623
+ // src/tui/ui/Field.tsx
15624
+ import { jsx as jsx5 } from "react/jsx-runtime";
15625
+ var fieldRows = (columns, rows) => rows < 24 ? 3 : columns >= 120 && rows >= 32 ? 8 : 6;
15626
+ var inHistory = (b2) => b2 !== null && b2.i < b2.items.length - 1;
15627
+ function Field({ value, onChange, onSubmit, onEscape, onBrowse, history = [], active, placeholder, title: title2, color: color2 = C.coral, titleColor }) {
15628
+ const { t, g } = useTui();
15629
+ const { columns, rows: height } = useScreenSize();
15630
+ const st = useRef6({ f: { text: value, cursor: value.length }, browse: null, pasted: 0 });
15631
+ const told = useRef6(value);
15632
+ if (value !== told.current) {
15633
+ told.current = value;
15634
+ st.current = { f: { text: value, cursor: value.length }, browse: null, pasted: 0 };
15635
+ }
15636
+ const [, render2] = useReducer((n) => n + 1, 0);
15637
+ const top = useRef6(0);
15638
+ const width = Math.max(10, columns - 4);
15639
+ const max = fieldRows(columns, height);
15640
+ const set = (f2, o = {}) => {
15641
+ const cur = st.current, changed = f2.text !== cur.f.text;
15642
+ let browse2 = cur.browse;
15643
+ if (browse2 && changed) {
15644
+ const items = [...browse2.items];
15645
+ items[browse2.i] = f2.text;
15646
+ browse2 = { ...browse2, items };
15647
+ }
15648
+ st.current = { f: f2, browse: browse2, pasted: o.paste ?? 0 };
15649
+ render2();
15650
+ if (changed && !inHistory(browse2)) {
15651
+ told.current = f2.text;
15652
+ onChange(f2.text);
16029
15653
  }
16030
15654
  };
16031
- all2.forEach((turn, i) => {
16032
- const recent = i >= all2.length - OPEN_TURNS;
16033
- const first = turn.prompt?.id ?? turn.items[0]?.id ?? String(i);
16034
- for (const it of turn.items) turnOf.set(it.id, first);
16035
- if (!recent && !o.unfolded) {
16036
- push2(first, [cut(foldedLine(turn, o), o.width)]);
15655
+ const show = (b2, i) => {
15656
+ const next2 = b2 && i !== void 0 ? { ...b2, i } : b2;
15657
+ const text5 = next2 ? next2.items[next2.i] : st.current.f.text;
15658
+ st.current = { f: { text: text5, cursor: text5.length }, browse: next2, pasted: 0 };
15659
+ render2();
15660
+ onBrowse?.(inHistory(next2));
15661
+ };
15662
+ useInput((input, key) => {
15663
+ const { f: f2, browse: browse2 } = st.current;
15664
+ if (key.return && !key.meta && !key.shift) {
15665
+ const fromHistory = inHistory(browse2);
15666
+ if (onSubmit(f2.text, { fromHistory }) === false) return;
15667
+ if (fromHistory) show(browse2, browse2.items.length - 1);
15668
+ if (browse2) show(null);
16037
15669
  return;
16038
15670
  }
16039
- if (lines.length) push2(first, [[{ text: "" }]]);
16040
- for (const it of turn.items) push2(it.id, itemLines(it, opts));
16041
- });
16042
- return { lines, keys, turns: turnOf };
15671
+ if (key.return || input === "\n" || key.ctrl && input === "j") {
15672
+ set(insert(f2, "\n"));
15673
+ return;
15674
+ }
15675
+ if (key.escape) {
15676
+ if (inHistory(browse2)) {
15677
+ show(browse2, browse2.items.length - 1);
15678
+ show(null);
15679
+ return;
15680
+ }
15681
+ if (browse2) show(null);
15682
+ onEscape?.();
15683
+ return;
15684
+ }
15685
+ if (key.backspace) {
15686
+ set(key.meta ? killWord(f2) : back(f2));
15687
+ return;
15688
+ }
15689
+ if (key.delete) {
15690
+ set(del(f2));
15691
+ return;
15692
+ }
15693
+ if (key.ctrl && input === "u") {
15694
+ set({ text: "", cursor: 0 });
15695
+ return;
15696
+ }
15697
+ if (key.ctrl && input === "w") {
15698
+ set(killWord(f2));
15699
+ return;
15700
+ }
15701
+ if (key.ctrl && input === "a" || key.home) {
15702
+ set(home(f2));
15703
+ return;
15704
+ }
15705
+ if (key.ctrl && input === "e" || key.end) {
15706
+ set(end(f2));
15707
+ return;
15708
+ }
15709
+ if (key.leftArrow || key.meta && input === "b") {
15710
+ set(key.ctrl || key.meta ? wordLeft(f2) : left(f2));
15711
+ return;
15712
+ }
15713
+ if (key.rightArrow || key.meta && input === "f") {
15714
+ set(key.ctrl || key.meta ? wordRight(f2) : right(f2));
15715
+ return;
15716
+ }
15717
+ if (key.upArrow) {
15718
+ const up = vertical(f2, width - 2, -1);
15719
+ if (up) {
15720
+ set(up);
15721
+ return;
15722
+ }
15723
+ const b2 = browse2 ?? (history.length ? { items: [...history, f2.text], i: history.length } : null);
15724
+ if (b2 && b2.i > 0) show(b2, b2.i - 1);
15725
+ return;
15726
+ }
15727
+ if (key.downArrow) {
15728
+ const down = vertical(f2, width - 2, 1);
15729
+ if (down) {
15730
+ set(down);
15731
+ return;
15732
+ }
15733
+ if (inHistory(browse2)) show(browse2, browse2.i + 1);
15734
+ return;
15735
+ }
15736
+ if (key.ctrl || key.meta || key.tab || key.pageUp || key.pageDown) return;
15737
+ if (input) {
15738
+ const add = newlines(input);
15739
+ set(insert(f2, add), { paste: add.includes("\n") ? add.split("\n").length : 0 });
15740
+ }
15741
+ }, { isActive: active });
15742
+ usePaste((text5) => {
15743
+ const add = newlines(text5);
15744
+ set(insert(st.current.f, add), { paste: add.includes("\n") ? add.split("\n").length : 0 });
15745
+ }, { isActive: active });
15746
+ const { f, browse, pasted } = st.current;
15747
+ const prefix = { text: `${g.cursor} `, color: C.coral };
15748
+ const lines = fieldLines(f.text, active ? f.cursor : null, width, { prefix, color: C.fg, ...placeholder ? { placeholder: { text: f.text ? "" : ` ${placeholder}`, color: C.faint } } : {} });
15749
+ const row2 = rowOf(layout(f.text, width - 2), f.cursor);
15750
+ top.current = scrollTop(top.current, row2, lines.length, max);
15751
+ const shown = lines.slice(top.current, top.current + max);
15752
+ const below = lines.length - top.current - shown.length;
15753
+ const where = lineAt(f);
15754
+ const foot = inHistory(browse) ? t.field.history(browse.i + 1, browse.items.length - 1) : [
15755
+ top.current > 0 ? t.field.above(top.current) : "",
15756
+ below > 0 ? t.field.below(below) : "",
15757
+ pasted > 1 ? t.field.pasted(pasted) : active && where.lines > 1 ? t.field.line(where.line, where.lines) : ""
15758
+ ].filter(Boolean).join(" \xB7 ");
15759
+ return /* @__PURE__ */ jsx5(TitledBox, { title: title2, color: active ? color2 : C.line, titleColor: titleColor ?? (active ? color2 : C.dim), foot: foot || void 0, flexShrink: 0, children: shown.map((l, i) => /* @__PURE__ */ jsx5(Box3, { flexShrink: 0, children: /* @__PURE__ */ jsx5(LineText, { line: l }) }, i)) });
16043
15760
  }
16044
- function anchorLine(v, anchor) {
16045
- const at = v.keys.indexOf(anchor.key);
16046
- if (at >= 0) return at + anchor.offset;
16047
- const turn = v.turns.get(anchor.key);
16048
- return turn === void 0 ? -1 : v.keys.indexOf(turn);
15761
+
15762
+ // src/tui/ui/Frame.tsx
15763
+ import { Box as Box5, Text as Text4 } from "ink";
15764
+
15765
+ // src/tui/ui/KeyBar.tsx
15766
+ import { Box as Box4, Text as Text3 } from "ink";
15767
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
15768
+ var itemCells = ([k, label]) => cells(` ${k} `) + cells(` ${label}`);
15769
+ function fitKeys(keys, width, help) {
15770
+ const list3 = keys.filter((k) => k[0] !== help?.[0]);
15771
+ const fits = (l) => 1 + l.reduce((n, x, i) => n + itemCells(x) + (i ? 2 : 0), 0) <= width - 1;
15772
+ const tail = help ? [help] : [];
15773
+ while (list3.length && !fits([...list3, ...tail])) list3.pop();
15774
+ return [...list3, ...tail];
16049
15775
  }
16050
- function threadImages(items, unfolded) {
16051
- const all2 = turns(items);
16052
- return (unfolded ? all2 : all2.slice(-OPEN_TURNS)).flatMap((turn) => turn.items.flatMap((it) => it.kind === "prompt" ? it.images ?? [] : []));
15776
+ function KeyBar({ keys }) {
15777
+ return /* @__PURE__ */ jsx6(Box4, { paddingX: 1, columnGap: 2, height: 1, overflow: "hidden", children: keys.map(([k, label]) => /* @__PURE__ */ jsx6(Box4, { flexShrink: 0, children: /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
15778
+ /* @__PURE__ */ jsxs3(Text3, { color: C.fg, backgroundColor: C.key, children: [
15779
+ " ",
15780
+ k,
15781
+ " "
15782
+ ] }),
15783
+ /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
15784
+ " ",
15785
+ label
15786
+ ] })
15787
+ ] }) }, k)) });
15788
+ }
15789
+
15790
+ // src/tui/ui/Frame.tsx
15791
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
15792
+ var MIN = { columns: 60, rows: 16 };
15793
+ function Frame({ crumb, crumbShort, keys, kept, children }) {
15794
+ const { d, machines, t, g, lastToast, editing, client } = useTui();
15795
+ const { rows, columns } = useScreenSize();
15796
+ const live = listed(d.sessions);
15797
+ const pending = live.filter(isPending);
15798
+ const waiting = pending.length + openDecisions(useTasks(client)).length;
15799
+ const status = d.status === "open" ? null : d.status === "connecting" ? t.bar.connecting : t.bar.unreachable;
15800
+ if (columns < MIN.columns || rows < MIN.rows) {
15801
+ const why = [...pending[0] ? [t.small.waits(pending[0].name, pending[0].pending?.kind === "question")] : [], ...kept ? [t.small.kept] : []].join(" \xB7 ");
15802
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", height: rows, width: columns, alignItems: "center", justifyContent: "center", children: [
15803
+ /* @__PURE__ */ jsxs4(Text4, { color: C.coral, bold: true, children: [
15804
+ g.logo,
15805
+ " ledge"
15806
+ ] }),
15807
+ /* @__PURE__ */ jsx7(Text4, { color: C.fg, children: t.small.tooSmall(columns, rows) }),
15808
+ /* @__PURE__ */ jsx7(Text4, { color: C.dim, children: t.small.atLeast(MIN.columns, MIN.rows) }),
15809
+ why ? /* @__PURE__ */ jsx7(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text4, { color: C.red, children: why }) }) : null
15810
+ ] });
15811
+ }
15812
+ const left2 = cells(` ${g.logo} ledge ${t.bar.sessions(live.length)} \xB7 ${t.bar.machines(Math.max(1, machines.length))}`) + (waiting > 0 ? cells(` \xB7 ${t.bar.waiting(waiting)}`) : 0) + (status ? cells(` ${status}`) : 0);
15813
+ const room = columns - left2 - 2;
15814
+ const shown = cells(crumb) <= room ? crumb : crumbShort && cells(crumbShort) <= room ? crumbShort : "";
15815
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", height: rows, width: columns, children: [
15816
+ /* @__PURE__ */ jsxs4(Box5, { paddingX: 1, justifyContent: "space-between", height: 1, backgroundColor: C.bar, children: [
15817
+ /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate", children: [
15818
+ /* @__PURE__ */ jsxs4(Text4, { color: C.coral, children: [
15819
+ g.logo,
15820
+ " ledge"
15821
+ ] }),
15822
+ /* @__PURE__ */ jsxs4(Text4, { color: C.dim, children: [
15823
+ " ",
15824
+ t.bar.sessions(live.length),
15825
+ " \xB7 ",
15826
+ t.bar.machines(Math.max(1, machines.length))
15827
+ ] }),
15828
+ waiting > 0 ? /* @__PURE__ */ jsxs4(Text4, { color: C.red, children: [
15829
+ " \xB7 ",
15830
+ t.bar.waiting(waiting)
15831
+ ] }) : null,
15832
+ status ? /* @__PURE__ */ jsxs4(Text4, { color: C.yellow, children: [
15833
+ " ",
15834
+ status
15835
+ ] }) : null
15836
+ ] }),
15837
+ shown ? /* @__PURE__ */ jsx7(Text4, { wrap: "truncate", color: C.dim, children: shown }) : null
15838
+ ] }),
15839
+ /* @__PURE__ */ jsx7(Box5, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children }),
15840
+ lastToast ? /* @__PURE__ */ jsx7(Box5, { paddingX: 1, height: 1, children: /* @__PURE__ */ jsx7(Text4, { wrap: "truncate", color: lastToast.tone === "error" ? C.red : lastToast.tone === "ok" ? C.green : lastToast.tone === "quiet" ? C.dim : C.yellow, children: lastToast.text }) }) : null,
15841
+ /* @__PURE__ */ jsx7(KeyBar, { keys: fitKeys(keys, columns, editing ? null : ["?", t.keys.help]) })
15842
+ ] });
15843
+ }
15844
+
15845
+ // src/tui/ui/SessionCard.tsx
15846
+ import { Box as Box6, Text as Text5 } from "ink";
15847
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
15848
+ function Button({ label, k, primary, danger, off }) {
15849
+ const bg = off ? void 0 : danger ? C.danger : primary ? C.primary : C.key;
15850
+ return /* @__PURE__ */ jsxs5(Text5, { children: [
15851
+ /* @__PURE__ */ jsxs5(Text5, { color: off ? C.faint : C.fg, backgroundColor: bg, children: [
15852
+ " ",
15853
+ label,
15854
+ " "
15855
+ ] }),
15856
+ /* @__PURE__ */ jsxs5(Text5, { color: C.dim, backgroundColor: bg, children: [
15857
+ k,
15858
+ " "
15859
+ ] })
15860
+ ] });
15861
+ }
15862
+ function SessionCard({ s, note }) {
15863
+ const tui = useTui();
15864
+ const { t, g } = tui;
15865
+ const threshold = useContextNear(tui.client);
15866
+ const state2 = botState(s);
15867
+ const helpers = liveHelpers(s);
15868
+ const p = s.pending;
15869
+ const canAct = !s.offline && s.source !== "hook_only";
15870
+ const verb = !p ? "" : p.kind === "question" ? t.overview.asks : p.kind === "plan" ? t.overview.plan : p.kind === "diff" ? t.overview.diff : t.overview.wantsToRun;
15871
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
15872
+ /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
15873
+ /* @__PURE__ */ jsxs5(Text5, { color: STATE_COLOR[state2], children: [
15874
+ g[state2],
15875
+ " ",
15876
+ t.state[state2]
15877
+ ] }),
15878
+ /* @__PURE__ */ jsxs5(Text5, { color: C.dim, children: [
15879
+ " ",
15880
+ TOOL_NAME[s.tool],
15881
+ " \xB7 ",
15882
+ modelLabel(s, t.session.defaultModel)
15883
+ ] }),
15884
+ s.context ? /* @__PURE__ */ jsxs5(Text5, { color: contextNear(s.context, threshold) ? C.yellow : C.dim, children: [
15885
+ " \xB7 ",
15886
+ t.overview.context(Math.round(s.context.used / s.context.window * 100))
15887
+ ] }) : null,
15888
+ /* @__PURE__ */ jsxs5(Text5, { color: C.dim, children: [
15889
+ " \xB7 ",
15890
+ machineLabel(s),
15891
+ s.branch ? ` \xB7 ${g.branch} ${s.branch}` : ""
15892
+ ] }),
15893
+ s.offline ? /* @__PURE__ */ jsxs5(Text5, { color: C.yellow, children: [
15894
+ " ",
15895
+ t.overview.offline
15896
+ ] }) : s.source === "hook_only" ? /* @__PURE__ */ jsxs5(Text5, { color: C.faint, children: [
15897
+ " ",
15898
+ t.overview.readOnly
15899
+ ] }) : null
15900
+ ] }),
15901
+ s.handover || s.relieves ? /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
15902
+ s.handover ? /* @__PURE__ */ jsxs5(Text5, { color: s.handover.state === "failed" ? C.red : C.yellow, children: [
15903
+ " \u21C4 ",
15904
+ s.handover.state === "done" && s.handover.toName ? t.overview.relievedBy(s.handover.toName) : t.overview.relief[s.handover.state],
15905
+ s.handover.reason ? ` : ${s.handover.reason}` : ""
15906
+ ] }) : null,
15907
+ s.relieves ? /* @__PURE__ */ jsxs5(Text5, { color: C.dim, children: [
15908
+ " \u21C4 ",
15909
+ t.overview.relieves(s.relieves.name)
15910
+ ] }) : null
15911
+ ] }) : null,
15912
+ helpers.length > 0 ? /* @__PURE__ */ jsxs5(Text5, { color: C.violet, wrap: "truncate", children: [
15913
+ " ",
15914
+ helpers.map((h) => `${g.helper} ${h.kind} \xB7 ${oneLine(h.task)}`).join(" ")
15915
+ ] }) : null,
15916
+ p ? /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
15917
+ /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
15918
+ /* @__PURE__ */ jsx8(Text5, { color: STATE_COLOR[state2], children: verb }),
15919
+ " ",
15920
+ /* @__PURE__ */ jsx8(Text5, { color: C.fg, bold: true, children: oneLine(p.text) }),
15921
+ p.tool ? /* @__PURE__ */ jsxs5(Text5, { color: C.faint, children: [
15922
+ " ",
15923
+ p.tool
15924
+ ] }) : null
15925
+ ] }),
15926
+ /* @__PURE__ */ jsx8(Box6, { marginTop: 1, columnGap: 2, flexWrap: "wrap", children: p.kind === "question" ? choices(p).slice(0, 9).map((c, i) => /* @__PURE__ */ jsx8(Button, { label: `${c.n} \xB7 ${c.label}`, k: String(c.n), primary: i === 0, off: !canAct }, c.n)) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
15927
+ /* @__PURE__ */ jsx8(Button, { label: t.overview.allow, k: "y", primary: true, off: !canAct }),
15928
+ /* @__PURE__ */ jsx8(Button, { label: t.overview.deny, k: "n", off: !canAct }),
15929
+ offers(s.tool, p.kind, "always") ? /* @__PURE__ */ jsx8(Button, { label: t.overview.always, k: "A", off: !canAct }) : null,
15930
+ note && offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx8(Button, { label: t.overview.denyWithNote, k: "r", off: !canAct }) : null
15931
+ ] }) })
15932
+ ] }) : s.lastReply && state2 === "idle" ? /* @__PURE__ */ jsxs5(Text5, { color: C.dim, wrap: "truncate", children: [
15933
+ " ",
15934
+ oneLine(s.lastReply.text)
15935
+ ] }) : null
15936
+ ] });
16053
15937
  }
16054
15938
 
16055
- // src/tui/ui/ModelPicker.tsx
16056
- import { useState as useState6 } from "react";
16057
- import { Box as Box7, Text as Text8, useInput } from "ink";
16058
-
16059
- // ../fleet/src/models.ts
16060
- import { useEffect as useEffect5, useState as useState5 } from "react";
16061
- var IDLE = { status: "idle", models: [] };
16062
- var LOADING = { status: "loading", models: [] };
16063
- var known = /* @__PURE__ */ new Map();
16064
- var next = 0;
16065
- function useModels(client, machine, tool) {
16066
- const key = machine === null || tool === null ? null : `${machine}|${tool}`;
16067
- const [answer, setAnswer] = useState5(null);
16068
- useEffect5(() => {
16069
- if (key === null || machine === null || tool === null || known.has(key)) return;
16070
- const requestId = `models-${++next}`;
16071
- const off = client.onMessage((m) => {
16072
- if (m.type !== "models.listing" || m.requestId !== requestId) return;
16073
- const state2 = { status: "ready", models: m.models, ...m.error ? { error: m.error } : {} };
16074
- if (!m.error) known.set(key, state2);
16075
- setAnswer({ key, state: state2 });
16076
- });
16077
- client.send({ type: "models.list", requestId, tool, machineId: machine });
16078
- return off;
16079
- }, [client, key, machine, tool]);
16080
- if (key === null) return IDLE;
16081
- return known.get(key) ?? (answer && answer.key === key ? answer.state : LOADING);
15939
+ // src/tui/ui/Tail.tsx
15940
+ import { useRef as useRef7 } from "react";
15941
+ import { Box as Box7, Text as Text6, useBoxMetrics as useBoxMetrics3 } from "ink";
15942
+ import { jsx as jsx9 } from "react/jsx-runtime";
15943
+ function Tail({ lines, color: color2, empty: empty3, emptyColor }) {
15944
+ const ref = useRef7(null);
15945
+ const { height } = useBoxMetrics3(ref);
15946
+ let end2 = lines.length;
15947
+ while (end2 > 0 && !lines[end2 - 1].trim()) end2--;
15948
+ const shown = end2 === 0 ? [] : lines.slice(0, end2).slice(-Math.max(1, height));
15949
+ return /* @__PURE__ */ jsx9(Box7, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.length === 0 ? /* @__PURE__ */ jsx9(Text6, { color: emptyColor, children: empty3 }) : shown.map((l, i) => /* @__PURE__ */ jsx9(Text6, { color: color2, wrap: "truncate", children: l || " " }, i)) });
16082
15950
  }
16083
15951
 
16084
- // src/tui/ui/ModelPicker.tsx
16085
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
16086
- function ModelPicker({ machine, tool, current: current2, onPick, onCancel }) {
16087
- const { client, t, g } = useTui();
16088
- useEditing(true);
16089
- const models = useModels(client, machine, tool);
16090
- 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 }))];
16091
- const [at, setAt] = useState6(() => Math.max(0, rows.findIndex((r) => r.id === current2.model)));
16092
- const [effort, setEffort] = useState6(current2.effort);
16093
- const row2 = rows[Math.min(at, rows.length - 1)];
16094
- useInput((input, key) => {
16095
- if (key.escape) {
16096
- onCancel();
16097
- return;
15952
+ // src/tui/screens/Overview.tsx
15953
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
15954
+ var WIDE = 100;
15955
+ function windowed(rows, selectedKey, height) {
15956
+ if (rows.length <= height) return [...rows];
15957
+ const at = Math.max(0, rows.findIndex((r) => r.key === selectedKey));
15958
+ const start = Math.min(Math.max(0, at - Math.floor(height / 2)), rows.length - height);
15959
+ return rows.slice(start, start + height);
15960
+ }
15961
+ function Overview() {
15962
+ const tui = useTui();
15963
+ const { d, projects, t, g, selected, select, actions, go, attach } = tui;
15964
+ const { columns, rows: height } = useScreenSize();
15965
+ const wide = columns >= WIDE;
15966
+ const left2 = wide ? Math.min(64, Math.max(44, Math.floor(columns * 0.42))) : columns;
15967
+ const teams = useTeams(tui.client);
15968
+ const tasks = useTasks(tui.client);
15969
+ const view = useMemo2(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
15970
+ const sel = selected?.id ?? null;
15971
+ const closed = closedLedgeTasks(tasks);
15972
+ const decisions = useMemo2(() => openDecisions(tasks), [tasks]);
15973
+ const [deciding, setDeciding] = useState3(null);
15974
+ const decision = deciding ? decisions.find((x) => x.id === deciding.id) : void 0;
15975
+ useEffect6(() => {
15976
+ if (deciding && !decision) setDeciding(null);
15977
+ }, [deciding, decision]);
15978
+ useEditing(!!deciding && !deciding.confirm);
15979
+ const answer = (x, text5) => {
15980
+ const rid = randomUUID();
15981
+ const off = tui.client.onMessage((m) => {
15982
+ if (m.type !== "action.result" || m.rid !== rid) return;
15983
+ off();
15984
+ tui.toast(m.reason === void 0 ? { text: t.toast.reply(x.giverName), tone: "ok" } : { text: t.toast.answerFailed(m.reason), tone: "error" });
15985
+ });
15986
+ setTimeout(off, 1e4);
15987
+ tui.client.send({ type: "action.run", rid, action: "task.answer", args: { task: x.id, answer: text5 }, confirmed: true });
15988
+ };
15989
+ const threshold = useContextNear(tui.client);
15990
+ const near = useMemo2(() => nearContext(d.sessions, threshold), [d.sessions, threshold]);
15991
+ const [tasksShown, setTasksShown] = useState3(true);
15992
+ const [joining, setJoining] = useState3(null);
15993
+ const node2 = selected ? view.tree.find((n) => n.branches.some((b2) => b2.sessions.includes(selected)) || n.teams.some((x) => x.sessions.includes(selected))) : void 0;
15994
+ const seatTeam = selected?.team ? teams?.find((x) => x.id === selected.team && x.state !== "dissolved") : void 0;
15995
+ const choices2 = node2?.teams.map((x) => x.team).filter((x) => x.state !== "dissolved" && x.id !== selected?.team) ?? [];
15996
+ useScreenKeys((input, key) => {
15997
+ if (deciding) {
15998
+ if (!deciding.confirm) {
15999
+ if (key.escape) setDeciding(null);
16000
+ return true;
16001
+ }
16002
+ if (input === "y" && decision) {
16003
+ setDeciding(null);
16004
+ answer(decision, deciding.draft.trim());
16005
+ return true;
16006
+ }
16007
+ if (input === "n" || key.escape) {
16008
+ setDeciding({ ...deciding, confirm: false });
16009
+ return true;
16010
+ }
16011
+ return true;
16098
16012
  }
16099
- if (key.upArrow || input === "k") {
16100
- setAt((i) => (i - 1 + rows.length) % rows.length);
16101
- setEffort(null);
16102
- return;
16013
+ if (input === "D") {
16014
+ if (decisions[0]) setDeciding({ id: decisions[0].id, draft: "", confirm: false });
16015
+ return true;
16103
16016
  }
16104
- if (key.downArrow || input === "j") {
16105
- setAt((i) => (i + 1) % rows.length);
16106
- setEffort(null);
16107
- return;
16017
+ if (joining) {
16018
+ if (key.escape) {
16019
+ setJoining(null);
16020
+ return true;
16021
+ }
16022
+ if (input === "j" || key.downArrow) {
16023
+ setJoining({ index: (joining.index + 1) % Math.max(1, choices2.length) });
16024
+ return true;
16025
+ }
16026
+ if (input === "k" || key.upArrow) {
16027
+ setJoining({ index: (joining.index + choices2.length - 1) % Math.max(1, choices2.length) });
16028
+ return true;
16029
+ }
16030
+ if (key.return && selected && choices2[joining.index]) {
16031
+ const team = choices2[joining.index];
16032
+ const role = selected.role ?? team.seats.find((x) => x.sessionId === null)?.role;
16033
+ setJoining(null);
16034
+ if (!role) {
16035
+ tui.toast({ text: t.overview.noTeamHere, tone: "info" });
16036
+ return true;
16037
+ }
16038
+ tui.client.send({ type: "team.join", team: team.id, session: selected.id, role });
16039
+ tui.toast({ text: t.toast.joined(selected.name, team.name), tone: "ok" });
16040
+ return true;
16041
+ }
16042
+ return true;
16108
16043
  }
16109
- if ((key.leftArrow || key.rightArrow) && row2.efforts.length) {
16110
- const cur = Math.max(0, row2.efforts.indexOf(effort ?? row2.defaultEffort ?? row2.efforts[0]));
16111
- setEffort(row2.efforts[(cur + (key.rightArrow ? 1 : -1) + row2.efforts.length) % row2.efforts.length]);
16112
- return;
16044
+ if (input === "J" && selected) {
16045
+ if (choices2.length) setJoining({ index: 0 });
16046
+ else tui.toast({ text: t.overview.noTeamHere, tone: "info" });
16047
+ return true;
16113
16048
  }
16114
- if (key.return) onPick({ model: row2.id, effort: row2.efforts.length ? effort ?? null : null });
16049
+ if (input === "R") {
16050
+ const resting = (seatTeam?.state === "resting" ? seatTeam : void 0) ?? node2?.teams.find((x) => x.team.state === "resting")?.team ?? view.tree.flatMap((n) => n.teams).find((x) => x.team.state === "resting")?.team;
16051
+ if (resting) {
16052
+ tui.client.send({ type: "team.resume", team: resting.id });
16053
+ tui.toast({ text: t.toast.resumed(resting.name), tone: "ok" });
16054
+ }
16055
+ return true;
16056
+ }
16057
+ if (input === "T") {
16058
+ setTasksShown((v) => !v);
16059
+ return true;
16060
+ }
16061
+ if (input === "H") {
16062
+ 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];
16063
+ if (!s) return true;
16064
+ const rid = randomUUID();
16065
+ const off = tui.client.onMessage((m) => {
16066
+ if (m.type === "session.handover.result" && m.rid === rid) {
16067
+ off();
16068
+ tui.toast(m.ok ? { text: t.toast.handover(s.name), tone: "ok" } : { text: t.toast.handoverRefused(m.reason ?? "?"), tone: "info" });
16069
+ }
16070
+ });
16071
+ setTimeout(off, 5e3);
16072
+ tui.client.send({ type: "session.handover", rid, id: s.id });
16073
+ return true;
16074
+ }
16075
+ if (input === "x" && closed[0]) {
16076
+ tui.client.send({ type: "task.cmd", rid: randomUUID(), by: LEDGE_GIVER, op: "seen", task: closed[0].id });
16077
+ return true;
16078
+ }
16079
+ if (input === "j" || key.downArrow) {
16080
+ select(step(view.order, sel, 1));
16081
+ return true;
16082
+ }
16083
+ if (input === "k" || key.upArrow) {
16084
+ select(step(view.order, sel, -1));
16085
+ return true;
16086
+ }
16087
+ if (key.tab) {
16088
+ const next2 = nextWaiting(view.yours, sel);
16089
+ if (next2) select(next2);
16090
+ return true;
16091
+ }
16092
+ if (key.return) {
16093
+ if (selected) go("session");
16094
+ return true;
16095
+ }
16096
+ if (!selected) return false;
16097
+ if (input === "a") {
16098
+ attach(selected);
16099
+ return true;
16100
+ }
16101
+ if (input === "r" && selected.pending) {
16102
+ go("session");
16103
+ return true;
16104
+ }
16105
+ if (/^[1-9]$/.test(input) && selected.pending?.kind !== "question") return false;
16106
+ if (input === "y" || input === "n" || input === "A" || /^[1-9]$/.test(input)) return actions.key(selected, input);
16107
+ if (input === "i" && selected.state === "working") {
16108
+ actions.interrupt(selected);
16109
+ return true;
16110
+ }
16111
+ return false;
16115
16112
  });
16116
- if (models.status !== "ready") return /* @__PURE__ */ jsx10(Text8, { color: C.faint, children: t.launch.loading });
16117
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
16118
- models.error ? /* @__PURE__ */ jsx10(Text8, { color: C.yellow, children: models.error }) : null,
16119
- rows.map((r, i) => /* @__PURE__ */ jsxs7(Text8, { wrap: "truncate", backgroundColor: i === at ? C.sel : void 0, children: [
16120
- /* @__PURE__ */ jsxs7(Text8, { color: i === at ? C.coral : C.faint, children: [
16121
- i === at ? g.cursor : " ",
16113
+ const tree2 = view.tree.flatMap((n) => {
16114
+ const teamed = n.teams.length > 0;
16115
+ const common = n.branches.length > 0;
16116
+ return [
16117
+ { kind: "project", key: n.key, label: n.label, color: projectColor(n.color), count: n.count },
16118
+ ...teamed && common ? [{ kind: "common", key: `${n.key}|common` }] : [],
16119
+ ...n.branches.flatMap((b2) => [{ kind: "branch", key: `${n.key}|${b2.branch}`, branch: b2.branch, indent: teamed ? 4 : 2 }, ...b2.sessions.map((s) => ({ kind: "session", key: s.id, s, indent: teamed ? 2 : 0 }))]),
16120
+ ...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 }))])
16121
+ ];
16122
+ });
16123
+ const yoursCount = view.yours.length + decisions.length + closed.length + near.length;
16124
+ const yoursHeight = Math.max(1, Math.min(yoursCount, 6));
16125
+ const joinHeight = joining && selected ? Math.max(1, choices2.length) + 2 : 0;
16126
+ const answerLines = deciding?.confirm ? wrap2(deciding.draft.trim(), Math.max(10, columns - 4)) : [];
16127
+ const decideHeight = !deciding ? 0 : deciding.confirm ? Math.min(answerLines.length, Math.max(1, height - 12)) + 2 : fieldRows(columns, height) + 2;
16128
+ const treeHeight = Math.max(3, height - 3 - (yoursHeight + 2) - joinHeight - decideHeight - 3);
16129
+ const inner = left2 - 4, nameW = 7;
16130
+ const stateW = Math.max(...Object.values(t.state).map((x) => x.length)) + 1;
16131
+ const topicW = Math.max(6, inner - 6 - nameW - stateW - 3);
16132
+ const askW = Math.max(6, inner - 2 - nameW - 9 - 7);
16133
+ const yoursBox = /* @__PURE__ */ jsx10(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__ */ jsx10(Text7, { color: C.faint, children: t.overview.nobodyWaits }) : [...view.yours.slice(0, yoursHeight).map((s) => {
16134
+ const st = botState(s);
16135
+ return /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
16136
+ /* @__PURE__ */ jsxs6(Text7, { color: STATE_COLOR[st], children: [
16137
+ g[st],
16122
16138
  " "
16123
16139
  ] }),
16124
- /* @__PURE__ */ jsx10(Text8, { color: C.fg, children: r.label }),
16125
- i === at && r.efforts.length ? /* @__PURE__ */ jsxs7(Text8, { color: C.dim, children: [
16126
- " ",
16127
- r.efforts.map((e) => e === (effort ?? r.defaultEffort) ? `${g.prev} ${e} ${g.next}` : e).join(" ")
16140
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, bold: true, children: fit(s.name, nameW) }),
16141
+ /* @__PURE__ */ jsx10(Text7, { color: C.dim, children: fit(s.repo ?? s.project, 9) }),
16142
+ /* @__PURE__ */ jsx10(Text7, { color: STATE_COLOR[st], children: fit(oneLine(s.pending?.text ?? ""), askW) }),
16143
+ /* @__PURE__ */ jsxs6(Text7, { color: C.faint, children: [
16144
+ " ",
16145
+ /* @__PURE__ */ jsx10(Ago, { iso: s.lastActivityAt })
16146
+ ] })
16147
+ ] }, s.id);
16148
+ }), ...decisions.slice(0, Math.max(0, yoursHeight - view.yours.length)).map((x) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16149
+ /* @__PURE__ */ jsxs6(Text7, { color: C.red, children: [
16150
+ g.question,
16151
+ " "
16152
+ ] }),
16153
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: t.overview.decision(x.giverName, x.title) }),
16154
+ x.text ? /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
16155
+ " \xB7 ",
16156
+ oneLine(x.text)
16157
+ ] }) : null
16158
+ ] }, `decision:${x.id}`)), ...closed.slice(0, Math.max(0, yoursHeight - view.yours.length - decisions.length)).map((x) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16159
+ /* @__PURE__ */ jsxs6(Text7, { color: x.state === "done" ? C.green : C.yellow, children: [
16160
+ x.state === "done" ? g.ok : g.fail,
16161
+ " "
16162
+ ] }),
16163
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: t.overview.ledgeTask(x.ownerName, x.title, x.state) })
16164
+ ] }, x.id)), ...near.slice(0, Math.max(0, yoursHeight - view.yours.length - decisions.length - closed.length)).map((s) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
16165
+ /* @__PURE__ */ jsxs6(Text7, { color: C.yellow, children: [
16166
+ s.handover?.state === "failed" ? g.fail : "\u25D4",
16167
+ " "
16168
+ ] }),
16169
+ /* @__PURE__ */ jsx10(Text7, { 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)) })
16170
+ ] }, `near:${s.id}`))] });
16171
+ const treeBox = /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.projects, flexGrow: 1, children: tree2.length === 0 ? /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.overview.noSession }) : windowed(tree2, sel, treeHeight).map((r) => r.kind === "project" ? /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16172
+ /* @__PURE__ */ jsx10(Text7, { color: r.color, children: g.project }),
16173
+ " ",
16174
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, bold: true, children: r.label }),
16175
+ " ",
16176
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.overview.sessions(r.count) })
16177
+ ] }, r.key) : r.kind === "common" ? /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", color: C.faint, children: [
16178
+ " ",
16179
+ g.common,
16180
+ " ",
16181
+ t.overview.common
16182
+ ] }, r.key) : r.kind === "team" ? /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16183
+ /* @__PURE__ */ jsx10(Text7, { children: " " }),
16184
+ /* @__PURE__ */ jsx10(Text7, { color: C.coral, children: g.team }),
16185
+ " ",
16186
+ /* @__PURE__ */ jsx10(Text7, { color: r.team.state === "resting" ? C.dim : C.fg, bold: true, children: r.team.name }),
16187
+ " ",
16188
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: r.team.state === "resting" ? t.overview.resting : t.overview.teamLine(r.done, r.given) })
16189
+ ] }, r.key) : r.kind === "branch" ? /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", color: C.faint, children: [
16190
+ " ".repeat(r.indent),
16191
+ g.branch,
16192
+ " ",
16193
+ r.branch
16194
+ ] }, r.key) : /* @__PURE__ */ jsx10(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)) });
16195
+ const keys = deciding?.confirm ? [["y", t.keys.sendAnswer], ["n esc", t.keys.edit]] : deciding ? [["\u21B5", t.keys.review], ["alt+\u21B5", t.keys.newLine], ["esc", t.keys.cancel]] : [
16196
+ ["j k", t.keys.navigate],
16197
+ ["\u21B5", t.keys.open],
16198
+ ...view.yours.length ? [["tab", t.keys.next], ["y n", t.keys.answer]] : [],
16199
+ ["a", t.keys.attach],
16200
+ ...seatTeam ? [["T", t.keys.tasks]] : [],
16201
+ ...choices2.length ? [["J", t.keys.join]] : [],
16202
+ ...view.tree.some((n) => n.teams.some((x) => x.team.state === "resting")) ? [["R", t.keys.resume]] : [],
16203
+ ...decisions.length ? [["D", t.keys.decide]] : [],
16204
+ ...near.length ? [["H", t.keys.handover]] : [],
16205
+ ["/", t.keys.ask],
16206
+ ["+", t.keys.launch],
16207
+ [",", t.keys.settings],
16208
+ ["?", t.keys.help],
16209
+ ["q", t.keys.quit]
16210
+ ];
16211
+ 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) : [];
16212
+ return /* @__PURE__ */ jsxs6(Frame, { crumb: tui.machines.map((m) => `${m.label} ${m.ready ? g.machine : "\u25CB"}`).join(" "), keys, children: [
16213
+ /* @__PURE__ */ jsxs6(Box8, { flexGrow: 1, children: [
16214
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", width: left2, flexShrink: 0, children: [
16215
+ yoursBox,
16216
+ joining && selected ? /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.joinWhich(selected.name), color: C.coral, titleColor: C.coral, height: joinHeight, children: choices2.map((x, i) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: i === joining.index ? C.sel : void 0, children: [
16217
+ /* @__PURE__ */ jsx10(Text7, { color: C.coral, children: g.team }),
16218
+ " ",
16219
+ x.name
16220
+ ] }, x.id)) }) : null,
16221
+ treeBox
16222
+ ] }),
16223
+ wide ? /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", flexGrow: 1, children: [
16224
+ selected ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
16225
+ /* @__PURE__ */ jsx10(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__ */ jsx10(SessionCard, { s: selected, note: true }) }),
16226
+ seatTeam && tasksShown ? /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.tasks(`${g.team} ${seatTeam.name}`), height: Math.max(3, teamTasks.length + 2), children: teamTasks.length === 0 ? /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.overview.noTask }) : teamTasks.map((x) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16227
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: x.id }),
16228
+ " ",
16229
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: x.title }),
16230
+ " ",
16231
+ /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
16232
+ x.giverName,
16233
+ " ",
16234
+ g.arrow,
16235
+ " ",
16236
+ x.ownerName
16237
+ ] }),
16238
+ " ",
16239
+ /* @__PURE__ */ jsx10(Text7, { color: x.state === "done" ? C.green : x.state === "taken" ? C.blue : C.faint, children: t.ask.taskState[x.state] })
16240
+ ] }, x.id)) }) : null,
16241
+ /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.terminal(paneName(selected)), flexGrow: 1, children: /* @__PURE__ */ jsx10(Tail, { lines: d.previews.get(selected.id) ?? [], color: C.dim, empty: t.overview.noPreview, emptyColor: C.faint }) })
16242
+ ] }) : /* @__PURE__ */ jsx10(TitledBox, { title: t.tabs.session, flexGrow: 1, children: /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.session.noSession }) }),
16243
+ /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.exchanges, height: 5, children: (() => {
16244
+ const list3 = exchanges(d.sessions, 3, tasks, t.overview.ledge);
16245
+ return list3.length === 0 ? /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.overview.noExchange }) : list3.map((x, i) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16246
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: clock(x.at) }),
16247
+ " ",
16248
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: x.from }),
16249
+ " ",
16250
+ /* @__PURE__ */ jsx10(Text7, { color: x.kind === "task" ? C.green : C.violet, children: g.arrow }),
16251
+ " ",
16252
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: x.to }),
16253
+ " ",
16254
+ x.kind === "task" ? /* @__PURE__ */ jsxs6(Text7, { color: C.green, children: [
16255
+ g.task,
16256
+ " "
16257
+ ] }) : null,
16258
+ /* @__PURE__ */ jsx10(Text7, { color: C.dim, children: oneLine(x.summary) })
16259
+ ] }, i));
16260
+ })() })
16128
16261
  ] }) : null
16129
- ] }, r.id ?? "-"))
16262
+ ] }),
16263
+ deciding && decision ? deciding.confirm ? /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.confirmAnswer(decision.giverName), color: C.red, titleColor: C.red, height: decideHeight, flexShrink: 0, children: answerLines.slice(0, decideHeight - 2).map((l, i) => /* @__PURE__ */ jsx10(Text7, { color: C.fg, wrap: "truncate", children: l || " " }, i)) }) : /* @__PURE__ */ jsx10(
16264
+ Field,
16265
+ {
16266
+ title: t.overview.decisionTitle(decision.giverName, decision.title),
16267
+ value: deciding.draft,
16268
+ onChange: (v) => setDeciding((x) => x ? { ...x, draft: v } : x),
16269
+ onSubmit: (v) => {
16270
+ if (!v.trim()) return false;
16271
+ setDeciding((x) => x ? { ...x, draft: v, confirm: true } : x);
16272
+ },
16273
+ onEscape: () => setDeciding(null),
16274
+ active: true,
16275
+ placeholder: t.overview.decisionHint
16276
+ }
16277
+ ) : null
16130
16278
  ] });
16131
16279
  }
16132
-
16133
- // src/tui/ui/Lines.tsx
16134
- import { memo, useEffect as useEffect6, useRef as useRef7 } from "react";
16135
- import { Box as Box8, Text as Text9, useBoxMetrics as useBoxMetrics3 } from "ink";
16136
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
16137
- function Live({ seg }) {
16138
- const now2 = useNow();
16139
- const { t } = useI18n();
16140
- const l = seg.live;
16141
- return /* @__PURE__ */ jsx11(Text9, { color: seg.color, bold: seg.bold, children: "spin" in l ? spin(now2) : took(Math.max(0, now2 - l.since), t.time.decimal).padStart(l.pad) });
16142
- }
16143
- var linked = (text5, url) => `\x1B]8;;${url.replace(/[\x00-\x1f\x7f]/g, "")}\x07${text5}\x1B]8;;\x07`;
16144
- var LineText = memo(function LineText2({ line }) {
16145
- return /* @__PURE__ */ jsxs8(Text9, { wrap: "truncate", children: [
16146
- line.map((x, k) => x.live ? /* @__PURE__ */ jsx11(Live, { seg: x }, k) : /* @__PURE__ */ jsx11(Text9, { color: x.color, backgroundColor: x.bg, bold: x.bold, italic: x.italic, underline: x.underline, strikethrough: x.strike, inverse: x.inverse, children: x.link && x.text ? linked(x.text, x.link) : x.text || (k === 0 ? " " : "") }, k)),
16147
- line.length === 0 ? " " : null
16280
+ function SessionRow({ s, on, nameW, topicW, stateW, indent: indent2 = 0 }) {
16281
+ const { t, g } = useTui();
16282
+ const st = botState(s);
16283
+ const n = helperCount(s);
16284
+ return /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: on ? C.sel : void 0, children: [
16285
+ /* @__PURE__ */ jsx10(Text7, { color: on ? C.coral : C.faint, children: on ? "\u258C" : " " }),
16286
+ /* @__PURE__ */ jsx10(Text7, { children: " ".repeat(2 + indent2) }),
16287
+ /* @__PURE__ */ jsxs6(Text7, { color: s.offline ? C.faint : STATE_COLOR[st], children: [
16288
+ g[st],
16289
+ " "
16290
+ ] }),
16291
+ /* @__PURE__ */ jsx10(Text7, { color: s.offline ? C.dim : C.fg, bold: true, children: fit(s.name, nameW) }),
16292
+ s.role ? /* @__PURE__ */ jsxs6(Text7, { color: C.violet, children: [
16293
+ "@",
16294
+ s.role,
16295
+ " "
16296
+ ] }) : null,
16297
+ /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
16298
+ fit(oneLine(s.topic ?? s.title), topicW),
16299
+ " "
16300
+ ] }),
16301
+ /* @__PURE__ */ jsx10(Text7, { color: s.offline ? C.faint : STATE_COLOR[st], children: fit(s.offline ? t.overview.offline : t.state[st], stateW).trimEnd() }),
16302
+ n > 0 ? /* @__PURE__ */ jsxs6(Text7, { color: C.violet, children: [
16303
+ " +",
16304
+ n
16305
+ ] }) : null,
16306
+ s.machineName && s.machineId !== "local" ? /* @__PURE__ */ jsxs6(Text7, { color: C.faint, children: [
16307
+ " \xB7 ",
16308
+ machineLabel(s)
16309
+ ] }) : null
16148
16310
  ] });
16149
- });
16150
- function Lines({ lines, up = 0, onHeight }) {
16151
- const ref = useRef7(null);
16152
- const { height } = useBoxMetrics3(ref);
16153
- useEffect6(() => {
16154
- if (height > 0) onHeight?.(height);
16155
- }, [height, onHeight]);
16156
- const h = Math.max(1, height);
16157
- const back2 = Math.min(up, Math.max(0, lines.length - h));
16158
- const shown = lines.slice(Math.max(0, lines.length - h - back2), lines.length - back2);
16159
- return /* @__PURE__ */ jsx11(Box8, { ref, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: shown.map((l, i) => /* @__PURE__ */ jsx11(Box8, { flexShrink: 0, children: /* @__PURE__ */ jsx11(LineText, { line: l }) }, i)) });
16160
16311
  }
16161
16312
 
16162
- // src/tui/ui/Field.tsx
16163
- import { useReducer, useRef as useRef8 } from "react";
16164
- import { Box as Box9, usePaste as usePaste2, useInput as useInput2 } from "ink";
16313
+ // src/tui/screens/Session.tsx
16314
+ import { useEffect as useEffect8, useMemo as useMemo3, useState as useState7 } from "react";
16315
+ import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
16165
16316
 
16166
- // src/tui/model/field.ts
16167
- var graphemes2 = new Intl.Segmenter(void 0, { granularity: "grapheme" });
16168
- var stops = (text5) => [...Array.from(graphemes2.segment(text5), (g) => g.index), text5.length];
16169
- var newlines = (s) => s.replace(/\r\n?/g, "\n");
16170
- var insert = (f, s) => {
16171
- const add = newlines(s);
16172
- return { text: f.text.slice(0, f.cursor) + add + f.text.slice(f.cursor), cursor: f.cursor + add.length };
16173
- };
16174
- var before = (f) => {
16175
- const a = stops(f.text);
16176
- let i = a.length - 1;
16177
- while (i > 0 && a[i] >= f.cursor) i--;
16178
- return a[i] < f.cursor ? a[i] : 0;
16179
- };
16180
- var after = (f) => stops(f.text).find((x) => x > f.cursor) ?? f.text.length;
16181
- var left = (f) => ({ ...f, cursor: before(f) });
16182
- var right = (f) => ({ ...f, cursor: after(f) });
16183
- var back = (f) => {
16184
- const to = before(f);
16185
- return { text: f.text.slice(0, to) + f.text.slice(f.cursor), cursor: to };
16186
- };
16187
- var del = (f) => ({ text: f.text.slice(0, f.cursor) + f.text.slice(after(f)), cursor: f.cursor });
16188
- function wordStart(f) {
16189
- let i = f.cursor;
16190
- while (i > 0 && /\s/.test(f.text[i - 1])) i--;
16191
- while (i > 0 && !/\s/.test(f.text[i - 1])) i--;
16192
- return i;
16193
- }
16194
- var wordLeft = (f) => ({ ...f, cursor: wordStart(f) });
16195
- function wordRight(f) {
16196
- let i = f.cursor;
16197
- while (i < f.text.length && /\s/.test(f.text[i])) i++;
16198
- while (i < f.text.length && !/\s/.test(f.text[i])) i++;
16199
- return { ...f, cursor: i };
16200
- }
16201
- var killWord = (f) => {
16202
- const to = wordStart(f);
16203
- return { text: f.text.slice(0, to) + f.text.slice(f.cursor), cursor: to };
16204
- };
16205
- var home = (f) => ({ ...f, cursor: f.text.lastIndexOf("\n", f.cursor - 1) + 1 });
16206
- var end = (f) => {
16207
- const n = f.text.indexOf("\n", f.cursor);
16208
- return { ...f, cursor: n < 0 ? f.text.length : n };
16209
- };
16210
- function layout(text5, width) {
16211
- const w = Math.max(2, width - 1);
16212
- const a = stops(text5), rows = [];
16213
- let start = 0;
16214
- for (let i = 0; i < a.length; i++) {
16215
- const at = a[i];
16216
- if (at < text5.length && text5[at] !== "\n") continue;
16217
- if (start === i) rows.push({ a: at, b: at, last: true });
16218
- for (let s = start; s < i; ) {
16219
- let n = 0, j = s, space = -1;
16220
- while (j < i) {
16221
- const c = cells(text5.slice(a[j], a[j + 1]));
16222
- if (n + c > w) break;
16223
- if (text5[a[j]] === " ") space = j;
16224
- n += c;
16225
- j++;
16317
+ // src/tui/images.tsx
16318
+ import { mkdir, writeFile } from "fs/promises";
16319
+ import { tmpdir as tmpdir2 } from "os";
16320
+ import { join as join3 } from "path";
16321
+ import { useRef as useRef8, useState as useState4 } from "react";
16322
+ import { Text as Text8 } from "ink";
16323
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
16324
+ var counter2 = 0;
16325
+ var INLINE_BUDGET = MAX_FRAME - 64 * 1024;
16326
+ function useChips(put) {
16327
+ const { t, toast } = useTui();
16328
+ const [items, setItems] = useState4([]);
16329
+ const held = useRef8([]);
16330
+ const commit = (next2) => {
16331
+ held.current = next2;
16332
+ setItems(next2);
16333
+ };
16334
+ const patch = (key, over) => commit(held.current.map((c) => c.key === key ? { ...c, ...over } : c));
16335
+ const reasonOf = (e) => {
16336
+ const reason = e instanceof AttachmentFailure ? e.reason : null;
16337
+ return reason === "too_large" ? t.images.tooLarge : reason === "not_image" ? t.images.notImage : t.images.failed;
16338
+ };
16339
+ return {
16340
+ items,
16341
+ add({ image: image2, width, height, bytes }) {
16342
+ if (held.current.length >= ATTACHMENTS_MAX) {
16343
+ toast({ text: t.images.max, tone: "info" });
16344
+ return;
16226
16345
  }
16227
- if (j === s) j = s + 1;
16228
- const e = j < i && space > s ? space + 1 : j;
16229
- rows.push({ a: a[s], b: a[e], last: e >= i });
16230
- s = e;
16346
+ const key = counter2++;
16347
+ const inline2 = held.current.reduce((n, c) => n + (c.image?.data.length ?? 0), 0);
16348
+ if (bytes > ATTACHMENT_LOCAL_MAX || !put && inline2 + image2.data.length > INLINE_BUDGET) {
16349
+ commit([...held.current, { key, width, height, state: "refused", reason: t.images.tooLarge }]);
16350
+ return;
16351
+ }
16352
+ if (!put) {
16353
+ commit([...held.current, { key, width, height, state: "ready", image: image2 }]);
16354
+ return;
16355
+ }
16356
+ commit([...held.current, { key, width, height, state: "uploading" }]);
16357
+ put(image2).then((ref) => patch(key, { state: "ready", id: ref.id }), (e) => patch(key, { state: "refused", reason: reasonOf(e) }));
16358
+ },
16359
+ removeLast() {
16360
+ commit(held.current.slice(0, -1));
16361
+ },
16362
+ ready: items.every((c) => c.state === "ready"),
16363
+ ids: () => held.current.flatMap((c) => c.state === "ready" && c.id ? [c.id] : []),
16364
+ uploads: () => held.current.flatMap((c) => c.state === "ready" && c.image ? [c.image] : []),
16365
+ clear() {
16366
+ const taken = held.current;
16367
+ commit([]);
16368
+ return taken;
16369
+ },
16370
+ putBack(taken) {
16371
+ commit([...taken, ...held.current].slice(0, ATTACHMENTS_MAX));
16231
16372
  }
16232
- start = i + 1;
16233
- }
16234
- return rows;
16373
+ };
16235
16374
  }
16236
- var rowOf = (rows, cursor) => {
16237
- const k = rows.findIndex((r) => cursor >= r.a && (cursor < r.b || cursor === r.b && r.last));
16238
- return k < 0 ? rows.length - 1 : k;
16239
- };
16240
- function vertical(f, width, by) {
16241
- const rows = layout(f.text, width), k = rowOf(rows, f.cursor), to = rows[k + by];
16242
- if (!to) return null;
16243
- const col = cells(f.text.slice(rows[k].a, f.cursor));
16244
- let cursor = to.a;
16245
- for (const x of stops(f.text)) {
16246
- if (x <= to.a) continue;
16247
- if (x > to.b || x === to.b && !to.last || cells(f.text.slice(to.a, x)) > col) break;
16248
- cursor = x;
16249
- }
16250
- return { ...f, cursor };
16375
+ function ChipsLine({ chips }) {
16376
+ const { t } = useTui();
16377
+ if (chips.length === 0) return null;
16378
+ return /* @__PURE__ */ jsx11(Text8, { wrap: "truncate", children: chips.map((c, i) => /* @__PURE__ */ jsxs7(Text8, { children: [
16379
+ i ? " " : "",
16380
+ /* @__PURE__ */ jsxs7(Text8, { color: c.state === "refused" ? C.red : C.dim, children: [
16381
+ "\u25A3 ",
16382
+ t.images.capture,
16383
+ " ",
16384
+ c.width,
16385
+ "\xD7",
16386
+ c.height,
16387
+ " ",
16388
+ c.state === "uploading" ? "\u2026" : "\u2715"
16389
+ ] }),
16390
+ c.reason ? /* @__PURE__ */ jsxs7(Text8, { color: C.red, children: [
16391
+ " ",
16392
+ c.reason
16393
+ ] }) : null
16394
+ ] }, c.key)) });
16395
+ }
16396
+ var EXT = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
16397
+ function usePaste2(chips) {
16398
+ const { readImage, toast, t } = useTui();
16399
+ const reading = useRef8(false);
16400
+ return () => {
16401
+ if (reading.current) return;
16402
+ reading.current = true;
16403
+ void readImage().then((got) => {
16404
+ if (got) chips.add(got);
16405
+ else toast({ text: t.images.none, tone: "info" });
16406
+ }, () => toast({ text: t.images.none, tone: "info" })).finally(() => {
16407
+ reading.current = false;
16408
+ });
16409
+ };
16251
16410
  }
16252
- function fieldLines(text5, cursor, width, o) {
16253
- const indent2 = { text: " ".repeat(cells(o.prefix.text)) };
16254
- const inv = (t) => ({ text: t, color: o.color, inverse: true });
16255
- if (!text5) return [[o.prefix, ...cursor !== null ? [inv(" ")] : [], ...o.placeholder ? [o.placeholder] : []]];
16256
- const rows = layout(text5, width - cells(o.prefix.text));
16257
- return rows.map((r, k) => {
16258
- const line = [k === 0 ? o.prefix : indent2];
16259
- const at = cursor !== null && cursor >= r.a && cursor < r.b ? cursor : -1;
16260
- if (at < 0) line.push({ text: text5.slice(r.a, r.b), color: o.color });
16261
- else {
16262
- const next2 = after({ text: text5, cursor: at });
16263
- if (at > r.a) line.push({ text: text5.slice(r.a, at), color: o.color });
16264
- line.push(inv(text5.slice(at, Math.min(next2, r.b))));
16265
- if (next2 < r.b) line.push({ text: text5.slice(next2, r.b), color: o.color });
16411
+ function useOpenImage() {
16412
+ const { client, openFile, toast, t } = useTui();
16413
+ const presses = useRef8(0);
16414
+ const last = useRef8("");
16415
+ return (items) => {
16416
+ const signature = items.map((x) => x.ref.id).join(",");
16417
+ if (signature !== last.current) {
16418
+ last.current = signature;
16419
+ presses.current = 0;
16266
16420
  }
16267
- if (cursor === r.b && r.last) line.push(inv(" "));
16268
- return line.filter((s) => s.text !== "" || s === line[0]);
16269
- });
16421
+ const item = items[items.length - 1 - presses.current++ % items.length];
16422
+ if (!item) return;
16423
+ const { ref, route } = item;
16424
+ void (async () => {
16425
+ const { mime, data } = await getImage(client, ref.id, route);
16426
+ const dir = join3(tmpdir2(), "ledge-images");
16427
+ await mkdir(dir, { recursive: true, mode: 448 });
16428
+ const file = join3(dir, `${ref.id}.${EXT[mime]}`);
16429
+ await writeFile(file, Buffer.from(data, "base64"), { mode: 384 });
16430
+ await openFile(file);
16431
+ })().catch((e) => toast({ text: t.images.openFailed(e instanceof AttachmentFailure ? e.reason : e.message), tone: "error" }));
16432
+ };
16270
16433
  }
16271
- var scrollTop = (top, row2, total, max) => Math.max(0, Math.min(Math.max(0, total - max), row2 < top ? row2 : row2 >= top + max ? row2 - max + 1 : top));
16272
- var lineAt = (f) => ({
16273
- line: f.text.slice(0, f.cursor).split("\n").length,
16274
- lines: f.text.split("\n").length
16275
- });
16276
16434
 
16277
- // src/tui/ui/Field.tsx
16278
- import { jsx as jsx12 } from "react/jsx-runtime";
16279
- var fieldRows = (columns, rows) => rows < 24 ? 3 : columns >= 120 && rows >= 32 ? 8 : 6;
16280
- var inHistory = (b2) => b2 !== null && b2.i < b2.items.length - 1;
16281
- function Field({ value, onChange, onSubmit, onEscape, onBrowse, history = [], active, placeholder, title: title2, color: color2 = C.coral, titleColor }) {
16282
- const { t, g } = useTui();
16283
- const { columns, rows: height } = useScreenSize();
16284
- const st = useRef8({ f: { text: value, cursor: value.length }, browse: null, pasted: 0 });
16285
- const told = useRef8(value);
16286
- if (value !== told.current) {
16287
- told.current = value;
16288
- st.current = { f: { text: value, cursor: value.length }, browse: null, pasted: 0 };
16289
- }
16290
- const [, render2] = useReducer((n) => n + 1, 0);
16291
- const top = useRef8(0);
16292
- const width = Math.max(10, columns - 4);
16293
- const max = fieldRows(columns, height);
16294
- const set = (f2, o = {}) => {
16295
- const cur = st.current, changed = f2.text !== cur.f.text;
16296
- let browse2 = cur.browse;
16297
- if (browse2 && changed) {
16298
- const items = [...browse2.items];
16299
- items[browse2.i] = f2.text;
16300
- browse2 = { ...browse2, items };
16301
- }
16302
- st.current = { f: f2, browse: browse2, pasted: o.paste ?? 0 };
16303
- render2();
16304
- if (changed && !inHistory(browse2)) {
16305
- told.current = f2.text;
16306
- onChange(f2.text);
16307
- }
16308
- };
16309
- const show = (b2, i) => {
16310
- const next2 = b2 && i !== void 0 ? { ...b2, i } : b2;
16311
- const text5 = next2 ? next2.items[next2.i] : st.current.f.text;
16312
- st.current = { f: { text: text5, cursor: text5.length }, browse: next2, pasted: 0 };
16313
- render2();
16314
- onBrowse?.(inHistory(next2));
16315
- };
16435
+ // src/tui/ui/ModelPicker.tsx
16436
+ import { useState as useState6 } from "react";
16437
+ import { Box as Box9, Text as Text9, useInput as useInput2 } from "ink";
16438
+
16439
+ // ../fleet/src/models.ts
16440
+ import { useEffect as useEffect7, useState as useState5 } from "react";
16441
+ var IDLE = { status: "idle", models: [] };
16442
+ var LOADING = { status: "loading", models: [] };
16443
+ var known = /* @__PURE__ */ new Map();
16444
+ var next = 0;
16445
+ function useModels(client, machine, tool) {
16446
+ const key = machine === null || tool === null ? null : `${machine}|${tool}`;
16447
+ const [answer, setAnswer] = useState5(null);
16448
+ useEffect7(() => {
16449
+ if (key === null || machine === null || tool === null || known.has(key)) return;
16450
+ const requestId = `models-${++next}`;
16451
+ const off = client.onMessage((m) => {
16452
+ if (m.type !== "models.listing" || m.requestId !== requestId) return;
16453
+ const state2 = { status: "ready", models: m.models, ...m.error ? { error: m.error } : {} };
16454
+ if (!m.error) known.set(key, state2);
16455
+ setAnswer({ key, state: state2 });
16456
+ });
16457
+ client.send({ type: "models.list", requestId, tool, machineId: machine });
16458
+ return off;
16459
+ }, [client, key, machine, tool]);
16460
+ if (key === null) return IDLE;
16461
+ return known.get(key) ?? (answer && answer.key === key ? answer.state : LOADING);
16462
+ }
16463
+
16464
+ // src/tui/ui/ModelPicker.tsx
16465
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
16466
+ function ModelPicker({ machine, tool, current: current2, onPick, onCancel }) {
16467
+ const { client, t, g } = useTui();
16468
+ useEditing(true);
16469
+ const models = useModels(client, machine, tool);
16470
+ 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 }))];
16471
+ const [at, setAt] = useState6(() => Math.max(0, rows.findIndex((r) => r.id === current2.model)));
16472
+ const [effort, setEffort] = useState6(current2.effort);
16473
+ const row2 = rows[Math.min(at, rows.length - 1)];
16316
16474
  useInput2((input, key) => {
16317
- const { f: f2, browse: browse2 } = st.current;
16318
- if (key.return && !key.meta && !key.shift) {
16319
- const fromHistory = inHistory(browse2);
16320
- if (onSubmit(f2.text, { fromHistory }) === false) return;
16321
- if (fromHistory) show(browse2, browse2.items.length - 1);
16322
- if (browse2) show(null);
16323
- return;
16324
- }
16325
- if (key.return || input === "\n" || key.ctrl && input === "j") {
16326
- set(insert(f2, "\n"));
16327
- return;
16328
- }
16329
16475
  if (key.escape) {
16330
- if (inHistory(browse2)) {
16331
- show(browse2, browse2.items.length - 1);
16332
- show(null);
16333
- return;
16334
- }
16335
- if (browse2) show(null);
16336
- onEscape?.();
16337
- return;
16338
- }
16339
- if (key.backspace) {
16340
- set(key.meta ? killWord(f2) : back(f2));
16341
- return;
16342
- }
16343
- if (key.delete) {
16344
- set(del(f2));
16345
- return;
16346
- }
16347
- if (key.ctrl && input === "u") {
16348
- set({ text: "", cursor: 0 });
16349
- return;
16350
- }
16351
- if (key.ctrl && input === "w") {
16352
- set(killWord(f2));
16353
- return;
16354
- }
16355
- if (key.ctrl && input === "a" || key.home) {
16356
- set(home(f2));
16357
- return;
16358
- }
16359
- if (key.ctrl && input === "e" || key.end) {
16360
- set(end(f2));
16361
- return;
16362
- }
16363
- if (key.leftArrow || key.meta && input === "b") {
16364
- set(key.ctrl || key.meta ? wordLeft(f2) : left(f2));
16476
+ onCancel();
16365
16477
  return;
16366
16478
  }
16367
- if (key.rightArrow || key.meta && input === "f") {
16368
- set(key.ctrl || key.meta ? wordRight(f2) : right(f2));
16479
+ if (key.upArrow || input === "k") {
16480
+ setAt((i) => (i - 1 + rows.length) % rows.length);
16481
+ setEffort(null);
16369
16482
  return;
16370
16483
  }
16371
- if (key.upArrow) {
16372
- const up = vertical(f2, width - 2, -1);
16373
- if (up) {
16374
- set(up);
16375
- return;
16376
- }
16377
- const b2 = browse2 ?? (history.length ? { items: [...history, f2.text], i: history.length } : null);
16378
- if (b2 && b2.i > 0) show(b2, b2.i - 1);
16484
+ if (key.downArrow || input === "j") {
16485
+ setAt((i) => (i + 1) % rows.length);
16486
+ setEffort(null);
16379
16487
  return;
16380
16488
  }
16381
- if (key.downArrow) {
16382
- const down = vertical(f2, width - 2, 1);
16383
- if (down) {
16384
- set(down);
16385
- return;
16386
- }
16387
- if (inHistory(browse2)) show(browse2, browse2.i + 1);
16489
+ if ((key.leftArrow || key.rightArrow) && row2.efforts.length) {
16490
+ const cur = Math.max(0, row2.efforts.indexOf(effort ?? row2.defaultEffort ?? row2.efforts[0]));
16491
+ setEffort(row2.efforts[(cur + (key.rightArrow ? 1 : -1) + row2.efforts.length) % row2.efforts.length]);
16388
16492
  return;
16389
16493
  }
16390
- if (key.ctrl || key.meta || key.tab || key.pageUp || key.pageDown) return;
16391
- if (input) {
16392
- const add = newlines(input);
16393
- set(insert(f2, add), { paste: add.includes("\n") ? add.split("\n").length : 0 });
16394
- }
16395
- }, { isActive: active });
16396
- usePaste2((text5) => {
16397
- const add = newlines(text5);
16398
- set(insert(st.current.f, add), { paste: add.includes("\n") ? add.split("\n").length : 0 });
16399
- }, { isActive: active });
16400
- const { f, browse, pasted } = st.current;
16401
- const prefix = { text: `${g.cursor} `, color: C.coral };
16402
- const lines = fieldLines(f.text, active ? f.cursor : null, width, { prefix, color: C.fg, ...placeholder ? { placeholder: { text: f.text ? "" : ` ${placeholder}`, color: C.faint } } : {} });
16403
- const row2 = rowOf(layout(f.text, width - 2), f.cursor);
16404
- top.current = scrollTop(top.current, row2, lines.length, max);
16405
- const shown = lines.slice(top.current, top.current + max);
16406
- const below = lines.length - top.current - shown.length;
16407
- const where = lineAt(f);
16408
- const foot = inHistory(browse) ? t.field.history(browse.i + 1, browse.items.length - 1) : [
16409
- top.current > 0 ? t.field.above(top.current) : "",
16410
- below > 0 ? t.field.below(below) : "",
16411
- pasted > 1 ? t.field.pasted(pasted) : active && where.lines > 1 ? t.field.line(where.line, where.lines) : ""
16412
- ].filter(Boolean).join(" \xB7 ");
16413
- return /* @__PURE__ */ jsx12(TitledBox, { title: title2, color: active ? color2 : C.line, titleColor: titleColor ?? (active ? color2 : C.dim), foot: foot || void 0, flexShrink: 0, children: shown.map((l, i) => /* @__PURE__ */ jsx12(Box9, { flexShrink: 0, children: /* @__PURE__ */ jsx12(LineText, { line: l }) }, i)) });
16494
+ if (key.return) onPick({ model: row2.id, effort: row2.efforts.length ? effort ?? null : null });
16495
+ });
16496
+ if (models.status !== "ready") return /* @__PURE__ */ jsx12(Text9, { color: C.faint, children: t.launch.loading });
16497
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
16498
+ models.error ? /* @__PURE__ */ jsx12(Text9, { color: C.yellow, children: models.error }) : null,
16499
+ rows.map((r, i) => /* @__PURE__ */ jsxs8(Text9, { wrap: "truncate", backgroundColor: i === at ? C.sel : void 0, children: [
16500
+ /* @__PURE__ */ jsxs8(Text9, { color: i === at ? C.coral : C.faint, children: [
16501
+ i === at ? g.cursor : " ",
16502
+ " "
16503
+ ] }),
16504
+ /* @__PURE__ */ jsx12(Text9, { color: C.fg, children: r.label }),
16505
+ i === at && r.efforts.length ? /* @__PURE__ */ jsxs8(Text9, { color: C.dim, children: [
16506
+ " ",
16507
+ r.efforts.map((e) => e === (effort ?? r.defaultEffort) ? `${g.prev} ${e} ${g.next}` : e).join(" ")
16508
+ ] }) : null
16509
+ ] }, r.id ?? "-"))
16510
+ ] });
16414
16511
  }
16415
16512
 
16416
16513
  // src/tui/screens/Session.tsx
@@ -16438,7 +16535,7 @@ function SessionView({ s }) {
16438
16535
  const [anchor, setAnchor] = useState7(null);
16439
16536
  useEditing(mode === "reply" || mode === "note");
16440
16537
  const items = d.threads.get(s.id);
16441
- useEffect7(() => {
16538
+ useEffect8(() => {
16442
16539
  if (!items && !s.offline) client.send({ type: "session.thread", id: s.id });
16443
16540
  }, [items === void 0, s.id, d.snapshotSeq]);
16444
16541
  const width = Math.max(20, columns - 6);
@@ -16460,7 +16557,7 @@ function SessionView({ s }) {
16460
16557
  };
16461
16558
  const images = useMemo3(() => (items ? threadImages(items, unfolded) : []).map((ref) => ({ ref, route: { id: s.id } })), [items, unfolded, s.id]);
16462
16559
  const chips = useChips((image2) => putImage(client, image2, { id: s.id }));
16463
- const paste = usePaste(chips);
16560
+ const paste = usePaste2(chips);
16464
16561
  const openImage2 = useOpenImage();
16465
16562
  const machine = splitId(s.id)?.machine ?? "local";
16466
16563
  const canSwitch = machines.find((m) => m.id === machine)?.daemon?.modelSwitch === true;
@@ -16673,7 +16770,7 @@ function PendingBox({ s, a }) {
16673
16770
 
16674
16771
  // src/tui/screens/Ask.tsx
16675
16772
  import { randomUUID as randomUUID2 } from "crypto";
16676
- import { useEffect as useEffect9, useMemo as useMemo4, useRef as useRef10, useState as useState9 } from "react";
16773
+ import { useEffect as useEffect10, useMemo as useMemo4, useRef as useRef10, useState as useState9 } from "react";
16677
16774
  import { Box as Box12, Text as Text12, useInput as useInput4 } from "ink";
16678
16775
 
16679
16776
  // ../ui/src/chat/actionRow.ts
@@ -16830,11 +16927,11 @@ function openCards(turns2) {
16830
16927
  }
16831
16928
 
16832
16929
  // src/tui/ui/throttle.ts
16833
- import { useEffect as useEffect8, useRef as useRef9, useState as useState8 } from "react";
16930
+ import { useEffect as useEffect9, useRef as useRef9, useState as useState8 } from "react";
16834
16931
  function useThrottled(value, ms) {
16835
16932
  const [shown, setShown] = useState8(value);
16836
16933
  const last = useRef9(0);
16837
- useEffect8(() => {
16934
+ useEffect9(() => {
16838
16935
  const show = () => {
16839
16936
  last.current = Date.now();
16840
16937
  setShown(value);
@@ -17013,7 +17110,7 @@ function Ask() {
17013
17110
  const [anchor, setAnchor] = useState9(null);
17014
17111
  const [view, setView] = useState9(10);
17015
17112
  const chips = useChips(null);
17016
- const paste = usePaste(chips);
17113
+ const paste = usePaste2(chips);
17017
17114
  const openImage2 = useOpenImage();
17018
17115
  const [busy, setBusy] = useState9(null);
17019
17116
  const [rowOf2, setRowOf] = useState9({});
@@ -17021,7 +17118,7 @@ function Ask() {
17021
17118
  const [review, setReview] = useState9(null);
17022
17119
  useEditing(focus === -1 && !confirm);
17023
17120
  const sentAsk = useRef10(null);
17024
- useEffect9(() => {
17121
+ useEffect10(() => {
17025
17122
  const s = sentAsk.current;
17026
17123
  const turn = s && d.chat.find((x) => x.qid === s.qid);
17027
17124
  if (!s || !turn) return;
@@ -17037,10 +17134,10 @@ function Ask() {
17037
17134
  height: (text5, indent2) => text5.split("\n").reduce((n, l) => n + Math.max(1, wrapSegs([{ text: l, color: C.fg }], Math.max(10, columns - 8 - indent2)).length), 0)
17038
17135
  };
17039
17136
  const planOf = (turn) => cardPlan(turn.actions ?? [], room);
17040
- useEffect9(() => {
17137
+ useEffect10(() => {
17041
17138
  if (focus >= cards.length) setFocus(cards.length ? cards.length - 1 : -1);
17042
17139
  }, [cards.length, focus]);
17043
- useEffect9(() => {
17140
+ useEffect10(() => {
17044
17141
  if (!review) return;
17045
17142
  const i = cards.findIndex((c) => c.qid === review);
17046
17143
  if (i >= 0) {
@@ -17048,21 +17145,21 @@ function Ask() {
17048
17145
  setReview(null);
17049
17146
  }
17050
17147
  }, [cards, review]);
17051
- useEffect9(() => {
17148
+ useEffect10(() => {
17052
17149
  if (confirming && !cards.some((c) => c.kind === "action" && c.qid === confirming.qid)) setConfirming(null);
17053
17150
  }, [confirming, cards]);
17054
- useEffect9(() => {
17151
+ useEffect10(() => {
17055
17152
  const turn = confirming && d.chat.find((x) => x.qid === confirming.qid);
17056
17153
  if (turn && !confirming.rows.every((i) => appliable(planOf(turn).shown[i]))) setConfirming(null);
17057
17154
  });
17058
17155
  const ignored = [...d.chat].filter((x) => x.ignoredAt).sort((a, b2) => b2.ignoredAt.localeCompare(a.ignoredAt))[0];
17059
- useEffect9(() => {
17156
+ useEffect10(() => {
17060
17157
  if (!busy?.rid) return;
17061
17158
  return client.onMessage((m) => {
17062
17159
  if (m.type === "chat.acted" && m.rid === busy.rid) setBusy(null);
17063
17160
  });
17064
17161
  }, [busy?.rid]);
17065
- useEffect9(() => {
17162
+ useEffect10(() => {
17066
17163
  if (!busy) return;
17067
17164
  const turn = d.chat.find((x) => x.qid === busy.qid);
17068
17165
  if (turn !== busy.turn && !busy.rid || (d.errors.at(-1)?.seq ?? 0) > busy.errorSeq) {
@@ -17278,7 +17375,7 @@ function Ask() {
17278
17375
  }
17279
17376
  if (input === "s" && cards[focus].kind === "team") {
17280
17377
  const team = d.chat.find((x) => x.qid === cards[focus].qid)?.team;
17281
- 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 } : {} })) });
17378
+ 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 } : {}, ...x.name ? { name: x.name } : {} })) });
17282
17379
  return true;
17283
17380
  }
17284
17381
  if (key.escape) {
@@ -17395,6 +17492,7 @@ function CardBox({ card, turn, focused, busy }) {
17395
17492
  ] }, x.role)),
17396
17493
  auto.length ? /* @__PURE__ */ jsx15(Text12, { color: C.yellow, wrap: "truncate", children: t.ask.autoWarning(auto.join(", ")) }) : null,
17397
17494
  team.error ? /* @__PURE__ */ jsx15(Text12, { color: C.red, wrap: "truncate", children: team.error }) : null,
17495
+ team.saveError && !team.savedPath ? /* @__PURE__ */ jsx15(Text12, { color: C.red, wrap: "truncate", children: team.saveError }) : null,
17398
17496
  /* @__PURE__ */ jsxs11(Box12, { columnGap: 2, children: [
17399
17497
  /* @__PURE__ */ jsx15(Button, { label: busy ? `${g.live} ${t.ask.launchTeam}` : t.ask.launchTeam, k: "\u21B5", primary: focused }),
17400
17498
  !team.template ? team.savedPath ? /* @__PURE__ */ jsx15(Text12, { color: C.faint, children: t.ask.saved }) : /* @__PURE__ */ jsx15(Button, { label: t.ask.save, k: "s" }) : null,
@@ -17481,7 +17579,7 @@ function CardBox({ card, turn, focused, busy }) {
17481
17579
  }
17482
17580
 
17483
17581
  // src/tui/screens/Launch.tsx
17484
- import { useEffect as useEffect10, useMemo as useMemo5, useRef as useRef11, useState as useState10 } from "react";
17582
+ import { useEffect as useEffect11, useMemo as useMemo5, useRef as useRef11, useState as useState10 } from "react";
17485
17583
  import { Box as Box13, Text as Text14, useInput as useInput6 } from "ink";
17486
17584
 
17487
17585
  // ../fleet/src/launch.ts
@@ -17633,7 +17731,7 @@ function SessionLaunch({ onTeam, prefill }) {
17633
17731
  const [repoAt, setRepoAt] = useState10(0);
17634
17732
  const [launching, setLaunching] = useState10(null);
17635
17733
  useEditing(true);
17636
- useEffect10(() => {
17734
+ useEffect11(() => {
17637
17735
  if (form.machine === null) {
17638
17736
  const up = machines.find((m) => m.ready);
17639
17737
  if (up) setForm((f) => withMachine(f, list3, up.id));
@@ -17656,7 +17754,7 @@ function SessionLaunch({ onTeam, prefill }) {
17656
17754
  const readHere = sameMachine && target !== null && target === prefill.cwd;
17657
17755
  const asked2 = sameMachine && !readHere && target !== null ? `${withFolder.machine}\0${target}` : null;
17658
17756
  const [seen, setSeen] = useState10(null);
17659
- useEffect10(() => {
17757
+ useEffect11(() => {
17660
17758
  if (!asked2) return;
17661
17759
  const [machineId, cwd] = asked2.split("\0");
17662
17760
  const requestId = `tui-role-${randomUUID3()}`;
@@ -17683,7 +17781,7 @@ function SessionLaunch({ onTeam, prefill }) {
17683
17781
  const [name, setName] = useState10(null);
17684
17782
  const shownName = name ?? drawn.name;
17685
17783
  const missing = missingOf(withFolder, machines);
17686
- useEffect10(() => {
17784
+ useEffect11(() => {
17687
17785
  if (!launching) return;
17688
17786
  if ((d.lastLaunched?.seq ?? 0) > launching.seq) {
17689
17787
  select(d.lastLaunched.id);
@@ -17894,7 +17992,7 @@ function TeamLaunch({ onSession, prefill }) {
17894
17992
  const [name, setName] = useState10("");
17895
17993
  const [launching, setLaunching] = useState10(false);
17896
17994
  useEditing(true);
17897
- useEffect10(() => {
17995
+ useEffect11(() => {
17898
17996
  if (!cwd) {
17899
17997
  setDefs({ roles: [], templates: [] });
17900
17998
  return;
@@ -18180,7 +18278,7 @@ function Help() {
18180
18278
  const k = t.keys;
18181
18279
  const groups = [
18182
18280
  [t.help.global, [["1\u20266", t.help.screens], ["/", k.ask], ["+", k.launch], [",", k.settings], ["?", k.help], ["esc", k.back], ["q", k.quit]]],
18183
- [t.help.overview, [["j k \u2191 \u2193", k.navigate], ["tab", k.next], ["\u21B5", k.open], ["y n A", k.answer], ["1\u20269", k.answer], ["r", t.overview.denyWithNote], ["a", k.attach], ["i", k.interrupt]]],
18281
+ [t.help.overview, [["j k \u2191 \u2193", k.navigate], ["tab", k.next], ["\u21B5", k.open], ["y n A", k.answer], ["1\u20269", k.answer], ["r", t.overview.denyWithNote], ["D", k.decide], ["a", k.attach], ["i", k.interrupt]]],
18184
18282
  [t.help.session, [["y n A 1\u20269", k.answer], ["r \u21B5", k.write], ["j k PgUp PgDn", k.scroll], [t.keys.space, k.unfold], ["d", k.diffs], ["t", k.terminal], ["a", k.attach], ["m", k.model], ["i", k.interrupt]]],
18185
18283
  [t.help.ask, [["\u21B5", k.send], ["tab", k.cycle], ["PgUp PgDn", k.scroll], ["j k G", k.scroll], ["ctrl+n", k.newChat], ["ctrl+c", k.stopAnswer]]],
18186
18284
  [t.help.field, [["\u21B5", k.sendOnly], ["alt+\u21B5 ctrl+j", k.newLine], ["\u2191 \u2193", k.lineHistory], ["ctrl \u2190 \u2192", k.word], ["ctrl+w", t.help.wordBefore], ["ctrl+u", k.clear], ["esc", k.keepAndLeave]]],
@@ -18218,7 +18316,7 @@ function Help() {
18218
18316
  }
18219
18317
 
18220
18318
  // src/tui/screens/Teams.tsx
18221
- import { useEffect as useEffect11, useMemo as useMemo7, useState as useState13 } from "react";
18319
+ import { useEffect as useEffect12, useMemo as useMemo7, useState as useState13 } from "react";
18222
18320
  import { Box as Box16, Text as Text17 } from "ink";
18223
18321
  import { randomUUID as randomUUID4 } from "crypto";
18224
18322
 
@@ -18298,7 +18396,7 @@ function Teams() {
18298
18396
  const [problems, setProblems] = useState13(false);
18299
18397
  const [silent, setSilent] = useState13(false);
18300
18398
  const [reads, setReads] = useState13(0);
18301
- useEffect11(() => {
18399
+ useEffect12(() => {
18302
18400
  setDefs(null);
18303
18401
  setSilent(false);
18304
18402
  const requestId = `tui-catalog-${randomUUID4()}`;
@@ -18579,7 +18677,7 @@ function Shell({ client, lang, setLang: setLangState, saveLang, theme, setThemeS
18579
18677
  const kept = useRef13({ drafts: /* @__PURE__ */ new Map(), sent: /* @__PURE__ */ new Map(), asked: [] }).current;
18580
18678
  const armed = useRef13(0);
18581
18679
  const catalog = useRef13({});
18582
- useEffect12(() => {
18680
+ useEffect13(() => {
18583
18681
  if (!lastToast) return;
18584
18682
  const timer2 = setTimeout(() => setToast(null), lastToast.ms ?? TOAST_MS);
18585
18683
  return () => clearTimeout(timer2);
@@ -18588,22 +18686,22 @@ function Shell({ client, lang, setLang: setLangState, saveLang, theme, setThemeS
18588
18686
  const setEditing = useCallback((on) => setEditingState(on), []);
18589
18687
  const view = useMemo8(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
18590
18688
  const order2 = useRef13([]);
18591
- useEffect12(() => {
18689
+ useEffect13(() => {
18592
18690
  setSelectedId((cur) => cur === null && view.yours[0] ? view.yours[0].id : keepSelection(view.order, cur, order2.current));
18593
18691
  order2.current = view.order;
18594
18692
  }, [view]);
18595
18693
  const selected = (selectedId ? d.byId.get(selectedId) : void 0) ?? null;
18596
18694
  const seen = useRef13(null);
18597
18695
  const titled = useRef13("");
18598
- useEffect12(() => {
18696
+ useEffect13(() => {
18599
18697
  if (seen.current) for (const text5 of asked(seen.current, d.sessions, t.notify)) write(notification(text5, notify, env));
18600
18698
  seen.current = d.byId;
18601
- const next2 = title(t.notify.title(listed(d.sessions).filter(isPending).length));
18699
+ const next2 = title(t.notify.title(listed(d.sessions).filter(isPending).length + openDecisions(tasks).length));
18602
18700
  if (next2 !== titled.current) {
18603
18701
  titled.current = next2;
18604
18702
  write(next2);
18605
18703
  }
18606
- }, [d.byId]);
18704
+ }, [d.byId, tasks]);
18607
18705
  const actions = useActions({ client, sessions: d.sessions, errors: d.errors, t, toast });
18608
18706
  const go = useCallback((next2) => {
18609
18707
  setScreen((cur) => {