amness 0.0.0-stage → 0.1.2

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -2
  3. package/dist/cli/acpcli.d.ts +7 -0
  4. package/dist/cli/acpcli.js +14 -0
  5. package/dist/cli/amness.d.ts +2 -0
  6. package/dist/cli/amness.js +694 -0
  7. package/dist/cli/benchcli.d.ts +5 -0
  8. package/dist/cli/benchcli.js +160 -0
  9. package/dist/cli/config.d.ts +79 -0
  10. package/dist/cli/config.js +111 -0
  11. package/dist/cli/demo.d.ts +13 -0
  12. package/dist/cli/demo.js +44 -0
  13. package/dist/cli/detect.d.ts +2 -0
  14. package/dist/cli/detect.js +20 -0
  15. package/dist/cli/evalcli.d.ts +15 -0
  16. package/dist/cli/evalcli.js +198 -0
  17. package/dist/cli/format.d.ts +5 -0
  18. package/dist/cli/format.js +55 -0
  19. package/dist/cli/headless.d.ts +2 -0
  20. package/dist/cli/headless.js +169 -0
  21. package/dist/cli/keystore.d.ts +9 -0
  22. package/dist/cli/keystore.js +108 -0
  23. package/dist/cli/model.d.ts +11 -0
  24. package/dist/cli/model.js +19 -0
  25. package/dist/cli/plugincli.d.ts +2 -0
  26. package/dist/cli/plugincli.js +176 -0
  27. package/dist/cli/schedulecli.d.ts +2 -0
  28. package/dist/cli/schedulecli.js +66 -0
  29. package/dist/cli/state.d.ts +11 -0
  30. package/dist/cli/state.js +19 -0
  31. package/dist/cli/tui/ansi.d.ts +38 -0
  32. package/dist/cli/tui/ansi.js +142 -0
  33. package/dist/cli/tui/app.d.ts +328 -0
  34. package/dist/cli/tui/app.js +1358 -0
  35. package/dist/cli/tui/blocks.d.ts +151 -0
  36. package/dist/cli/tui/blocks.js +128 -0
  37. package/dist/cli/tui/brand.d.ts +9 -0
  38. package/dist/cli/tui/brand.js +16 -0
  39. package/dist/cli/tui/canvas.d.ts +92 -0
  40. package/dist/cli/tui/canvas.js +199 -0
  41. package/dist/cli/tui/cards.d.ts +44 -0
  42. package/dist/cli/tui/cards.js +215 -0
  43. package/dist/cli/tui/clipboard.d.ts +5 -0
  44. package/dist/cli/tui/clipboard.js +29 -0
  45. package/dist/cli/tui/commands.d.ts +10 -0
  46. package/dist/cli/tui/commands.js +650 -0
  47. package/dist/cli/tui/complete.d.ts +39 -0
  48. package/dist/cli/tui/complete.js +113 -0
  49. package/dist/cli/tui/custom.d.ts +9 -0
  50. package/dist/cli/tui/custom.js +34 -0
  51. package/dist/cli/tui/diffview.d.ts +25 -0
  52. package/dist/cli/tui/diffview.js +130 -0
  53. package/dist/cli/tui/editor.d.ts +60 -0
  54. package/dist/cli/tui/editor.js +230 -0
  55. package/dist/cli/tui/full.d.ts +86 -0
  56. package/dist/cli/tui/full.js +630 -0
  57. package/dist/cli/tui/keys.d.ts +31 -0
  58. package/dist/cli/tui/keys.js +134 -0
  59. package/dist/cli/tui/markdown.d.ts +37 -0
  60. package/dist/cli/tui/markdown.js +155 -0
  61. package/dist/cli/tui/mentions.d.ts +15 -0
  62. package/dist/cli/tui/mentions.js +48 -0
  63. package/dist/cli/tui/modal.d.ts +26 -0
  64. package/dist/cli/tui/modal.js +107 -0
  65. package/dist/cli/tui/overlays.d.ts +98 -0
  66. package/dist/cli/tui/overlays.js +237 -0
  67. package/dist/cli/tui/pager.d.ts +15 -0
  68. package/dist/cli/tui/pager.js +66 -0
  69. package/dist/cli/tui/pricing.d.ts +9 -0
  70. package/dist/cli/tui/pricing.js +23 -0
  71. package/dist/cli/tui/rich.d.ts +16 -0
  72. package/dist/cli/tui/rich.js +72 -0
  73. package/dist/cli/tui/screen.d.ts +30 -0
  74. package/dist/cli/tui/screen.js +62 -0
  75. package/dist/cli/tui/session.d.ts +77 -0
  76. package/dist/cli/tui/session.js +103 -0
  77. package/dist/cli/tui/sidebar.d.ts +84 -0
  78. package/dist/cli/tui/sidebar.js +114 -0
  79. package/dist/cli/tui/text.d.ts +15 -0
  80. package/dist/cli/tui/text.js +94 -0
  81. package/dist/cli/tui/theme.d.ts +59 -0
  82. package/dist/cli/tui/theme.js +148 -0
  83. package/dist/cli/tui/themes.d.ts +56 -0
  84. package/dist/cli/tui/themes.js +80 -0
  85. package/dist/cli/tui/tokens.d.ts +39 -0
  86. package/dist/cli/tui/tokens.js +71 -0
  87. package/dist/cli/tui/view.d.ts +60 -0
  88. package/dist/cli/tui/view.js +178 -0
  89. package/dist/cli/ui.d.ts +34 -0
  90. package/dist/cli/ui.js +224 -0
  91. package/dist/src/acp.d.ts +28 -0
  92. package/dist/src/acp.js +111 -0
  93. package/dist/src/agents.d.ts +23 -0
  94. package/dist/src/agents.js +71 -0
  95. package/dist/src/anthropic.d.ts +30 -0
  96. package/dist/src/anthropic.js +91 -0
  97. package/dist/src/approver.d.ts +15 -0
  98. package/dist/src/approver.js +40 -0
  99. package/dist/src/bench.d.ts +67 -0
  100. package/dist/src/bench.js +201 -0
  101. package/dist/src/bundle.d.ts +36 -0
  102. package/dist/src/bundle.js +55 -0
  103. package/dist/src/chaos.d.ts +15 -0
  104. package/dist/src/chaos.js +26 -0
  105. package/dist/src/code.d.ts +187 -0
  106. package/dist/src/code.js +590 -0
  107. package/dist/src/codeindex.d.ts +65 -0
  108. package/dist/src/codeindex.js +300 -0
  109. package/dist/src/deep.d.ts +62 -0
  110. package/dist/src/deep.js +177 -0
  111. package/dist/src/diff.d.ts +8 -0
  112. package/dist/src/diff.js +74 -0
  113. package/dist/src/dynamic.d.ts +42 -0
  114. package/dist/src/dynamic.js +133 -0
  115. package/dist/src/embed.d.ts +8 -0
  116. package/dist/src/embed.js +25 -0
  117. package/dist/src/evalcases.d.ts +11 -0
  118. package/dist/src/evalcases.js +196 -0
  119. package/dist/src/evals.d.ts +197 -0
  120. package/dist/src/evals.js +340 -0
  121. package/dist/src/gitflow.d.ts +27 -0
  122. package/dist/src/gitflow.js +56 -0
  123. package/dist/src/grade.d.ts +42 -0
  124. package/dist/src/grade.js +96 -0
  125. package/dist/src/guidance.d.ts +15 -0
  126. package/dist/src/guidance.js +68 -0
  127. package/dist/src/hardcases.d.ts +2 -0
  128. package/dist/src/hardcases.js +94 -0
  129. package/dist/src/harness.d.ts +411 -0
  130. package/dist/src/harness.js +725 -0
  131. package/dist/src/hooks.d.ts +44 -0
  132. package/dist/src/hooks.js +116 -0
  133. package/dist/src/images.d.ts +13 -0
  134. package/dist/src/images.js +27 -0
  135. package/dist/src/index.d.ts +57 -0
  136. package/dist/src/index.js +57 -0
  137. package/dist/src/ledger.d.ts +58 -0
  138. package/dist/src/ledger.js +78 -0
  139. package/dist/src/live.d.ts +16 -0
  140. package/dist/src/live.js +80 -0
  141. package/dist/src/lsp.d.ts +31 -0
  142. package/dist/src/lsp.js +147 -0
  143. package/dist/src/memory.d.ts +128 -0
  144. package/dist/src/memory.js +390 -0
  145. package/dist/src/metrics.d.ts +3 -0
  146. package/dist/src/metrics.js +42 -0
  147. package/dist/src/mining.d.ts +49 -0
  148. package/dist/src/mining.js +126 -0
  149. package/dist/src/openai.d.ts +50 -0
  150. package/dist/src/openai.js +307 -0
  151. package/dist/src/parallel.d.ts +66 -0
  152. package/dist/src/parallel.js +135 -0
  153. package/dist/src/patch.d.ts +45 -0
  154. package/dist/src/patch.js +219 -0
  155. package/dist/src/permissions.d.ts +46 -0
  156. package/dist/src/permissions.js +245 -0
  157. package/dist/src/plugins.d.ts +148 -0
  158. package/dist/src/plugins.js +225 -0
  159. package/dist/src/pluginscan.d.ts +8 -0
  160. package/dist/src/pluginscan.js +20 -0
  161. package/dist/src/procs.d.ts +41 -0
  162. package/dist/src/procs.js +94 -0
  163. package/dist/src/profiles.d.ts +36 -0
  164. package/dist/src/profiles.js +61 -0
  165. package/dist/src/progress.d.ts +29 -0
  166. package/dist/src/progress.js +75 -0
  167. package/dist/src/prompts.d.ts +10 -0
  168. package/dist/src/prompts.js +28 -0
  169. package/dist/src/ratelimit.d.ts +34 -0
  170. package/dist/src/ratelimit.js +116 -0
  171. package/dist/src/replay.d.ts +25 -0
  172. package/dist/src/replay.js +99 -0
  173. package/dist/src/review.d.ts +20 -0
  174. package/dist/src/review.js +49 -0
  175. package/dist/src/route.d.ts +9 -0
  176. package/dist/src/route.js +30 -0
  177. package/dist/src/router.d.ts +61 -0
  178. package/dist/src/router.js +126 -0
  179. package/dist/src/sandbox.d.ts +13 -0
  180. package/dist/src/sandbox.js +68 -0
  181. package/dist/src/schedule.d.ts +20 -0
  182. package/dist/src/schedule.js +59 -0
  183. package/dist/src/schema.d.ts +13 -0
  184. package/dist/src/schema.js +36 -0
  185. package/dist/src/secrets.d.ts +10 -0
  186. package/dist/src/secrets.js +41 -0
  187. package/dist/src/shell.d.ts +34 -0
  188. package/dist/src/shell.js +95 -0
  189. package/dist/src/skills.d.ts +20 -0
  190. package/dist/src/skills.js +112 -0
  191. package/dist/src/snapshot.d.ts +20 -0
  192. package/dist/src/snapshot.js +93 -0
  193. package/dist/src/stats.d.ts +24 -0
  194. package/dist/src/stats.js +62 -0
  195. package/dist/src/strategies.d.ts +16 -0
  196. package/dist/src/strategies.js +35 -0
  197. package/dist/src/suites.d.ts +30 -0
  198. package/dist/src/suites.js +112 -0
  199. package/dist/src/trace.d.ts +63 -0
  200. package/dist/src/trace.js +153 -0
  201. package/dist/src/tune.d.ts +38 -0
  202. package/dist/src/tune.js +49 -0
  203. package/dist/src/web.d.ts +19 -0
  204. package/dist/src/web.js +56 -0
  205. package/dist/src/worktree.d.ts +24 -0
  206. package/dist/src/worktree.js +85 -0
  207. package/docs/agent-loop.md +222 -0
  208. package/docs/architecture.md +228 -0
  209. package/docs/bench.md +292 -0
  210. package/docs/budgets.md +185 -0
  211. package/docs/build-your-own-harness.md +424 -0
  212. package/docs/cookbook.md +375 -0
  213. package/docs/development.md +150 -0
  214. package/docs/evaluation.md +450 -0
  215. package/docs/features.md +1320 -0
  216. package/docs/getting-started.md +205 -0
  217. package/docs/headless.md +188 -0
  218. package/docs/images/approval.png +0 -0
  219. package/docs/images/chat.png +0 -0
  220. package/docs/images/daylight.png +0 -0
  221. package/docs/images/loop.png +0 -0
  222. package/docs/images/palette.png +0 -0
  223. package/docs/images/running.png +0 -0
  224. package/docs/images/subagent-done.png +0 -0
  225. package/docs/images/subagent-running.png +0 -0
  226. package/docs/images/welcome.png +0 -0
  227. package/docs/index.md +25 -0
  228. package/docs/library.md +267 -0
  229. package/docs/plugins.md +691 -0
  230. package/docs/providers.md +175 -0
  231. package/docs/safety.md +250 -0
  232. package/docs/usage.md +348 -0
  233. package/docs/why.md +155 -0
  234. package/docs/writing-guide.md +39 -0
  235. package/package.json +66 -4
