@paradow/ledge 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/cli/{action-MAPPYLUM.js → action-IL5A2O4K.js} +4 -4
  2. package/dist/cli/{attach-OC64CLB2.js → attach-XFRWCGMD.js} +8 -8
  3. package/dist/cli/{bridge-TWX4V37E.js → bridge-KEDZGBEK.js} +1 -1
  4. package/dist/cli/{chunk-XZYQMKKO.js → chunk-2GSAJOLJ.js} +3 -3
  5. package/dist/cli/{chunk-WIUJNSAZ.js → chunk-4JQHF3HM.js} +5 -5
  6. package/dist/cli/{chunk-ETGOWBUL.js → chunk-D2EYVNJ6.js} +3 -3
  7. package/dist/cli/{chunk-73YNZ3BN.js → chunk-GOBUJGB6.js} +1 -1
  8. package/dist/cli/{chunk-TNYBWWRO.js → chunk-H563AKHN.js} +2 -2
  9. package/dist/cli/{chunk-WCBXS3EU.js → chunk-HH6LFLPR.js} +107 -15
  10. package/dist/cli/{chunk-RFFTWAAF.js → chunk-JBYVS2F4.js} +1 -1
  11. package/dist/cli/{chunk-353WRRGG.js → chunk-KESZUXUI.js} +3 -3
  12. package/dist/cli/{chunk-UWDE365N.js → chunk-LHWJBSLO.js} +47 -3
  13. package/dist/cli/{chunk-TREGH6WM.js → chunk-OOPOJK6Y.js} +206 -175
  14. package/dist/cli/chunk-P2X2M3TR.js +6 -0
  15. package/dist/cli/{chunk-MZVMW356.js → chunk-RGYQFRG5.js} +2 -2
  16. package/dist/cli/{chunk-7S7ZLGIY.js → chunk-U44NT4XJ.js} +35 -7
  17. package/dist/cli/{chunk-K3SDHHZF.js → chunk-XHC6EPZE.js} +1 -1
  18. package/dist/cli/{daemon-ZTZ7VMYA.js → daemon-A54MACKQ.js} +7 -7
  19. package/dist/cli/{decide-4BIYS3Y2.js → decide-2HKZF4VI.js} +8 -6
  20. package/dist/cli/{handover-IS7BJYGZ.js → handover-3PV3MDK3.js} +4 -4
  21. package/dist/cli/{hook-3VKVD4IP.js → hook-S5226MJF.js} +34 -13
  22. package/dist/cli/index.js +16 -16
  23. package/dist/cli/{launch-XAPN2M3Q.js → launch-NGWPGU6E.js} +8 -8
  24. package/dist/cli/{send-ZLF2ZXMJ.js → send-NYKZRELC.js} +4 -4
  25. package/dist/cli/{status-NONO6E2S.js → status-6USM7SJE.js} +2 -2
  26. package/dist/cli/{task-47MVWDCY.js → task-E5J6UT7S.js} +4 -4
  27. package/dist/cli/{team-EJ4YIXJH.js → team-AP7A5VMP.js} +6 -6
  28. package/dist/cli/{tui-CAP4O4YQ.js → tui-EK7VC7YP.js} +1273 -1176
  29. package/dist/cli/{version-EHBPJO7K.js → version-KORN5SQT.js} +1 -1
  30. package/dist/cli/{who-FHAXYCM2.js → who-RA4QBVUA.js} +3 -3
  31. package/package.json +1 -1
  32. 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-4JQHF3HM.js";
4
4
  import {
5
5
  imageInfo
6
- } from "./chunk-TREGH6WM.js";
6
+ } from "./chunk-OOPOJK6Y.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-HH6LFLPR.js";
11
+ import "./chunk-LHWJBSLO.js";
12
+ import "./chunk-GOBUJGB6.js";
13
+ import "./chunk-XHC6EPZE.js";
14
+ import "./chunk-JBYVS2F4.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,7 @@ import {
43
44
  readDaemonToken,
44
45
  taskClosed,
45
46
  transmissions
46
- } from "./chunk-7S7ZLGIY.js";
47
+ } from "./chunk-U44NT4XJ.js";
47
48
  import "./chunk-NLL2EQJ2.js";
48
49
  import "./chunk-6IYPRUQG.js";
