@garuda-agent/garuda 0.0.1 → 0.14.1

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 (61) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +4 -0
  4. package/README.md +1040 -0
  5. package/SECURITY.md +32 -0
  6. package/dist/anthropic-NQDBCHJU.js +327 -0
  7. package/dist/chunk-35GMLLJW.js +157 -0
  8. package/dist/chunk-36GQIVGT.js +526 -0
  9. package/dist/chunk-3CEPHS4C.js +231 -0
  10. package/dist/chunk-3NJNVZTK.js +123 -0
  11. package/dist/chunk-42DSA4EA.js +116 -0
  12. package/dist/chunk-4P26OKTP.js +100 -0
  13. package/dist/chunk-5DEWP3YX.js +139 -0
  14. package/dist/chunk-6TWBYMY7.js +9 -0
  15. package/dist/chunk-A2QAIRQL.js +45 -0
  16. package/dist/chunk-ATW7BNIG.js +31 -0
  17. package/dist/chunk-D5O5CIWK.js +1339 -0
  18. package/dist/chunk-EU7PA45O.js +324 -0
  19. package/dist/chunk-FARS4R67.js +206 -0
  20. package/dist/chunk-G2AX7MGS.js +36 -0
  21. package/dist/chunk-HUIK3WXO.js +234 -0
  22. package/dist/chunk-ITITLNAR.js +9341 -0
  23. package/dist/chunk-KKB5W47S.js +231 -0
  24. package/dist/chunk-KWRJREUH.js +283 -0
  25. package/dist/chunk-N4I6YS3E.js +73 -0
  26. package/dist/chunk-OUCV3FCT.js +205 -0
  27. package/dist/chunk-PZS3V6P3.js +46 -0
  28. package/dist/chunk-QOW5TK4L.js +320 -0
  29. package/dist/chunk-U4OQVT2T.js +73 -0
  30. package/dist/chunk-V7WPGQLB.js +207 -0
  31. package/dist/chunk-W7BPYQMW.js +173 -0
  32. package/dist/chunk-WMOLXTBU.js +61 -0
  33. package/dist/chunk-YIZWYUBG.js +138 -0
  34. package/dist/chunk-YM6T4J5V.js +1350 -0
  35. package/dist/chunk-YQTGMCU6.js +111 -0
  36. package/dist/chunk-ZAE25JM2.js +158 -0
  37. package/dist/chunk-ZGZ2ZADL.js +49 -0
  38. package/dist/cli/index.js +1191 -0
  39. package/dist/create-Y5SXJUXO.js +223 -0
  40. package/dist/deadline-F5RJJ2QW.js +86 -0
  41. package/dist/detect-LARDSLLZ.js +10 -0
  42. package/dist/evalCommand-5TG2CKZY.js +2795 -0
  43. package/dist/export-HN3CT7AV.js +158 -0
  44. package/dist/inkChat-3J26R24L.js +1190 -0
  45. package/dist/install-XWLU22OH.js +17 -0
  46. package/dist/job-3NYDMP2M.js +31 -0
  47. package/dist/jobCommand-IYWZWTSI.js +30 -0
  48. package/dist/launchd-2FL5UN2U.js +40 -0
  49. package/dist/lspCommand-TJEZBV2E.js +99 -0
  50. package/dist/manager-4PMSXT47.js +14 -0
  51. package/dist/manager-EBTYO2GR.js +847 -0
  52. package/dist/nightCommand-DUNWJASI.js +247 -0
  53. package/dist/oauth-W6OK6XXO.js +23 -0
  54. package/dist/openaiCompatible-CJWVJP5M.js +501 -0
  55. package/dist/policy-EJPLEEXP.js +31 -0
  56. package/dist/proxy-XJAHCZB2.js +192 -0
  57. package/dist/remove-AENULZZE.js +59 -0
  58. package/dist/run-66Y6R2KU.js +886 -0
  59. package/dist/servers-6YDGWOZ6.js +24 -0
  60. package/dist/text-J2GD647J.js +11 -0
  61. package/package.json +83 -3