@@ -0,0 +1,630 @@
1
+ // The full-screen UI: header, scrollable transcript, sidebar, input panel, modals, toasts and mouse handling, drawn on a cell canvas.
2
+ import { copyToClipboard } from "./clipboard.js";
3
+ import { basename } from "node:path";
4
+ import { A, mix } from "./ansi.js";
5
+ import { brandOfModel } from "./brand.js";
6
+ import { Canvas } from "./canvas.js";
7
+ import { isQuiet, renderBlock, renderGroup } from "./cards.js";
8
+ import { MOUSE_OFF, MOUSE_ON } from "./keys.js";
9
+ import { drawModal } from "./modal.js";
10
+ import { ChoiceOverlay, InputOverlay, PickerOverlay, TextOverlay } from "./overlays.js";
11
+ import { contextWindow, estimateCost, fmtDuration, fmtTokens } from "./pricing.js";
12
+ import { BOLD, F, ITAL, chip, fit, split } from "./rich.js";
13
+ import { renderSidebar } from "./sidebar.js";
14
+ import { cellWidth, clipWidth } from "./text.js";
15
+ import { getTheme } from "./themes.js";
16
+ import { level as colorLevel, TIPS } from "./theme.js";
17
+ import { contextMap } from "./tokens.js";
18
+ import { LOGO_ART } from "./brand.js";
19
+ import { argSummary, toolLabel } from "./view.js";
20
+ const SPIN = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", BARS = "▁▂▃▄▅▆▇█";
21
+ export const MODE_LABEL = { default: "ask", acceptEdits: "auto-edit", plan: "plan", yolo: "yolo" };
22
+ export class FullView {
23
+ app;
24
+ canvas;
25
+ scroll = 0; // rows scrolled up from the bottom (0 = following)
26
+ unseen = 0; // new rows that arrived while scrolled up
27
+ showSidebar = true;
28
+ expandAll = false;
29
+ diffMode = "auto";
30
+ mouseOn = process.env.AMNESS_NO_MOUSE !== "1";
31
+ toasts = [];
32
+ cache = new Map();
33
+ regions = [];
34
+ prevTotal = 0;
35
+ viewTop = 0;
36
+ viewH = 0;
37
+ viewX = 0;
38
+ viewW = 0;
39
+ total = 0;
40
+ items = [];
41
+ /** Groups of quiet tool calls (reads, searches) the user has opened, by the id of the first call. */
42
+ groupOpen = new Set();
43
+ sel;
44
+ down;
45
+ lastClick = { t: 0, x: -1, y: -1, n: 0 };
46
+ sideX = 1e9;
47
+ mainW = 80;
48
+ toastBase = 20;
49
+ lastWheel = 0;
50
+ ctxCache;
51
+ forceFull = true;
52
+ tipIdx = Math.floor(Math.random() * TIPS.length);
53
+ startedAt = Date.now();
54
+ constructor(app) {
55
+ this.app = app;
56
+ this.canvas = new Canvas(app.io.output.columns || 80, app.io.output.rows || 24);
57
+ }
58
+ get theme() { return getTheme(this.app.cfg.theme); }
59
+ // ------------------------------------------------------------------ terminal modes
60
+ enter() {
61
+ const o = this.app.io.output;
62
+ o.write("\x1b[?1049h\x1b[2J\x1b[H" + (this.mouseOn ? MOUSE_ON : ""));
63
+ this.forceFull = true;
64
+ }
65
+ leave() { this.app.io.output.write(MOUSE_OFF + "\x1b[?25h\x1b[?1049l"); }
66
+ setMouse(on) { this.mouseOn = on; this.app.io.output.write(on ? MOUSE_ON : MOUSE_OFF); this.toast(on ? "mouse on (drag to select, wheel to scroll)" : "mouse off: native selection restored", "info"); }
67
+ toast(text, kind = "info", ms = 2600) { this.toasts.push({ text, kind, until: Date.now() + ms }); this.toasts = this.toasts.slice(-3); }
68
+ invalidate() { this.forceFull = true; this.cache.clear(); }
69
+ // ------------------------------------------------------------------ frame
70
+ draw() {
71
+ const app = this.app, o = app.io.output, W = o.columns || 80, H = o.rows || 24, t = this.theme;
72
+ if (this.canvas.w !== W || this.canvas.h !== H) {
73
+ this.canvas = new Canvas(W, H, t.bg);
74
+ this.forceFull = true;
75
+ }
76
+ const c = this.canvas;
77
+ c.defaultBg = t.bg;
78
+ c.level = colorLevel;
79
+ c.clear(t.bg);
80
+ c.cursor = null;
81
+ this.regions = [];
82
+ const now = Date.now();
83
+ this.toasts = this.toasts.filter((x) => x.until > now);
84
+ const sbW = this.showSidebar && W >= 104 ? Math.max(30, Math.min(40, Math.floor(W * 0.27))) : 0, mainW = W - sbW;
85
+ const edRows = this.editorRows(mainW - 8), inputH = edRows + 2, footerY = H - 1, inputY = footerY - inputH;
86
+ const status = app.running || app.queued.length ? 1 + Math.min(2, app.queued.length) : 0, statusY = inputY - status;
87
+ const trY = 1, trH = Math.max(1, statusY - trY);
88
+ this.drawHeader(c, t, W);
89
+ this.drawTranscript(c, t, 0, trY, mainW, trH, now);
90
+ this.sideX = sbW ? mainW : 1e9;
91
+ this.mainW = mainW;
92
+ this.toastBase = statusY;
93
+ if (sbW)
94
+ this.drawSidebar(c, t, mainW, trY, sbW, H - 1 - trY);
95
+ if (status)
96
+ this.drawStatus(c, t, 1, statusY, mainW - 2, now);
97
+ this.drawInput(c, t, 1, inputY, mainW - 2, inputH);
98
+ this.drawSuggestions(c, t, 1, inputY, mainW - 2);
99
+ this.drawFooter(c, t, W, footerY, now);
100
+ let modal;
101
+ if (app.overlay)
102
+ modal = drawModal(c, t, app.overlay, app.frame);
103
+ if (modal) {
104
+ for (const h of modal.hits)
105
+ this.regions.push({ x: h.x, y: h.y, w: h.w, h: h.h, kind: "modal", click: () => this.modalClick(h) });
106
+ if (modal.cursor)
107
+ c.cursor = modal.cursor;
108
+ }
109
+ this.drawToasts(c, t, W);
110
+ this.drawSelection(c, t);
111
+ const seq = c.flush(this.forceFull);
112
+ this.forceFull = false;
113
+ o.write("\x1b[?2026h\x1b[?25l" + seq + (c.cursor ? `\x1b[${c.cursor.y + 1};${c.cursor.x + 1}H\x1b[?25h` : "") + "\x1b[?2026l");
114
+ }
115
+ // ------------------------------------------------------------------ header
116
+ drawHeader(c, t, W) {
117
+ const app = this.app, brand = brandOfModel(app.cfg.model, app.cfg.provider, t), bg = t.surface;
118
+ const mode = app.mode, modeCol = mode === "yolo" ? t.err : mode === "plan" ? t.info : mode === "acceptEdits" ? t.ok : t.accent;
119
+ c.fill(0, 0, W, 1, bg);
120
+ const logo = gradientText("✻ Amness", t.gradient), proj = BOLD(F(t.text, basename(app.cwd))), br = app.branch ? F(t.dim, " ⎇ " + app.branch) : "";
121
+ const left = " " + logo + F(t.border, " │ ") + proj + br;
122
+ const modelChip = chip(`${F(brand.color, brand.icon)} ${app.cfg.model}`, t.text, t.surface2), modeChip = chip(MODE_LABEL[mode] ?? mode, t.onAccent, modeCol, true), sideChip = chip(this.showSidebar ? "▥" : "▤", t.dim, t.surface2);
123
+ const right = modelChip + " " + modeChip + " " + sideChip + " ";
124
+ const title = app.session.title ? F(t.faint, ITAL(clipWidth(app.session.title, Math.max(0, W - cellWidth(left) - cellWidth(right) - 6)))) : "";
125
+ const mid = W - cellWidth(left) - cellWidth(right);
126
+ c.ansi(0, 0, fit(left + (mid > 12 && title ? " " + F(t.border, "·") + " " + title : ""), W - cellWidth(right), bg));
127
+ c.ansi(W - cellWidth(right), 0, right);
128
+ // clickable chips
129
+ const rw = cellWidth(right), mx = W - rw, mlen = cellWidth(modelChip), dlen = cellWidth(modeChip), slen = cellWidth(sideChip);
130
+ this.regions.push({ x: mx, y: 0, w: mlen, h: 1, kind: "model", click: () => void app.runCommand("/model") }, { x: mx + mlen + 1, y: 0, w: dlen, h: 1, kind: "mode", click: () => { app.cycleMode(); app.render(); } }, { x: mx + mlen + dlen + 2, y: 0, w: slen, h: 1, kind: "sidebar", click: () => { this.showSidebar = !this.showSidebar; this.invalidate(); } });
131
+ }
132
+ // ------------------------------------------------------------------ transcript
133
+ ctxFor(width, now) {
134
+ const app = this.app, brand = brandOfModel(app.cfg.model, app.cfg.provider, this.theme);
135
+ return { theme: this.theme, width, now, frame: app.frame, expandAll: this.expandAll, live: app.running, brand, diffMode: this.diffMode, kids: (id) => app.blocks.kids(id) };
136
+ }
137
+ sig(b, ctx) {
138
+ const base = `${ctx.theme.name}|${ctx.width}|${ctx.diffMode}|${ctx.expandAll}|${ctx.brand.icon}|`;
139
+ switch (b.kind) {
140
+ case "user": return base + `${b.text.length}|${b.note ?? ""}|${b.queued ? 1 : 0}`;
141
+ case "assistant": return base + `${b.text.length}|${b.streaming ? 1 + (ctx.frame >> 2) % 2 : 0}|${ctx.live}`;
142
+ case "tool": return base + (b.name === "task" ? (() => { const k = ctx.kids?.(b.callId) ?? []; return `k${k.length}:${k.at(-1)?.status ?? ""}:${k.at(-1)?.id ?? ""}|`; })() : "") + `${b.status}|${b.result?.length ?? 0}|${b.expanded}|${b.more}|${b.edit?.diff.length ?? 0}|${b.ms ?? 0}|${b.status === "running" ? Math.floor(ctx.now / 100) + ":" + ctx.frame : ""}`;
143
+ case "thinking": return base + `${b.text.length}|${b.active}|${b.expanded}|${b.active ? ctx.frame >> 1 : 0}`;
144
+ case "notice":
145
+ case "summary": return base + b.text.length;
146
+ case "raw": return base + b.lines.length + ":" + (b.lines[0]?.length ?? 0);
147
+ }
148
+ }
149
+ drawTranscript(c, t, x0, y0, w, h, now) {
150
+ const app = this.app, W = w - 1, ctx = this.ctxFor(W, now); // last column is the scrollbar
151
+ this.items = [];
152
+ let total = 0;
153
+ if (!app.blocks.blocks.length) {
154
+ this.drawEmpty(c, t, x0, y0, w, h);
155
+ this.viewTop = y0;
156
+ this.viewH = h;
157
+ this.viewX = x0;
158
+ this.viewW = w;
159
+ this.total = 0;
160
+ return;
161
+ }
162
+ const live = new Set();
163
+ const blocks = app.blocks.blocks.filter((b) => !(b.kind === "tool" && (b.name === "todo_write" || (b.parent && b.agent && b.agent !== "lead")))); // todos live in the sidebar; a subagent's steps are drawn inside its card
164
+ let prevTool = false;
165
+ for (let i = 0; i < blocks.length; i++) {
166
+ const b = blocks[i];
167
+ live.add(b.id);
168
+ let run = 1;
169
+ if (isQuiet(b))
170
+ while (i + run < blocks.length && isQuiet(blocks[i + run]))
171
+ run++;
172
+ const tctx = { ...ctx, tight: prevTool };
173
+ if (run >= 2) {
174
+ const members = blocks.slice(i, i + run), open = this.expandAll || this.groupOpen.has(b.id);
175
+ const key = `g|${tctx.tight}|${open}|${ctx.theme.name}|${ctx.width}|` + members.map((m) => `${m.id}:${m.status}:${m.ms ?? 0}`).join(",") + (members.some((m) => m.status === "running") ? `|${ctx.frame}` : ""), hit = this.cache.get(b.id);
176
+ const r = hit && hit.key === key ? hit.r : (() => { const rr = renderGroup(members, open, tctx); this.cache.set(b.id, { key, r: rr }); return rr; })();
177
+ for (const m of members)
178
+ live.add(m.id);
179
+ this.items.push({ b, r, start: total, group: true });
180
+ total += r.rows.length;
181
+ i += run - 1;
182
+ prevTool = r.rows.length <= 2;
183
+ continue;
184
+ }
185
+ const key = this.sig(b, tctx) + (tctx.tight ? "|t" : ""), hit = this.cache.get(b.id);
186
+ const r = hit && hit.key === key ? hit.r : (() => { const rr = renderBlock(b, tctx); this.cache.set(b.id, { key, r: rr }); return rr; })();
187
+ this.items.push({ b, r, start: total });
188
+ total += r.rows.length;
189
+ prevTool = b.kind === "tool" && (!b.agent || b.agent === "lead") && r.rows.length <= 2;
190
+ }
191
+ for (const id of this.cache.keys())
192
+ if (!live.has(id))
193
+ this.cache.delete(id);
194
+ if (this.scroll > 0 && total > this.prevTotal) {
195
+ this.scroll += total - this.prevTotal;
196
+ this.unseen += total - this.prevTotal;
197
+ } // keep the view anchored while scrolled up
198
+ this.prevTotal = total;
199
+ const maxScroll = Math.max(0, total - h);
200
+ this.scroll = Math.max(0, Math.min(this.scroll, maxScroll));
201
+ if (this.scroll === 0)
202
+ this.unseen = 0;
203
+ const start = total <= h ? 0 : total - h - this.scroll;
204
+ this.viewTop = y0;
205
+ this.viewH = h;
206
+ this.viewX = x0;
207
+ this.viewW = w;
208
+ this.total = total;
209
+ let yy = 0;
210
+ for (const it of this.items) {
211
+ if (it.start + it.r.rows.length <= start)
212
+ continue;
213
+ if (it.start >= start + h)
214
+ break;
215
+ for (let k = 0; k < it.r.rows.length; k++) {
216
+ const row = it.start + k - start;
217
+ if (row >= 0 && row < h)
218
+ c.put(x0, y0 + row, it.r.rows[k], W);
219
+ }
220
+ for (const hh of it.r.hits) {
221
+ const row = it.start + hh.row - start;
222
+ if (row >= 0 && row < h)
223
+ this.regions.push({ x: x0 + hh.x0, y: y0 + row, w: hh.x1 - hh.x0, h: 1, kind: hh.kind, click: () => this.hitClick(hh, it.b, it.group) });
224
+ }
225
+ yy = it.start + it.r.rows.length;
226
+ }
227
+ void yy;
228
+ // scrollbar
229
+ if (total > h) {
230
+ const thumb = Math.max(1, Math.round((h * h) / total)), top = Math.round(((start) / Math.max(1, total - h)) * (h - thumb));
231
+ for (let r = 0; r < h; r++)
232
+ c.text(x0 + W, y0 + r, r >= top && r < top + thumb ? "┃" : "│", { fg: r >= top && r < top + thumb ? t.dim : t.border });
233
+ this.regions.push({ x: x0 + W, y: y0, w: 1, h, kind: "scrollbar", click: () => this.scrollTo(0) });
234
+ }
235
+ // sticky prompt: the last user message that scrolled off the top
236
+ if (this.scroll > 0) {
237
+ const above = [...this.items].reverse().find((i) => i.b.kind === "user" && i.start + i.r.rows.length <= start);
238
+ if (above && above.b.kind === "user") {
239
+ const text = clipWidth(above.b.text.replace(/\s+/g, " "), w - 18);
240
+ c.ansi(x0, y0, fit(" " + F(t.accent, "▎") + " " + F(t.dim, text) + F(t.faint, " ↑ prompt"), W, mix(t.surface2, t.bg, 0.2)));
241
+ this.regions.push({ x: x0, y: y0, w: W, h: 1, kind: "sticky", click: () => this.scrollToBlock(above.b.id) });
242
+ }
243
+ const label = this.unseen > 0 ? ` ↓ Jump to bottom · ${this.unseen} new rows ` : " ↓ Jump to bottom ", lw = cellWidth(label), px = x0 + Math.floor((W - lw) / 2);
244
+ c.text(px, y0 + h - 1, label, { fg: t.onAccent, bg: t.accent, attr: A.bold });
245
+ this.regions.push({ x: px, y: y0 + h - 1, w: lw, h: 1, kind: "jump", click: () => this.scrollTo(0) });
246
+ }
247
+ }
248
+ hitClick(h, b, group = false) {
249
+ const before = this.total;
250
+ if (h.kind === "copy" && h.text !== undefined) {
251
+ this.copy(h.text);
252
+ this.toast(`copied code block (${h.text.length} characters)`, "ok");
253
+ return;
254
+ }
255
+ if (group) {
256
+ if (this.groupOpen.has(b.id))
257
+ this.groupOpen.delete(b.id);
258
+ else
259
+ this.groupOpen.add(b.id);
260
+ }
261
+ else if (h.kind === "toggle")
262
+ this.app.blocks.toggle(b.id);
263
+ else if (b.kind === "tool") {
264
+ b.more = true;
265
+ this.app.blocks.version++;
266
+ }
267
+ this.cache.delete(b.id);
268
+ this.app.render();
269
+ void before;
270
+ }
271
+ scrollTo(fromBottom) { this.scroll = Math.max(0, fromBottom); if (this.scroll === 0)
272
+ this.unseen = 0; this.app.render(); }
273
+ scrollBy(rows) { this.scroll = Math.max(0, this.scroll + rows); if (this.scroll === 0)
274
+ this.unseen = 0; this.app.render(); }
275
+ scrollToBlock(id) {
276
+ const it = this.items.find((i) => i.b.id === id);
277
+ if (!it)
278
+ return;
279
+ this.scroll = Math.max(0, this.total - this.viewH - it.start);
280
+ this.app.render();
281
+ }
282
+ /** Keep a toggled block's header where it is on screen. */
283
+ pin(blockId, fn) {
284
+ const it = this.items.find((i) => i.b.id === blockId), startBefore = it?.start ?? 0, totalBefore = this.total;
285
+ fn();
286
+ void startBefore;
287
+ void totalBefore;
288
+ }
289
+ // ------------------------------------------------------------------ empty state
290
+ drawEmpty(c, t, x0, y0, w, h) {
291
+ const app = this.app, lines = [];
292
+ const lw0 = Math.max(...LOGO_ART.map((l) => cellWidth(l)));
293
+ for (const [i, l] of LOGO_ART.entries())
294
+ lines.push(gradientText(l + " ".repeat(lw0 - cellWidth(l)), t.gradient, i * 0.06));
295
+ lines.push("", F(t.dim, "reliable agents for any model"), "");
296
+ const chips = [["Explain this repo", "Explain what this repository does and how it is organised. Keep it short."], ["Find & fix bugs", "Look for likely bugs in this repository and fix the clearest ones, verifying each fix."],
297
+ ["Write tests", "Find the least-tested important code here and add focused tests for it."], ["Plan a feature", "__plan__"]];
298
+ const y = y0 + Math.max(1, Math.floor((h - lines.length - 9) / 3));
299
+ lines.forEach((l, i) => { const lw = i < LOGO_ART.length ? lw0 : cellWidth(l.replace(/\x1b\[[0-9;]*m/g, "")); c.ansi(x0 + Math.floor((w - lw) / 2), y + i, l); });
300
+ let cx = 0;
301
+ const widths = chips.map(([label]) => label.length + 4), totalW = widths.reduce((a, b) => a + b + 2, -2), startX = x0 + Math.floor((w - totalW) / 2), cy = y + lines.length;
302
+ chips.forEach(([label, prompt], i) => {
303
+ const ch = chip(label, t.text, t.surface2);
304
+ c.ansi(startX + cx, cy, ch);
305
+ const px = startX + cx, pw = widths[i];
306
+ cx += pw + 2;
307
+ this.regions.push({ x: px, y: cy, w: pw, h: 1, kind: "action", click: () => { if (prompt === "__plan__") {
308
+ app.setMode("plan");
309
+ app.editor.setText("Plan how to add: ");
310
+ }
311
+ else
312
+ app.editor.setText(prompt); app.render(); } });
313
+ });
314
+ const recent = app.store.list().slice(0, 3);
315
+ let ry = cy + 2;
316
+ if (recent.length) {
317
+ const hd = F(t.faint, BOLD("RECENT"));
318
+ c.ansi(x0 + Math.floor((w - 6) / 2), ry++, hd);
319
+ for (const s of recent) {
320
+ const label = ` ↻ ${clipWidth(s.title || "(untitled)", 44)} ${F(t.faint, `${s.turns} turn${s.turns === 1 ? "" : "s"} · ${ago(s.updated)}`)} `, lw = cellWidth(label.replace(/\x1b\[[0-9;]*m/g, ""));
321
+ const rx = x0 + Math.floor((w - lw) / 2);
322
+ c.ansi(rx, ry, F(t.dim, label));
323
+ this.regions.push({ x: rx, y: ry, w: lw, h: 1, kind: "recent", click: () => void app.resumeById(s.id) });
324
+ ry++;
325
+ }
326
+ }
327
+ const hint = F(t.faint, "drag to select & copy · Shift+drag for the terminal's own selection · /copy copies the last reply");
328
+ c.ansi(x0 + Math.max(0, Math.floor((w - 94) / 2)), Math.min(y0 + h - 2, ry + 4), hint);
329
+ const tip = F(t.faint, "tip ") + F(t.dim, TIPS[this.tipIdx]);
330
+ c.ansi(x0 + Math.max(0, Math.floor((w - cellWidth(TIPS[this.tipIdx]) - 5) / 2)), Math.min(y0 + h - 2, ry + 2), tip);
331
+ }
332
+ // ------------------------------------------------------------------ sidebar
333
+ drawSidebar(c, t, x0, y0, w, h) {
334
+ const app = this.app, u = app.session.usage, win = contextWindow(app.cfg.model), now = Date.now();
335
+ const mkey = `${app.session.messages.length}|${app.lastPrompt}`;
336
+ if (this.ctxCache?.key !== mkey) {
337
+ const m = contextMap(app.session.messages, app.lastPrompt, JSON.stringify(app.agent.registry.visible({ step: 0, messages: [] }).map((x) => x.name + x.description)).length);
338
+ const tot = Math.max(1, m.total), cols = { tools: t.tool.read, assistant: t.tool.edit, you: t.ok, system: t.accent, defs: t.accent2 };
339
+ this.ctxCache = { key: mkey, map: m.parts.map((p) => ({ label: p.label, pct: Math.round((p.tokens / tot) * 100), color: cols[p.key] })).sort((a, b) => b.pct - a.pct) };
340
+ }
341
+ const changes = new Map();
342
+ for (const b of app.blocks.blocks)
343
+ if (b.kind === "tool" && b.edit) {
344
+ const p = changes.get(b.edit.path);
345
+ changes.set(b.edit.path, { path: b.edit.path, added: (p?.added ?? 0) + b.edit.added, removed: (p?.removed ?? 0) + b.edit.removed, created: p?.created ?? b.edit.created, blockId: b.id });
346
+ }
347
+ const used = app.lastPrompt || this.ctxCache.map.reduce((n, m) => n + m.pct, 0) * 0;
348
+ const { rows, hits } = renderSidebar({
349
+ title: app.session.title, model: app.cfg.model, provider: app.cfg.provider, api: app.model.api?.(), branch: app.branch, cwd: shortPath(app.cwd), mode: app.mode, effort: app.cfg.effort,
350
+ pluginStatus: app.pluginStatus(), budget: { used: Math.round(app.budget.effective), max: app.budget.maxTokens }, ctxPct: app.ctxPct, ctxUsed: used, ctxMax: win, series: u.series ?? [], map: this.ctxCache.map,
351
+ usage: { prompt: u.prompt, completion: u.completion, calls: u.calls, cached: u.cached ?? 0, reasoning: u.reasoning ?? 0 }, cost: estimateCost(app.cfg.model, u.prompt, u.completion, u.cached ?? 0),
352
+ todos: app.todos, changes: [...changes.values()].reverse(), tools: app.agent.registry.visible({ step: 0, messages: [] }).length, mcp: app.mcp.length, memory: app.memory?.list().length ?? 0,
353
+ sandbox: app.cfg.sandbox,
354
+ procs: app.agent.procs?.list().map((p) => ({ id: p.id, name: p.name, status: p.status, secs: Math.round((now - p.startedAt) / 1000) })),
355
+ running: app.running, elapsedMs: now - this.startedAt, tip: TIPS[this.tipIdx],
356
+ run: app.running ? { phase: app.phase?.phase ?? "model", step: app.phase?.step ?? 0, max: app.phase?.max ?? 30, elapsedMs: now - app.runStart, frame: app.frame,
357
+ live: [...app.inflight.values()].map((x) => ({ label: toolVerb(x.name, x.args), ms: now - x.t0, agent: x.agent })), done: app.activity.map((x) => ({ label: toolVerb(x.name, x.args, true), ok: x.ok, ms: x.ms })) } : undefined,
358
+ }, t, w, h);
359
+ rows.forEach((r, i) => c.put(x0 + 1, y0 + i, r, w - 1));
360
+ for (let i = 0; i < h; i++)
361
+ c.text(x0, y0 + i, "│", { fg: t.border, bg: t.surface });
362
+ for (const hh of hits)
363
+ if (hh.row < h)
364
+ this.regions.push({ x: x0, y: y0 + hh.row, w, h: 1, kind: "change", click: () => hh.blockId !== undefined && this.scrollToBlock(hh.blockId) });
365
+ }
366
+ // ------------------------------------------------------------------ status line, input, footer
367
+ drawStatus(c, t, x, y, w, now) {
368
+ const app = this.app, f = app.frame, glyph = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"][f % 10];
369
+ if (app.running) {
370
+ const fl = [...app.inflight.values()], ph = app.phase?.phase;
371
+ const verb = app.overlay ? "Waiting for you" : ph === "verify" ? "Verifying" : ph === "compact" ? "Compacting" : fl.length > 1 ? `Running ${fl.length} tools in parallel` : fl.length === 1 ? toolVerb(fl[0].name, fl[0].args)
372
+ : app.mode === "plan" ? "Planning" : app.thinkBuf.trim() ? "Thinking" : app.streamBuf ? "Writing" : "Working", chars = [...clipWidth(verb, Math.max(8, w - 44)) + "…"];
373
+ const shim = chars.map((ch, i) => F(mix(t.accent, 0xffffff, Math.max(0, 1 - Math.abs(i - ((f * 0.9) % (chars.length + 8) - 4)) / 3)), ch)).join("");
374
+ const spark = app.runSeries.length > 1 ? " " + F(t.accent2, app.runSeries.slice(-10).map((v) => BARS[Math.min(7, Math.round((v / Math.max(1, ...app.runSeries)) * 7))]).join("")) : "";
375
+ c.ansi(x + 1, y, F(mix(t.accent, t.accent2, (f % 20) / 20), glyph) + " " + BOLD(shim) + F(t.dim, ` ${fmtDuration(now - app.runStart)} · ↓ ${fmtTokens(app.runTokens)}`) + (app.phase ? F(t.faint, ` · step ${app.phase.step + 1}/${app.phase.max}`) : "") + spark + F(t.faint, " esc to interrupt"), {}, w - 2);
376
+ if (app.thinkBuf.trim() && app.queued.length === 0) { /* reasoning summary lives in the transcript's thinking block */ }
377
+ }
378
+ app.queued.slice(0, 2).forEach((q, i) => c.ansi(x + 1, y + (app.running ? 1 : 0) + i, F(t.warn, "▸ queued ") + F(t.dim, clipWidth(q.text.replace(/\s+/g, " "), w - 14)), {}, w - 2));
379
+ }
380
+ editorRows(width) { const r = this.app.editor.render(Math.max(10, width), "", ""); return Math.max(1, Math.min(8, r.lines.length)); }
381
+ drawInput(c, t, x, y, w, h) {
382
+ const app = this.app, mode = app.mode, col = mode === "yolo" ? t.err : mode === "plan" ? t.info : mode === "acceptEdits" ? t.ok : t.accent;
383
+ const dimBorder = app.running ? mix(col, t.bg, 0.55) : col;
384
+ c.box(x, y, w, h, { border: dimBorder, bg: t.surface, style: "round" });
385
+ const label = ` ${MODE_LABEL[mode] ?? mode} `;
386
+ c.text(x + 2, y, label, { fg: t.onAccent, bg: col, attr: A.bold });
387
+ this.regions.push({ x: x + 2, y, w: label.length, h: 1, kind: "mode", click: () => { app.cycleMode(); app.render(); } });
388
+ const hint = app.running ? " enter queues · esc interrupts " : " ⇧⇥ mode ⌥↵ newline ";
389
+ c.text(x + w - cellWidth(hint) - 2, y, hint, { fg: t.faint, bg: t.surface });
390
+ const ed = app.editor, innerW = w - 6, r = ed.render(innerW, "", ""), rows = Math.min(8, r.lines.length);
391
+ const first = Math.max(0, Math.min(r.cursor.row - rows + 1, r.lines.length - rows)), shown = r.lines.slice(first, first + rows);
392
+ const empty = ed.empty;
393
+ shown.forEach((ln, i) => {
394
+ const yy = y + 1 + i, prompt = i === 0 && first === 0 ? F(col, BOLD("❯")) : " ";
395
+ c.ansi(x + 2, yy, prompt, { bg: t.surface });
396
+ if (empty && i === 0)
397
+ c.ansi(x + 4, yy, F(t.faint, ITAL(app.running ? "Type to steer the agent…" : "Ask anything… @ files · / commands · !cmd run a command · # remember")), { bg: t.surface }, innerW);
398
+ else
399
+ c.ansi(x + 4, yy, ln.replace(/(@[^\s]+)/g, (m) => F(t.accent2, m)).replace(/\[Pasted text #\d+ \+\d+ lines?\]/g, (m) => F(t.warn, m)), { bg: t.surface, fg: t.text }, innerW);
400
+ });
401
+ if (!app.overlay)
402
+ c.cursor = { x: x + 4 + r.cursor.col, y: y + 1 + (r.cursor.row - first) };
403
+ this.regions.push({ x: x + 1, y: y + 1, w: w - 2, h: rows, kind: "input", click: () => { } });
404
+ }
405
+ drawSuggestions(c, t, x, inputY, mainW) {
406
+ const s = this.app.sugg;
407
+ if (!s || !s.items.length || this.app.overlay)
408
+ return;
409
+ const rows = Math.min(8, s.items.length), w = Math.min(70, mainW), top = Math.max(0, Math.min(s.sel - 3, s.items.length - rows)), y = inputY - rows - 2;
410
+ if (y < 1)
411
+ return;
412
+ c.box(x + 1, y, w, rows + 2, { border: t.border, bg: t.surface, style: "round", title: s.kind === "slash" ? "commands" : "files", titleFg: t.dim });
413
+ s.items.slice(top, top + rows).forEach((it, i) => {
414
+ const idx = top + i, on = idx === s.sel, bg = on ? t.selection : t.surface;
415
+ const label = s.kind === "file" ? (() => { const n = it.label.split("/").pop(), d = it.label.slice(0, it.label.length - n.length); return F(t.faint, d) + BOLD(F(t.text, n)); })() : BOLD(F(t.accent2, it.label));
416
+ c.ansi(x + 2, y + 1 + i, fit((on ? F(t.accent, "▌") : " ") + " " + label + (it.detail ? " " + F(t.faint, it.detail) : ""), w - 2, bg), {}, w - 2);
417
+ this.regions.push({ x: x + 2, y: y + 1 + i, w: w - 2, h: 1, kind: "sugg", click: () => { s.sel = idx; this.app.acceptSuggPublic(true); } });
418
+ });
419
+ }
420
+ drawFooter(c, t, W, y, now) {
421
+ const app = this.app, hints = app.overlay ? [] : app.running ? [["esc", "interrupt"], ["enter", "queue message"], ["ctrl+o", "expand all"]]
422
+ : app.editor.empty ? [["enter", "send"], ["ctrl+p", "commands"], ["ctrl+o", "expand tools"], ["ctrl+b", "sidebar"], ["pgup/dn", "scroll"], ["?", "help"]] : [["enter", "send"], ["⌥enter", "newline"], ["tab", "complete"], ["esc esc", "clear"]];
423
+ const left = hints.map(([k, l]) => F(t.dim, k) + " " + F(t.faint, l)).join(F(t.border, " · "));
424
+ const p = app.ctxPct, cost = app.cost, right = [p !== null ? F(p > 85 ? t.err : p > 60 ? t.warn : t.dim, `ctx ${p}%`) : "", cost ? F(t.dim, cost) : "", this.mouseOn ? "" : F(t.warn, "mouse off")].filter(Boolean).join(F(t.border, " · "));
425
+ c.ansi(0, y, fit(" " + (app.notice && now < app.noticeUntil ? F(t.warn, app.notice) : left), W - cellWidth(right) - 1, t.bg));
426
+ if (right)
427
+ c.ansi(W - cellWidth(right) - 1, y, right);
428
+ }
429
+ drawToasts(c, t, W) {
430
+ this.toasts.forEach((ts, i) => {
431
+ const col = ts.kind === "ok" ? t.ok : ts.kind === "warn" ? t.warn : ts.kind === "error" ? t.err : t.accent2, icon = ts.kind === "ok" ? "✓" : ts.kind === "warn" ? "⚠" : ts.kind === "error" ? "✗" : "●";
432
+ const n = this.toasts.length, text = clipWidth(` ${icon} ${ts.text} `, Math.max(10, this.mainW - 6)), w = cellWidth(text) + 2, x = 2, y = this.toastBase - 3 * (n - i); // bottom-left of the transcript: never covers the header or sidebar
433
+ c.box(x, y, w, 3, { border: col, bg: t.surface2, style: "round" });
434
+ c.text(x + 1, y + 1, text, { fg: t.text, bg: t.surface2 });
435
+ c.text(x + 2, y + 1, icon, { fg: col, bg: t.surface2 });
436
+ });
437
+ }
438
+ drawSelection(c, t) {
439
+ const s = this.sel;
440
+ if (!s)
441
+ return;
442
+ const [a, b] = ordered(s);
443
+ if (s.mode === "block") {
444
+ const lo = Math.min(s.ax, s.hx), hi = Math.max(s.ax, s.hx);
445
+ for (let y = a.y; y <= b.y; y++)
446
+ c.tint(lo, y, hi - lo + 1, 1, t.selection);
447
+ return;
448
+ }
449
+ for (let y = a.y; y <= b.y; y++) {
450
+ const x0 = y === a.y ? a.x : this.viewX, x1 = y === b.y ? b.x : this.viewX + this.viewW - 2;
451
+ c.tint(x0, y, x1 - x0 + 1, 1, t.selection);
452
+ }
453
+ }
454
+ // ------------------------------------------------------------------ mouse
455
+ mouse(e) {
456
+ const app = this.app;
457
+ if (e.action === "wheelup" || e.action === "wheeldown") {
458
+ const now = Date.now(), step = now - this.lastWheel < 70 ? 5 : 3;
459
+ this.lastWheel = now;
460
+ const o = app.overlay;
461
+ if (o instanceof PickerOverlay) {
462
+ o.key({ name: e.action === "wheelup" ? "up" : "down" });
463
+ app.render();
464
+ return;
465
+ }
466
+ if (o instanceof TextOverlay) {
467
+ o.scroll += e.action === "wheelup" ? -3 : 3;
468
+ app.render();
469
+ return;
470
+ }
471
+ if (o)
472
+ return;
473
+ this.scrollBy(e.action === "wheelup" ? step : -step);
474
+ return;
475
+ }
476
+ if (e.button !== "left")
477
+ return;
478
+ const inTr = e.x >= this.viewX && e.x < this.viewX + this.viewW - 1 && e.y >= this.viewTop && e.y < this.viewTop + this.viewH;
479
+ if (e.action === "press") {
480
+ const inTr0 = !app.overlay && e.x >= this.viewX && e.x < this.viewX + this.viewW - 1 && e.y >= this.viewTop && e.y < this.viewTop + this.viewH;
481
+ this.down = { x: e.x, y: e.y, moved: false, mode: inTr0 ? "lines" : "block" };
482
+ this.sel = undefined;
483
+ if (this.input(e))
484
+ return;
485
+ }
486
+ else if (e.action === "drag" && this.down) {
487
+ if (Math.abs(e.x - this.down.x) + Math.abs(e.y - this.down.y) > 0) {
488
+ this.down.moved = true;
489
+ const lines = this.down.mode === "lines";
490
+ const y = lines ? Math.max(this.viewTop, Math.min(this.viewTop + this.viewH - 1, e.y)) : Math.max(0, Math.min(this.canvas.h - 1, e.y)), x = lines ? Math.max(this.viewX, Math.min(this.viewX + this.viewW - 2, e.x)) : Math.max(0, Math.min(this.canvas.w - 1, e.x));
491
+ this.sel = { ax: this.down.x, ay: lines ? Math.max(this.viewTop, Math.min(this.viewTop + this.viewH - 1, this.down.y)) : this.down.y, hx: x, hy: y, done: false, mode: this.down.mode };
492
+ if (lines) {
493
+ if (e.y < this.viewTop)
494
+ this.scrollBy(1);
495
+ else if (e.y >= this.viewTop + this.viewH)
496
+ this.scrollBy(-1);
497
+ }
498
+ app.render();
499
+ }
500
+ }
501
+ else if (e.action === "release" && this.down) {
502
+ const d = this.down;
503
+ this.down = undefined;
504
+ if (d.moved && this.sel) {
505
+ this.sel.done = true;
506
+ this.copySelection();
507
+ app.render();
508
+ return;
509
+ }
510
+ if (d.moved)
511
+ return;
512
+ const now = Date.now(), n = now - this.lastClick.t < 450 && this.lastClick.x === e.x && this.lastClick.y === e.y ? this.lastClick.n + 1 : 1;
513
+ this.lastClick = { t: now, x: e.x, y: e.y, n };
514
+ const handled = this.click(e.x, e.y);
515
+ if (!handled && n >= 2)
516
+ this.selectAt(e.x, e.y, n >= 3 ? "line" : "word");
517
+ }
518
+ }
519
+ /** Double click selects the word under the pointer, triple click the whole line; both copy at once. */
520
+ selectAt(x, y, what) {
521
+ const lo = x >= this.sideX ? this.sideX : this.viewX, hi = x >= this.sideX ? this.canvas.w - 1 : this.viewX + this.viewW - 2;
522
+ const row = this.canvas.readRow(y, lo, hi + 1), at = x - lo;
523
+ if (at < 0 || at >= row.length)
524
+ return;
525
+ let a = 0, z = row.length - 1;
526
+ if (what === "word") {
527
+ if (/\s/.test(row[at]))
528
+ return;
529
+ a = at;
530
+ z = at;
531
+ while (a > 0 && !/\s/.test(row[a - 1]))
532
+ a--;
533
+ while (z < row.length - 1 && !/\s/.test(row[z + 1]))
534
+ z++;
535
+ }
536
+ else {
537
+ while (z > 0 && /\s/.test(row[z]))
538
+ z--;
539
+ while (a < z && /\s/.test(row[a]))
540
+ a++;
541
+ }
542
+ this.sel = { ax: lo + a, ay: y, hx: lo + z, hy: y, done: true, mode: "block" };
543
+ this.copySelection();
544
+ this.app.render();
545
+ }
546
+ /** Clicks inside the input panel move the text cursor. */
547
+ input(e) { void e; return false; }
548
+ click(x, y) {
549
+ for (let i = this.regions.length - 1; i >= 0; i--) {
550
+ const r = this.regions[i];
551
+ if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h && r.click) {
552
+ r.click();
553
+ return true;
554
+ }
555
+ }
556
+ return false;
557
+ }
558
+ modalClick(h) {
559
+ const o = this.app.overlay;
560
+ if (o instanceof ChoiceOverlay && h.kind === "option")
561
+ o.done(h.index);
562
+ else if (o instanceof PickerOverlay && h.kind === "item")
563
+ o.pick(h.index);
564
+ this.app.render();
565
+ }
566
+ copySelection() {
567
+ const s = this.sel;
568
+ if (!s)
569
+ return;
570
+ const [a, b] = ordered(s);
571
+ const lines = [];
572
+ for (let y = a.y; y <= b.y; y++) {
573
+ if (s.mode === "block")
574
+ lines.push(this.canvas.readRow(y, Math.min(s.ax, s.hx), Math.max(s.ax, s.hx) + 1));
575
+ else
576
+ lines.push(this.canvas.readRow(y, y === a.y ? a.x : this.viewX, (y === b.y ? b.x : this.viewX + this.viewW - 2) + 1));
577
+ }
578
+ const text = lines.map((l) => l.replace(/\s+$/, "")).join("\n").replace(/\s+$/g, "");
579
+ if (!text)
580
+ return;
581
+ this.copy(text);
582
+ this.toast(`copied ${text.length} characters`, "ok");
583
+ }
584
+ copy(text) { copyToClipboard(this.app.io.output, text, this.app.persist); }
585
+ clearSelection() { if (this.sel) {
586
+ this.sel = undefined;
587
+ return true;
588
+ } return false; }
589
+ /** Scroll keys; returns whether the key was used. */
590
+ key(name, ctrl) {
591
+ const page = Math.max(3, Math.floor(this.viewH / 2));
592
+ if (name === "pageup") {
593
+ this.scrollBy(page);
594
+ return true;
595
+ }
596
+ if (name === "pagedown") {
597
+ this.scrollBy(-page);
598
+ return true;
599
+ }
600
+ if (ctrl && name === "home") {
601
+ this.scrollTo(Math.max(0, this.total));
602
+ return true;
603
+ }
604
+ if (ctrl && name === "end") {
605
+ this.scrollTo(0);
606
+ return true;
607
+ }
608
+ return false;
609
+ }
610
+ }
611
+ const ordered = (s) => (s.ay < s.hy || (s.ay === s.hy && s.ax <= s.hx) ? [{ x: s.ax, y: s.ay }, { x: s.hx, y: s.hy }] : [{ x: s.hx, y: s.hy }, { x: s.ax, y: s.ay }]);
612
+ export function gradientText(text, stops, phase = 0) {
613
+ const chars = [...text], n = Math.max(1, chars.length - 1);
614
+ return chars.map((ch, i) => (ch === " " ? ch : F(sample(stops, (i / n + phase) % 1), ch))).join("");
615
+ }
616
+ const sample = (stops, x) => { const p = Math.max(0, Math.min(1, x)) * (stops.length - 1), i = Math.min(stops.length - 2, Math.floor(p)); return stops.length === 1 ? stops[0] : mix(stops[i], stops[i + 1], p - i); };
617
+ const VERBS = { read_file: "Reading", list_files: "Listing", grep: "Searching", write_file: "Writing", edit_file: "Editing", sh: "Running", web_search: "Searching the web for", fetch_url: "Fetching", task: "Delegating", memory_save: "Saving note", memory_search: "Recalling", todo_write: "Planning" };
618
+ /** "Reading src/app.ts": what a tool call is doing, for the status line and the run panel. */
619
+ export function toolVerb(name, args, past = false) {
620
+ const a = argSummary(name, args), v = VERBS[name] ?? toolLabel(name);
621
+ return past ? `${toolLabel(name)} ${a}`.trim() : `${v} ${a}`.trim();
622
+ }
623
+ const shortPath = (p) => { const h = process.env.HOME ?? ""; return h && p.startsWith(h) ? "~" + p.slice(h.length) : p; };
624
+ function ago(iso) { const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000); return s < 90 ? "just now" : s < 5400 ? `${Math.round(s / 60)}m ago` : s < 129600 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`; }
625
+ void InputOverlay;
626
+ void ITAL;
627
+ void BOLD;
628
+ void split;
629
+ void estimateCost;
630
+ void {};
@@ -0,0 +1,31 @@
1
+ export type MouseEv = {
2
+ x: number;
3
+ y: number;
4
+ button: "left" | "middle" | "right" | "none";
5
+ action: "press" | "release" | "drag" | "move" | "wheelup" | "wheeldown";
6
+ shift?: boolean;
7
+ alt?: boolean;
8
+ ctrl?: boolean;
9
+ };
10
+ export type Key = {
11
+ name: string;
12
+ text?: string;
13
+ ctrl?: boolean;
14
+ alt?: boolean;
15
+ shift?: boolean;
16
+ mouse?: MouseEv;
17
+ };
18
+ export declare const PASTE_ON = "\u001B[?2004h", PASTE_OFF = "\u001B[?2004l";
19
+ /** Mouse reporting: button events + drag motion, SGR encoding (so coordinates beyond 223 work). */
20
+ export declare const MOUSE_ON = "\u001B[?1000h\u001B[?1002h\u001B[?1006h", MOUSE_OFF = "\u001B[?1006l\u001B[?1002l\u001B[?1000l";
21
+ export declare class KeyParser {
22
+ private buf;
23
+ private pasting;
24
+ /** Feed raw input; returns the keys that are complete. A trailing lone ESC (or partial sequence) is held until `flush()`. */
25
+ feed(chunk: string): Key[];
26
+ /** Called after a short idle: whatever is pending (a lone ESC) is a real key press. */
27
+ flush(): Key[];
28
+ get pending(): boolean;
29
+ private one;
30
+ private ctrlOrChar;
31
+ }