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,237 @@
1
+ // Modal pieces drawn in the live region in place of the input box: choices, pickers, text input, help.
2
+ import { bold, cyan, dim, gray } from "../ui.js";
3
+ import { Editor } from "./editor.js";
4
+ import { cellWidth, clipWidth, strip } from "./text.js";
5
+ import { framed, helpLines, optionList } from "./view.js";
6
+ /** Standard text-editing keys, shared by the main input and text overlays. Returns true if the key was consumed. */
7
+ export function editKey(ed, k) {
8
+ if (k.name === "paste") {
9
+ ed.paste(k.text ?? "");
10
+ return true;
11
+ }
12
+ if (k.name === "char") {
13
+ if (k.alt && k.text === "b")
14
+ ed.wordLeft();
15
+ else if (k.alt && k.text === "f")
16
+ ed.wordRight();
17
+ else if (k.alt && k.text === "d") {
18
+ ed.wordRight();
19
+ ed.deleteWordBack();
20
+ }
21
+ else if (!k.alt && !k.ctrl)
22
+ ed.insert(k.text ?? "");
23
+ else
24
+ return false;
25
+ return true;
26
+ }
27
+ if (k.ctrl && !k.alt) {
28
+ switch (k.name) {
29
+ case "a":
30
+ ed.home();
31
+ return true;
32
+ case "e":
33
+ ed.end();
34
+ return true;
35
+ case "b":
36
+ ed.left();
37
+ return true;
38
+ case "f":
39
+ ed.right();
40
+ return true;
41
+ case "k":
42
+ ed.killToEnd();
43
+ return true;
44
+ case "u":
45
+ ed.killToStart();
46
+ return true;
47
+ case "w":
48
+ ed.deleteWordBack();
49
+ return true;
50
+ case "h":
51
+ ed.backspace();
52
+ return true;
53
+ case "j":
54
+ ed.newline();
55
+ return true;
56
+ case "d":
57
+ if (!ed.empty) {
58
+ ed.del();
59
+ return true;
60
+ }
61
+ return false;
62
+ case "left":
63
+ ed.wordLeft();
64
+ return true;
65
+ case "right":
66
+ ed.wordRight();
67
+ return true;
68
+ }
69
+ }
70
+ switch (k.name) {
71
+ case "left":
72
+ k.alt || k.ctrl ? ed.wordLeft() : ed.left();
73
+ return true;
74
+ case "right":
75
+ k.alt || k.ctrl ? ed.wordRight() : ed.right();
76
+ return true;
77
+ case "home":
78
+ ed.home();
79
+ return true;
80
+ case "end":
81
+ ed.end();
82
+ return true;
83
+ case "backspace":
84
+ k.alt ? ed.deleteWordBack() : ed.backspace();
85
+ return true;
86
+ case "delete":
87
+ ed.del();
88
+ return true;
89
+ case "enter":
90
+ if (k.alt || k.shift) {
91
+ ed.newline();
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+ return false;
97
+ }
98
+ /** Numbered choices inside a titled box (permission prompts). Resolves with the chosen index, or undefined on Esc. */
99
+ export class ChoiceOverlay {
100
+ sel = 0;
101
+ title;
102
+ body;
103
+ question;
104
+ options;
105
+ done;
106
+ detail;
107
+ constructor(title, body, question, options, done, paint, detail) {
108
+ this.title = title;
109
+ this.body = body;
110
+ this.question = question;
111
+ this.options = options;
112
+ this.done = done;
113
+ this.paint = paint;
114
+ this.detail = detail;
115
+ }
116
+ paint;
117
+ render(width) {
118
+ return { lines: [...framed(this.title, this.body, Math.min(width, 100), this.paint), " " + bold(this.question), ...optionList(this.options, this.sel).map((l) => " " + l), dim(" ↑/↓ choose · enter confirm · esc cancel")] };
119
+ }
120
+ key(k) {
121
+ if (k.name === "up" || (k.name === "char" && k.text === "k"))
122
+ this.sel = (this.sel + this.options.length - 1) % this.options.length;
123
+ else if (k.name === "down" || k.name === "tab" || (k.name === "char" && k.text === "j"))
124
+ this.sel = (this.sel + 1) % this.options.length;
125
+ else if (k.name === "char" && /^[1-9]$/.test(k.text ?? "") && Number(k.text) <= this.options.length)
126
+ this.done(Number(k.text) - 1);
127
+ else if (k.name === "enter")
128
+ this.done(this.sel);
129
+ else if (k.name === "escape" || (k.ctrl && k.name === "c"))
130
+ this.done(undefined);
131
+ }
132
+ }
133
+ /** Filterable list. Type to filter, ↑/↓ to move, enter to pick, esc to cancel. */
134
+ export class PickerOverlay {
135
+ sel = 0;
136
+ filter = "";
137
+ title;
138
+ items;
139
+ done;
140
+ max;
141
+ constructor(title, items, done, initial = 0, max = 10) { this.title = title; this.items = items; this.done = done; this.sel = initial; this.max = max; }
142
+ visible() { const f = this.filter.toLowerCase(); return f ? this.items.filter((i) => `${i.label} ${i.hint ?? ""}`.toLowerCase().includes(f)) : this.items; }
143
+ /** Called whenever the highlighted item changes (live theme preview). */
144
+ onMove;
145
+ /** Choose the i-th visible item (mouse click). */
146
+ pick(i) { const it = this.visible()[i]; if (it)
147
+ this.done(it.value); }
148
+ moved() { this.onMove?.(this.visible()[this.sel]?.value); }
149
+ render(width) {
150
+ const list = this.visible();
151
+ this.sel = Math.max(0, Math.min(this.sel, list.length - 1));
152
+ const top = Math.max(0, Math.min(this.sel - Math.floor(this.max / 2), list.length - this.max)), slice = list.slice(top, top + this.max);
153
+ const lw = Math.min(40, Math.max(0, ...slice.map((i) => cellWidth(i.label))));
154
+ const rows = slice.map((it, k) => {
155
+ const on = top + k === this.sel, label = clipWidth(it.label, lw) + " ".repeat(Math.max(0, lw - cellWidth(it.label)));
156
+ const line = (on ? cyan("❯ ") + bold(cyan(label)) : " " + label) + (it.hint ? " " + (on ? gray(it.hint) : dim(it.hint)) : "");
157
+ return cellWidth(strip(line)) > width - 1 ? line.slice(0, width + 10) : line;
158
+ });
159
+ const head = bold(this.title) + (this.filter ? " " + cyan("/") + this.filter : dim(this.items.length > this.max ? " (type to filter)" : ""));
160
+ return { lines: [head, ...(top > 0 ? [dim(` ↑ ${top} more`)] : []), ...rows, ...(top + this.max < list.length ? [dim(` ↓ ${list.length - top - this.max} more`)] : []), ...(list.length ? [] : [dim(" no matches")]), dim(" ↑/↓ move · enter select · esc cancel")] };
161
+ }
162
+ key(k) {
163
+ const list = this.visible();
164
+ if (k.name === "up") {
165
+ this.sel = (this.sel + list.length - 1) % Math.max(1, list.length);
166
+ this.moved();
167
+ }
168
+ else if (k.name === "down") {
169
+ this.sel = (this.sel + 1) % Math.max(1, list.length);
170
+ this.moved();
171
+ }
172
+ else if (k.name === "enter") {
173
+ if (list.length)
174
+ this.done(list[this.sel].value);
175
+ }
176
+ else if (k.name === "escape" || (k.ctrl && k.name === "c"))
177
+ this.done(undefined);
178
+ else if (k.name === "backspace") {
179
+ this.filter = this.filter.slice(0, -1);
180
+ this.sel = 0;
181
+ }
182
+ else if (k.name === "char" && !k.alt && !k.ctrl) {
183
+ this.filter += k.text;
184
+ this.sel = 0;
185
+ }
186
+ }
187
+ }
188
+ /** One-shot text prompt ("what should it do instead?", API key). Esc cancels. */
189
+ export class InputOverlay {
190
+ ed = new Editor();
191
+ prompt;
192
+ secret;
193
+ done;
194
+ hint;
195
+ constructor(prompt, done, o = {}) {
196
+ this.prompt = prompt;
197
+ this.done = done;
198
+ this.secret = !!o.secret;
199
+ this.hint = o.hint ?? "enter to confirm · esc to cancel";
200
+ if (o.initial)
201
+ this.ed.insert(o.initial);
202
+ }
203
+ render(width) {
204
+ if (this.secret) {
205
+ const n = this.ed.text.length;
206
+ return { lines: [" " + bold(this.prompt), "❯ " + "•".repeat(n), dim(" " + this.hint)], cursor: { row: 1, col: 2 + n } };
207
+ }
208
+ const r = this.ed.render(width - 1);
209
+ return { lines: [" " + bold(this.prompt), ...r.lines, dim(" " + this.hint)], cursor: { row: 1 + r.cursor.row, col: r.cursor.col } };
210
+ }
211
+ key(k) {
212
+ if (k.name === "escape" || (k.ctrl && k.name === "c"))
213
+ return this.done(undefined);
214
+ if (k.name === "enter" && !k.alt && !k.shift)
215
+ return this.done(this.ed.text.trim());
216
+ if (k.name === "paste" && this.secret)
217
+ return void this.ed.insert((k.text ?? "").replace(/\s+/g, ""));
218
+ editKey(this.ed, k);
219
+ }
220
+ }
221
+ /** Static text (help) closed by any key. */
222
+ export class TextOverlay {
223
+ lines;
224
+ done;
225
+ scroll = 0;
226
+ title;
227
+ constructor(lines, done, title = "Help") { this.lines = lines; this.done = done; this.title = title; }
228
+ render() { return { lines: [...this.lines, dim(" press any key to close")] }; }
229
+ key(k) {
230
+ const step = k?.name === "up" ? -1 : k?.name === "down" ? 1 : k?.name === "pageup" ? -10 : k?.name === "pagedown" ? 10 : k?.name === "home" ? -1e6 : k?.name === "end" ? 1e6 : 0;
231
+ if (step)
232
+ this.scroll = Math.max(0, this.scroll + step);
233
+ else
234
+ this.done(); // arrows scroll long text; anything else closes it
235
+ }
236
+ }
237
+ export const helpOverlay = (cmds, done, width) => new TextOverlay(helpLines(cmds, width), done);
@@ -0,0 +1,15 @@
1
+ import type { Message } from "../../src/index.ts";
2
+ import type { Key } from "./keys.ts";
3
+ import type { Out } from "./screen.ts";
4
+ export declare function transcriptLines(messages: Message[], width: number): string[];
5
+ export declare class Pager {
6
+ top: number;
7
+ lines: string[];
8
+ out: Out;
9
+ done: () => void;
10
+ title: string;
11
+ constructor(lines: string[], out: Out, done: () => void, title?: string, start?: "top" | "bottom");
12
+ private page;
13
+ draw(): void;
14
+ key(k: Key): void;
15
+ }
@@ -0,0 +1,66 @@
1
+ import { bold, cyan, dim, gray, green, red } from "../ui.js";
2
+ import { renderMarkdown } from "./markdown.js";
3
+ import { clipAnsi, wrapAnsi } from "./text.js";
4
+ import { argSummary, isFailure, toolLabel } from "./view.js";
5
+ export function transcriptLines(messages, width) {
6
+ const out = [], names = new Map();
7
+ for (const m of messages) {
8
+ if (m.role === "system")
9
+ continue;
10
+ if (m.role === "user") {
11
+ out.push("", ...wrapAnsi(m.content ?? "", width - 2).map((l, i) => (i ? " " : bold(cyan("❯ "))) + bold(l)));
12
+ continue;
13
+ }
14
+ if (m.role === "assistant") {
15
+ if (m.content?.trim())
16
+ out.push("", ...renderMarkdown(m.content, width - 2).map((l, i) => (i ? " " : bold("● ")) + l));
17
+ for (const c of m.tool_calls ?? []) {
18
+ names.set(c.id, c.function.name);
19
+ out.push(`${bold(green("●"))} ${bold(toolLabel(c.function.name))}${dim("(")}${argSummary(c.function.name, c.function.arguments)}${dim(")")}`);
20
+ }
21
+ continue;
22
+ }
23
+ const body = (m.content ?? "").split("\n"), name = names.get(m.tool_call_id ?? "") ?? "";
24
+ const paint = isFailure(m.content ?? "") ? red : gray;
25
+ out.push(...body.slice(0, 400).flatMap((l) => wrapAnsi(paint(l), width - 5, 0)).map((l) => dim(" ⎿ ") + l));
26
+ if (body.length > 400)
27
+ out.push(dim(` ⎿ … +${body.length - 400} more lines (${name})`));
28
+ }
29
+ return out;
30
+ }
31
+ export class Pager {
32
+ top = 0;
33
+ lines;
34
+ out;
35
+ done;
36
+ title;
37
+ constructor(lines, out, done, title = "transcript", start = "bottom") { this.lines = lines; this.out = out; this.done = done; this.title = title; this.top = start === "top" ? 0 : Math.max(0, lines.length - this.page()); this.draw(); }
38
+ page() { return Math.max(3, (this.out.rows || 24) - 2); }
39
+ draw() {
40
+ const cols = this.out.columns || 80, page = this.page(), max = Math.max(0, this.lines.length - page);
41
+ this.top = Math.max(0, Math.min(this.top, max));
42
+ const pct = this.lines.length <= page ? 100 : Math.round((this.top / max) * 100);
43
+ const body = this.lines.slice(this.top, this.top + page).map((l) => clipAnsi(l, cols));
44
+ while (body.length < page)
45
+ body.push("");
46
+ this.out.write("\x1b[H\x1b[2J" + body.join("\r\n") + "\r\n" + dim(` ${this.title} · ${pct}% · ↑/↓ j/k scroll · space/b page · g/G top/bottom · q/esc/ctrl+o close`));
47
+ }
48
+ key(k) {
49
+ const page = this.page();
50
+ if (k.name === "escape" || (k.name === "char" && k.text === "q") || (k.ctrl && (k.name === "o" || k.name === "c")))
51
+ return this.done();
52
+ if (k.name === "up" || (k.name === "char" && k.text === "k"))
53
+ this.top--;
54
+ else if (k.name === "down" || (k.name === "char" && k.text === "j") || k.name === "enter")
55
+ this.top++;
56
+ else if (k.name === "pagedown" || (k.name === "char" && k.text === " "))
57
+ this.top += page - 1;
58
+ else if (k.name === "pageup" || (k.name === "char" && k.text === "b"))
59
+ this.top -= page - 1;
60
+ else if (k.name === "home" || (k.name === "char" && k.text === "g"))
61
+ this.top = 0;
62
+ else if (k.name === "end" || (k.name === "char" && k.text === "G"))
63
+ this.top = this.lines.length;
64
+ this.draw();
65
+ }
66
+ }
@@ -0,0 +1,9 @@
1
+ export declare const priceOf: (model: string) => [RegExp, number, number] | undefined;
2
+ export declare const contextWindow: (model: string) => number;
3
+ /** Estimated dollars for a token split, or null when the model's price is unknown. */
4
+ /** Cached input is billed at roughly a tenth of the normal input price on both OpenAI and Anthropic; this is an estimate, not an invoice. */
5
+ export declare const CACHED_RATE = 0.1;
6
+ export declare function estimateCost(model: string, promptTokens: number, completionTokens: number, cachedTokens?: number): number | null;
7
+ export declare const fmtCost: (c: number | null) => string;
8
+ export declare const fmtTokens: (n: number) => string;
9
+ export declare const fmtDuration: (ms: number) => string;
@@ -0,0 +1,23 @@
1
+ // Rough price + context-window tables so the footer can show cost and context use. Estimates only: cache discounts are not modelled.
2
+ const PRICES = [
3
+ [/gpt-6-astra/, 10, 50], [/gpt-6(\.\d+)?-sol/, 2, 10], [/gpt-6(\.\d+)?-terra/, 1, 5], [/gpt-6(\.\d+)?-luna/, 0.1, 0.5],
4
+ [/claude-fable/, 10, 50], [/claude-mythos/, 10, 50], [/claude-opus-5-5/, 4, 20], [/claude-opus/, 5, 25],
5
+ [/claude-sonnet-5/, 2, 10], [/claude-sonnet/, 3, 15], [/claude-haiku-5/, 0.1, 0.5], [/claude-haiku/, 1, 5],
6
+ [/gpt-4\.1-mini/, 0.4, 1.6], [/gpt-4\.1/, 2, 8], [/gpt-4o-mini/, 0.15, 0.6], [/gpt-4o/, 2.5, 10], [/gpt-5/, 1.25, 10], [/o[134]-?mini/, 1.1, 4.4],
7
+ ];
8
+ const WINDOWS = [[/gpt-6|gpt-5\.[5-9]/, 1_050_000], [/claude-(fable|mythos|opus-[45]|sonnet-[45]|haiku-5)/, 1_000_000], [/claude/, 200_000], [/gpt-4\.1/, 1_000_000], [/gpt-5/, 400_000], [/gpt-4o|o[134]/, 128_000]];
9
+ export const priceOf = (model) => PRICES.find(([re]) => re.test(model));
10
+ export const contextWindow = (model) => WINDOWS.find(([re]) => re.test(model))?.[1] ?? 128_000;
11
+ /** Estimated dollars for a token split, or null when the model's price is unknown. */
12
+ /** Cached input is billed at roughly a tenth of the normal input price on both OpenAI and Anthropic; this is an estimate, not an invoice. */
13
+ export const CACHED_RATE = 0.1;
14
+ export function estimateCost(model, promptTokens, completionTokens, cachedTokens = 0) {
15
+ const p = priceOf(model);
16
+ if (!p)
17
+ return null;
18
+ const cached = Math.min(cachedTokens, promptTokens);
19
+ return ((promptTokens - cached) * p[1] + cached * p[1] * CACHED_RATE + completionTokens * p[2]) / 1_000_000;
20
+ }
21
+ export const fmtCost = (c) => (c === null ? "" : c < 0.01 ? "<$0.01" : "$" + c.toFixed(2));
22
+ export const fmtTokens = (n) => (n >= 1_000_000 ? (n / 1e6).toFixed(1) + "M" : n >= 1000 ? (n / 1000).toFixed(n >= 10_000 ? 0 : 1) + "k" : String(n));
23
+ export const fmtDuration = (ms) => { const s = Math.round(ms / 1000); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; };
@@ -0,0 +1,16 @@
1
+ import { type Skin } from "./markdown.ts";
2
+ import type { Theme } from "./themes.ts";
3
+ export declare const F: (c: number, s: string) => string;
4
+ export declare const B: (c: number, s: string) => string;
5
+ export declare const BOLD: (s: string) => string;
6
+ export declare const DIMM: (s: string) => string;
7
+ export declare const ITAL: (s: string) => string;
8
+ export declare const UNDER: (s: string) => string;
9
+ export declare const STRIKE: (s: string) => string;
10
+ /** Make `content` exactly `width` columns: clip if longer, pad with spaces on background `bg` if shorter. */
11
+ export declare function fit(content: string, width: number, bg: number): string;
12
+ /** Left part, flexible gap, right part, all on one background. */
13
+ export declare function split(left: string, right: string, width: number, bg: number): string;
14
+ /** A small pill: padded text on its own background. */
15
+ export declare const chip: (text: string, fg: number, bg: number, bold?: boolean) => string;
16
+ export declare function themeSkin(t: Theme): Skin;
@@ -0,0 +1,72 @@
1
+ // Row-building helpers and the themed markdown skin for the full-screen UI.
2
+ // Convention: helpers reset only what they set (fg -> 39, bg -> 49, bold -> 22), so a row can carry one background across many styled spans.
3
+ import { bgSeq, fgSeq } from "./ansi.js";
4
+ import { highlightWith } from "./markdown.js";
5
+ import { cellWidth, clipAnsi, strip, wrapAnsi } from "./text.js";
6
+ export const F = (c, s) => (s ? fgSeq(c) + s + "\x1b[39m" : s);
7
+ export const B = (c, s) => (s ? bgSeq(c) + s + "\x1b[49m" : s);
8
+ export const BOLD = (s) => `\x1b[1m${s}\x1b[22m`;
9
+ export const DIMM = (s) => `\x1b[2m${s}\x1b[22m`;
10
+ export const ITAL = (s) => `\x1b[3m${s}\x1b[23m`;
11
+ export const UNDER = (s) => `\x1b[4m${s}\x1b[24m`;
12
+ export const STRIKE = (s) => `\x1b[9m${s}\x1b[29m`;
13
+ /** Make `content` exactly `width` columns: clip if longer, pad with spaces on background `bg` if shorter. */
14
+ export function fit(content, width, bg) {
15
+ const w = cellWidth(content), b = bgSeq(bg);
16
+ const c = (w > width ? clipAnsi(content, width) : content).replace(/\x1b\[49m/g, b).replace(/\x1b\[0m/g, "\x1b[0m" + b); // inner resets must fall back to the ROW background, not the terminal's
17
+ return b + c + b + " ".repeat(Math.max(0, width - Math.min(w, width))) + "\x1b[49m";
18
+ }
19
+ /** Left part, flexible gap, right part, all on one background. */
20
+ export function split(left, right, width, bg) {
21
+ const gap = width - cellWidth(left) - cellWidth(right);
22
+ return gap >= 1 ? fit(left + " ".repeat(gap) + right, width, bg) : fit(left, width, bg);
23
+ }
24
+ /** A small pill: padded text on its own background. */
25
+ export const chip = (text, fg, bg, bold = false) => bgSeq(bg) + fgSeq(fg) + (bold ? "\x1b[1m" : "") + ` ${text} ` + (bold ? "\x1b[22m" : "") + "\x1b[39m\x1b[49m";
26
+ export function themeSkin(t) {
27
+ const hl = (k, s) => (k === "plain" ? F(t.syn.plain, s) : F(t.syn[k === "keyword" ? "keyword" : k === "string" ? "string" : k === "number" ? "number" : "comment"], k === "comment" ? ITAL(s) : s));
28
+ const skin = {
29
+ bold: BOLD, italic: ITAL, strike: STRIKE, dim: (s) => F(t.dim, s),
30
+ code: (s) => B(t.inlineCodeBg, F(t.inlineCode, ` ${s} `)),
31
+ link: (text, url) => F(t.link, UNDER(text)) + F(t.faint, ` ${url}`),
32
+ heading(level, text, width) {
33
+ if (level === 1)
34
+ return [BOLD(F(t.accent, strip(text).toUpperCase() === strip(text) ? text : text)), F(t.border, "━".repeat(Math.min(width, Math.max(10, cellWidth(strip(text))))))];
35
+ if (level === 2)
36
+ return [BOLD(F(t.accent2, text)), F(t.border, "─".repeat(Math.min(width, Math.max(8, cellWidth(strip(text))))))];
37
+ return [BOLD(F(t.text, text))];
38
+ },
39
+ quote: (inner) => F(t.accent, "▎ ") + ITAL(F(t.dim, inner)),
40
+ bullet: (m) => F(t.accent, m),
41
+ hr: (w) => F(t.border, "─".repeat(Math.min(w, 72))),
42
+ hl,
43
+ codeBlock(body, lang, width) {
44
+ const out = [], inner = width - 4;
45
+ const head = split(" " + F(t.dim, (lang || "text").toLowerCase()), F(t.faint, "copy "), width, t.surface2);
46
+ out.push(head);
47
+ for (const b of body.length ? body : [""]) {
48
+ const styled = lang === "diff" ? (b.startsWith("+") ? F(t.addFg, b) : b.startsWith("-") ? F(t.delFg, b) : b.startsWith("@@") ? F(t.accent2, b) : F(t.dim, b)) : highlightWith(b, lang, hl);
49
+ const rows = wrapAnsi(styled, inner, 2);
50
+ for (const r of rows)
51
+ out.push(fit(" " + r, width, t.surface));
52
+ }
53
+ out.push(fit("", width, t.surface));
54
+ return out;
55
+ },
56
+ table(rows, width) {
57
+ const n = Math.max(...rows.map((r) => r.length)), cellOf = (s) => inlineCell(s, skin);
58
+ const widths = Array.from({ length: n }, (_, i) => Math.max(...rows.map((r) => cellWidth(cellOf(r[i] ?? "")))));
59
+ if (widths.reduce((a, b) => a + b, 0) + 3 * n + 1 > width)
60
+ return rows.map((r, i) => (i === 0 ? BOLD(r.join(" │ ")) : r.join(" │ ")));
61
+ const v = F(t.border, "│"), rule = (l, m, r) => F(t.border, l + widths.map((w) => "─".repeat(w + 2)).join(m) + r);
62
+ const line = (r, head) => {
63
+ const cells = widths.map((w, i) => { const c = cellOf(r[i] ?? ""); return " " + (head ? BOLD(F(t.accent2, c)) : c) + " ".repeat(w - cellWidth(c)) + " "; });
64
+ return v + cells.map((c) => (head ? B(t.surface2, c) : c)).join(v) + v;
65
+ };
66
+ return [rule("╭", "┬", "╮"), line(rows[0], true), rule("├", "┼", "┤"), ...rows.slice(1).map((r) => line(r, false)), rule("╰", "┴", "╯")];
67
+ },
68
+ };
69
+ return skin;
70
+ }
71
+ import { inline } from "./markdown.js";
72
+ const inlineCell = (s, skin) => inline(s, skin);
@@ -0,0 +1,30 @@
1
+ export type Out = {
2
+ write(s: string): unknown;
3
+ columns?: number;
4
+ rows?: number;
5
+ isTTY?: boolean;
6
+ };
7
+ export declare class Screen {
8
+ out: Out;
9
+ private cursorRow;
10
+ private cursorCol;
11
+ private lastCols;
12
+ private lastWidths;
13
+ height: number;
14
+ alt: boolean;
15
+ constructor(out: Out);
16
+ get cols(): number;
17
+ get rows(): number;
18
+ /**
19
+ * One atomic frame: erase the old live region, print `committed` lines into scrollback, draw the new live region and park the cursor at `cursor`.
20
+ * Wrapped in synchronized-output markers so supporting terminals never show a half-drawn frame.
21
+ */
22
+ frame(committed: string[], live: string[], cursor?: {
23
+ row: number;
24
+ col: number;
25
+ }): void;
26
+ /** Remove the live region entirely (before exiting, or before handing the terminal to something else). */
27
+ clear(): void;
28
+ enterAlt(): void;
29
+ leaveAlt(): void;
30
+ }
@@ -0,0 +1,62 @@
1
+ // Inline terminal renderer: finished output goes into normal scrollback; a small "live region" (status, input box, overlays)
2
+ // is redrawn in place at the bottom. Same model as Claude Code / Copilot CLI: no alternate screen, so native scrollback and copy/paste still work.
3
+ import { cellWidth, clipAnsi } from "./text.js";
4
+ export class Screen {
5
+ out;
6
+ cursorRow = 0; // where the cursor sits inside the live region (0 = its first line)
7
+ cursorCol = 0;
8
+ lastCols = 0;
9
+ lastWidths = [];
10
+ height = 0; // lines currently occupied by the live region
11
+ alt = false;
12
+ constructor(out) { this.out = out; }
13
+ get cols() { return Math.max(20, this.out.columns || 80); }
14
+ get rows() { return Math.max(6, this.out.rows || 24); }
15
+ /**
16
+ * One atomic frame: erase the old live region, print `committed` lines into scrollback, draw the new live region and park the cursor at `cursor`.
17
+ * Wrapped in synchronized-output markers so supporting terminals never show a half-drawn frame.
18
+ */
19
+ frame(committed, live, cursor) {
20
+ if (this.alt)
21
+ return;
22
+ const cols = this.cols, maxLive = this.rows - 1;
23
+ const lines = (live.length > maxLive ? live.slice(live.length - maxLive) : live).map((l) => clipAnsi(l, cols));
24
+ const shifted = live.length - lines.length; // rows dropped from the top when the region is taller than the terminal
25
+ let s = "\x1b[?2026h\x1b[?25l";
26
+ if (this.height > 0) {
27
+ let up = this.cursorRow;
28
+ if (this.lastCols && this.lastCols !== cols) { // the window was resized: terminals re-wrap old rows, so count how many rows the region really spans now
29
+ const rows = (w) => Math.max(1, Math.ceil(w / cols));
30
+ up = this.lastWidths.slice(0, this.cursorRow).reduce((n, w) => n + rows(w), 0) + Math.floor(this.cursorCol / cols);
31
+ }
32
+ s += (up > 0 ? `\x1b[${up}A` : "") + "\r\x1b[J";
33
+ }
34
+ for (const l of committed)
35
+ s += l + "\x1b[0m\r\n";
36
+ s += lines.join("\r\n");
37
+ let row = Math.max(0, lines.length - 1);
38
+ if (lines.length && cursor) {
39
+ const target = Math.max(0, Math.min(lines.length - 1, cursor.row - shifted));
40
+ if (row > target)
41
+ s += `\x1b[${row - target}A`;
42
+ s += "\r" + (cursor.col > 0 ? `\x1b[${Math.min(cursor.col, cols - 1)}C` : "");
43
+ row = target;
44
+ }
45
+ s += "\x1b[?25h\x1b[?2026l";
46
+ this.out.write(s);
47
+ this.cursorRow = row;
48
+ this.height = lines.length;
49
+ this.cursorCol = cursor ? Math.min(cursor.col, cols - 1) : cellWidth(lines.at(-1) ?? "");
50
+ this.lastCols = cols;
51
+ this.lastWidths = lines.map((l) => cellWidth(l));
52
+ }
53
+ /** Remove the live region entirely (before exiting, or before handing the terminal to something else). */
54
+ clear() {
55
+ if (this.height > 0)
56
+ this.out.write((this.cursorRow > 0 ? `\x1b[${this.cursorRow}A` : "") + "\r\x1b[J");
57
+ this.height = 0;
58
+ this.cursorRow = 0;
59
+ }
60
+ enterAlt() { this.alt = true; this.out.write("\x1b[?1049h\x1b[?25l"); }
61
+ leaveAlt() { this.alt = false; this.out.write("\x1b[?1049l\x1b[?25h"); }
62
+ }
@@ -0,0 +1,77 @@
1
+ import type { Message } from "../../src/index.ts";
2
+ import type { Snapshots } from "../../src/index.ts";
3
+ export type Turn = {
4
+ shell?: {
5
+ tree?: string;
6
+ cmds: string[];
7
+ irreversible: string[];
8
+ };
9
+ at: number;
10
+ text: string;
11
+ t: number; /** path -> content before this turn's first edit (null = file did not exist) */
12
+ snaps: Record<string, string | null>;
13
+ };
14
+ export type SessionData = {
15
+ /** set on a branch: the conversation it was forked from and the turn it was forked after */ parent?: {
16
+ id: string;
17
+ turn: number;
18
+ };
19
+ id: string;
20
+ created: string;
21
+ updated: string;
22
+ title: string;
23
+ cwd: string;
24
+ model: string;
25
+ messages: Message[];
26
+ turns: Turn[];
27
+ usage: {
28
+ prompt: number;
29
+ completion: number;
30
+ calls: number; /** prompt tokens of each request, newest last */
31
+ series?: number[]; /** prompt tokens served from the provider cache / output tokens spent reasoning */
32
+ cached?: number;
33
+ reasoning?: number;
34
+ };
35
+ };
36
+ export type SessionInfo = {
37
+ id: string;
38
+ title: string;
39
+ updated: string;
40
+ turns: number;
41
+ model: string;
42
+ };
43
+ export declare const newSession: (cwd: string, model: string) => SessionData;
44
+ export declare class SessionStore {
45
+ dir: string;
46
+ constructor(cwd: string);
47
+ private file;
48
+ save(s: SessionData): void;
49
+ load(id: string): SessionData | undefined;
50
+ list(): SessionInfo[];
51
+ latest(): SessionData | undefined;
52
+ remove(id: string): void;
53
+ }
54
+ /** Record the pre-edit content for `path` the first time this turn touches it. */
55
+ export declare function snapshot(turn: Turn | undefined, path: string, before: string | null | undefined): void;
56
+ /**
57
+ * Roll the conversation back to just before turn `idx`: truncate messages, drop later turns, and restore every file those turns changed
58
+ * (the earliest snapshot wins). Never touches paths outside `cwd`.
59
+ */
60
+ export declare function rewind(s: SessionData, cwd: string, idx: number): {
61
+ restored: string[];
62
+ removed: string[];
63
+ skipped: string[];
64
+ };
65
+ /** Like `rewind`, but first puts the working tree back to the earliest shell snapshot in the turns being undone (undoing what commands did), and reports what could not be undone. */
66
+ export declare function rewindFull(s: SessionData, cwd: string, idx: number, snaps?: Snapshots): Promise<ReturnType<typeof rewind> & {
67
+ tree?: {
68
+ restored: string[];
69
+ removed: string[];
70
+ };
71
+ irreversible: string[];
72
+ treeError?: string;
73
+ }>;
74
+ /** A new conversation that continues from the end of turn `idx` (0-based) of `s`. The original is untouched. */
75
+ export declare function forkSession(s: SessionData, idx: number): SessionData;
76
+ /** Every conversation connected to `id` by branching (its ancestors, descendants and siblings), `id` itself included. */
77
+ export declare function relatives(all: Pick<SessionData, "id" | "parent">[], id: string): string[];