@@ -0,0 +1,1190 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FOCUS_REPORTING_OFF,
4
+ FOCUS_REPORTING_ON,
5
+ colorPreview,
6
+ focusEvent,
7
+ fuzzyFiles,
8
+ header,
9
+ runChat,
10
+ statusOf
11
+ } from "./chunk-YM6T4J5V.js";
12
+ import {
13
+ retryText,
14
+ serverToolOutput,
15
+ serverToolText,
16
+ summariseCall,
17
+ summariseResult,
18
+ todoLines
19
+ } from "./chunk-FARS4R67.js";
20
+ import "./chunk-ITITLNAR.js";
21
+ import "./chunk-A2QAIRQL.js";
22
+ import "./chunk-W7BPYQMW.js";
23
+ import "./chunk-G2AX7MGS.js";
24
+ import "./chunk-KWRJREUH.js";
25
+ import "./chunk-EU7PA45O.js";
26
+ import "./chunk-6TWBYMY7.js";
27
+ import "./chunk-5DEWP3YX.js";
28
+ import "./chunk-ZAE25JM2.js";
29
+ import "./chunk-ATW7BNIG.js";
30
+ import "./chunk-PZS3V6P3.js";
31
+ import "./chunk-OUCV3FCT.js";
32
+ import "./chunk-D5O5CIWK.js";
33
+ import "./chunk-WMOLXTBU.js";
34
+ import "./chunk-HUIK3WXO.js";
35
+ import "./chunk-KKB5W47S.js";
36
+ import "./chunk-3NJNVZTK.js";
37
+
38
+ // src/cli/chat/inkChat.ts
39
+ import { render } from "ink";
40
+ import { createElement } from "react";
41
+
42
+ // src/cli/chat/externalEditor.ts
43
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
44
+ import { tmpdir } from "os";
45
+ import { join } from "path";
46
+
47
+ // src/sandbox/terminal.ts
48
+ import { spawnSync } from "child_process";
49
+ function runInTerminal(argv) {
50
+ const [program, ...args] = argv;
51
+ if (program === void 0) return { exitCode: null, error: "no program" };
52
+ const result = spawnSync(program, args, { stdio: "inherit" });
53
+ if (result.error !== void 0) return { exitCode: null, error: result.error.message };
54
+ return { exitCode: result.status };
55
+ }
56
+
57
+ // src/cli/chat/externalEditor.ts
58
+ function editorCommand(env = process.env) {
59
+ const raw = (env.VISUAL || env.EDITOR || "vi").trim();
60
+ return [...raw.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
61
+ }
62
+ function editInEditor(text, options = {}) {
63
+ const dir = mkdtempSync(join(tmpdir(), "garuda-prompt-"));
64
+ const file = join(dir, "prompt.md");
65
+ try {
66
+ writeFileSync(file, text, { mode: 384 });
67
+ const argv = [...editorCommand(options.env), file];
68
+ const result = (options.run ?? runInTerminal)(argv);
69
+ if (result.error !== void 0) {
70
+ return { problem: `Could not start ${argv[0]}: ${result.error}. Set $EDITOR.` };
71
+ }
72
+ if (result.exitCode !== 0)
73
+ return { problem: `${argv[0]} ended with code ${result.exitCode}; the line did not change.` };
74
+ return { text: readFileSync(file, "utf8").replace(/\n+$/, "") };
75
+ } finally {
76
+ rmSync(dir, { recursive: true, force: true });
77
+ }
78
+ }
79
+
80
+ // src/cli/chat/hunkStaging.ts
81
+ function parseHunksFromDiff(diffText, fallbackFile = "patch") {
82
+ const lines = diffText.split("\n");
83
+ let file = fallbackFile;
84
+ const hunks = [];
85
+ let current = null;
86
+ let hunkIndex = 0;
87
+ for (const line of lines) {
88
+ if (current === null && (line.startsWith("--- ") || line.startsWith("+++ "))) {
89
+ const match = /^[+-]{3}\s+[ab]\/(.+)$/.exec(line);
90
+ if (match?.[1]) file = match[1];
91
+ continue;
92
+ }
93
+ const hunkMatch = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/.exec(line);
94
+ if (hunkMatch) {
95
+ if (current) {
96
+ hunks.push({
97
+ id: hunkIndex++,
98
+ ...current,
99
+ lines: [...current.lines]
100
+ });
101
+ }
102
+ current = {
103
+ oldStart: Number.parseInt(hunkMatch[1] ?? "1", 10),
104
+ oldLines: Number.parseInt(hunkMatch[2] ?? "1", 10),
105
+ newStart: Number.parseInt(hunkMatch[3] ?? "1", 10),
106
+ newLines: Number.parseInt(hunkMatch[4] ?? "1", 10),
107
+ header: line,
108
+ heading: (hunkMatch[5] ?? "").trim(),
109
+ lines: [],
110
+ staged: true
111
+ };
112
+ continue;
113
+ }
114
+ if (current) {
115
+ if (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ") || line.startsWith("\\")) {
116
+ current.lines.push(line);
117
+ }
118
+ }
119
+ }
120
+ if (current) {
121
+ hunks.push({
122
+ id: hunkIndex++,
123
+ ...current,
124
+ lines: [...current.lines]
125
+ });
126
+ }
127
+ return { file, hunks };
128
+ }
129
+ function createHunkStaging(diffText, fallbackFile) {
130
+ const { file, hunks } = parseHunksFromDiff(diffText, fallbackFile);
131
+ if (hunks.length === 0) return void 0;
132
+ return {
133
+ file,
134
+ hunks,
135
+ currentHunk: 0
136
+ };
137
+ }
138
+ function stageCurrentHunk(state, staged) {
139
+ const current = state.hunks[state.currentHunk];
140
+ if (current) current.staged = staged;
141
+ const hasStaged = state.hunks.some((h) => h.staged);
142
+ if (state.currentHunk < state.hunks.length - 1) {
143
+ state.currentHunk++;
144
+ return { finished: false, hasStaged };
145
+ }
146
+ return { finished: true, hasStaged };
147
+ }
148
+ function stageAllRemaining(state) {
149
+ for (let i = state.currentHunk; i < state.hunks.length; i++) {
150
+ const h = state.hunks[i];
151
+ if (h) h.staged = true;
152
+ }
153
+ }
154
+ function discardAllHunks(state) {
155
+ for (const h of state.hunks) {
156
+ h.staged = false;
157
+ }
158
+ }
159
+ function navigateHunk(state, delta) {
160
+ const next = state.currentHunk + delta;
161
+ state.currentHunk = Math.max(0, Math.min(state.hunks.length - 1, next));
162
+ }
163
+
164
+ // src/cli/chat/lineEditor.ts
165
+ var emptyEditor = (history = []) => ({
166
+ text: "",
167
+ cursor: 0,
168
+ history,
169
+ historyIndex: -1,
170
+ draft: ""
171
+ });
172
+ function edit(state, action) {
173
+ const { text, cursor } = state;
174
+ const set = (next, at) => ({
175
+ ...state,
176
+ text: next,
177
+ cursor: Math.max(0, Math.min(at, next.length)),
178
+ historyIndex: -1
179
+ });
180
+ switch (action.type) {
181
+ case "insert": {
182
+ const add = action.text.replace(/\r\n?/g, "\n");
183
+ return set(text.slice(0, cursor) + add + text.slice(cursor), cursor + add.length);
184
+ }
185
+ case "backspace":
186
+ return cursor === 0 ? state : set(text.slice(0, cursor - 1) + text.slice(cursor), cursor - 1);
187
+ case "delete":
188
+ return cursor === text.length ? state : set(text.slice(0, cursor) + text.slice(cursor + 1), cursor);
189
+ case "left":
190
+ return { ...state, cursor: Math.max(0, cursor - 1) };
191
+ case "right":
192
+ return { ...state, cursor: Math.min(text.length, cursor + 1) };
193
+ case "home":
194
+ return { ...state, cursor: 0 };
195
+ case "end":
196
+ return { ...state, cursor: text.length };
197
+ case "wordLeft":
198
+ return { ...state, cursor: wordStart(text, cursor) };
199
+ case "wordRight":
200
+ return { ...state, cursor: wordEnd(text, cursor) };
201
+ case "deleteWord": {
202
+ const start = wordStart(text, cursor);
203
+ return set(text.slice(0, start) + text.slice(cursor), start);
204
+ }
205
+ case "killToStart":
206
+ return set(text.slice(cursor), 0);
207
+ case "killToEnd":
208
+ return set(text.slice(0, cursor), cursor);
209
+ case "clear":
210
+ return { ...emptyEditor(state.history) };
211
+ case "set":
212
+ return set(action.text, action.cursor);
213
+ case "up": {
214
+ if (state.historyIndex + 1 >= state.history.length) return state;
215
+ const index = state.historyIndex + 1;
216
+ const entry = state.history[index] ?? "";
217
+ return {
218
+ ...state,
219
+ text: entry,
220
+ cursor: entry.length,
221
+ historyIndex: index,
222
+ draft: state.historyIndex === -1 ? text : state.draft
223
+ };
224
+ }
225
+ case "down": {
226
+ if (state.historyIndex === -1) return state;
227
+ const index = state.historyIndex - 1;
228
+ const entry = index === -1 ? state.draft : state.history[index] ?? "";
229
+ return { ...state, text: entry, cursor: entry.length, historyIndex: index };
230
+ }
231
+ }
232
+ }
233
+ function submit(state) {
234
+ const text = state.text;
235
+ const history = text.trim() === "" || state.history[0] === text ? state.history : [text, ...state.history];
236
+ return { text, state: emptyEditor(history.slice(0, 200)) };
237
+ }
238
+ function wordStart(text, cursor) {
239
+ let i = cursor;
240
+ while (i > 0 && /\s/.test(text[i - 1] ?? "")) i--;
241
+ while (i > 0 && !/\s/.test(text[i - 1] ?? "")) i--;
242
+ return i;
243
+ }
244
+ function wordEnd(text, cursor) {
245
+ let i = cursor;
246
+ while (i < text.length && /\s/.test(text[i] ?? "")) i++;
247
+ while (i < text.length && !/\s/.test(text[i] ?? "")) i++;
248
+ return i;
249
+ }
250
+
251
+ // src/cli/chat/markdown.ts
252
+ import { styleText } from "util";
253
+ var ansi = (style, text) => styleText(style, text, { validateStream: false });
254
+ function takeBlocks(buffer) {
255
+ const blocks = [];
256
+ const lines = buffer.split("\n");
257
+ const open = lines.pop() ?? "";
258
+ let current = [];
259
+ let inFence = false;
260
+ for (const line of lines) {
261
+ if (/^\s*(```|~~~)/.test(line)) {
262
+ current.push(line);
263
+ inFence = !inFence;
264
+ if (!inFence) {
265
+ blocks.push(current.join("\n"));
266
+ current = [];
267
+ }
268
+ continue;
269
+ }
270
+ if (!inFence && line.trim() === "") {
271
+ if (current.length > 0) blocks.push(current.join("\n"));
272
+ current = [];
273
+ continue;
274
+ }
275
+ current.push(line);
276
+ }
277
+ return { blocks, rest: [...current, open].join("\n") };
278
+ }
279
+ function renderMarkdown(block, paint = ansi) {
280
+ const lines = block.split("\n");
281
+ const out = [];
282
+ let inFence = false;
283
+ for (const line of lines) {
284
+ if (/^\s*(```|~~~)/.test(line)) {
285
+ inFence = !inFence;
286
+ continue;
287
+ }
288
+ if (inFence) {
289
+ out.push(` ${paint("cyan", line)}`);
290
+ continue;
291
+ }
292
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
293
+ if (heading) {
294
+ const text = inline(heading[2] ?? "", paint);
295
+ out.push(heading[1] === "#" ? paint(["bold", "underline"], text) : paint("bold", text));
296
+ continue;
297
+ }
298
+ const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
299
+ if (bullet) {
300
+ out.push(`${bullet[1]}\u2022 ${inline(bullet[2] ?? "", paint)}`);
301
+ continue;
302
+ }
303
+ const quote = /^>\s?(.*)$/.exec(line);
304
+ if (quote) {
305
+ out.push(paint("dim", `\u2502 ${quote[1] ?? ""}`));
306
+ continue;
307
+ }
308
+ if (/^\s*(---|\*\*\*|___)\s*$/.test(line)) {
309
+ out.push(paint("dim", "\u2500".repeat(40)));
310
+ continue;
311
+ }
312
+ out.push(inline(line, paint));
313
+ }
314
+ return out.join("\n");
315
+ }
316
+ function inline(text, paint = ansi) {
317
+ return text.split(/(`[^`]+`)/).map((part) => {
318
+ if (part.startsWith("`") && part.endsWith("`") && part.length > 1) {
319
+ return paint("cyan", part.slice(1, -1));
320
+ }
321
+ return part.replace(/\*\*([^*]+)\*\*/g, (_m, t) => paint("bold", t)).replace(
322
+ /(^|[^\w*])\*([^*\s][^*]*)\*(?!\w)/g,
323
+ (_m, pre, t) => pre + paint("italic", t)
324
+ ).replace(
325
+ /(^|[^\w])_([^_\s][^_]*)_(?!\w)/g,
326
+ (_m, pre, t) => pre + paint("italic", t)
327
+ ).replace(
328
+ /\[([^\]]+)\]\(([^)]+)\)/g,
329
+ (_m, t, url) => `${t} ${paint("dim", `(${url})`)}`
330
+ );
331
+ }).join("");
332
+ }
333
+
334
+ // src/cli/chat/palette.ts
335
+ var PALETTE_ROWS = 10;
336
+ function filterPalette(entries, query) {
337
+ const q = query.trim().replace(/^\//, "").toLowerCase();
338
+ if (q === "") return [...entries];
339
+ const byName = new Map(entries.map((e) => [e.name, e]));
340
+ const names = fuzzyFiles(q, [...byName.keys()], entries.length);
341
+ const out = names.flatMap((n) => {
342
+ const entry = byName.get(n);
343
+ return entry === void 0 ? [] : [entry];
344
+ });
345
+ if (out.length > 0) return out;
346
+ return entries.filter((entry) => entry.hint.toLowerCase().includes(q));
347
+ }
348
+ function paletteWindow(selected, count) {
349
+ if (count <= PALETTE_ROWS) return 0;
350
+ return Math.min(Math.max(0, selected - PALETTE_ROWS + 1), count - PALETTE_ROWS);
351
+ }
352
+
353
+ // src/cli/chat/store.ts
354
+ var APPROVAL_KEY_DELAY_MS = 400;
355
+ var APPROVAL_CHOICES = [
356
+ { choice: "once", label: "Yes, once" },
357
+ { choice: "session", label: "Yes, for this session" },
358
+ { choice: "deny", label: "No, deny" }
359
+ ];
360
+ var EXIT_WINDOW_MS = 2e3;
361
+ var ChatStore = class {
362
+ /** Set by runTurnInTerminal during a turn: it stops the turn. */
363
+ onInterrupt = () => {
364
+ };
365
+ state;
366
+ listeners = /* @__PURE__ */ new Set();
367
+ nextId = 0;
368
+ waiting;
369
+ answer;
370
+ lastOutput;
371
+ /** All palette rows while the palette is open (0.9). */
372
+ paletteAll = [];
373
+ interruptAt = 0;
374
+ /** A stop of the running turn was asked (Esc or Ctrl-C): Esc does not ask again. */
375
+ stopping = false;
376
+ /** Tab (0.6): the chat sets it; see complete.ts. */
377
+ completer;
378
+ /** The command palette's rows (0.9): the chat sets it. */
379
+ paletteSource;
380
+ /** Ctrl-G and /editor (0.6): the chat sets it; it edits the text in the user's editor. */
381
+ externalEdit;
382
+ paint;
383
+ now;
384
+ constructor(status, options = {}) {
385
+ this.paint = options.paint ?? ansi;
386
+ this.now = options.now ?? Date.now;
387
+ this.approvalKeyDelayMs = options.approvalKeyDelayMs ?? APPROVAL_KEY_DELAY_MS;
388
+ this.state = {
389
+ items: [],
390
+ streaming: "",
391
+ running: [],
392
+ approval: void 0,
393
+ queue: [],
394
+ editor: emptyEditor(options.history),
395
+ busy: false,
396
+ status,
397
+ exiting: false
398
+ };
399
+ }
400
+ // The external store for React (useSyncExternalStore).
401
+ subscribe = (listener) => {
402
+ this.listeners.add(listener);
403
+ return () => this.listeners.delete(listener);
404
+ };
405
+ getState = () => this.state;
406
+ update(change) {
407
+ this.state = { ...this.state, ...change };
408
+ for (const listener of this.listeners) listener();
409
+ }
410
+ add(item) {
411
+ this.update({ items: [...this.state.items, { ...item, id: this.nextId++ }] });
412
+ }
413
+ // Input.
414
+ /** The next line to run: from the queue, or the next Enter. undefined means exit. */
415
+ nextInput() {
416
+ if (this.state.exiting) return Promise.resolve(void 0);
417
+ const [first, ...rest] = this.state.queue;
418
+ if (first !== void 0) {
419
+ this.update({ queue: rest });
420
+ return Promise.resolve(first);
421
+ }
422
+ return new Promise((resolve) => {
423
+ this.waiting = resolve;
424
+ });
425
+ }
426
+ editLine(action) {
427
+ this.update({ editor: edit(this.state.editor, action) });
428
+ }
429
+ /** Enter: run the line now, or queue it while a turn runs (type-ahead). */
430
+ submitLine() {
431
+ const { text, state } = submit(this.state.editor);
432
+ this.update({ editor: state });
433
+ if (text.trim() === "") return;
434
+ if (this.waiting !== void 0 && !this.state.busy) {
435
+ const resolve = this.waiting;
436
+ this.waiting = void 0;
437
+ resolve(text);
438
+ } else {
439
+ this.update({ queue: [...this.state.queue, text] });
440
+ }
441
+ }
442
+ /** Queue a line to run next, as if typed (garuda init). */
443
+ enqueue(text) {
444
+ this.update({ queue: [...this.state.queue, text] });
445
+ }
446
+ /** Esc: drop queued lines. */
447
+ /** Ctrl-P (0.9): open the command palette, or close it when it is open. */
448
+ togglePalette() {
449
+ if (this.state.palette !== void 0) {
450
+ this.closePalette();
451
+ return;
452
+ }
453
+ const all = this.paletteSource?.() ?? [];
454
+ this.paletteAll = all;
455
+ this.update({ palette: { query: "", entries: all, selected: 0 } });
456
+ }
457
+ closePalette() {
458
+ const { palette: _closed, ...rest } = this.state;
459
+ this.state = rest;
460
+ for (const listener of this.listeners) listener();
461
+ }
462
+ /** Type into the palette's filter (text), or take a character off (null). */
463
+ paletteQuery(text) {
464
+ const palette = this.state.palette;
465
+ if (palette === void 0) return;
466
+ const query = text === null ? palette.query.slice(0, -1) : palette.query + text;
467
+ this.update({
468
+ palette: { query, entries: filterPalette(this.paletteAll, query), selected: 0 }
469
+ });
470
+ }
471
+ movePalette(delta) {
472
+ const palette = this.state.palette;
473
+ if (palette === void 0 || palette.entries.length === 0) return;
474
+ const n = palette.entries.length;
475
+ this.update({ palette: { ...palette, selected: (palette.selected + delta + n) % n } });
476
+ }
477
+ /** Enter: run the selected command, or put it in the input line for its arguments. */
478
+ pickPalette() {
479
+ const entry = this.state.palette?.entries[this.state.palette.selected];
480
+ this.closePalette();
481
+ if (entry === void 0) return;
482
+ if (entry.runs) {
483
+ this.editLine({ type: "set", text: `/${entry.name}`, cursor: entry.name.length + 1 });
484
+ this.submitLine();
485
+ } else {
486
+ this.editLine({ type: "set", text: `/${entry.name} `, cursor: entry.name.length + 2 });
487
+ }
488
+ }
489
+ clearQueue() {
490
+ if (this.state.queue.length > 0) this.update({ queue: [] });
491
+ }
492
+ /**
493
+ * Esc (0.6). During a turn: drop queued lines and stop the turn, like the first Ctrl-C. Esc never
494
+ * exits Garuda: a second Esc while the turn stops does nothing.
495
+ */
496
+ stopTurn() {
497
+ this.clearQueue();
498
+ if (!this.state.busy || this.stopping) return;
499
+ this.stopping = true;
500
+ this.onInterrupt();
501
+ }
502
+ /** Tab (0.6): complete a /command or an @path at the cursor; several matches are listed. */
503
+ completeLine() {
504
+ const { text, cursor } = this.state.editor;
505
+ const result = this.completer?.(text, cursor);
506
+ if (result === void 0) return;
507
+ this.editLine({ type: "set", text: result.text, cursor: result.cursor });
508
+ if (result.candidates.length > 0) {
509
+ this.info(result.candidates.join(result.lines === true ? "\n" : " "));
510
+ }
511
+ }
512
+ /** Ctrl-G or /editor (0.6): edit the input line in $VISUAL or $EDITOR. The text is not sent. */
513
+ openEditor() {
514
+ if (this.externalEdit === void 0) {
515
+ this.warn("The external editor works only in the full chat on a terminal.");
516
+ return;
517
+ }
518
+ const result = this.externalEdit(this.state.editor.text);
519
+ if (result.problem !== void 0) this.warn(result.problem);
520
+ if (result.text !== void 0) {
521
+ this.editLine({ type: "clear" });
522
+ this.editLine({ type: "insert", text: result.text });
523
+ }
524
+ }
525
+ /**
526
+ * Ctrl-C. During a turn: stop it (a second Ctrl-C exits, see runTurnInTerminal).
527
+ * At the prompt: clear the line, or exit on a second Ctrl-C within 2 s.
528
+ */
529
+ interrupt() {
530
+ if (this.state.busy) {
531
+ this.stopping = true;
532
+ this.onInterrupt();
533
+ return;
534
+ }
535
+ if (this.state.editor.text !== "") {
536
+ this.editLine({ type: "clear" });
537
+ return;
538
+ }
539
+ if (this.now() - this.interruptAt < EXIT_WINDOW_MS) {
540
+ this.exit();
541
+ return;
542
+ }
543
+ this.interruptAt = this.now();
544
+ this.info("Press Ctrl-C again to exit, or type /exit.");
545
+ }
546
+ /** Ctrl-D on an empty line, or the end of input. */
547
+ exit() {
548
+ this.update({ exiting: true });
549
+ const resolve = this.waiting;
550
+ this.waiting = void 0;
551
+ resolve?.(void 0);
552
+ }
553
+ // A turn.
554
+ /** Update the footer, for example after /plan. */
555
+ refreshStatus(status) {
556
+ this.update({ status });
557
+ }
558
+ /** Shift+Tab: the chat sets the handler; it switches the mode and returns the new status. */
559
+ onToggleMode;
560
+ toggleMode() {
561
+ const status = this.onToggleMode?.();
562
+ if (status !== void 0) this.update({ status });
563
+ }
564
+ /** Start a turn. `show: false` when the line is already on screen (a custom command). */
565
+ begin(prompt, show = true) {
566
+ if (show) this.add({ kind: "user", text: prompt });
567
+ this.stopping = false;
568
+ this.update({ busy: true });
569
+ }
570
+ end(status) {
571
+ this.flushText();
572
+ this.stopping = false;
573
+ this.update({ busy: false, running: [], status: { ...this.state.status, ...status } });
574
+ }
575
+ /** Print text as it is (it may hold its own colors), for example the start banner. */
576
+ print(text) {
577
+ this.add({ kind: "output", text });
578
+ }
579
+ /** Echo a command line, for commands that do not start a turn. */
580
+ echo(prompt) {
581
+ this.add({ kind: "user", text: prompt });
582
+ }
583
+ /** The terminal window got (true) or lost (false) focus (0.6). The Ink chat sets it. */
584
+ onFocus = () => {
585
+ };
586
+ /** Keep a long text for Ctrl-O, as for a tool call (0.6: /diff). */
587
+ keepOutput(title, text) {
588
+ this.lastOutput = { title, text };
589
+ }
590
+ /** Ctrl-O: print the full output of the last tool call. */
591
+ showLastOutput() {
592
+ if (this.lastOutput === void 0) {
593
+ this.info("No tool output yet.");
594
+ return;
595
+ }
596
+ const { title, text } = this.lastOutput;
597
+ this.add({ kind: "output", text: `${this.paint("bold", title)}
598
+ ${text}` });
599
+ }
600
+ // Renderer.
601
+ event(event) {
602
+ switch (event.type) {
603
+ case "thinking_delta":
604
+ this.thinking += event.text;
605
+ return;
606
+ case "text_delta": {
607
+ this.flushThinking();
608
+ const { blocks, rest } = takeBlocks(this.state.streaming + event.text);
609
+ for (const block of blocks)
610
+ this.add({ kind: "text", text: renderMarkdown(block, this.paint) });
611
+ this.update({ streaming: rest });
612
+ return;
613
+ }
614
+ case "tool_call":
615
+ this.flushText();
616
+ this.update({
617
+ running: [
618
+ ...this.state.running,
619
+ { id: event.call.id, line: `${event.call.name} ${summariseCall(event.call)}` }
620
+ ]
621
+ });
622
+ return;
623
+ case "model_retry":
624
+ this.update({ streaming: "" });
625
+ this.warn(retryText(event));
626
+ return;
627
+ case "tool_progress": {
628
+ const base = `${event.call.name} ${summariseCall(event.call)}`;
629
+ this.update({
630
+ running: this.state.running.map(
631
+ (r) => r.id === event.call.id ? { ...r, line: `${base} \xB7 ${event.text}` } : r
632
+ )
633
+ });
634
+ return;
635
+ }
636
+ case "tool_result": {
637
+ const summary = summariseResult(event.call, event.outcome);
638
+ const line = `${this.paint("cyan", "\u25CF")} ${this.paint("bold", event.call.name)} ${summariseCall(event.call)}`;
639
+ const result = ` ${this.paint("dim", "\u23BF")} ${event.outcome.isError ? this.paint("red", summary) : this.paint("dim", summary)}`;
640
+ const todos = todoLines(event.call, event.outcome).map((l) => ` ${l}`);
641
+ const shown = this.details || event.outcome.isError ? [result] : [];
642
+ this.add({ kind: "tool", text: [line, ...shown, ...todos].join("\n") });
643
+ this.lastOutput = {
644
+ title: `${event.call.name} ${summariseCall(event.call)}`,
645
+ text: event.outcome.content
646
+ };
647
+ this.update({ running: this.state.running.filter((r) => r.id !== event.call.id) });
648
+ return;
649
+ }
650
+ case "compaction": {
651
+ const { stage, beforeTokens, afterTokens } = event.result;
652
+ this.info(`Context compacted (${stage}): ${beforeTokens} \u2192 about ${afterTokens} tokens.`);
653
+ return;
654
+ }
655
+ case "step_end":
656
+ this.flushText();
657
+ return;
658
+ case "notice":
659
+ this.info(event.text);
660
+ return;
661
+ case "server_tool": {
662
+ this.flushText();
663
+ const { call, result } = serverToolText(event);
664
+ const failed = event.result?.error !== void 0;
665
+ const head = `${this.paint("cyan", "\u25CF")} ${this.paint("bold", call)}`;
666
+ this.add({
667
+ kind: "tool",
668
+ text: this.details || failed ? `${head}
669
+ ${this.paint("dim", "\u23BF")} ${this.paint(failed ? "red" : "dim", result)}` : head
670
+ });
671
+ this.lastOutput = { title: call, text: serverToolOutput(event) };
672
+ return;
673
+ }
674
+ }
675
+ }
676
+ /** /details (0.9): show the result line of each tool call (errors always show). */
677
+ details = true;
678
+ info(text) {
679
+ this.note("info", text);
680
+ }
681
+ warn(text) {
682
+ this.note("warn", text);
683
+ }
684
+ error(text) {
685
+ this.note("error", text);
686
+ }
687
+ note(level, text) {
688
+ this.flushText();
689
+ const trimmed = text.replace(/^\n+/, "");
690
+ const style = level === "info" ? "dim" : level === "warn" ? "yellow" : "red";
691
+ this.add({ kind: "note", level, text: this.paint(style, trimmed) });
692
+ }
693
+ /** Readable thinking of the current block (0.9, /thinking show). */
694
+ thinking = "";
695
+ /** Show the thinking so far as a dimmed item: the first lines, Ctrl-O for all (0.9). */
696
+ flushThinking() {
697
+ const text = this.thinking.trim();
698
+ this.thinking = "";
699
+ if (text === "") return;
700
+ this.add({ kind: "output", text: thinkingPreview(text, (s) => this.paint("dim", s)) });
701
+ this.lastOutput = { title: "thinking", text };
702
+ }
703
+ flushText() {
704
+ this.flushThinking();
705
+ const rest = this.state.streaming;
706
+ if (rest.trim() !== "")
707
+ this.add({ kind: "text", text: renderMarkdown(rest.trim(), this.paint) });
708
+ if (rest !== "") this.update({ streaming: "" });
709
+ }
710
+ // Approver.
711
+ /**
712
+ * One question at a time (0.14.1, review): calls in a parallel batch can both ask, and a second
713
+ * question replaced the first, whose call then waited forever. Now the second one waits.
714
+ */
715
+ ask(request, signal) {
716
+ const next = this.asking === 0 ? this.askNow(request, signal) : this.askQueue.then(() => this.askNow(request, signal));
717
+ this.asking++;
718
+ this.askQueue = next.catch(() => {
719
+ }).finally(() => {
720
+ this.asking--;
721
+ });
722
+ return next;
723
+ }
724
+ /**
725
+ * False in the first moment after a question appears: keys typed for the next prompt must not
726
+ * answer it ("a" in "add a test" allowed the call for the session; 0.14.1, review).
727
+ */
728
+ takesApprovalKeys() {
729
+ const shownAt = this.state.approval?.shownAt;
730
+ return shownAt === void 0 || this.now() - shownAt >= this.approvalKeyDelayMs;
731
+ }
732
+ askQueue = Promise.resolve();
733
+ asking = 0;
734
+ approvalKeyDelayMs;
735
+ askNow(request, signal) {
736
+ this.flushText();
737
+ this.add({ kind: "output", text: `${header(request)}
738
+ ${colorPreview(request)}` });
739
+ return new Promise((resolve, reject) => {
740
+ const onAbort = () => {
741
+ this.answer = void 0;
742
+ this.update({ approval: void 0 });
743
+ reject(signal.reason);
744
+ };
745
+ if (signal.aborted) {
746
+ onAbort();
747
+ return;
748
+ }
749
+ signal.addEventListener("abort", onAbort, { once: true });
750
+ this.answer = (choice) => {
751
+ signal.removeEventListener("abort", onAbort);
752
+ const label = this.state.approval?.choices.find((c) => c.choice === choice)?.label ?? choice;
753
+ this.answer = void 0;
754
+ this.update({ approval: void 0 });
755
+ this.add({
756
+ kind: "note",
757
+ level: "info",
758
+ text: choice === "deny" ? this.paint("red", `\u2717 ${label}`) : this.paint("green", `\u2714 ${label}`)
759
+ });
760
+ resolve(choice);
761
+ };
762
+ const host = request.target.kind === "url" ? request.target.host : void 0;
763
+ const shown = APPROVAL_CHOICES.filter((c) => request.choices?.includes(c.choice) ?? true);
764
+ const choices = shown.map((c) => ({
765
+ choice: c.choice,
766
+ label: request.labels?.[c.choice] ?? (c.choice === "session" && host !== void 0 ? `Yes, allow ${host} for this session` : c.label)
767
+ }));
768
+ this.update({ approval: { request, choices, selected: 0, shownAt: this.now() } });
769
+ });
770
+ }
771
+ moveApproval(delta) {
772
+ const approval = this.state.approval;
773
+ if (approval === void 0) return;
774
+ const n = approval.choices.length;
775
+ this.update({ approval: { ...approval, selected: (approval.selected + delta + n) % n } });
776
+ }
777
+ /** Answer with the highlighted choice, or with a given one. */
778
+ choose(choice) {
779
+ const approval = this.state.approval;
780
+ if (approval === void 0 || this.answer === void 0) return;
781
+ if (choice !== void 0 && !approval.choices.some((c) => c.choice === choice)) return;
782
+ this.answer(choice ?? approval.choices[approval.selected]?.choice ?? "deny");
783
+ }
784
+ /**
785
+ * Enter hunk-by-hunk review (0.14). Only when the tool can apply single hunks (the request has
786
+ * `selectHunks`, U0): otherwise the choice of hunks would not change what is written.
787
+ */
788
+ startHunkReview() {
789
+ const approval = this.state.approval;
790
+ if (approval === void 0 || approval.request.selectHunks === void 0) return false;
791
+ const fallback = approval.request.target.kind === "path" ? approval.request.target.path : "diff";
792
+ const hunkReview = createHunkStaging(approval.request.preview, fallback);
793
+ if (hunkReview === void 0) return false;
794
+ this.update({ approval: { ...approval, hunkReview } });
795
+ return true;
796
+ }
797
+ /** Exit hunk staging back to standard approval choice view. */
798
+ closeHunkReview() {
799
+ const approval = this.state.approval;
800
+ if (approval === void 0 || approval.hunkReview === void 0) return;
801
+ this.update({ approval: { ...approval, hunkReview: void 0 } });
802
+ }
803
+ /** Stage or unstage the current hunk in hunk review mode. */
804
+ stageHunk(staged) {
805
+ const approval = this.state.approval;
806
+ if (approval === void 0 || approval.hunkReview === void 0) return;
807
+ const { finished } = stageCurrentHunk(approval.hunkReview, staged);
808
+ if (finished) {
809
+ this.finishHunkReview();
810
+ } else {
811
+ this.update({ approval: { ...approval, hunkReview: { ...approval.hunkReview } } });
812
+ }
813
+ }
814
+ /** Stage all remaining hunks and approve. */
815
+ stageAllHunks() {
816
+ const approval = this.state.approval;
817
+ if (approval === void 0 || approval.hunkReview === void 0) return;
818
+ stageAllRemaining(approval.hunkReview);
819
+ this.finishHunkReview();
820
+ }
821
+ /**
822
+ * Answer from the staged hunks (U0): all staged = the whole change; none = deny; some = only those
823
+ * hunks, passed to the tool through `selectHunks`, so a rejected hunk never reaches the disk.
824
+ */
825
+ finishHunkReview() {
826
+ const approval = this.state.approval;
827
+ if (approval === void 0 || approval.hunkReview === void 0) return;
828
+ const staged = approval.hunkReview.hunks.filter((h) => h.staged).map((h) => h.id);
829
+ if (staged.length === 0) {
830
+ this.choose("deny");
831
+ return;
832
+ }
833
+ if (staged.length < approval.hunkReview.hunks.length) {
834
+ approval.request.selectHunks?.(staged);
835
+ }
836
+ this.choose("once");
837
+ }
838
+ /** Discard all hunks and deny. */
839
+ discardAllHunks() {
840
+ const approval = this.state.approval;
841
+ if (approval === void 0 || approval.hunkReview === void 0) return;
842
+ discardAllHunks(approval.hunkReview);
843
+ this.choose("deny");
844
+ }
845
+ /** Navigate between hunks in hunk review mode. */
846
+ moveHunk(delta) {
847
+ const approval = this.state.approval;
848
+ if (approval === void 0 || approval.hunkReview === void 0) return;
849
+ navigateHunk(approval.hunkReview, delta);
850
+ this.update({ approval: { ...approval, hunkReview: { ...approval.hunkReview } } });
851
+ }
852
+ };
853
+ var THINKING_LINES = 4;
854
+ function thinkingPreview(text, dim) {
855
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
856
+ const shown = lines.slice(0, THINKING_LINES).map((l) => l.length > 160 ? `${l.slice(0, 159)}\u2026` : l);
857
+ const more = lines.length > THINKING_LINES ? [`\u2026 ${lines.length - THINKING_LINES} more line(s): Ctrl-O`] : [];
858
+ return [...shown, ...more].map((l, i) => dim(`${i === 0 ? "\u273B" : " "} ${l}`)).join("\n");
859
+ }
860
+
861
+ // src/cli/chat/ui.tsx
862
+ import { Box, Static, Text, useInput, usePaste } from "ink";
863
+ import { useEffect, useState, useSyncExternalStore } from "react";
864
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
865
+ function App({ store }) {
866
+ const state = useSyncExternalStore(store.subscribe, store.getState);
867
+ useInput((input, key) => onKey(store, state, input, key));
868
+ usePaste((text) => {
869
+ if (store.getState().approval === void 0) store.editLine({ type: "insert", text });
870
+ });
871
+ const spinning = state.busy && state.approval === void 0;
872
+ const frame = useSpinner(spinning);
873
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
874
+ /* @__PURE__ */ jsx(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx(ItemView, { item }, item.id) }),
875
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
876
+ state.streaming !== "" && /* @__PURE__ */ jsx(Text, { children: state.streaming }),
877
+ state.running.map((tool) => /* @__PURE__ */ jsxs(Text, { children: [
878
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: frame }),
879
+ " ",
880
+ tool.line
881
+ ] }, tool.id)),
882
+ spinning && state.running.length === 0 && state.streaming === "" && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
883
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: frame }),
884
+ " Working\u2026 (Ctrl-C stops)"
885
+ ] }),
886
+ state.approval !== void 0 && state.approval.hunkReview !== void 0 && /* @__PURE__ */ jsx(HunkStagingView, { staging: state.approval.hunkReview }),
887
+ state.approval !== void 0 && state.approval.hunkReview === void 0 && /* @__PURE__ */ jsx(
888
+ ApprovalView,
889
+ {
890
+ question: state.approval.request.question ?? "Allow?",
891
+ choices: state.approval.choices,
892
+ selected: state.approval.selected,
893
+ hasHunks: state.approval.request.selectHunks !== void 0
894
+ }
895
+ ),
896
+ state.queue.map((line, i) => (
897
+ // biome-ignore lint/suspicious/noArrayIndexKey: queued lines can repeat.
898
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
899
+ " ",
900
+ "queued: ",
901
+ firstLine(line)
902
+ ] }, i)
903
+ )),
904
+ state.approval === void 0 && !state.exiting && state.palette !== void 0 && /* @__PURE__ */ jsx(PaletteView, { palette: state.palette }),
905
+ state.approval === void 0 && !state.exiting && state.palette === void 0 && /* @__PURE__ */ jsx(InputLine, { state }),
906
+ /* @__PURE__ */ jsx(Footer, { state })
907
+ ] })
908
+ ] });
909
+ }
910
+ function ItemView({ item }) {
911
+ switch (item.kind) {
912
+ case "user":
913
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
914
+ "\u203A ",
915
+ item.text
916
+ ] }) });
917
+ case "text":
918
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: item.text }) });
919
+ default:
920
+ return /* @__PURE__ */ jsx(Text, { children: item.text });
921
+ }
922
+ }
923
+ function HunkStagingView({ staging }) {
924
+ const current = staging.hunks[staging.currentHunk];
925
+ if (!current) return null;
926
+ const total = staging.hunks.length;
927
+ const stagedCount = staging.hunks.filter((h) => h.staged).length;
928
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
929
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
930
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
931
+ "Hunk ",
932
+ staging.currentHunk + 1,
933
+ " of ",
934
+ total,
935
+ " \xB7 ",
936
+ staging.file
937
+ ] }),
938
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
939
+ "[",
940
+ stagedCount,
941
+ "/",
942
+ total,
943
+ " staged]"
944
+ ] })
945
+ ] }),
946
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: current.header }),
947
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: 1, children: current.lines.map((line, i) => {
948
+ const color = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : void 0;
949
+ return (
950
+ // biome-ignore lint/suspicious/noArrayIndexKey: diff lines can be identical
951
+ /* @__PURE__ */ jsx(Text, { ...color ? { color } : {}, children: line }, i)
952
+ );
953
+ }) }),
954
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[y] stage hunk \xB7 [n] skip \xB7 [a] stage all \xB7 [d] discard all \xB7 [\u2190/\u2192] prev/next \xB7 [Esc] back" })
955
+ ] });
956
+ }
957
+ function ApprovalView({
958
+ question,
959
+ choices,
960
+ selected,
961
+ hasHunks = false
962
+ }) {
963
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
964
+ /* @__PURE__ */ jsx(Text, { bold: true, children: question }),
965
+ choices.map((c, i) => /* @__PURE__ */ jsxs(Text, { ...i === selected ? { color: "cyan" } : {}, children: [
966
+ i === selected ? "\u276F " : " ",
967
+ i + 1,
968
+ ". ",
969
+ c.label
970
+ ] }, c.choice)),
971
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: approvalKeys(
972
+ choices.map((c) => c.choice),
973
+ hasHunks
974
+ ) })
975
+ ] });
976
+ }
977
+ function approvalKeys(choices, hasHunks = false) {
978
+ const letters = { once: "y", session: "a", deny: "n or Esc" };
979
+ const numbers = choices.map((_, i) => i + 1).join(" ");
980
+ const keys = choices.map((c, i) => `${letters[c] ?? "?"} = ${i + 1}`).join(" \xB7 ");
981
+ const hunksHint = hasHunks ? " \xB7 h = stage hunks" : "";
982
+ return `\u2191\u2193 and Enter, or ${numbers} \xB7 ${keys}${hunksHint}`;
983
+ }
984
+ function PaletteView({ palette }) {
985
+ const first = paletteWindow(palette.selected, palette.entries.length);
986
+ const rows = palette.entries.slice(first, first + PALETTE_ROWS);
987
+ const width = Math.max(8, ...rows.map((e) => e.name.length + 1));
988
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
989
+ /* @__PURE__ */ jsxs(Text, { children: [
990
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "Command \u203A " }),
991
+ palette.query,
992
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
993
+ ] }),
994
+ rows.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " No command matches." }),
995
+ rows.map((entry, i) => {
996
+ const selected = first + i === palette.selected;
997
+ return /* @__PURE__ */ jsxs(Text, { ...selected ? { color: "cyan" } : {}, children: [
998
+ selected ? "\u276F " : " ",
999
+ `/${entry.name}`.padEnd(width + 1),
1000
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.hint })
1001
+ ] }, entry.name);
1002
+ }),
1003
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1004
+ palette.entries.length > PALETTE_ROWS ? `${palette.selected + 1} of ${palette.entries.length} \xB7 ` : "",
1005
+ "type to filter \xB7 \u2191\u2193 \xB7 Enter picks \xB7 Esc closes"
1006
+ ] })
1007
+ ] });
1008
+ }
1009
+ function InputLine({ state }) {
1010
+ const { text, cursor } = state.editor;
1011
+ const at = text[cursor];
1012
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
1013
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u203A " }),
1014
+ text.slice(0, cursor),
1015
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: at === void 0 || at === "\n" ? " " : at }),
1016
+ at === "\n" ? "\n" : "",
1017
+ text.slice(cursor + 1)
1018
+ ] }) });
1019
+ }
1020
+ function Footer({ state }) {
1021
+ const { model, sandbox, contextPercent, costUsd, mode } = state.status;
1022
+ const parts = [model, sandbox];
1023
+ if (contextPercent !== void 0) parts.push(`context ${contextPercent}%`);
1024
+ if (costUsd !== void 0) parts.push(`$${costUsd.toFixed(costUsd < 0.01 ? 4 : 2)}`);
1025
+ const keys = state.busy ? "Esc stop \xB7 Ctrl-O output" : "\\ then Enter: new line \xB7 Ctrl-G editor \xB7 Ctrl-P commands";
1026
+ return /* @__PURE__ */ jsxs(Text, { children: [
1027
+ mode === "plan" ? /* @__PURE__ */ jsxs(Text, { color: "yellow", bold: true, children: [
1028
+ "PLAN",
1029
+ " "
1030
+ ] }) : null,
1031
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1032
+ parts.join(" \xB7 "),
1033
+ " ",
1034
+ " ",
1035
+ keys
1036
+ ] })
1037
+ ] });
1038
+ }
1039
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1040
+ function useSpinner(active) {
1041
+ const [i, setI] = useState(0);
1042
+ useEffect(() => {
1043
+ if (!active) return;
1044
+ const timer = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 80);
1045
+ return () => clearInterval(timer);
1046
+ }, [active]);
1047
+ return FRAMES[i] ?? "\u280B";
1048
+ }
1049
+ var firstLine = (text) => {
1050
+ const [first = "", ...rest] = text.split("\n");
1051
+ return rest.length > 0 ? `${first} \u2026` : first;
1052
+ };
1053
+ function onKey(store, state, input, key) {
1054
+ const focus = focusEvent(input);
1055
+ if (focus !== void 0 && !key.ctrl && !key.meta) {
1056
+ store.onFocus(focus);
1057
+ return;
1058
+ }
1059
+ if (key.ctrl && input === "c") {
1060
+ store.interrupt();
1061
+ return;
1062
+ }
1063
+ if (state.palette !== void 0 && state.approval === void 0) {
1064
+ if (key.escape || key.ctrl && input === "p") store.closePalette();
1065
+ else if (key.upArrow) store.movePalette(-1);
1066
+ else if (key.downArrow) store.movePalette(1);
1067
+ else if (key.return) store.pickPalette();
1068
+ else if (key.backspace || key.delete) store.paletteQuery(null);
1069
+ else if (input !== "" && !key.ctrl && !key.meta && !/[\r\n\t]/.test(input))
1070
+ store.paletteQuery(input);
1071
+ return;
1072
+ }
1073
+ if (state.approval !== void 0) {
1074
+ if (!store.takesApprovalKeys()) return;
1075
+ if (state.approval.hunkReview !== void 0) {
1076
+ if (key.leftArrow || key.upArrow) store.moveHunk(-1);
1077
+ else if (key.rightArrow || key.downArrow) store.moveHunk(1);
1078
+ else if (input === "y" || key.return) store.stageHunk(true);
1079
+ else if (input === "n") store.stageHunk(false);
1080
+ else if (input === "a") store.stageAllHunks();
1081
+ else if (input === "d") store.discardAllHunks();
1082
+ else if (key.escape || input === "q") store.closeHunkReview();
1083
+ return;
1084
+ }
1085
+ if (key.upArrow) store.moveApproval(-1);
1086
+ else if (key.downArrow) store.moveApproval(1);
1087
+ else if (key.return) store.choose();
1088
+ else if (/^[1-9]$/.test(input)) {
1089
+ const picked = state.approval.choices[Number(input) - 1];
1090
+ if (picked !== void 0) store.choose(picked.choice);
1091
+ } else if (input === "y") store.choose("once");
1092
+ else if (input === "a") store.choose("session");
1093
+ else if (input === "n" || key.escape) store.choose("deny");
1094
+ else if (input === "h" || input === "p") store.startHunkReview();
1095
+ return;
1096
+ }
1097
+ if (key.ctrl) {
1098
+ const actions = {
1099
+ o: () => store.showLastOutput(),
1100
+ p: () => store.togglePalette(),
1101
+ g: () => store.openEditor(),
1102
+ a: () => store.editLine({ type: "home" }),
1103
+ e: () => store.editLine({ type: "end" }),
1104
+ u: () => store.editLine({ type: "killToStart" }),
1105
+ k: () => store.editLine({ type: "killToEnd" }),
1106
+ w: () => store.editLine({ type: "deleteWord" }),
1107
+ d: () => state.editor.text === "" && !state.busy ? store.exit() : store.editLine({ type: "delete" })
1108
+ };
1109
+ actions[input]?.();
1110
+ return;
1111
+ }
1112
+ if (key.return) {
1113
+ const before = state.editor.text.slice(0, state.editor.cursor);
1114
+ if (key.meta) store.editLine({ type: "insert", text: "\n" });
1115
+ else if (before.endsWith("\\")) {
1116
+ store.editLine({ type: "backspace" });
1117
+ store.editLine({ type: "insert", text: "\n" });
1118
+ } else store.submitLine();
1119
+ } else if (key.escape) {
1120
+ if (state.busy) store.stopTurn();
1121
+ else store.clearQueue();
1122
+ } else if (key.backspace || key.delete) store.editLine({ type: "backspace" });
1123
+ else if (key.leftArrow) store.editLine({ type: key.meta ? "wordLeft" : "left" });
1124
+ else if (key.rightArrow) store.editLine({ type: key.meta ? "wordRight" : "right" });
1125
+ else if (key.upArrow) store.editLine({ type: "up" });
1126
+ else if (key.downArrow) store.editLine({ type: "down" });
1127
+ else if (key.tab) {
1128
+ if (key.shift) store.toggleMode();
1129
+ else store.completeLine();
1130
+ } else if (/[\r\n]/.test(input)) typeAhead(store, input);
1131
+ else if (input !== "" && !key.meta) store.editLine({ type: "insert", text: input });
1132
+ }
1133
+ function typeAhead(store, chunk) {
1134
+ const parts = chunk.split(/\r\n|\r|\n/);
1135
+ parts.forEach((part, i) => {
1136
+ const last = i === parts.length - 1;
1137
+ if (!last && part.endsWith("\\")) {
1138
+ store.editLine({ type: "insert", text: `${part.slice(0, -1)}
1139
+ ` });
1140
+ return;
1141
+ }
1142
+ if (part !== "") store.editLine({ type: "insert", text: part });
1143
+ if (!last) store.submitLine();
1144
+ });
1145
+ }
1146
+
1147
+ // src/cli/chat/inkChat.ts
1148
+ async function runInkChat(runtime, approver, setEventTarget, banner, sessionPath, exitNow, firstInput, notifier, focus) {
1149
+ const store = new ChatStore(statusOf(runtime));
1150
+ store.print(banner);
1151
+ if (firstInput !== void 0) store.enqueue(firstInput);
1152
+ approver.current = store;
1153
+ setEventTarget(store);
1154
+ const ink = render(createElement(App, { store }), { exitOnCtrlC: false });
1155
+ const reportFocus = (on) => {
1156
+ if (focus !== void 0) process.stdout.write(on ? FOCUS_REPORTING_ON : FOCUS_REPORTING_OFF);
1157
+ };
1158
+ if (focus !== void 0) store.onFocus = (focused) => focus.update(focused);
1159
+ reportFocus(true);
1160
+ store.externalEdit = (text) => {
1161
+ const stdin = process.stdin;
1162
+ const raw = stdin.isTTY === true && stdin.isRaw;
1163
+ if (raw) stdin.setRawMode(false);
1164
+ stdin.pause();
1165
+ reportFocus(false);
1166
+ try {
1167
+ return editInEditor(text);
1168
+ } finally {
1169
+ if (raw) stdin.setRawMode(true);
1170
+ stdin.resume();
1171
+ focus?.update(true);
1172
+ reportFocus(true);
1173
+ ink.clear();
1174
+ }
1175
+ };
1176
+ const exitClean = () => {
1177
+ ink.unmount();
1178
+ return exitNow();
1179
+ };
1180
+ try {
1181
+ await runChat(runtime, store, sessionPath, exitClean, notifier);
1182
+ } finally {
1183
+ reportFocus(false);
1184
+ ink.unmount();
1185
+ }
1186
+ }
1187
+ export {
1188
+ runInkChat
1189
+ };
1190
+ //# sourceMappingURL=inkChat-3J26R24L.js.map