49
50
  import {
@@ -1303,7 +1304,7 @@ function createFleetClient(config, makeClient = createDaemonClient, opts = {}) {
1303
1304
  const { machineId: _m, ...rest } = outgoing;
1304
1305
  outgoing = rest;
1305
1306
  }
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";
1307
+ 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
1308
  if (outgoing.type === "team.join" || outgoing.type === "team.leave") {
1308
1309
  const o = splitId(outgoing.session);
1309
1310
  if (o) {
@@ -1510,7 +1511,7 @@ function getImage(client, attachment, route = {}, preview = false, timeoutMs = T
1510
1511
  }
1511
1512
 
1512
1513
  // src/tui/App.tsx
1513
- import { useCallback, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef13, useState as useState14 } from "react";
1514
+ import { useCallback, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef13, useState as useState14 } from "react";
1514
1515
  import { useApp, useInput as useInput7, useStdout, useWindowSize as useWindowSize2 } from "ink";
1515
1516
 
1516
1517
  // src/tui/actions.ts
@@ -1935,7 +1936,8 @@ var fr = {
1935
1936
  "team.dissolve": (v) => need(v, ["team"], (w) => `Dissoudre l\u2019\xE9quipe ${b(w.team)}`),
1936
1937
  "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
1938
  "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)}`)
1939
+ "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.answer": (v) => need(v, ["task", "session"], (w) => `R\xE9pondre \xE0 la d\xE9cision \xAB ${short(w.task)} \xBB de ${b(w.session)}`)
1939
1941
  },
1940
1942
  why: (code3, action, v) => {
1941
1943
  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 +1987,8 @@ var fr = {
1985
1987
  return k ? `la t\xE2che ${k} est d\xE9j\xE0 close.` : gone;
1986
1988
  case "holder":
1987
1989
  return s && k ? `${s} tient d\xE9j\xE0 la t\xE2che ${k}.` : gone;
1990
+ case "not_decision":
1991
+ return k ? `la t\xE2che ${k} n\u2019est pas une d\xE9cision qui t\u2019est pos\xE9e.` : gone;
1988
1992
  case "changed":
1989
1993
  return "la situation a chang\xE9 depuis ma r\xE9ponse : redemande-moi.";
1990
1994
  case "failed":
@@ -2011,7 +2015,8 @@ var en = {
2011
2015
  "team.dissolve": (v) => need(v, ["team"], (w) => `Dissolve the team ${b(w.team)}`),
2012
2016
  "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
2017
  "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)}`)
2018
+ "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.answer": (v) => need(v, ["task", "session"], (w) => `Answer ${b(w.session)}\u2019s decision \u201C${short(w.task)}\u201D`)
2015
2020
  },
2016
2021
  why: (code3, action, v) => {
2017
2022
  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 +2066,8 @@ var en = {
2061
2066
  return k ? `the task ${k} is already closed.` : gone;
2062
2067
  case "holder":
2063
2068
  return s && k ? `${s} already holds the task ${k}.` : gone;
2069
+ case "not_decision":
2070
+ return k ? `the task ${k} is not a decision put to you.` : gone;
2064
2071
  case "changed":
2065
2072
  return "things have changed since my answer: ask me again.";
2066
2073
  case "failed":
@@ -2095,7 +2102,8 @@ var fr2 = {
2095
2102
  "team.dissolve": "Dissoudre",
2096
2103
  "team.save": "Enregistrer",
2097
2104
  "task.cancel": "Abandonner",
2098
- "task.pass": "Passer le relais"
2105
+ "task.pass": "Passer le relais",
2106
+ "task.answer": "R\xE9pondre"
2099
2107
  },
2100
2108
  apply: "Appliquer",
2101
2109
  deny: "Refuser",
@@ -2161,7 +2169,8 @@ var en2 = {
2161
2169
  "team.dissolve": "Dissolve",
2162
2170
  "team.save": "Save",
2163
2171
  "task.cancel": "Drop",
2164
- "task.pass": "Hand over"
2172
+ "task.pass": "Hand over",
2173
+ "task.answer": "Answer"
2165
2174
  },
2166
2175
  apply: "Apply",
2167
2176
  deny: "Deny",
@@ -2270,6 +2279,9 @@ var fr3 = {
2270
2279
  resume: "reprendre l\u2019\xE9quipe",
2271
2280
  teamMode: "session ou \xE9quipe",
2272
2281
  handover: "passer le relais",
2282
+ decide: "r\xE9pondre \xE0 la d\xE9cision",
2283
+ sendAnswer: "envoyer la r\xE9ponse",
2284
+ edit: "modifier",
2273
2285
  sendOnly: "envoyer",
2274
2286
  newLine: "nouvelle ligne",
2275
2287
  keepAndLeave: "garder et sortir",
@@ -2336,6 +2348,10 @@ var fr3 = {
2336
2348
  relievedBy: (name) => `relev\xE9e par ${name}`,
2337
2349
  relieves: (name) => `rel\xE8ve de ${name}`,
2338
2350
  ledgeTask: (owner, title2, state2) => `${owner} a ${state2 === "done" ? "fini" : state2 === "refused" ? "refus\xE9" : "abandonn\xE9"} \xAB ${title2} \xBB \xB7 t\xE2che de Ledge`,
2351
+ decision: (giver, title2) => `${giver} te demande : ${title2}`,
2352
+ decisionTitle: (giver, title2) => `R\xE9pondre \xE0 ${giver} \xB7 ${title2}`,
2353
+ decisionHint: "Ta r\xE9ponse, telle que la session la lira\u2026",
2354
+ confirmAnswer: (giver) => `Envoyer \xE0 ${giver} ? Une r\xE9ponse ne se reprend pas`,
2339
2355
  joinWhich: (name) => `${name} rejoint quelle \xE9quipe ? (\u2191\u2193 \u21B5, esc)`,
2340
2356
  noTeamHere: "Aucune \xE9quipe dans ce projet.",
2341
2357
  notInTeam: "Cette session n\u2019est dans aucune \xE9quipe."
@@ -2555,6 +2571,7 @@ var fr3 = {
2555
2571
  openFailed: (why) => `Image non ouverte : ${why}`
2556
2572
  },
2557
2573
  toast: {
2574
+ answerFailed: (why) => `R\xE9ponse non envoy\xE9e : ${why}`,
2558
2575
  handover: (n) => `${n} \xE9crit son bilan : une nouvelle session prendra sa place`,
2559
2576
  handoverRefused: (why) => `Rel\xE8ve refus\xE9e : ${why}`,
2560
2577
  accept: (n) => `\u2713 ${n} : autoris\xE9`,
@@ -2657,6 +2674,9 @@ var en3 = {
2657
2674
  resume: "resume the team",
2658
2675
  teamMode: "session or team",
2659
2676
  handover: "hand over",
2677
+ decide: "answer the decision",
2678
+ sendAnswer: "send the answer",
2679
+ edit: "edit",
2660
2680
  sendOnly: "send",
2661
2681
  newLine: "new line",
2662
2682
  keepAndLeave: "keep and leave",
@@ -2723,6 +2743,10 @@ var en3 = {
2723
2743
  relievedBy: (name) => `relieved by ${name}`,
2724
2744
  relieves: (name) => `relieves ${name}`,
2725
2745
  ledgeTask: (owner, title2, state2) => `${owner} ${state2 === "done" ? "finished" : state2 === "refused" ? "refused" : "dropped"} \u201C${title2}\u201D \xB7 Ledge\u2019s task`,
2746
+ decision: (giver, title2) => `${giver} asks you: ${title2}`,
2747
+ decisionTitle: (giver, title2) => `Answer ${giver} \xB7 ${title2}`,
2748
+ decisionHint: "Your answer, as the session will read it\u2026",
2749
+ confirmAnswer: (giver) => `Send to ${giver}? An answer cannot be taken back`,
2726
2750
  joinWhich: (name) => `${name} joins which team? (\u2191\u2193 \u21B5, esc)`,
2727
2751
  noTeamHere: "No team in this project.",
2728
2752
  notInTeam: "This session is in no team."
@@ -2940,6 +2964,7 @@ var en3 = {
2940
2964
  openFailed: (why) => `Image not opened: ${why}`
2941
2965
  },
2942
2966
  toast: {
2967
+ answerFailed: (why) => `Answer not sent: ${why}`,
2943
2968
  handover: (n) => `${n} is writing its brief: a fresh session will take its place`,
2944
2969
  handoverRefused: (why) => `Relief refused: ${why}`,
2945
2970
  accept: (n) => `\u2713 ${n}: allowed`,
@@ -3078,6 +3103,7 @@ function exchanges(sessions, max, tasks = [], ledge = "Ledge") {
3078
3103
  }
3079
3104
  var nearContext = (sessions, near) => sessions.filter((s) => s.state !== "exited" && !s.archivedAt && (s.handover ? s.handover.state === "failed" : contextNear(s.context, near)));
3080
3105
  var closedLedgeTasks = (tasks) => tasks.filter((x) => x.giver === LEDGE_GIVER && taskClosed(x) && !x.seen);
3106
+ var openDecisions = (tasks) => tasks.filter(openDecision);
3081
3107
 
3082
3108
  // src/tui/model/time.ts
3083
3109
  function ago(iso, now2, t) {
@@ -3126,8 +3152,8 @@ function notification(text5, mode, env) {
3126
3152
  var title = (text5) => `\x1B]0;${text5.replace(/[\u0000-\u001f\u007f]/g, " ")}\x07`;
3127
3153
 
3128
3154
  // src/tui/screens/Overview.tsx
3129
- import { useMemo as useMemo2 } from "react";
3130
- import { Box as Box6, Text as Text6 } from "ink";
3155
+ import { useEffect as useEffect6, useMemo as useMemo2 } from "react";
3156
+ import { Box as Box8, Text as Text7 } from "ink";
3131
3157
  import { randomUUID } from "crypto";
3132
3158
  import { useState as useState3 } from "react";
3133
3159
 
@@ -3146,49 +3172,48 @@ function botState(session) {
3146
3172
  return FROM_SESSION[session.state];
3147
3173
  }
3148
3174
 
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);
3175
+ // ../ui/src/thread/turns.ts
3176
+ var OPEN_TURNS = 3;
3177
+ var byTime = (a, b2) => a.at < b2.at ? -1 : a.at > b2.at ? 1 : 0;
3178
+ function withoutEcho(items, reply) {
3179
+ if (!reply) return items;
3180
+ let last = -1;
3181
+ items.forEach((it, i) => {
3182
+ if (it.kind === "message") last = i;
3183
+ });
3184
+ const echo = items[last];
3185
+ return echo?.kind === "message" && echo.text.trim() === reply.text.trim() ? items.filter((_, i) => i !== last) : items;
3158
3186
  }
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?.();
3187
+ function turns(items) {
3188
+ const map3 = /* @__PURE__ */ new Map();
3189
+ for (const it of items) {
3190
+ if (it.helperId) continue;
3191
+ const t = map3.get(it.turn) ?? { n: it.turn, items: [] };
3192
+ t.items.push(it);
3193
+ if (it.kind === "prompt") t.prompt = it;
3194
+ if (it.kind === "reply") t.reply = it;
3195
+ map3.set(it.turn, t);
3174
3196
  }
3175
- return () => {
3176
- listeners.delete(fn);
3177
- if (listeners.size === 0 && timer) {
3178
- clearInterval(timer);
3179
- timer = null;
3180
- }
3181
- };
3197
+ return [...map3.values()].sort((a, b2) => a.n - b2.n).map((t) => ({ ...t, items: withoutEcho([...t.items].sort(byTime), t.reply) }));
3182
3198
  }
3183
- var useNow = () => useSyncExternalStore4(subscribe, () => now);
3184
3199
 
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) });
3200
+ // ../ui/src/thread/tools.ts
3201
+ function rawTarget(item) {
3202
+ let s = item.label.trim();
3203
+ const prefix = `${item.tool} \xB7 `;
3204
+ if (s.startsWith(prefix)) s = s.slice(prefix.length);
3205
+ if (s === item.tool) return "";
3206
+ if (s.startsWith("{")) {
3207
+ try {
3208
+ const o = JSON.parse(s);
3209
+ const v = ["pattern", "query", "url", "file_path", "path", "command", "description", "prompt"].map((k) => o[k]).find((x) => typeof x === "string" && x);
3210
+ if (typeof v === "string") s = v;
3211
+ } catch {
3212
+ }
3213
+ }
3214
+ return (s.split("\n")[0] ?? "").trim();
3191
3215
  }
3216
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3192
3217
 
3193
3218
  // src/tui/theme.ts
3194
3219
  var THEMES = ["auto", "dark", "light"];
@@ -3253,9 +3278,6 @@ function setTone(next2) {
3253
3278
  }
3254
3279
  var projectColor = (color2) => color2 ? PROJECT_COLORS[color2] : C.dim;
3255
3280
 
3256
- // src/tui/ui/Frame.tsx
3257
- import { Box as Box2, Text as Text2 } from "ink";
3258
-
3259
3281
  // src/tui/model/cells.ts
3260
3282
  import stringWidth from "string-width";
3261
3283
  var graphemes = new Intl.Segmenter(void 0, { granularity: "grapheme" });
@@ -3332,753 +3354,63 @@ function row(prefix, body, meta, width, o = {}) {
3332
3354
  });
3333
3355
  }
3334
3356
 
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)) });
3357
+ // ../../node_modules/.pnpm/ccount@2.0.1/node_modules/ccount/index.js
3358
+ function ccount(value, character) {
3359
+ const source2 = String(value);
3360
+ if (typeof character !== "string") {
3361
+ throw new TypeError("Expected character");
3362
+ }
3363
+ let count = 0;
3364
+ let index2 = source2.indexOf(character);
3365
+ while (index2 !== -1) {
3366
+ count++;
3367
+ index2 = source2.indexOf(character, index2 + character.length);
3368
+ }
3369
+ return count;
3358
3370
  }
3359
3371
 
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
- ] });
3372
+ // ../../node_modules/.pnpm/devlop@1.1.0/node_modules/devlop/lib/default.js
3373
+ function ok() {
3413
3374
  }
3414
3375
 
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
- ] });
3376
+ // ../../node_modules/.pnpm/micromark-util-character@2.1.1/node_modules/micromark-util-character/index.js
3377
+ var asciiAlpha = regexCheck(/[A-Za-z]/);
3378
+ var asciiAlphanumeric = regexCheck(/[\dA-Za-z]/);
3379
+ var asciiAtext = regexCheck(/[#-'*+\--9=?A-Z^-~]/);
3380
+ function asciiControl(code3) {
3381
+ return (
3382
+ // Special whitespace codes (which have negative values), C0 and Control
3383
+ // character DEL
3384
+ code3 !== null && (code3 < 32 || code3 === 127)
3385
+ );
3431
3386
  }
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
- ] });
3387
+ var asciiDigit = regexCheck(/\d/);
3388
+ var asciiHexDigit = regexCheck(/[\dA-Fa-f]/);
3389
+ var asciiPunctuation = regexCheck(/[!-/:-@[-`{-~]/);
3390
+ function markdownLineEnding(code3) {
3391
+ return code3 !== null && code3 < -2;
3392
+ }
3393
+ function markdownLineEndingOrSpace(code3) {
3394
+ return code3 !== null && (code3 < 0 || code3 === 32);
3395
+ }
3396
+ function markdownSpace(code3) {
3397
+ return code3 === -2 || code3 === -1 || code3 === 32;
3398
+ }
3399
+ var unicodePunctuation = regexCheck(new RegExp("\\p{P}|\\p{S}", "u"));
3400
+ var unicodeWhitespace = regexCheck(/\s/);
3401
+ function regexCheck(regex) {
3402
+ return check;
3403
+ function check(code3) {
3404
+ return code3 !== null && code3 > -1 && regex.test(String.fromCharCode(code3));
3405
+ }
3507
3406
  }
3508
3407
 
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");
3408
+ // ../../node_modules/.pnpm/escape-string-regexp@5.0.0/node_modules/escape-string-regexp/index.js
3409
+ function escapeStringRegexp(string3) {
3410
+ if (typeof string3 !== "string") {
3411
+ throw new TypeError("Expected a string");
3412
+ }
3413
+ return string3.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
4082
3414
  }
4083
3415
 
4084
3416
  // ../../node_modules/.pnpm/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.js
@@ -15927,6 +15259,9 @@ function imageLine(ref, t, key) {
15927
15259
  var DIFF_MAX = 30;
15928
15260
  var CAP = { tool: 3, error: 3, message: 3, notice: 2, permission: 3, helper: 2 };
15929
15261
  var LIVE_PAD = 9;
15262
+ function wrap2(text5, width) {
15263
+ return wrapSegs([{ text: text5.replace(/\r/g, "") }], Math.max(10, width)).map((l) => l.map((s) => s.text).join(""));
15264
+ }
15930
15265
  function diffCount(diff) {
15931
15266
  let add = 0, del2 = 0;
15932
15267
  for (const l of (diff ?? "").split("\n")) {
@@ -16018,399 +15353,1160 @@ function foldedLine(turn, o) {
16018
15353
  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
15354
  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
15355
  }
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);
15356
+ function threadView(items, o) {
15357
+ const all2 = turns(items);
15358
+ const lines = [], keys = [], turnOf = /* @__PURE__ */ new Map();
15359
+ const opts = { ...o, cap: (n) => o.unfolded ? Infinity : n };
15360
+ const push2 = (key, l) => {
15361
+ for (const x of l) {
15362
+ lines.push(x);
15363
+ keys.push(key);
15364
+ }
15365
+ };
15366
+ all2.forEach((turn, i) => {
15367
+ const recent = i >= all2.length - OPEN_TURNS;
15368
+ const first = turn.prompt?.id ?? turn.items[0]?.id ?? String(i);
15369
+ for (const it of turn.items) turnOf.set(it.id, first);
15370
+ if (!recent && !o.unfolded) {
15371
+ push2(first, [cut(foldedLine(turn, o), o.width)]);
15372
+ return;
15373
+ }
15374
+ if (lines.length) push2(first, [[{ text: "" }]]);
15375
+ for (const it of turn.items) push2(it.id, itemLines(it, opts));
15376
+ });
15377
+ return { lines, keys, turns: turnOf };
15378
+ }
15379
+ function anchorLine(v, anchor) {
15380
+ const at = v.keys.indexOf(anchor.key);
15381
+ if (at >= 0) return at + anchor.offset;
15382
+ const turn = v.turns.get(anchor.key);
15383
+ return turn === void 0 ? -1 : v.keys.indexOf(turn);
15384
+ }
15385
+ function threadImages(items, unfolded) {
15386
+ const all2 = turns(items);
15387
+ return (unfolded ? all2 : all2.slice(-OPEN_TURNS)).flatMap((turn) => turn.items.flatMap((it) => it.kind === "prompt" ? it.images ?? [] : []));
15388
+ }
15389
+
15390
+ // src/tui/model/labels.ts
15391
+ var TOOL_NAME = { claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", omp: "OMP" };
15392
+ var modelLabel = (s, fallback) => [s.model ?? (s.effort ? fallback : ""), s.effort ?? ""].filter(Boolean).join(" \xB7 ") || fallback;
15393
+ var machineLabel = (s) => s.machineName ?? splitId(s.id)?.machine ?? "Local";
15394
+ var paneName = (s) => s.tmuxSession ?? `ledge-${splitId(s.id)?.id ?? s.id}`;
15395
+ function fit(s, n) {
15396
+ const cps = [...s];
15397
+ if (n <= 0) return "";
15398
+ return cps.length > n ? `${cps.slice(0, Math.max(0, n - 1)).join("")}\u2026` : s + " ".repeat(n - cps.length);
15399
+ }
15400
+
15401
+ // src/tui/ui/now.ts
15402
+ import { useSyncExternalStore as useSyncExternalStore4 } from "react";
15403
+ var listeners = /* @__PURE__ */ new Set();
15404
+ var now = Date.now();
15405
+ var timer = null;
15406
+ function subscribe(fn) {
15407
+ listeners.add(fn);
15408
+ if (!timer) {
15409
+ now = Date.now();
15410
+ timer = setInterval(() => {
15411
+ now = Date.now();
15412
+ for (const l of listeners) l();
15413
+ }, 1e3);
15414
+ timer.unref?.();
15415
+ }
15416
+ return () => {
15417
+ listeners.delete(fn);
15418
+ if (listeners.size === 0 && timer) {
15419
+ clearInterval(timer);
15420
+ timer = null;
15421
+ }
15422
+ };
15423
+ }
15424
+ var useNow = () => useSyncExternalStore4(subscribe, () => now);
15425
+
15426
+ // src/tui/ui/Ago.tsx
15427
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
15428
+ function Ago({ iso }) {
15429
+ const now2 = useNow();
15430
+ const { t } = useI18n();
15431
+ return /* @__PURE__ */ jsx2(Fragment, { children: ago(iso, now2, t.time) });
15432
+ }
15433
+
15434
+ // src/tui/ui/Field.tsx
15435
+ import { useReducer, useRef as useRef6 } from "react";
15436
+ import { Box as Box3, usePaste, useInput } from "ink";
15437
+
15438
+ // src/tui/model/field.ts
15439
+ var graphemes2 = new Intl.Segmenter(void 0, { granularity: "grapheme" });
15440
+ var stops = (text5) => [...Array.from(graphemes2.segment(text5), (g) => g.index), text5.length];
15441
+ var newlines = (s) => s.replace(/\r\n?/g, "\n");
15442
+ var insert = (f, s) => {
15443
+ const add = newlines(s);
15444
+ return { text: f.text.slice(0, f.cursor) + add + f.text.slice(f.cursor), cursor: f.cursor + add.length };
15445
+ };
15446
+ var before = (f) => {
15447
+ const a = stops(f.text);
15448
+ let i = a.length - 1;
15449
+ while (i > 0 && a[i] >= f.cursor) i--;
15450
+ return a[i] < f.cursor ? a[i] : 0;
15451
+ };
15452
+ var after = (f) => stops(f.text).find((x) => x > f.cursor) ?? f.text.length;
15453
+ var left = (f) => ({ ...f, cursor: before(f) });
15454
+ var right = (f) => ({ ...f, cursor: after(f) });
15455
+ var back = (f) => {
15456
+ const to = before(f);
15457
+ return { text: f.text.slice(0, to) + f.text.slice(f.cursor), cursor: to };
15458
+ };
15459
+ var del = (f) => ({ text: f.text.slice(0, f.cursor) + f.text.slice(after(f)), cursor: f.cursor });
15460
+ function wordStart(f) {
15461
+ let i = f.cursor;
15462
+ while (i > 0 && /\s/.test(f.text[i - 1])) i--;
15463
+ while (i > 0 && !/\s/.test(f.text[i - 1])) i--;
15464
+ return i;
15465
+ }
15466
+ var wordLeft = (f) => ({ ...f, cursor: wordStart(f) });
15467
+ function wordRight(f) {
15468
+ let i = f.cursor;
15469
+ while (i < f.text.length && /\s/.test(f.text[i])) i++;
15470
+ while (i < f.text.length && !/\s/.test(f.text[i])) i++;
15471
+ return { ...f, cursor: i };
15472
+ }
15473
+ var killWord = (f) => {
15474
+ const to = wordStart(f);
15475
+ return { text: f.text.slice(0, to) + f.text.slice(f.cursor), cursor: to };
15476
+ };
15477
+ var home = (f) => ({ ...f, cursor: f.text.lastIndexOf("\n", f.cursor - 1) + 1 });
15478
+ var end = (f) => {
15479
+ const n = f.text.indexOf("\n", f.cursor);
15480
+ return { ...f, cursor: n < 0 ? f.text.length : n };
15481
+ };
15482
+ function layout(text5, width) {
15483
+ const w = Math.max(2, width - 1);
15484
+ const a = stops(text5), rows = [];
15485
+ let start = 0;
15486
+ for (let i = 0; i < a.length; i++) {
15487
+ const at = a[i];
15488
+ if (at < text5.length && text5[at] !== "\n") continue;
15489
+ if (start === i) rows.push({ a: at, b: at, last: true });
15490
+ for (let s = start; s < i; ) {
15491
+ let n = 0, j = s, space = -1;
15492
+ while (j < i) {
15493
+ const c = cells(text5.slice(a[j], a[j + 1]));
15494
+ if (n + c > w) break;
15495
+ if (text5[a[j]] === " ") space = j;
15496
+ n += c;
15497
+ j++;
15498
+ }
15499
+ if (j === s) j = s + 1;
15500
+ const e = j < i && space > s ? space + 1 : j;
15501
+ rows.push({ a: a[s], b: a[e], last: e >= i });
15502
+ s = e;
15503
+ }
15504
+ start = i + 1;
15505
+ }
15506
+ return rows;
15507
+ }
15508
+ var rowOf = (rows, cursor) => {
15509
+ const k = rows.findIndex((r) => cursor >= r.a && (cursor < r.b || cursor === r.b && r.last));
15510
+ return k < 0 ? rows.length - 1 : k;
15511
+ };
15512
+ function vertical(f, width, by) {
15513
+ const rows = layout(f.text, width), k = rowOf(rows, f.cursor), to = rows[k + by];
15514
+ if (!to) return null;
15515
+ const col = cells(f.text.slice(rows[k].a, f.cursor));
15516
+ let cursor = to.a;
15517
+ for (const x of stops(f.text)) {
15518
+ if (x <= to.a) continue;
15519
+ if (x > to.b || x === to.b && !to.last || cells(f.text.slice(to.a, x)) > col) break;
15520
+ cursor = x;
15521
+ }
15522
+ return { ...f, cursor };
15523
+ }
15524
+ function fieldLines(text5, cursor, width, o) {
15525
+ const indent2 = { text: " ".repeat(cells(o.prefix.text)) };
15526
+ const inv = (t) => ({ text: t, color: o.color, inverse: true });
15527
+ if (!text5) return [[o.prefix, ...cursor !== null ? [inv(" ")] : [], ...o.placeholder ? [o.placeholder] : []]];
15528
+ const rows = layout(text5, width - cells(o.prefix.text));
15529
+ return rows.map((r, k) => {
15530
+ const line = [k === 0 ? o.prefix : indent2];
15531
+ const at = cursor !== null && cursor >= r.a && cursor < r.b ? cursor : -1;
15532
+ if (at < 0) line.push({ text: text5.slice(r.a, r.b), color: o.color });
15533
+ else {
15534
+ const next2 = after({ text: text5, cursor: at });
15535
+ if (at > r.a) line.push({ text: text5.slice(r.a, at), color: o.color });
15536
+ line.push(inv(text5.slice(at, Math.min(next2, r.b))));
15537
+ if (next2 < r.b) line.push({ text: text5.slice(next2, r.b), color: o.color });
15538
+ }
15539
+ if (cursor === r.b && r.last) line.push(inv(" "));
15540
+ return line.filter((s) => s.text !== "" || s === line[0]);
15541
+ });
15542
+ }
15543
+ 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));
15544
+ var lineAt = (f) => ({
15545
+ line: f.text.slice(0, f.cursor).split("\n").length,
15546
+ lines: f.text.split("\n").length
15547
+ });
15548
+
15549
+ // src/tui/ui/Lines.tsx
15550
+ import { memo, useEffect as useEffect5, useRef as useRef4 } from "react";
15551
+ import { Box, Text, useBoxMetrics } from "ink";
15552
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
15553
+ function Live({ seg }) {
15554
+ const now2 = useNow();
15555
+ const { t } = useI18n();
15556
+ const l = seg.live;
15557
+ 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) });
15558
+ }
15559
+ var linked = (text5, url) => `\x1B]8;;${url.replace(/[\x00-\x1f\x7f]/g, "")}\x07${text5}\x1B]8;;\x07`;
15560
+ var LineText = memo(function LineText2({ line }) {
15561
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
15562
+ 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)),
15563
+ line.length === 0 ? " " : null
15564
+ ] });
15565
+ });
15566
+ function Lines({ lines, up = 0, onHeight }) {
15567
+ const ref = useRef4(null);
15568
+ const { height } = useBoxMetrics(ref);
15569
+ useEffect5(() => {
15570
+ if (height > 0) onHeight?.(height);
15571
+ }, [height, onHeight]);
15572
+ const h = Math.max(1, height);
15573
+ const back2 = Math.min(up, Math.max(0, lines.length - h));
15574
+ const shown = lines.slice(Math.max(0, lines.length - h - back2), lines.length - back2);
15575
+ 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)) });
15576
+ }
15577
+
15578
+ // src/tui/ui/TitledBox.tsx
15579
+ import { useRef as useRef5 } from "react";
15580
+ import { Box as Box2, Text as Text2, useBoxMetrics as useBoxMetrics2 } from "ink";
15581
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
15582
+ function TitledBox({ title: title2, mark, color: color2 = C.line, titleColor = C.dim, titleBold, double, foot, footColor = C.faint, children, flexGrow, height, width, flexShrink, minHeight }) {
15583
+ const b2 = double ? { tl: "\u2554", h: "\u2550", tr: "\u2557", bl: "\u255A", br: "\u255D" } : { tl: "\u256D", h: "\u2500", tr: "\u256E", bl: "\u2570", br: "\u256F" };
15584
+ const ref = useRef5(null);
15585
+ const { width: w } = useBoxMetrics2(ref);
15586
+ const text5 = (l) => l.map((s) => s.text).join("");
15587
+ const tail = mark ? [{ text: ` ${mark.text.toUpperCase()}`, color: mark.color }] : [];
15588
+ const room = Math.max(0, w - 4 - (cells(text5(tail)) < w - 8 ? cells(text5(tail)) : 0));
15589
+ const head = [{ text: ` ${title2.toUpperCase()}`, color: titleColor }];
15590
+ 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 }];
15591
+ const rest = Math.max(0, w - 3 - cells(text5(shown)));
15592
+ const f = foot ? text5(cut([{ text: foot }], Math.max(0, w - 6))) : "";
15593
+ return /* @__PURE__ */ jsxs2(Box2, { ref, flexDirection: "column", flexGrow, flexShrink, height, width, minHeight, overflow: "hidden", children: [
15594
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
15595
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15596
+ b2.tl,
15597
+ b2.h
15598
+ ] }),
15599
+ w > 0 ? shown.map((s, i) => /* @__PURE__ */ jsx4(Text2, { color: s.color, bold: titleBold, children: s.text }, i)) : null,
15600
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15601
+ b2.h.repeat(rest),
15602
+ b2.tr
15603
+ ] })
15604
+ ] }),
15605
+ /* @__PURE__ */ jsx4(Box2, { flexDirection: "column", borderStyle: double ? "double" : "round", borderTop: false, borderBottom: !f, borderColor: color2, paddingX: 1, flexGrow: 1, overflow: "hidden", children }),
15606
+ f ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
15607
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15608
+ b2.bl,
15609
+ b2.h.repeat(Math.max(1, w - 5 - cells(f))),
15610
+ " "
15611
+ ] }),
15612
+ /* @__PURE__ */ jsx4(Text2, { color: footColor, children: f }),
15613
+ /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
15614
+ " ",
15615
+ b2.h,
15616
+ b2.br
15617
+ ] })
15618
+ ] }) : null
15619
+ ] });
15620
+ }
15621
+
15622
+ // src/tui/ui/Field.tsx
15623
+ import { jsx as jsx5 } from "react/jsx-runtime";
15624
+ var fieldRows = (columns, rows) => rows < 24 ? 3 : columns >= 120 && rows >= 32 ? 8 : 6;
15625
+ var inHistory = (b2) => b2 !== null && b2.i < b2.items.length - 1;
15626
+ function Field({ value, onChange, onSubmit, onEscape, onBrowse, history = [], active, placeholder, title: title2, color: color2 = C.coral, titleColor }) {
15627
+ const { t, g } = useTui();
15628
+ const { columns, rows: height } = useScreenSize();
15629
+ const st = useRef6({ f: { text: value, cursor: value.length }, browse: null, pasted: 0 });
15630
+ const told = useRef6(value);
15631
+ if (value !== told.current) {
15632
+ told.current = value;
15633
+ st.current = { f: { text: value, cursor: value.length }, browse: null, pasted: 0 };
15634
+ }
15635
+ const [, render2] = useReducer((n) => n + 1, 0);
15636
+ const top = useRef6(0);
15637
+ const width = Math.max(10, columns - 4);
15638
+ const max = fieldRows(columns, height);
15639
+ const set = (f2, o = {}) => {
15640
+ const cur = st.current, changed = f2.text !== cur.f.text;
15641
+ let browse2 = cur.browse;
15642
+ if (browse2 && changed) {
15643
+ const items = [...browse2.items];
15644
+ items[browse2.i] = f2.text;
15645
+ browse2 = { ...browse2, items };
15646
+ }
15647
+ st.current = { f: f2, browse: browse2, pasted: o.paste ?? 0 };
15648
+ render2();
15649
+ if (changed && !inHistory(browse2)) {
15650
+ told.current = f2.text;
15651
+ onChange(f2.text);
16029
15652
  }
16030
15653
  };
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)]);
15654
+ const show = (b2, i) => {
15655
+ const next2 = b2 && i !== void 0 ? { ...b2, i } : b2;
15656
+ const text5 = next2 ? next2.items[next2.i] : st.current.f.text;
15657
+ st.current = { f: { text: text5, cursor: text5.length }, browse: next2, pasted: 0 };
15658
+ render2();
15659
+ onBrowse?.(inHistory(next2));
15660
+ };
15661
+ useInput((input, key) => {
15662
+ const { f: f2, browse: browse2 } = st.current;
15663
+ if (key.return && !key.meta && !key.shift) {
15664
+ const fromHistory = inHistory(browse2);
15665
+ if (onSubmit(f2.text, { fromHistory }) === false) return;
15666
+ if (fromHistory) show(browse2, browse2.items.length - 1);
15667
+ if (browse2) show(null);
16037
15668
  return;
16038
15669
  }
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 };
15670
+ if (key.return || input === "\n" || key.ctrl && input === "j") {
15671
+ set(insert(f2, "\n"));
15672
+ return;
15673
+ }
15674
+ if (key.escape) {
15675
+ if (inHistory(browse2)) {
15676
+ show(browse2, browse2.items.length - 1);
15677
+ show(null);
15678
+ return;
15679
+ }
15680
+ if (browse2) show(null);
15681
+ onEscape?.();
15682
+ return;
15683
+ }
15684
+ if (key.backspace) {
15685
+ set(key.meta ? killWord(f2) : back(f2));
15686
+ return;
15687
+ }
15688
+ if (key.delete) {
15689
+ set(del(f2));
15690
+ return;
15691
+ }
15692
+ if (key.ctrl && input === "u") {
15693
+ set({ text: "", cursor: 0 });
15694
+ return;
15695
+ }
15696
+ if (key.ctrl && input === "w") {
15697
+ set(killWord(f2));
15698
+ return;
15699
+ }
15700
+ if (key.ctrl && input === "a" || key.home) {
15701
+ set(home(f2));
15702
+ return;
15703
+ }
15704
+ if (key.ctrl && input === "e" || key.end) {
15705
+ set(end(f2));
15706
+ return;
15707
+ }
15708
+ if (key.leftArrow || key.meta && input === "b") {
15709
+ set(key.ctrl || key.meta ? wordLeft(f2) : left(f2));
15710
+ return;
15711
+ }
15712
+ if (key.rightArrow || key.meta && input === "f") {
15713
+ set(key.ctrl || key.meta ? wordRight(f2) : right(f2));
15714
+ return;
15715
+ }
15716
+ if (key.upArrow) {
15717
+ const up = vertical(f2, width - 2, -1);
15718
+ if (up) {
15719
+ set(up);
15720
+ return;
15721
+ }
15722
+ const b2 = browse2 ?? (history.length ? { items: [...history, f2.text], i: history.length } : null);
15723
+ if (b2 && b2.i > 0) show(b2, b2.i - 1);
15724
+ return;
15725
+ }
15726
+ if (key.downArrow) {
15727
+ const down = vertical(f2, width - 2, 1);
15728
+ if (down) {
15729
+ set(down);
15730
+ return;
15731
+ }
15732
+ if (inHistory(browse2)) show(browse2, browse2.i + 1);
15733
+ return;
15734
+ }
15735
+ if (key.ctrl || key.meta || key.tab || key.pageUp || key.pageDown) return;
15736
+ if (input) {
15737
+ const add = newlines(input);
15738
+ set(insert(f2, add), { paste: add.includes("\n") ? add.split("\n").length : 0 });
15739
+ }
15740
+ }, { isActive: active });
15741
+ usePaste((text5) => {
15742
+ const add = newlines(text5);
15743
+ set(insert(st.current.f, add), { paste: add.includes("\n") ? add.split("\n").length : 0 });
15744
+ }, { isActive: active });
15745
+ const { f, browse, pasted } = st.current;
15746
+ const prefix = { text: `${g.cursor} `, color: C.coral };
15747
+ const lines = fieldLines(f.text, active ? f.cursor : null, width, { prefix, color: C.fg, ...placeholder ? { placeholder: { text: f.text ? "" : ` ${placeholder}`, color: C.faint } } : {} });
15748
+ const row2 = rowOf(layout(f.text, width - 2), f.cursor);
15749
+ top.current = scrollTop(top.current, row2, lines.length, max);
15750
+ const shown = lines.slice(top.current, top.current + max);
15751
+ const below = lines.length - top.current - shown.length;
15752
+ const where = lineAt(f);
15753
+ const foot = inHistory(browse) ? t.field.history(browse.i + 1, browse.items.length - 1) : [
15754
+ top.current > 0 ? t.field.above(top.current) : "",
15755
+ below > 0 ? t.field.below(below) : "",
15756
+ pasted > 1 ? t.field.pasted(pasted) : active && where.lines > 1 ? t.field.line(where.line, where.lines) : ""
15757
+ ].filter(Boolean).join(" \xB7 ");
15758
+ 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
15759
  }
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);
15760
+
15761
+ // src/tui/ui/Frame.tsx
15762
+ import { Box as Box5, Text as Text4 } from "ink";
15763
+
15764
+ // src/tui/ui/KeyBar.tsx
15765
+ import { Box as Box4, Text as Text3 } from "ink";
15766
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
15767
+ var itemCells = ([k, label]) => cells(` ${k} `) + cells(` ${label}`);
15768
+ function fitKeys(keys, width, help) {
15769
+ const list3 = keys.filter((k) => k[0] !== help?.[0]);
15770
+ const fits = (l) => 1 + l.reduce((n, x, i) => n + itemCells(x) + (i ? 2 : 0), 0) <= width - 1;
15771
+ const tail = help ? [help] : [];
15772
+ while (list3.length && !fits([...list3, ...tail])) list3.pop();
15773
+ return [...list3, ...tail];
16049
15774
  }
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 ?? [] : []));
15775
+ function KeyBar({ keys }) {
15776
+ 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: [
15777
+ /* @__PURE__ */ jsxs3(Text3, { color: C.fg, backgroundColor: C.key, children: [
15778
+ " ",
15779
+ k,
15780
+ " "
15781
+ ] }),
15782
+ /* @__PURE__ */ jsxs3(Text3, { color: C.dim, children: [
15783
+ " ",
15784
+ label
15785
+ ] })
15786
+ ] }) }, k)) });
15787
+ }
15788
+
15789
+ // src/tui/ui/Frame.tsx
15790
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
15791
+ var MIN = { columns: 60, rows: 16 };
15792
+ function Frame({ crumb, crumbShort, keys, kept, children }) {
15793
+ const { d, machines, t, g, lastToast, editing, client } = useTui();
15794
+ const { rows, columns } = useScreenSize();
15795
+ const live = listed(d.sessions);
15796
+ const pending = live.filter(isPending);
15797
+ const waiting = pending.length + openDecisions(useTasks(client)).length;
15798
+ const status = d.status === "open" ? null : d.status === "connecting" ? t.bar.connecting : t.bar.unreachable;
15799
+ if (columns < MIN.columns || rows < MIN.rows) {
15800
+ const why = [...pending[0] ? [t.small.waits(pending[0].name, pending[0].pending?.kind === "question")] : [], ...kept ? [t.small.kept] : []].join(" \xB7 ");
15801
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", height: rows, width: columns, alignItems: "center", justifyContent: "center", children: [
15802
+ /* @__PURE__ */ jsxs4(Text4, { color: C.coral, bold: true, children: [
15803
+ g.logo,
15804
+ " ledge"
15805
+ ] }),
15806
+ /* @__PURE__ */ jsx7(Text4, { color: C.fg, children: t.small.tooSmall(columns, rows) }),
15807
+ /* @__PURE__ */ jsx7(Text4, { color: C.dim, children: t.small.atLeast(MIN.columns, MIN.rows) }),
15808
+ why ? /* @__PURE__ */ jsx7(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text4, { color: C.red, children: why }) }) : null
15809
+ ] });
15810
+ }
15811
+ 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);
15812
+ const room = columns - left2 - 2;
15813
+ const shown = cells(crumb) <= room ? crumb : crumbShort && cells(crumbShort) <= room ? crumbShort : "";
15814
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", height: rows, width: columns, children: [
15815
+ /* @__PURE__ */ jsxs4(Box5, { paddingX: 1, justifyContent: "space-between", height: 1, backgroundColor: C.bar, children: [
15816
+ /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate", children: [
15817
+ /* @__PURE__ */ jsxs4(Text4, { color: C.coral, children: [
15818
+ g.logo,
15819
+ " ledge"
15820
+ ] }),
15821
+ /* @__PURE__ */ jsxs4(Text4, { color: C.dim, children: [
15822
+ " ",
15823
+ t.bar.sessions(live.length),
15824
+ " \xB7 ",
15825
+ t.bar.machines(Math.max(1, machines.length))
15826
+ ] }),
15827
+ waiting > 0 ? /* @__PURE__ */ jsxs4(Text4, { color: C.red, children: [
15828
+ " \xB7 ",
15829
+ t.bar.waiting(waiting)
15830
+ ] }) : null,
15831
+ status ? /* @__PURE__ */ jsxs4(Text4, { color: C.yellow, children: [
15832
+ " ",
15833
+ status
15834
+ ] }) : null
15835
+ ] }),
15836
+ shown ? /* @__PURE__ */ jsx7(Text4, { wrap: "truncate", color: C.dim, children: shown }) : null
15837
+ ] }),
15838
+ /* @__PURE__ */ jsx7(Box5, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children }),
15839
+ 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,
15840
+ /* @__PURE__ */ jsx7(KeyBar, { keys: fitKeys(keys, columns, editing ? null : ["?", t.keys.help]) })
15841
+ ] });
15842
+ }
15843
+
15844
+ // src/tui/ui/SessionCard.tsx
15845
+ import { Box as Box6, Text as Text5 } from "ink";
15846
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
15847
+ function Button({ label, k, primary, danger, off }) {
15848
+ const bg = off ? void 0 : danger ? C.danger : primary ? C.primary : C.key;
15849
+ return /* @__PURE__ */ jsxs5(Text5, { children: [
15850
+ /* @__PURE__ */ jsxs5(Text5, { color: off ? C.faint : C.fg, backgroundColor: bg, children: [
15851
+ " ",
15852
+ label,
15853
+ " "
15854
+ ] }),
15855
+ /* @__PURE__ */ jsxs5(Text5, { color: C.dim, backgroundColor: bg, children: [
15856
+ k,
15857
+ " "
15858
+ ] })
15859
+ ] });
15860
+ }
15861
+ function SessionCard({ s, note }) {
15862
+ const tui = useTui();
15863
+ const { t, g } = tui;
15864
+ const threshold = useContextNear(tui.client);
15865
+ const state2 = botState(s);
15866
+ const helpers = liveHelpers(s);
15867
+ const p = s.pending;
15868
+ const canAct = !s.offline && s.source !== "hook_only";
15869
+ const verb = !p ? "" : p.kind === "question" ? t.overview.asks : p.kind === "plan" ? t.overview.plan : p.kind === "diff" ? t.overview.diff : t.overview.wantsToRun;
15870
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
15871
+ /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
15872
+ /* @__PURE__ */ jsxs5(Text5, { color: STATE_COLOR[state2], children: [
15873
+ g[state2],
15874
+ " ",
15875
+ t.state[state2]
15876
+ ] }),
15877
+ /* @__PURE__ */ jsxs5(Text5, { color: C.dim, children: [
15878
+ " ",
15879
+ TOOL_NAME[s.tool],
15880
+ " \xB7 ",
15881
+ modelLabel(s, t.session.defaultModel)
15882
+ ] }),
15883
+ s.context ? /* @__PURE__ */ jsxs5(Text5, { color: contextNear(s.context, threshold) ? C.yellow : C.dim, children: [
15884
+ " \xB7 ",
15885
+ t.overview.context(Math.round(s.context.used / s.context.window * 100))
15886
+ ] }) : null,
15887
+ /* @__PURE__ */ jsxs5(Text5, { color: C.dim, children: [
15888
+ " \xB7 ",
15889
+ machineLabel(s),
15890
+ s.branch ? ` \xB7 ${g.branch} ${s.branch}` : ""
15891
+ ] }),
15892
+ s.offline ? /* @__PURE__ */ jsxs5(Text5, { color: C.yellow, children: [
15893
+ " ",
15894
+ t.overview.offline
15895
+ ] }) : s.source === "hook_only" ? /* @__PURE__ */ jsxs5(Text5, { color: C.faint, children: [
15896
+ " ",
15897
+ t.overview.readOnly
15898
+ ] }) : null
15899
+ ] }),
15900
+ s.handover || s.relieves ? /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
15901
+ s.handover ? /* @__PURE__ */ jsxs5(Text5, { color: s.handover.state === "failed" ? C.red : C.yellow, children: [
15902
+ " \u21C4 ",
15903
+ s.handover.state === "done" && s.handover.toName ? t.overview.relievedBy(s.handover.toName) : t.overview.relief[s.handover.state],
15904
+ s.handover.reason ? ` : ${s.handover.reason}` : ""
15905
+ ] }) : null,
15906
+ s.relieves ? /* @__PURE__ */ jsxs5(Text5, { color: C.dim, children: [
15907
+ " \u21C4 ",
15908
+ t.overview.relieves(s.relieves.name)
15909
+ ] }) : null
15910
+ ] }) : null,
15911
+ helpers.length > 0 ? /* @__PURE__ */ jsxs5(Text5, { color: C.violet, wrap: "truncate", children: [
15912
+ " ",
15913
+ helpers.map((h) => `${g.helper} ${h.kind} \xB7 ${oneLine(h.task)}`).join(" ")
15914
+ ] }) : null,
15915
+ p ? /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginTop: 1, children: [
15916
+ /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
15917
+ /* @__PURE__ */ jsx8(Text5, { color: STATE_COLOR[state2], children: verb }),
15918
+ " ",
15919
+ /* @__PURE__ */ jsx8(Text5, { color: C.fg, bold: true, children: oneLine(p.text) }),
15920
+ p.tool ? /* @__PURE__ */ jsxs5(Text5, { color: C.faint, children: [
15921
+ " ",
15922
+ p.tool
15923
+ ] }) : null
15924
+ ] }),
15925
+ /* @__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: [
15926
+ /* @__PURE__ */ jsx8(Button, { label: t.overview.allow, k: "y", primary: true, off: !canAct }),
15927
+ /* @__PURE__ */ jsx8(Button, { label: t.overview.deny, k: "n", off: !canAct }),
15928
+ offers(s.tool, p.kind, "always") ? /* @__PURE__ */ jsx8(Button, { label: t.overview.always, k: "A", off: !canAct }) : null,
15929
+ note && offers(s.tool, p.kind, "note") ? /* @__PURE__ */ jsx8(Button, { label: t.overview.denyWithNote, k: "r", off: !canAct }) : null
15930
+ ] }) })
15931
+ ] }) : s.lastReply && state2 === "idle" ? /* @__PURE__ */ jsxs5(Text5, { color: C.dim, wrap: "truncate", children: [
15932
+ " ",
15933
+ oneLine(s.lastReply.text)
15934
+ ] }) : null
15935
+ ] });
16053
15936
  }
16054
15937
 
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);
15938
+ // src/tui/ui/Tail.tsx
15939
+ import { useRef as useRef7 } from "react";
15940
+ import { Box as Box7, Text as Text6, useBoxMetrics as useBoxMetrics3 } from "ink";
15941
+ import { jsx as jsx9 } from "react/jsx-runtime";
15942
+ function Tail({ lines, color: color2, empty: empty3, emptyColor }) {
15943
+ const ref = useRef7(null);
15944
+ const { height } = useBoxMetrics3(ref);
15945
+ let end2 = lines.length;
15946
+ while (end2 > 0 && !lines[end2 - 1].trim()) end2--;
15947
+ const shown = end2 === 0 ? [] : lines.slice(0, end2).slice(-Math.max(1, height));
15948
+ 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
15949
  }
16083
15950
 
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;
15951
+ // src/tui/screens/Overview.tsx
15952
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
15953
+ var WIDE = 100;
15954
+ function windowed(rows, selectedKey, height) {
15955
+ if (rows.length <= height) return [...rows];
15956
+ const at = Math.max(0, rows.findIndex((r) => r.key === selectedKey));
15957
+ const start = Math.min(Math.max(0, at - Math.floor(height / 2)), rows.length - height);
15958
+ return rows.slice(start, start + height);
15959
+ }
15960
+ function Overview() {
15961
+ const tui = useTui();
15962
+ const { d, projects, t, g, selected, select, actions, go, attach } = tui;
15963
+ const { columns, rows: height } = useScreenSize();
15964
+ const wide = columns >= WIDE;
15965
+ const left2 = wide ? Math.min(64, Math.max(44, Math.floor(columns * 0.42))) : columns;
15966
+ const teams = useTeams(tui.client);
15967
+ const tasks = useTasks(tui.client);
15968
+ const view = useMemo2(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
15969
+ const sel = selected?.id ?? null;
15970
+ const closed = closedLedgeTasks(tasks);
15971
+ const decisions = useMemo2(() => openDecisions(tasks), [tasks]);
15972
+ const [deciding, setDeciding] = useState3(null);
15973
+ const decision = deciding ? decisions.find((x) => x.id === deciding.id) : void 0;
15974
+ useEffect6(() => {
15975
+ if (deciding && !decision) setDeciding(null);
15976
+ }, [deciding, decision]);
15977
+ useEditing(!!deciding && !deciding.confirm);
15978
+ const answer = (x, text5) => {
15979
+ const rid = randomUUID();
15980
+ const off = tui.client.onMessage((m) => {
15981
+ if (m.type !== "action.result" || m.rid !== rid) return;
15982
+ off();
15983
+ tui.toast(m.reason === void 0 ? { text: t.toast.reply(x.giverName), tone: "ok" } : { text: t.toast.answerFailed(m.reason), tone: "error" });
15984
+ });
15985
+ setTimeout(off, 1e4);
15986
+ tui.client.send({ type: "action.run", rid, action: "task.answer", args: { task: x.id, answer: text5 }, confirmed: true });
15987
+ };
15988
+ const threshold = useContextNear(tui.client);
15989
+ const near = useMemo2(() => nearContext(d.sessions, threshold), [d.sessions, threshold]);
15990
+ const [tasksShown, setTasksShown] = useState3(true);
15991
+ const [joining, setJoining] = useState3(null);
15992
+ 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;
15993
+ const seatTeam = selected?.team ? teams?.find((x) => x.id === selected.team && x.state !== "dissolved") : void 0;
15994
+ const choices2 = node2?.teams.map((x) => x.team).filter((x) => x.state !== "dissolved" && x.id !== selected?.team) ?? [];
15995
+ useScreenKeys((input, key) => {
15996
+ if (deciding) {
15997
+ if (!deciding.confirm) {
15998
+ if (key.escape) setDeciding(null);
15999
+ return true;
16000
+ }
16001
+ if (input === "y" && decision) {
16002
+ setDeciding(null);
16003
+ answer(decision, deciding.draft.trim());
16004
+ return true;
16005
+ }
16006
+ if (input === "n" || key.escape) {
16007
+ setDeciding({ ...deciding, confirm: false });
16008
+ return true;
16009
+ }
16010
+ return true;
16098
16011
  }
16099
- if (key.upArrow || input === "k") {
16100
- setAt((i) => (i - 1 + rows.length) % rows.length);
16101
- setEffort(null);
16102
- return;
16012
+ if (input === "D") {
16013
+ if (decisions[0]) setDeciding({ id: decisions[0].id, draft: "", confirm: false });
16014
+ return true;
16103
16015
  }
16104
- if (key.downArrow || input === "j") {
16105
- setAt((i) => (i + 1) % rows.length);
16106
- setEffort(null);
16107
- return;
16016
+ if (joining) {
16017
+ if (key.escape) {
16018
+ setJoining(null);
16019
+ return true;
16020
+ }
16021
+ if (input === "j" || key.downArrow) {
16022
+ setJoining({ index: (joining.index + 1) % Math.max(1, choices2.length) });
16023
+ return true;
16024
+ }
16025
+ if (input === "k" || key.upArrow) {
16026
+ setJoining({ index: (joining.index + choices2.length - 1) % Math.max(1, choices2.length) });
16027
+ return true;
16028
+ }
16029
+ if (key.return && selected && choices2[joining.index]) {
16030
+ const team = choices2[joining.index];
16031
+ const role = selected.role ?? team.seats.find((x) => x.sessionId === null)?.role;
16032
+ setJoining(null);
16033
+ if (!role) {
16034
+ tui.toast({ text: t.overview.noTeamHere, tone: "info" });
16035
+ return true;
16036
+ }
16037
+ tui.client.send({ type: "team.join", team: team.id, session: selected.id, role });
16038
+ tui.toast({ text: t.toast.joined(selected.name, team.name), tone: "ok" });
16039
+ return true;
16040
+ }
16041
+ return true;
16108
16042
  }
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;
16043
+ if (input === "J" && selected) {
16044
+ if (choices2.length) setJoining({ index: 0 });
16045
+ else tui.toast({ text: t.overview.noTeamHere, tone: "info" });
16046
+ return true;
16113
16047
  }
16114
- if (key.return) onPick({ model: row2.id, effort: row2.efforts.length ? effort ?? null : null });
16048
+ if (input === "R") {
16049
+ 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;
16050
+ if (resting) {
16051
+ tui.client.send({ type: "team.resume", team: resting.id });
16052
+ tui.toast({ text: t.toast.resumed(resting.name), tone: "ok" });
16053
+ }
16054
+ return true;
16055
+ }
16056
+ if (input === "T") {
16057
+ setTasksShown((v) => !v);
16058
+ return true;
16059
+ }
16060
+ if (input === "H") {
16061
+ 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];
16062
+ if (!s) return true;
16063
+ const rid = randomUUID();
16064
+ const off = tui.client.onMessage((m) => {
16065
+ if (m.type === "session.handover.result" && m.rid === rid) {
16066
+ off();
16067
+ tui.toast(m.ok ? { text: t.toast.handover(s.name), tone: "ok" } : { text: t.toast.handoverRefused(m.reason ?? "?"), tone: "info" });
16068
+ }
16069
+ });
16070
+ setTimeout(off, 5e3);
16071
+ tui.client.send({ type: "session.handover", rid, id: s.id });
16072
+ return true;
16073
+ }
16074
+ if (input === "x" && closed[0]) {
16075
+ tui.client.send({ type: "task.cmd", rid: randomUUID(), by: LEDGE_GIVER, op: "seen", task: closed[0].id });
16076
+ return true;
16077
+ }
16078
+ if (input === "j" || key.downArrow) {
16079
+ select(step(view.order, sel, 1));
16080
+ return true;
16081
+ }
16082
+ if (input === "k" || key.upArrow) {
16083
+ select(step(view.order, sel, -1));
16084
+ return true;
16085
+ }
16086
+ if (key.tab) {
16087
+ const next2 = nextWaiting(view.yours, sel);
16088
+ if (next2) select(next2);
16089
+ return true;
16090
+ }
16091
+ if (key.return) {
16092
+ if (selected) go("session");
16093
+ return true;
16094
+ }
16095
+ if (!selected) return false;
16096
+ if (input === "a") {
16097
+ attach(selected);
16098
+ return true;
16099
+ }
16100
+ if (input === "r" && selected.pending) {
16101
+ go("session");
16102
+ return true;
16103
+ }
16104
+ if (/^[1-9]$/.test(input) && selected.pending?.kind !== "question") return false;
16105
+ if (input === "y" || input === "n" || input === "A" || /^[1-9]$/.test(input)) return actions.key(selected, input);
16106
+ if (input === "i" && selected.state === "working") {
16107
+ actions.interrupt(selected);
16108
+ return true;
16109
+ }
16110
+ return false;
16115
16111
  });
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 : " ",
16112
+ const tree2 = view.tree.flatMap((n) => {
16113
+ const teamed = n.teams.length > 0;
16114
+ const common = n.branches.length > 0;
16115
+ return [
16116
+ { kind: "project", key: n.key, label: n.label, color: projectColor(n.color), count: n.count },
16117
+ ...teamed && common ? [{ kind: "common", key: `${n.key}|common` }] : [],
16118
+ ...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 }))]),
16119
+ ...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 }))])
16120
+ ];
16121
+ });
16122
+ const yoursCount = view.yours.length + decisions.length + closed.length + near.length;
16123
+ const yoursHeight = Math.max(1, Math.min(yoursCount, 6));
16124
+ const joinHeight = joining && selected ? Math.max(1, choices2.length) + 2 : 0;
16125
+ const answerLines = deciding?.confirm ? wrap2(deciding.draft.trim(), Math.max(10, columns - 4)) : [];
16126
+ const decideHeight = !deciding ? 0 : deciding.confirm ? Math.min(answerLines.length, Math.max(1, height - 12)) + 2 : fieldRows(columns, height) + 2;
16127
+ const treeHeight = Math.max(3, height - 3 - (yoursHeight + 2) - joinHeight - decideHeight - 3);
16128
+ const inner = left2 - 4, nameW = 7;
16129
+ const stateW = Math.max(...Object.values(t.state).map((x) => x.length)) + 1;
16130
+ const topicW = Math.max(6, inner - 6 - nameW - stateW - 3);
16131
+ const askW = Math.max(6, inner - 2 - nameW - 9 - 7);
16132
+ 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) => {
16133
+ const st = botState(s);
16134
+ return /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
16135
+ /* @__PURE__ */ jsxs6(Text7, { color: STATE_COLOR[st], children: [
16136
+ g[st],
16122
16137
  " "
16123
16138
  ] }),
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(" ")
16139
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, bold: true, children: fit(s.name, nameW) }),
16140
+ /* @__PURE__ */ jsx10(Text7, { color: C.dim, children: fit(s.repo ?? s.project, 9) }),
16141
+ /* @__PURE__ */ jsx10(Text7, { color: STATE_COLOR[st], children: fit(oneLine(s.pending?.text ?? ""), askW) }),
16142
+ /* @__PURE__ */ jsxs6(Text7, { color: C.faint, children: [
16143
+ " ",
16144
+ /* @__PURE__ */ jsx10(Ago, { iso: s.lastActivityAt })
16145
+ ] })
16146
+ ] }, s.id);
16147
+ }), ...decisions.slice(0, Math.max(0, yoursHeight - view.yours.length)).map((x) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16148
+ /* @__PURE__ */ jsxs6(Text7, { color: C.red, children: [
16149
+ g.question,
16150
+ " "
16151
+ ] }),
16152
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: t.overview.decision(x.giverName, x.title) }),
16153
+ x.text ? /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
16154
+ " \xB7 ",
16155
+ oneLine(x.text)
16156
+ ] }) : null
16157
+ ] }, `decision:${x.id}`)), ...closed.slice(0, Math.max(0, yoursHeight - view.yours.length - decisions.length)).map((x) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16158
+ /* @__PURE__ */ jsxs6(Text7, { color: x.state === "done" ? C.green : C.yellow, children: [
16159
+ x.state === "done" ? g.ok : g.fail,
16160
+ " "
16161
+ ] }),
16162
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: t.overview.ledgeTask(x.ownerName, x.title, x.state) })
16163
+ ] }, 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: [
16164
+ /* @__PURE__ */ jsxs6(Text7, { color: C.yellow, children: [
16165
+ s.handover?.state === "failed" ? g.fail : "\u25D4",
16166
+ " "
16167
+ ] }),
16168
+ /* @__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)) })
16169
+ ] }, `near:${s.id}`))] });
16170
+ 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: [
16171
+ /* @__PURE__ */ jsx10(Text7, { color: r.color, children: g.project }),
16172
+ " ",
16173
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, bold: true, children: r.label }),
16174
+ " ",
16175
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.overview.sessions(r.count) })
16176
+ ] }, r.key) : r.kind === "common" ? /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", color: C.faint, children: [
16177
+ " ",
16178
+ g.common,
16179
+ " ",
16180
+ t.overview.common
16181
+ ] }, r.key) : r.kind === "team" ? /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16182
+ /* @__PURE__ */ jsx10(Text7, { children: " " }),
16183
+ /* @__PURE__ */ jsx10(Text7, { color: C.coral, children: g.team }),
16184
+ " ",
16185
+ /* @__PURE__ */ jsx10(Text7, { color: r.team.state === "resting" ? C.dim : C.fg, bold: true, children: r.team.name }),
16186
+ " ",
16187
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: r.team.state === "resting" ? t.overview.resting : t.overview.teamLine(r.done, r.given) })
16188
+ ] }, r.key) : r.kind === "branch" ? /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", color: C.faint, children: [
16189
+ " ".repeat(r.indent),
16190
+ g.branch,
16191
+ " ",
16192
+ r.branch
16193
+ ] }, 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)) });
16194
+ 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]] : [
16195
+ ["j k", t.keys.navigate],
16196
+ ["\u21B5", t.keys.open],
16197
+ ...view.yours.length ? [["tab", t.keys.next], ["y n", t.keys.answer]] : [],
16198
+ ["a", t.keys.attach],
16199
+ ...seatTeam ? [["T", t.keys.tasks]] : [],
16200
+ ...choices2.length ? [["J", t.keys.join]] : [],
16201
+ ...view.tree.some((n) => n.teams.some((x) => x.team.state === "resting")) ? [["R", t.keys.resume]] : [],
16202
+ ...decisions.length ? [["D", t.keys.decide]] : [],
16203
+ ...near.length ? [["H", t.keys.handover]] : [],
16204
+ ["/", t.keys.ask],
16205
+ ["+", t.keys.launch],
16206
+ [",", t.keys.settings],
16207
+ ["?", t.keys.help],
16208
+ ["q", t.keys.quit]
16209
+ ];
16210
+ 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) : [];
16211
+ return /* @__PURE__ */ jsxs6(Frame, { crumb: tui.machines.map((m) => `${m.label} ${m.ready ? g.machine : "\u25CB"}`).join(" "), keys, children: [
16212
+ /* @__PURE__ */ jsxs6(Box8, { flexGrow: 1, children: [
16213
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", width: left2, flexShrink: 0, children: [
16214
+ yoursBox,
16215
+ 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: [
16216
+ /* @__PURE__ */ jsx10(Text7, { color: C.coral, children: g.team }),
16217
+ " ",
16218
+ x.name
16219
+ ] }, x.id)) }) : null,
16220
+ treeBox
16221
+ ] }),
16222
+ wide ? /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", flexGrow: 1, children: [
16223
+ selected ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
16224
+ /* @__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 }) }),
16225
+ 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: [
16226
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: x.id }),
16227
+ " ",
16228
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: x.title }),
16229
+ " ",
16230
+ /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
16231
+ x.giverName,
16232
+ " ",
16233
+ g.arrow,
16234
+ " ",
16235
+ x.ownerName
16236
+ ] }),
16237
+ " ",
16238
+ /* @__PURE__ */ jsx10(Text7, { color: x.state === "done" ? C.green : x.state === "taken" ? C.blue : C.faint, children: t.ask.taskState[x.state] })
16239
+ ] }, x.id)) }) : null,
16240
+ /* @__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 }) })
16241
+ ] }) : /* @__PURE__ */ jsx10(TitledBox, { title: t.tabs.session, flexGrow: 1, children: /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.session.noSession }) }),
16242
+ /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.exchanges, height: 5, children: (() => {
16243
+ const list3 = exchanges(d.sessions, 3, tasks, t.overview.ledge);
16244
+ return list3.length === 0 ? /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.overview.noExchange }) : list3.map((x, i) => /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", children: [
16245
+ /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: clock(x.at) }),
16246
+ " ",
16247
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: x.from }),
16248
+ " ",
16249
+ /* @__PURE__ */ jsx10(Text7, { color: x.kind === "task" ? C.green : C.violet, children: g.arrow }),
16250
+ " ",
16251
+ /* @__PURE__ */ jsx10(Text7, { color: C.fg, children: x.to }),
16252
+ " ",
16253
+ x.kind === "task" ? /* @__PURE__ */ jsxs6(Text7, { color: C.green, children: [
16254
+ g.task,
16255
+ " "
16256
+ ] }) : null,
16257
+ /* @__PURE__ */ jsx10(Text7, { color: C.dim, children: oneLine(x.summary) })
16258
+ ] }, i));
16259
+ })() })
16128
16260
  ] }) : null
16129
- ] }, r.id ?? "-"))
16261
+ ] }),
16262
+ 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(
16263
+ Field,
16264
+ {
16265
+ title: t.overview.decisionTitle(decision.giverName, decision.title),
16266
+ value: deciding.draft,
16267
+ onChange: (v) => setDeciding((x) => x ? { ...x, draft: v } : x),
16268
+ onSubmit: (v) => {
16269
+ if (!v.trim()) return false;
16270
+ setDeciding((x) => x ? { ...x, draft: v, confirm: true } : x);
16271
+ },
16272
+ onEscape: () => setDeciding(null),
16273
+ active: true,
16274
+ placeholder: t.overview.decisionHint
16275
+ }
16276
+ ) : null
16130
16277
  ] });
16131
16278
  }
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
16279
+ function SessionRow({ s, on, nameW, topicW, stateW, indent: indent2 = 0 }) {
16280
+ const { t, g } = useTui();
16281
+ const st = botState(s);
16282
+ const n = helperCount(s);
16283
+ return /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: on ? C.sel : void 0, children: [
16284
+ /* @__PURE__ */ jsx10(Text7, { color: on ? C.coral : C.faint, children: on ? "\u258C" : " " }),
16285
+ /* @__PURE__ */ jsx10(Text7, { children: " ".repeat(2 + indent2) }),
16286
+ /* @__PURE__ */ jsxs6(Text7, { color: s.offline ? C.faint : STATE_COLOR[st], children: [
16287
+ g[st],
16288
+ " "
16289
+ ] }),
16290
+ /* @__PURE__ */ jsx10(Text7, { color: s.offline ? C.dim : C.fg, bold: true, children: fit(s.name, nameW) }),
16291
+ s.role ? /* @__PURE__ */ jsxs6(Text7, { color: C.violet, children: [
16292
+ "@",
16293
+ s.role,
16294
+ " "
16295
+ ] }) : null,
16296
+ /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
16297
+ fit(oneLine(s.topic ?? s.title), topicW),
16298
+ " "
16299
+ ] }),
16300
+ /* @__PURE__ */ jsx10(Text7, { color: s.offline ? C.faint : STATE_COLOR[st], children: fit(s.offline ? t.overview.offline : t.state[st], stateW).trimEnd() }),
16301
+ n > 0 ? /* @__PURE__ */ jsxs6(Text7, { color: C.violet, children: [
16302
+ " +",
16303
+ n
16304
+ ] }) : null,
16305
+ s.machineName && s.machineId !== "local" ? /* @__PURE__ */ jsxs6(Text7, { color: C.faint, children: [
16306
+ " \xB7 ",
16307
+ machineLabel(s)
16308
+ ] }) : null
16148
16309
  ] });
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
16310
  }
16161
16311
 
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";
16312
+ // src/tui/screens/Session.tsx
16313
+ import { useEffect as useEffect8, useMemo as useMemo3, useState as useState7 } from "react";
16314
+ import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
16165
16315
 
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++;
16316
+ // src/tui/images.tsx
16317
+ import { mkdir, writeFile } from "fs/promises";
16318
+ import { tmpdir as tmpdir2 } from "os";
16319
+ import { join as join3 } from "path";
16320
+ import { useRef as useRef8, useState as useState4 } from "react";
16321
+ import { Text as Text8 } from "ink";
16322
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
16323
+ var counter2 = 0;
16324
+ var INLINE_BUDGET = MAX_FRAME - 64 * 1024;
16325
+ function useChips(put) {
16326
+ const { t, toast } = useTui();
16327
+ const [items, setItems] = useState4([]);
16328
+ const held = useRef8([]);
16329
+ const commit = (next2) => {
16330
+ held.current = next2;
16331
+ setItems(next2);
16332
+ };
16333
+ const patch = (key, over) => commit(held.current.map((c) => c.key === key ? { ...c, ...over } : c));
16334
+ const reasonOf = (e) => {
16335
+ const reason = e instanceof AttachmentFailure ? e.reason : null;
16336
+ return reason === "too_large" ? t.images.tooLarge : reason === "not_image" ? t.images.notImage : t.images.failed;
16337
+ };
16338
+ return {
16339
+ items,
16340
+ add({ image: image2, width, height, bytes }) {
16341
+ if (held.current.length >= ATTACHMENTS_MAX) {
16342
+ toast({ text: t.images.max, tone: "info" });
16343
+ return;
16226
16344
  }
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;
16345
+ const key = counter2++;
16346
+ const inline2 = held.current.reduce((n, c) => n + (c.image?.data.length ?? 0), 0);
16347
+ if (bytes > ATTACHMENT_LOCAL_MAX || !put && inline2 + image2.data.length > INLINE_BUDGET) {
16348
+ commit([...held.current, { key, width, height, state: "refused", reason: t.images.tooLarge }]);
16349
+ return;
16350
+ }
16351
+ if (!put) {
16352
+ commit([...held.current, { key, width, height, state: "ready", image: image2 }]);
16353
+ return;
16354
+ }
16355
+ commit([...held.current, { key, width, height, state: "uploading" }]);
16356
+ put(image2).then((ref) => patch(key, { state: "ready", id: ref.id }), (e) => patch(key, { state: "refused", reason: reasonOf(e) }));
16357
+ },
16358
+ removeLast() {
16359
+ commit(held.current.slice(0, -1));
16360
+ },
16361
+ ready: items.every((c) => c.state === "ready"),
16362
+ ids: () => held.current.flatMap((c) => c.state === "ready" && c.id ? [c.id] : []),
16363
+ uploads: () => held.current.flatMap((c) => c.state === "ready" && c.image ? [c.image] : []),
16364
+ clear() {
16365
+ const taken = held.current;
16366
+ commit([]);
16367
+ return taken;
16368
+ },
16369
+ putBack(taken) {
16370
+ commit([...taken, ...held.current].slice(0, ATTACHMENTS_MAX));
16231
16371
  }
16232
- start = i + 1;
16233
- }
16234
- return rows;
16372
+ };
16235
16373
  }
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 };
16374
+ function ChipsLine({ chips }) {
16375
+ const { t } = useTui();
16376
+ if (chips.length === 0) return null;
16377
+ return /* @__PURE__ */ jsx11(Text8, { wrap: "truncate", children: chips.map((c, i) => /* @__PURE__ */ jsxs7(Text8, { children: [
16378
+ i ? " " : "",
16379
+ /* @__PURE__ */ jsxs7(Text8, { color: c.state === "refused" ? C.red : C.dim, children: [
16380
+ "\u25A3 ",
16381
+ t.images.capture,
16382
+ " ",
16383
+ c.width,
16384
+ "\xD7",
16385
+ c.height,
16386
+ " ",
16387
+ c.state === "uploading" ? "\u2026" : "\u2715"
16388
+ ] }),
16389
+ c.reason ? /* @__PURE__ */ jsxs7(Text8, { color: C.red, children: [
16390
+ " ",
16391
+ c.reason
16392
+ ] }) : null
16393
+ ] }, c.key)) });
16394
+ }
16395
+ var EXT = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" };
16396
+ function usePaste2(chips) {
16397
+ const { readImage, toast, t } = useTui();
16398
+ const reading = useRef8(false);
16399
+ return () => {
16400
+ if (reading.current) return;
16401
+ reading.current = true;
16402
+ void readImage().then((got) => {
16403
+ if (got) chips.add(got);
16404
+ else toast({ text: t.images.none, tone: "info" });
16405
+ }, () => toast({ text: t.images.none, tone: "info" })).finally(() => {
16406
+ reading.current = false;
16407
+ });
16408
+ };
16251
16409
  }
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 });
16410
+ function useOpenImage() {
16411
+ const { client, openFile, toast, t } = useTui();
16412
+ const presses = useRef8(0);
16413
+ const last = useRef8("");
16414
+ return (items) => {
16415
+ const signature = items.map((x) => x.ref.id).join(",");
16416
+ if (signature !== last.current) {
16417
+ last.current = signature;
16418
+ presses.current = 0;
16266
16419
  }
16267
- if (cursor === r.b && r.last) line.push(inv(" "));
16268
- return line.filter((s) => s.text !== "" || s === line[0]);
16269
- });
16420
+ const item = items[items.length - 1 - presses.current++ % items.length];
16421
+ if (!item) return;
16422
+ const { ref, route } = item;
16423
+ void (async () => {
16424
+ const { mime, data } = await getImage(client, ref.id, route);
16425
+ const dir = join3(tmpdir2(), "ledge-images");
16426
+ await mkdir(dir, { recursive: true, mode: 448 });
16427
+ const file = join3(dir, `${ref.id}.${EXT[mime]}`);
16428
+ await writeFile(file, Buffer.from(data, "base64"), { mode: 384 });
16429
+ await openFile(file);
16430
+ })().catch((e) => toast({ text: t.images.openFailed(e instanceof AttachmentFailure ? e.reason : e.message), tone: "error" }));
16431
+ };
16270
16432
  }
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
16433
 
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
- };
16434
+ // src/tui/ui/ModelPicker.tsx
16435
+ import { useState as useState6 } from "react";
16436
+ import { Box as Box9, Text as Text9, useInput as useInput2 } from "ink";
16437
+
16438
+ // ../fleet/src/models.ts
16439
+ import { useEffect as useEffect7, useState as useState5 } from "react";
16440
+ var IDLE = { status: "idle", models: [] };
16441
+ var LOADING = { status: "loading", models: [] };
16442
+ var known = /* @__PURE__ */ new Map();
16443
+ var next = 0;
16444
+ function useModels(client, machine, tool) {
16445
+ const key = machine === null || tool === null ? null : `${machine}|${tool}`;
16446
+ const [answer, setAnswer] = useState5(null);
16447
+ useEffect7(() => {
16448
+ if (key === null || machine === null || tool === null || known.has(key)) return;
16449
+ const requestId = `models-${++next}`;
16450
+ const off = client.onMessage((m) => {
16451
+ if (m.type !== "models.listing" || m.requestId !== requestId) return;
16452
+ const state2 = { status: "ready", models: m.models, ...m.error ? { error: m.error } : {} };
16453
+ if (!m.error) known.set(key, state2);
16454
+ setAnswer({ key, state: state2 });
16455
+ });
16456
+ client.send({ type: "models.list", requestId, tool, machineId: machine });
16457
+ return off;
16458
+ }, [client, key, machine, tool]);
16459
+ if (key === null) return IDLE;
16460
+ return known.get(key) ?? (answer && answer.key === key ? answer.state : LOADING);
16461
+ }
16462
+
16463
+ // src/tui/ui/ModelPicker.tsx
16464
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
16465
+ function ModelPicker({ machine, tool, current: current2, onPick, onCancel }) {
16466
+ const { client, t, g } = useTui();
16467
+ useEditing(true);
16468
+ const models = useModels(client, machine, tool);
16469
+ 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 }))];
16470
+ const [at, setAt] = useState6(() => Math.max(0, rows.findIndex((r) => r.id === current2.model)));
16471
+ const [effort, setEffort] = useState6(current2.effort);
16472
+ const row2 = rows[Math.min(at, rows.length - 1)];
16316
16473
  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
16474
  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));
16475
+ onCancel();
16365
16476
  return;
16366
16477
  }
16367
- if (key.rightArrow || key.meta && input === "f") {
16368
- set(key.ctrl || key.meta ? wordRight(f2) : right(f2));
16478
+ if (key.upArrow || input === "k") {
16479
+ setAt((i) => (i - 1 + rows.length) % rows.length);
16480
+ setEffort(null);
16369
16481
  return;
16370
16482
  }
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);
16483
+ if (key.downArrow || input === "j") {
16484
+ setAt((i) => (i + 1) % rows.length);
16485
+ setEffort(null);
16379
16486
  return;
16380
16487
  }
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);
16488
+ if ((key.leftArrow || key.rightArrow) && row2.efforts.length) {
16489
+ const cur = Math.max(0, row2.efforts.indexOf(effort ?? row2.defaultEffort ?? row2.efforts[0]));
16490
+ setEffort(row2.efforts[(cur + (key.rightArrow ? 1 : -1) + row2.efforts.length) % row2.efforts.length]);
16388
16491
  return;
16389
16492
  }
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)) });
16493
+ if (key.return) onPick({ model: row2.id, effort: row2.efforts.length ? effort ?? null : null });
16494
+ });
16495
+ if (models.status !== "ready") return /* @__PURE__ */ jsx12(Text9, { color: C.faint, children: t.launch.loading });
16496
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
16497
+ models.error ? /* @__PURE__ */ jsx12(Text9, { color: C.yellow, children: models.error }) : null,
16498
+ rows.map((r, i) => /* @__PURE__ */ jsxs8(Text9, { wrap: "truncate", backgroundColor: i === at ? C.sel : void 0, children: [
16499
+ /* @__PURE__ */ jsxs8(Text9, { color: i === at ? C.coral : C.faint, children: [
16500
+ i === at ? g.cursor : " ",
16501
+ " "
16502
+ ] }),
16503
+ /* @__PURE__ */ jsx12(Text9, { color: C.fg, children: r.label }),
16504
+ i === at && r.efforts.length ? /* @__PURE__ */ jsxs8(Text9, { color: C.dim, children: [
16505
+ " ",
16506
+ r.efforts.map((e) => e === (effort ?? r.defaultEffort) ? `${g.prev} ${e} ${g.next}` : e).join(" ")
16507
+ ] }) : null
16508
+ ] }, r.id ?? "-"))
16509
+ ] });
16414
16510
  }
16415
16511
 
16416
16512
  // src/tui/screens/Session.tsx
@@ -16438,7 +16534,7 @@ function SessionView({ s }) {
16438
16534
  const [anchor, setAnchor] = useState7(null);
16439
16535
  useEditing(mode === "reply" || mode === "note");
16440
16536
  const items = d.threads.get(s.id);
16441
- useEffect7(() => {
16537
+ useEffect8(() => {
16442
16538
  if (!items && !s.offline) client.send({ type: "session.thread", id: s.id });
16443
16539
  }, [items === void 0, s.id, d.snapshotSeq]);
16444
16540
  const width = Math.max(20, columns - 6);
@@ -16460,7 +16556,7 @@ function SessionView({ s }) {
16460
16556
  };
16461
16557
  const images = useMemo3(() => (items ? threadImages(items, unfolded) : []).map((ref) => ({ ref, route: { id: s.id } })), [items, unfolded, s.id]);
16462
16558
  const chips = useChips((image2) => putImage(client, image2, { id: s.id }));
16463
- const paste = usePaste(chips);
16559
+ const paste = usePaste2(chips);
16464
16560
  const openImage2 = useOpenImage();
16465
16561
  const machine = splitId(s.id)?.machine ?? "local";
16466
16562
  const canSwitch = machines.find((m) => m.id === machine)?.daemon?.modelSwitch === true;
@@ -16673,7 +16769,7 @@ function PendingBox({ s, a }) {
16673
16769
 
16674
16770
  // src/tui/screens/Ask.tsx
16675
16771
  import { randomUUID as randomUUID2 } from "crypto";
16676
- import { useEffect as useEffect9, useMemo as useMemo4, useRef as useRef10, useState as useState9 } from "react";
16772
+ import { useEffect as useEffect10, useMemo as useMemo4, useRef as useRef10, useState as useState9 } from "react";
16677
16773
  import { Box as Box12, Text as Text12, useInput as useInput4 } from "ink";
16678
16774
 
16679
16775
  // ../ui/src/chat/actionRow.ts
@@ -16830,11 +16926,11 @@ function openCards(turns2) {
16830
16926
  }
16831
16927
 
16832
16928
  // src/tui/ui/throttle.ts
16833
- import { useEffect as useEffect8, useRef as useRef9, useState as useState8 } from "react";
16929
+ import { useEffect as useEffect9, useRef as useRef9, useState as useState8 } from "react";
16834
16930
  function useThrottled(value, ms) {
16835
16931
  const [shown, setShown] = useState8(value);
16836
16932
  const last = useRef9(0);
16837
- useEffect8(() => {
16933
+ useEffect9(() => {
16838
16934
  const show = () => {
16839
16935
  last.current = Date.now();
16840
16936
  setShown(value);
@@ -17013,7 +17109,7 @@ function Ask() {
17013
17109
  const [anchor, setAnchor] = useState9(null);
17014
17110
  const [view, setView] = useState9(10);
17015
17111
  const chips = useChips(null);
17016
- const paste = usePaste(chips);
17112
+ const paste = usePaste2(chips);
17017
17113
  const openImage2 = useOpenImage();
17018
17114
  const [busy, setBusy] = useState9(null);
17019
17115
  const [rowOf2, setRowOf] = useState9({});
@@ -17021,7 +17117,7 @@ function Ask() {
17021
17117
  const [review, setReview] = useState9(null);
17022
17118
  useEditing(focus === -1 && !confirm);
17023
17119
  const sentAsk = useRef10(null);
17024
- useEffect9(() => {
17120
+ useEffect10(() => {
17025
17121
  const s = sentAsk.current;
17026
17122
  const turn = s && d.chat.find((x) => x.qid === s.qid);
17027
17123
  if (!s || !turn) return;
@@ -17037,10 +17133,10 @@ function Ask() {
17037
17133
  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
17134
  };
17039
17135
  const planOf = (turn) => cardPlan(turn.actions ?? [], room);
17040
- useEffect9(() => {
17136
+ useEffect10(() => {
17041
17137
  if (focus >= cards.length) setFocus(cards.length ? cards.length - 1 : -1);
17042
17138
  }, [cards.length, focus]);
17043
- useEffect9(() => {
17139
+ useEffect10(() => {
17044
17140
  if (!review) return;
17045
17141
  const i = cards.findIndex((c) => c.qid === review);
17046
17142
  if (i >= 0) {
@@ -17048,21 +17144,21 @@ function Ask() {
17048
17144
  setReview(null);
17049
17145
  }
17050
17146
  }, [cards, review]);
17051
- useEffect9(() => {
17147
+ useEffect10(() => {
17052
17148
  if (confirming && !cards.some((c) => c.kind === "action" && c.qid === confirming.qid)) setConfirming(null);
17053
17149
  }, [confirming, cards]);
17054
- useEffect9(() => {
17150
+ useEffect10(() => {
17055
17151
  const turn = confirming && d.chat.find((x) => x.qid === confirming.qid);
17056
17152
  if (turn && !confirming.rows.every((i) => appliable(planOf(turn).shown[i]))) setConfirming(null);
17057
17153
  });
17058
17154
  const ignored = [...d.chat].filter((x) => x.ignoredAt).sort((a, b2) => b2.ignoredAt.localeCompare(a.ignoredAt))[0];
17059
- useEffect9(() => {
17155
+ useEffect10(() => {
17060
17156
  if (!busy?.rid) return;
17061
17157
  return client.onMessage((m) => {
17062
17158
  if (m.type === "chat.acted" && m.rid === busy.rid) setBusy(null);
17063
17159
  });
17064
17160
  }, [busy?.rid]);
17065
- useEffect9(() => {
17161
+ useEffect10(() => {
17066
17162
  if (!busy) return;
17067
17163
  const turn = d.chat.find((x) => x.qid === busy.qid);
17068
17164
  if (turn !== busy.turn && !busy.rid || (d.errors.at(-1)?.seq ?? 0) > busy.errorSeq) {
@@ -17278,7 +17374,7 @@ function Ask() {
17278
17374
  }
17279
17375
  if (input === "s" && cards[focus].kind === "team") {
17280
17376
  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 } : {} })) });
17377
+ 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
17378
  return true;
17283
17379
  }
17284
17380
  if (key.escape) {
@@ -17395,6 +17491,7 @@ function CardBox({ card, turn, focused, busy }) {
17395
17491
  ] }, x.role)),
17396
17492
  auto.length ? /* @__PURE__ */ jsx15(Text12, { color: C.yellow, wrap: "truncate", children: t.ask.autoWarning(auto.join(", ")) }) : null,
17397
17493
  team.error ? /* @__PURE__ */ jsx15(Text12, { color: C.red, wrap: "truncate", children: team.error }) : null,
17494
+ team.saveError && !team.savedPath ? /* @__PURE__ */ jsx15(Text12, { color: C.red, wrap: "truncate", children: team.saveError }) : null,
17398
17495
  /* @__PURE__ */ jsxs11(Box12, { columnGap: 2, children: [
17399
17496
  /* @__PURE__ */ jsx15(Button, { label: busy ? `${g.live} ${t.ask.launchTeam}` : t.ask.launchTeam, k: "\u21B5", primary: focused }),
17400
17497
  !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 +17578,7 @@ function CardBox({ card, turn, focused, busy }) {
17481
17578
  }
17482
17579
 
17483
17580
  // src/tui/screens/Launch.tsx
17484
- import { useEffect as useEffect10, useMemo as useMemo5, useRef as useRef11, useState as useState10 } from "react";
17581
+ import { useEffect as useEffect11, useMemo as useMemo5, useRef as useRef11, useState as useState10 } from "react";
17485
17582
  import { Box as Box13, Text as Text14, useInput as useInput6 } from "ink";
17486
17583
 
17487
17584
  // ../fleet/src/launch.ts
@@ -17633,7 +17730,7 @@ function SessionLaunch({ onTeam, prefill }) {
17633
17730
  const [repoAt, setRepoAt] = useState10(0);
17634
17731
  const [launching, setLaunching] = useState10(null);
17635
17732
  useEditing(true);
17636
- useEffect10(() => {
17733
+ useEffect11(() => {
17637
17734
  if (form.machine === null) {
17638
17735
  const up = machines.find((m) => m.ready);
17639
17736
  if (up) setForm((f) => withMachine(f, list3, up.id));
@@ -17656,7 +17753,7 @@ function SessionLaunch({ onTeam, prefill }) {
17656
17753
  const readHere = sameMachine && target !== null && target === prefill.cwd;
17657
17754
  const asked2 = sameMachine && !readHere && target !== null ? `${withFolder.machine}\0${target}` : null;
17658
17755
  const [seen, setSeen] = useState10(null);
17659
- useEffect10(() => {
17756
+ useEffect11(() => {
17660
17757
  if (!asked2) return;
17661
17758
  const [machineId, cwd] = asked2.split("\0");
17662
17759
  const requestId = `tui-role-${randomUUID3()}`;
@@ -17683,7 +17780,7 @@ function SessionLaunch({ onTeam, prefill }) {
17683
17780
  const [name, setName] = useState10(null);
17684
17781
  const shownName = name ?? drawn.name;
17685
17782
  const missing = missingOf(withFolder, machines);
17686
- useEffect10(() => {
17783
+ useEffect11(() => {
17687
17784
  if (!launching) return;
17688
17785
  if ((d.lastLaunched?.seq ?? 0) > launching.seq) {
17689
17786
  select(d.lastLaunched.id);
@@ -17894,7 +17991,7 @@ function TeamLaunch({ onSession, prefill }) {
17894
17991
  const [name, setName] = useState10("");
17895
17992
  const [launching, setLaunching] = useState10(false);
17896
17993
  useEditing(true);
17897
- useEffect10(() => {
17994
+ useEffect11(() => {
17898
17995
  if (!cwd) {
17899
17996
  setDefs({ roles: [], templates: [] });
17900
17997
  return;
@@ -18180,7 +18277,7 @@ function Help() {
18180
18277
  const k = t.keys;
18181
18278
  const groups = [
18182
18279
  [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]]],
18280
+ [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
18281
  [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
18282
  [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
18283
  [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 +18315,7 @@ function Help() {
18218
18315
  }
18219
18316
 
18220
18317
  // src/tui/screens/Teams.tsx
18221
- import { useEffect as useEffect11, useMemo as useMemo7, useState as useState13 } from "react";
18318
+ import { useEffect as useEffect12, useMemo as useMemo7, useState as useState13 } from "react";
18222
18319
  import { Box as Box16, Text as Text17 } from "ink";
18223
18320
  import { randomUUID as randomUUID4 } from "crypto";
18224
18321
 
@@ -18298,7 +18395,7 @@ function Teams() {
18298
18395
  const [problems, setProblems] = useState13(false);
18299
18396
  const [silent, setSilent] = useState13(false);
18300
18397
  const [reads, setReads] = useState13(0);
18301
- useEffect11(() => {
18398
+ useEffect12(() => {
18302
18399
  setDefs(null);
18303
18400
  setSilent(false);
18304
18401
  const requestId = `tui-catalog-${randomUUID4()}`;
@@ -18579,7 +18676,7 @@ function Shell({ client, lang, setLang: setLangState, saveLang, theme, setThemeS
18579
18676
  const kept = useRef13({ drafts: /* @__PURE__ */ new Map(), sent: /* @__PURE__ */ new Map(), asked: [] }).current;
18580
18677
  const armed = useRef13(0);
18581
18678
  const catalog = useRef13({});
18582
- useEffect12(() => {
18679
+ useEffect13(() => {
18583
18680
  if (!lastToast) return;
18584
18681
  const timer2 = setTimeout(() => setToast(null), lastToast.ms ?? TOAST_MS);
18585
18682
  return () => clearTimeout(timer2);
@@ -18588,22 +18685,22 @@ function Shell({ client, lang, setLang: setLangState, saveLang, theme, setThemeS
18588
18685
  const setEditing = useCallback((on) => setEditingState(on), []);
18589
18686
  const view = useMemo8(() => overview(d.sessions, projects, teams ?? [], tasks), [d.sessions, projects, teams, tasks]);
18590
18687
  const order2 = useRef13([]);
18591
- useEffect12(() => {
18688
+ useEffect13(() => {
18592
18689
  setSelectedId((cur) => cur === null && view.yours[0] ? view.yours[0].id : keepSelection(view.order, cur, order2.current));
18593
18690
  order2.current = view.order;
18594
18691
  }, [view]);
18595
18692
  const selected = (selectedId ? d.byId.get(selectedId) : void 0) ?? null;
18596
18693
  const seen = useRef13(null);
18597
18694
  const titled = useRef13("");
18598
- useEffect12(() => {
18695
+ useEffect13(() => {
18599
18696
  if (seen.current) for (const text5 of asked(seen.current, d.sessions, t.notify)) write(notification(text5, notify, env));
18600
18697
  seen.current = d.byId;
18601
- const next2 = title(t.notify.title(listed(d.sessions).filter(isPending).length));
18698
+ const next2 = title(t.notify.title(listed(d.sessions).filter(isPending).length + openDecisions(tasks).length));
18602
18699
  if (next2 !== titled.current) {
18603
18700
  titled.current = next2;
18604
18701
  write(next2);
18605
18702
  }
18606
- }, [d.byId]);
18703
+ }, [d.byId, tasks]);
18607
18704
  const actions = useActions({ client, sessions: d.sessions, errors: d.errors, t, toast });
18608
18705
  const go = useCallback((next2) => {
18609
18706
  setScreen((cur) => {