chivgent 0.19.0 → 1.0.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 (195) hide show
  1. package/README.md +136 -886
  2. package/README.zh-CN.md +71 -29
  3. package/dist/agent.d.ts +14 -1
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +195 -28
  6. package/dist/agent.js.map +1 -1
  7. package/dist/cli-options.d.ts +2 -0
  8. package/dist/cli-options.d.ts.map +1 -1
  9. package/dist/cli-options.js +12 -2
  10. package/dist/cli-options.js.map +1 -1
  11. package/dist/cli-runtime.d.ts +24 -0
  12. package/dist/cli-runtime.d.ts.map +1 -0
  13. package/dist/cli-runtime.js +64 -0
  14. package/dist/cli-runtime.js.map +1 -0
  15. package/dist/cli.js +29 -97
  16. package/dist/cli.js.map +1 -1
  17. package/dist/context/compaction.d.ts +5 -2
  18. package/dist/context/compaction.d.ts.map +1 -1
  19. package/dist/context/compaction.js +58 -32
  20. package/dist/context/compaction.js.map +1 -1
  21. package/dist/context/context-manager.d.ts +2 -0
  22. package/dist/context/context-manager.d.ts.map +1 -1
  23. package/dist/context/context-manager.js +4 -3
  24. package/dist/context/context-manager.js.map +1 -1
  25. package/dist/context/project-instructions.d.ts +17 -0
  26. package/dist/context/project-instructions.d.ts.map +1 -0
  27. package/dist/context/project-instructions.js +191 -0
  28. package/dist/context/project-instructions.js.map +1 -0
  29. package/dist/evals/baseline.d.ts +41 -0
  30. package/dist/evals/baseline.d.ts.map +1 -0
  31. package/dist/evals/baseline.js +121 -0
  32. package/dist/evals/baseline.js.map +1 -0
  33. package/dist/evals/cli.js +27 -1
  34. package/dist/evals/cli.js.map +1 -1
  35. package/dist/evals/parse-args.d.ts +2 -0
  36. package/dist/evals/parse-args.d.ts.map +1 -1
  37. package/dist/evals/parse-args.js +5 -0
  38. package/dist/evals/parse-args.js.map +1 -1
  39. package/dist/evals/report.d.ts +2 -0
  40. package/dist/evals/report.d.ts.map +1 -1
  41. package/dist/evals/report.js +12 -8
  42. package/dist/evals/report.js.map +1 -1
  43. package/dist/evals/runner.d.ts +3 -0
  44. package/dist/evals/runner.d.ts.map +1 -1
  45. package/dist/evals/runner.js +5 -1
  46. package/dist/evals/runner.js.map +1 -1
  47. package/dist/evals/task.d.ts.map +1 -1
  48. package/dist/evals/task.js +1 -0
  49. package/dist/evals/task.js.map +1 -1
  50. package/dist/evals/tools.d.ts +4 -1
  51. package/dist/evals/tools.d.ts.map +1 -1
  52. package/dist/evals/tools.js +11 -4
  53. package/dist/evals/tools.js.map +1 -1
  54. package/dist/events.d.ts +7 -1
  55. package/dist/events.d.ts.map +1 -1
  56. package/dist/events.js.map +1 -1
  57. package/dist/patch/parse.d.ts +26 -0
  58. package/dist/patch/parse.d.ts.map +1 -0
  59. package/dist/patch/parse.js +117 -0
  60. package/dist/patch/parse.js.map +1 -0
  61. package/dist/prompts.d.ts +2 -2
  62. package/dist/prompts.d.ts.map +1 -1
  63. package/dist/prompts.js +4 -3
  64. package/dist/prompts.js.map +1 -1
  65. package/dist/providers/deferred-client.d.ts +2 -1
  66. package/dist/providers/deferred-client.d.ts.map +1 -1
  67. package/dist/providers/deferred-client.js +4 -0
  68. package/dist/providers/deferred-client.js.map +1 -1
  69. package/dist/providers/interactive.d.ts +31 -0
  70. package/dist/providers/interactive.d.ts.map +1 -0
  71. package/dist/providers/interactive.js +184 -0
  72. package/dist/providers/interactive.js.map +1 -0
  73. package/dist/remote/repl.d.ts.map +1 -1
  74. package/dist/remote/repl.js +6 -5
  75. package/dist/remote/repl.js.map +1 -1
  76. package/dist/repl.d.ts +43 -4
  77. package/dist/repl.d.ts.map +1 -1
  78. package/dist/repl.js +513 -32
  79. package/dist/repl.js.map +1 -1
  80. package/dist/session-store.d.ts +6 -2
  81. package/dist/session-store.d.ts.map +1 -1
  82. package/dist/session-store.js +5 -1
  83. package/dist/session-store.js.map +1 -1
  84. package/dist/session.d.ts +1 -0
  85. package/dist/session.d.ts.map +1 -1
  86. package/dist/session.js +38 -12
  87. package/dist/session.js.map +1 -1
  88. package/dist/shell/approval.d.ts +15 -0
  89. package/dist/shell/approval.d.ts.map +1 -0
  90. package/dist/shell/approval.js +25 -0
  91. package/dist/shell/approval.js.map +1 -0
  92. package/dist/shell/docker.d.ts +18 -0
  93. package/dist/shell/docker.d.ts.map +1 -0
  94. package/dist/shell/docker.js +83 -0
  95. package/dist/shell/docker.js.map +1 -0
  96. package/dist/shell/local.d.ts +1 -1
  97. package/dist/shell/local.d.ts.map +1 -1
  98. package/dist/shell/local.js +1 -1
  99. package/dist/shell/local.js.map +1 -1
  100. package/dist/tools/apply-patch.d.ts +18 -0
  101. package/dist/tools/apply-patch.d.ts.map +1 -0
  102. package/dist/tools/apply-patch.js +53 -0
  103. package/dist/tools/apply-patch.js.map +1 -0
  104. package/dist/tools/bash.d.ts +4 -0
  105. package/dist/tools/bash.d.ts.map +1 -1
  106. package/dist/tools/bash.js +28 -3
  107. package/dist/tools/bash.js.map +1 -1
  108. package/dist/tools/parallel.d.ts +14 -0
  109. package/dist/tools/parallel.d.ts.map +1 -0
  110. package/dist/tools/parallel.js +20 -0
  111. package/dist/tools/parallel.js.map +1 -0
  112. package/dist/tools/tool.d.ts +7 -0
  113. package/dist/tools/tool.d.ts.map +1 -1
  114. package/dist/tools/tool.js +7 -1
  115. package/dist/tools/tool.js.map +1 -1
  116. package/dist/tools/update-plan.d.ts +57 -0
  117. package/dist/tools/update-plan.d.ts.map +1 -0
  118. package/dist/tools/update-plan.js +99 -0
  119. package/dist/tools/update-plan.js.map +1 -0
  120. package/dist/tui/activity.d.ts +17 -0
  121. package/dist/tui/activity.d.ts.map +1 -0
  122. package/dist/tui/activity.js +188 -0
  123. package/dist/tui/activity.js.map +1 -0
  124. package/dist/tui/diff.d.ts +14 -0
  125. package/dist/tui/diff.d.ts.map +1 -0
  126. package/dist/tui/diff.js +57 -0
  127. package/dist/tui/diff.js.map +1 -0
  128. package/dist/tui/editor-input.d.ts +53 -0
  129. package/dist/tui/editor-input.d.ts.map +1 -0
  130. package/dist/tui/editor-input.js +353 -0
  131. package/dist/tui/editor-input.js.map +1 -0
  132. package/dist/tui/editor.d.ts +72 -0
  133. package/dist/tui/editor.d.ts.map +1 -0
  134. package/dist/tui/editor.js +321 -0
  135. package/dist/tui/editor.js.map +1 -0
  136. package/dist/tui/git-diff.d.ts +7 -0
  137. package/dist/tui/git-diff.d.ts.map +1 -0
  138. package/dist/tui/git-diff.js +48 -0
  139. package/dist/tui/git-diff.js.map +1 -0
  140. package/dist/tui/input.d.ts +20 -0
  141. package/dist/tui/input.d.ts.map +1 -1
  142. package/dist/tui/input.js +175 -13
  143. package/dist/tui/input.js.map +1 -1
  144. package/dist/tui/live.d.ts +8 -0
  145. package/dist/tui/live.d.ts.map +1 -1
  146. package/dist/tui/live.js +37 -5
  147. package/dist/tui/live.js.map +1 -1
  148. package/dist/tui/markdown.d.ts +7 -0
  149. package/dist/tui/markdown.d.ts.map +1 -0
  150. package/dist/tui/markdown.js +55 -0
  151. package/dist/tui/markdown.js.map +1 -0
  152. package/dist/tui/menu.d.ts +26 -0
  153. package/dist/tui/menu.d.ts.map +1 -0
  154. package/dist/tui/menu.js +75 -0
  155. package/dist/tui/menu.js.map +1 -0
  156. package/dist/tui/review.d.ts +27 -0
  157. package/dist/tui/review.d.ts.map +1 -0
  158. package/dist/tui/review.js +114 -0
  159. package/dist/tui/review.js.map +1 -0
  160. package/dist/tui/slash.d.ts +11 -0
  161. package/dist/tui/slash.d.ts.map +1 -0
  162. package/dist/tui/slash.js +13 -0
  163. package/dist/tui/slash.js.map +1 -0
  164. package/dist/tui/state.d.ts +5 -0
  165. package/dist/tui/state.d.ts.map +1 -1
  166. package/dist/tui/state.js +10 -1
  167. package/dist/tui/state.js.map +1 -1
  168. package/dist/tui/syntax.d.ts +6 -0
  169. package/dist/tui/syntax.d.ts.map +1 -0
  170. package/dist/tui/syntax.js +48 -0
  171. package/dist/tui/syntax.js.map +1 -0
  172. package/dist/tui/text.d.ts +2 -0
  173. package/dist/tui/text.d.ts.map +1 -1
  174. package/dist/tui/text.js +19 -0
  175. package/dist/tui/text.js.map +1 -1
  176. package/dist/tui/view.d.ts +1 -1
  177. package/dist/tui/view.d.ts.map +1 -1
  178. package/dist/tui/view.js +77 -31
  179. package/dist/tui/view.js.map +1 -1
  180. package/dist/tui/welcome.d.ts +7 -1
  181. package/dist/tui/welcome.d.ts.map +1 -1
  182. package/dist/tui/welcome.js +40 -23
  183. package/dist/tui/welcome.js.map +1 -1
  184. package/dist/workspace/patch.d.ts +11 -0
  185. package/dist/workspace/patch.d.ts.map +1 -0
  186. package/dist/workspace/patch.js +124 -0
  187. package/dist/workspace/patch.js.map +1 -0
  188. package/dist/workspace/types.d.ts +7 -0
  189. package/dist/workspace/types.d.ts.map +1 -1
  190. package/dist/workspace/types.js.map +1 -1
  191. package/dist/workspace.d.ts +2 -1
  192. package/dist/workspace.d.ts.map +1 -1
  193. package/dist/workspace.js +5 -0
  194. package/dist/workspace.js.map +1 -1
  195. package/package.json +2 -2
package/dist/repl.js CHANGED
@@ -1,25 +1,49 @@
1
1
  import { TuiInput } from "./tui/input.js";
2
+ import { EditorController } from "./tui/editor-input.js";
3
+ import { readLiveGitDiff } from "./tui/git-diff.js";
4
+ import { terminalText } from "./tui/text.js";
5
+ import { PatchReviewController } from "./tui/review.js";
6
+ import { InputMenu } from "./tui/menu.js";
2
7
  import { createInterface, emitKeypressEvents } from "node:readline";
3
8
  import { formatTokens } from "./providers/usage.js";
9
+ import { approvalPreview, commandReviewable } from "./tui/activity.js";
10
+ import { terminalWidth } from "./tui/live.js";
11
+ import { formatDiffPage, mostRecentSuccessfulPatch } from "./tui/diff.js";
4
12
  export const REPL_PROMPT = "› ";
5
13
  export const BUILT_IN_COMMANDS = [
6
14
  "help",
7
15
  "session",
8
16
  "tools",
17
+ "diff",
18
+ "compose",
19
+ "editor",
20
+ "review",
21
+ "gitdiff",
9
22
  "clear",
10
23
  "exit",
11
24
  "quit",
12
25
  "login",
26
+ "provider",
27
+ "model",
28
+ "endpoint",
13
29
  ];
14
30
  const HELP = `Commands:
15
31
  /help Show this help
16
32
  /session Show the current session id, workspace, and size
17
33
  /tools List the tools available to the model
34
+ /diff [N] Review the latest successful apply_patch, page N
35
+ /compose Write a multi-line prompt (/send to submit, /cancel to discard)
36
+ /editor Open the cursor-editable multiline composer (Ctrl+S sends)
37
+ /review Browse last applied patch with arrows, q to close
38
+ /gitdiff [--staged] Review actual workspace Git changes after explicit approval
18
39
  /clear Start a new transcript in the same session
19
- /login Store an API key for this Provider
20
- /exit Leave chivgent (Ctrl+D also works)
40
+ /provider Choose a Provider (↑↓ and Enter in the TUI)
41
+ /model Show or change the current model: /model MODEL
42
+ /endpoint Set the URL for an OpenAI-compatible Provider
43
+ /login Store a key for the current Provider; /login NAME switches first
44
+ /exit Leave chivgent (Ctrl+C or Ctrl+D also works)
21
45
 
22
- Anything else is sent to the model. Ctrl+C stops the answer in progress.
46
+ Anything else is sent to the model. Ctrl+C stops a running answer; at the prompt it exits.
23
47
  `;
24
48
  function helpText(context) {
25
49
  const extras = context.extensionCommands ?? [];
@@ -52,16 +76,73 @@ export function handleSlashCommand(line, context) {
52
76
  case "/tools":
53
77
  context.write(`${context.session.toolNames.map((name) => ` ${name}`).join("\n")}\n`);
54
78
  return "handled";
79
+ case "/diff": {
80
+ const argument = trimmed.slice("/diff".length).trim();
81
+ if (argument.length > 0 && !/^[1-9]\d*$/.test(argument)) {
82
+ context.write("Usage: /diff [positive page number]\n");
83
+ return "handled";
84
+ }
85
+ const patch = mostRecentSuccessfulPatch(context.session.messages);
86
+ if (patch === undefined) {
87
+ context.write("No successfully applied patch found in this session.\n");
88
+ return "handled";
89
+ }
90
+ const page = argument.length === 0 ? 1 : Number(argument);
91
+ context.write(formatDiffPage(patch, page, context.width ?? 80, context.color === true).text);
92
+ return "handled";
93
+ }
94
+ case "/compose":
95
+ context.write("Use /compose at the interactive prompt to enter multi-line mode.\n");
96
+ return "handled";
97
+ case "/editor":
98
+ context.write("Use /editor in an interactive TUI to open the multiline editor.\n");
99
+ return "handled";
100
+ case "/review":
101
+ context.write("Use /review in an interactive TUI to browse the most recent successful patch.\n");
102
+ return "handled";
103
+ case "/gitdiff":
104
+ context.write("Use /gitdiff [--staged] in an interactive TUI to approve read-only Git diff.\n");
105
+ return "handled";
55
106
  case "/clear":
56
107
  context.session.clear();
57
108
  context.write("Transcript cleared.\n");
58
109
  return "handled";
110
+ case "/provider":
111
+ if (context.providers === undefined) {
112
+ context.write("Provider switching is unavailable in this session.\n");
113
+ return "handled";
114
+ }
115
+ if (trimmed === "/provider") {
116
+ context.write(context.providers.describe());
117
+ return "handled";
118
+ }
119
+ return { kind: "provider", argument: trimmed.slice("/provider".length).trim() };
120
+ case "/model":
121
+ if (context.providers === undefined) {
122
+ context.write("Model switching is unavailable in this session.\n");
123
+ return "handled";
124
+ }
125
+ if (trimmed === "/model") {
126
+ context.write(`Current model: ${context.providers.model ?? "not set"}. Use /model MODEL to change it.\n`);
127
+ return "handled";
128
+ }
129
+ return { kind: "model", argument: trimmed.slice("/model".length).trim() };
130
+ case "/endpoint":
131
+ if (context.providers === undefined) {
132
+ context.write("Endpoint configuration is unavailable in this session.\n");
133
+ return "handled";
134
+ }
135
+ if (trimmed === "/endpoint") {
136
+ context.write("Use /endpoint https://api.example.com/v1 for an OpenAI-compatible Provider.\n");
137
+ return "handled";
138
+ }
139
+ return { kind: "endpoint", argument: trimmed.slice("/endpoint".length).trim() };
59
140
  case "/login":
60
141
  if (context.signIn === undefined) {
61
142
  context.write("This session cannot store a key; it is attached to a server that already has one.\n");
62
143
  return "handled";
63
144
  }
64
- return { kind: "login" };
145
+ return { kind: "login", providerArgument: trimmed.slice("/login".length).trim() };
65
146
  case "/exit":
66
147
  case "/quit":
67
148
  return "exit";
@@ -130,11 +211,7 @@ async function withoutEcho(readline, read) {
130
211
  }
131
212
  }
132
213
  async function runSignIn(readline, readNextLine, write, signIn) {
133
- write([
134
- `Paste an API key for ${signIn.provider}. It is not echoed.`,
135
- `It will be stored in ${signIn.authFile}, readable only by you.`,
136
- "API key: ",
137
- ].join("\n"));
214
+ write(`API key for ${signIn.provider} (hidden, saved locally; Enter skips): `);
138
215
  // The key is read from the same line source the loop uses. readline's
139
216
  // question() competes with the loop's own iterator for input, so the answer
140
217
  // would be swallowed or never arrive.
@@ -153,12 +230,13 @@ async function runSignIn(readline, readNextLine, write, signIn) {
153
230
  write(`${failure}\n`);
154
231
  return;
155
232
  }
156
- write(`Stored the key for ${signIn.provider}. The key is not checked until your next prompt.\n`);
233
+ write(signIn.ready()
234
+ ? `Stored the key for ${signIn.provider}. Ready for your next prompt.\n`
235
+ : `Stored the key for ${signIn.provider}. ${signIn.unavailableMessage?.() ?? "Finish Provider setup before sending a prompt."}\n`);
157
236
  }
158
237
  /**
159
- * Reads prompts until the user leaves. One run at a time: Ctrl+C cancels the
160
- * answer in progress instead of killing the process, so the transcript and the
161
- * session log survive an interrupt.
238
+ * Reads prompts until the user leaves. Ctrl+C cancels a running answer, keeping
239
+ * its transcript and log; at an idle prompt it closes the REPL.
162
240
  */
163
241
  export async function runRepl(options) {
164
242
  const write = (text) => {
@@ -175,24 +253,123 @@ export async function runRepl(options) {
175
253
  output: options.output,
176
254
  terminal: true,
177
255
  prompt: REPL_PROMPT,
178
- ...(options.tui ? {
179
- completer: (line) => {
180
- const commands = [...BUILT_IN_COMMANDS, ...(options.extensionCommands ?? []).map((command) => command.name)];
181
- const matches = line.startsWith("/") && !/\s/.test(line)
182
- ? [...new Set(commands)].map((name) => `/${name}`).filter((name) => name.startsWith(line))
183
- : [];
184
- return [matches, line];
185
- },
186
- } : {}),
187
256
  });
257
+ const menu = tuiInput === undefined ? undefined : new InputMenu(readline, options.output);
258
+ let dismissedLine;
259
+ let refreshQueued = false;
260
+ const menuPriority = ["provider", "model", "editor", "review", "help"];
261
+ const commands = [...BUILT_IN_COMMANDS, ...(options.extensionCommands ?? []).map((command) => command.name)]
262
+ .filter((name, index, all) => all.indexOf(name) === index)
263
+ .sort((a, b) => {
264
+ const rank = (name) => {
265
+ const index = menuPriority.indexOf(name);
266
+ return index < 0 ? menuPriority.length : index;
267
+ };
268
+ return rank(a) - rank(b);
269
+ })
270
+ .map((name) => ({ value: name, label: `/${name}${name === "provider" ? " · choose a Provider" : name === "model" ? " · change model" : ""}` }));
271
+ const menuItems = (line) => {
272
+ if (line === "/provider" || line.startsWith("/provider ")) {
273
+ const query = line.slice("/provider".length).trim().toLowerCase();
274
+ return (options.providers?.providerIds() ?? []).filter((id) => id.includes(query))
275
+ .map((id) => ({ value: `provider ${id}`, label: `${id === options.providers?.provider ? "●" : "○"} ${id}` }));
276
+ }
277
+ if (line.startsWith("/") && !/\s/.test(line)) {
278
+ return commands.filter((item) => item.label.startsWith(line));
279
+ }
280
+ return [];
281
+ };
282
+ const refreshMenu = () => {
283
+ if (menu === undefined || inputClosed || signingIn || controller !== undefined)
284
+ return;
285
+ const line = readline.line;
286
+ menu.show(line === dismissedLine ? [] : menuItems(line), true);
287
+ };
288
+ const queueRefresh = () => {
289
+ if (refreshQueued)
290
+ return;
291
+ refreshQueued = true;
292
+ setImmediate(() => { refreshQueued = false; refreshMenu(); });
293
+ };
294
+ const replaceDraft = (value) => {
295
+ menu?.reset();
296
+ readline.write(null, { ctrl: true, name: "a" });
297
+ readline.write(null, { ctrl: true, name: "k" });
298
+ readline.write(value);
299
+ };
188
300
  let controller;
189
301
  let promptVisible = false;
302
+ let inputClosed = false;
303
+ let signingIn = false;
304
+ let composing = false;
305
+ let composeCancelled = false;
306
+ let interrupted = false;
307
+ if (tuiInput !== undefined) {
308
+ tuiInput.setMenuControls(() => {
309
+ menu?.clear();
310
+ dismissedLine = undefined;
311
+ }, (key) => {
312
+ if (signingIn || controller !== undefined)
313
+ return false;
314
+ if (key === "enter" && !menu?.active && readline.line === "/provider" && options.providers !== undefined) {
315
+ menu?.show(menuItems(readline.line));
316
+ return true;
317
+ }
318
+ if (key === "tab" && !menu?.active) {
319
+ const first = menuItems(readline.line)[0]?.value;
320
+ if (first !== undefined) {
321
+ replaceDraft(`/${first}${first === "model" || first === "endpoint" ? " " : ""}`);
322
+ queueRefresh();
323
+ return true;
324
+ }
325
+ }
326
+ if (!menu?.active)
327
+ return false;
328
+ if (key === "up" || key === "down") {
329
+ menu.move(key === "up" ? -1 : 1);
330
+ return true;
331
+ }
332
+ if (key === "escape") {
333
+ dismissedLine = readline.line;
334
+ menu.clear();
335
+ return true;
336
+ }
337
+ const chosen = menu.selection;
338
+ if (chosen === undefined)
339
+ return false;
340
+ const replacement = `/${chosen}`;
341
+ if (key === "tab") {
342
+ replaceDraft(replacement + (chosen === "model" || chosen === "endpoint" ? " " : ""));
343
+ queueRefresh();
344
+ return true;
345
+ }
346
+ if (key === "enter") {
347
+ replaceDraft(replacement);
348
+ if (chosen === "provider" || chosen === "model" || chosen === "endpoint") {
349
+ if (chosen !== "provider")
350
+ readline.write(" ");
351
+ queueRefresh();
352
+ }
353
+ else {
354
+ tuiInput.markSubmitted();
355
+ readline.write("\r");
356
+ }
357
+ return true;
358
+ }
359
+ return false;
360
+ });
361
+ tuiInput.on("keypress", queueRefresh);
362
+ }
190
363
  const showPrompt = () => {
364
+ if (inputClosed || options.inlineComposer)
365
+ return;
191
366
  promptVisible = true;
192
367
  readline.prompt();
193
368
  };
194
- readline.on("line", () => { promptVisible = false; });
369
+ readline.on("line", () => { menu?.reset(); promptVisible = false; });
195
370
  readline.on("close", () => {
371
+ menu?.reset();
372
+ inputClosed = true;
196
373
  controller?.abort();
197
374
  if (options.tui && promptVisible) {
198
375
  // Ctrl+D closes an empty input line without printing a newline.
@@ -201,16 +378,22 @@ export async function runRepl(options) {
201
378
  }
202
379
  });
203
380
  readline.on("SIGINT", () => {
381
+ if (composing) {
382
+ composeCancelled = true;
383
+ readline.write("\r");
384
+ return;
385
+ }
204
386
  if (controller === undefined) {
205
387
  if (options.tui) {
206
- // Use readline's editing operations so wrapped input is also erased.
388
+ // Erase wrapped drafts before removing the prompt on close.
207
389
  readline.write(null, { ctrl: true, name: "e" });
208
390
  readline.write(null, { ctrl: true, name: "u" });
209
391
  }
210
392
  else {
211
- write("Press Ctrl+D or /exit to leave.\n");
393
+ options.output.write("\n");
212
394
  }
213
- showPrompt();
395
+ interrupted = true;
396
+ readline.close();
214
397
  return;
215
398
  }
216
399
  controller.abort();
@@ -225,19 +408,283 @@ export async function runRepl(options) {
225
408
  const next = await lines.next();
226
409
  return next.done === true ? undefined : next.value;
227
410
  };
411
+ // Approval and normal input share the same readline iterator.
412
+ if (options.shellApproval !== undefined) {
413
+ options.shellApproval.setHandler(async (command, signal) => {
414
+ if (signal?.aborted)
415
+ return false;
416
+ menu?.clear();
417
+ options.liveRegion?.pause();
418
+ tuiInput?.setApprovalMode(true);
419
+ tuiInput?.setBusy(false);
420
+ try {
421
+ const width = terminalWidth({ columns: options.output.columns });
422
+ write(approvalPreview(command, width));
423
+ if (!commandReviewable(command, width)) {
424
+ write(" Denied: command cannot be fully reviewed at this width. Ask for a shorter command.\n");
425
+ return false;
426
+ }
427
+ readline.setPrompt("approve> ");
428
+ readline.prompt();
429
+ // Ctrl+C must unblock the pending line read, not leave the agent
430
+ // stuck waiting for a reply to an approval that was cancelled.
431
+ const unblockOnAbort = () => {
432
+ readline.write("\n");
433
+ };
434
+ signal?.addEventListener("abort", unblockOnAbort, { once: true });
435
+ try {
436
+ if (signal?.aborted)
437
+ return false;
438
+ const answer = await readNextLine();
439
+ return answer?.trim().toLowerCase() === "y" && signal?.aborted !== true;
440
+ }
441
+ finally {
442
+ signal?.removeEventListener("abort", unblockOnAbort);
443
+ }
444
+ }
445
+ finally {
446
+ readline.setPrompt(REPL_PROMPT);
447
+ tuiInput?.setApprovalMode(false);
448
+ tuiInput?.setBusy(true);
449
+ options.liveRegion?.resume();
450
+ }
451
+ });
452
+ }
453
+ /** Modal composer retains readline editing, Unicode and native history. */
454
+ const collectMultiline = async () => {
455
+ const parts = [];
456
+ let bytes = 0;
457
+ composing = true;
458
+ composeCancelled = false;
459
+ tuiInput?.setComposeMode(true);
460
+ menu?.clear();
461
+ dismissedLine = undefined;
462
+ readline.setPrompt(" │ ");
463
+ write("\nCompose prompt · Enter adds a line · /send submits · /cancel discards\n");
464
+ try {
465
+ for (;;) {
466
+ readline.prompt();
467
+ const part = await readNextLine();
468
+ if (tuiInput?.composeLimitExceeded) {
469
+ write("Draft too large (64 KiB paste buffer). Nothing was sent.\n");
470
+ return undefined;
471
+ }
472
+ if (composeCancelled || part === undefined || part.trim() === "/cancel") {
473
+ write("Draft discarded.\n");
474
+ return undefined;
475
+ }
476
+ if (part.trim() === "/send") {
477
+ const message = parts.join("\n");
478
+ if (message.trim().length === 0) {
479
+ write("Empty draft discarded.\n");
480
+ return undefined;
481
+ }
482
+ write(`Submitted ${parts.length} lines.\n`);
483
+ return message;
484
+ }
485
+ bytes += Buffer.byteLength(part, "utf8") + 1;
486
+ if (parts.length >= 200 || bytes > 64 * 1024) {
487
+ write("Draft too large (200 lines / 64 KiB). Nothing was sent.\n");
488
+ return undefined;
489
+ }
490
+ // //send and //cancel are literal lines instead of commands.
491
+ parts.push(part.startsWith("//send") || part.startsWith("//cancel") ? part.slice(1) : part);
492
+ }
493
+ }
494
+ finally {
495
+ composing = false;
496
+ composeCancelled = false;
497
+ tuiInput?.setComposeMode(false);
498
+ readline.setPrompt(REPL_PROMPT);
499
+ }
500
+ };
501
+ /** Cursor-editable modal, with exclusive keyboard and paint ownership. */
502
+ const collectEditor = async (isDefault = false) => {
503
+ if (tuiInput === undefined) {
504
+ write("The cursor editor requires --tui. Use /compose here instead.\n");
505
+ return undefined;
506
+ }
507
+ menu?.clear();
508
+ dismissedLine = undefined;
509
+ composing = true;
510
+ let resolveDraft;
511
+ const pending = new Promise(resolve => { resolveDraft = resolve; });
512
+ const output = options.output;
513
+ const recent = options.session.messages
514
+ .filter((message) => message.role === "user")
515
+ .map(message => message.content)
516
+ .filter(message => message.length > 0)
517
+ .slice(-100);
518
+ const editor = new EditorController(output, resolveDraft, {
519
+ submitOnEnter: isDefault,
520
+ history: recent,
521
+ slashCommands: commands.map(item => ({ value: item.value, label: item.label })),
522
+ mouse: isDefault && options.tui === true && output.isTTY === true,
523
+ });
524
+ const resized = () => { editor.resized(); };
525
+ const closed = () => { editor.cancel(); };
526
+ tuiInput.setRawHandler(chunk => { editor.receive(chunk); });
527
+ output.on("resize", resized);
528
+ readline.once("close", closed);
529
+ options.input.once("end", closed);
530
+ try {
531
+ editor.begin();
532
+ const result = await pending;
533
+ if (editor.exitRequested.value)
534
+ readline.close();
535
+ if (!isDefault)
536
+ write(result === undefined ? "Draft discarded.\n" : "Draft submitted.\n");
537
+ return result;
538
+ }
539
+ finally {
540
+ tuiInput.setRawHandler(undefined);
541
+ output.off("resize", resized);
542
+ readline.off("close", closed);
543
+ options.input.off("end", closed);
544
+ composing = false;
545
+ }
546
+ };
547
+ /** Read-only patch viewer, safe and separate from the model's tool calls. */
548
+ const inspectPatch = async (override, label) => {
549
+ const patch = override ?? mostRecentSuccessfulPatch(options.session.messages);
550
+ if (patch === undefined) {
551
+ write("No successfully applied patch found in this session.\n");
552
+ return;
553
+ }
554
+ if (tuiInput === undefined) {
555
+ write("Interactive patch review requires --tui; use /diff instead.\n");
556
+ return;
557
+ }
558
+ menu?.clear();
559
+ let resolveReview;
560
+ const pending = new Promise(resolve => { resolveReview = resolve; });
561
+ const output = options.output;
562
+ const review = new PatchReviewController(output, patch, resolveReview, process.env.NO_COLOR === undefined && process.env.TERM !== "dumb", label);
563
+ const resized = () => { review.resized(); };
564
+ const closed = () => { review.cancel(); };
565
+ tuiInput.setRawHandler(chunk => { review.receive(chunk); });
566
+ output.on("resize", resized);
567
+ readline.once("close", closed);
568
+ try {
569
+ review.begin();
570
+ await pending;
571
+ }
572
+ finally {
573
+ tuiInput.setRawHandler(undefined);
574
+ output.off("resize", resized);
575
+ readline.off("close", closed);
576
+ }
577
+ };
578
+ const inspectLiveGitDiff = async (line) => {
579
+ if (tuiInput === undefined) {
580
+ write("Live Git review requires interactive --tui.\n");
581
+ return;
582
+ }
583
+ if (line !== "/gitdiff" && line !== "/gitdiff --staged") {
584
+ write("Usage: /gitdiff [--staged]\n");
585
+ return;
586
+ }
587
+ const mode = line.endsWith("--staged") ? "staged" : "working";
588
+ // Explicit user confirmation before starting any local Git process.
589
+ const root = terminalText(options.session.cwd);
590
+ write(`\nRead-only local Git diff (${mode}) in:\n${root}\nNo network, hooks, external diff or file writes. Approve once? [y/N]\n`);
591
+ tuiInput.acceptLine();
592
+ tuiInput.setApprovalMode(true);
593
+ readline.setPrompt("gitdiff> ");
594
+ let allowed = false;
595
+ try {
596
+ readline.prompt();
597
+ const answer = await readNextLine();
598
+ allowed = answer?.trim().toLowerCase() === "y";
599
+ }
600
+ finally {
601
+ tuiInput.setApprovalMode(false);
602
+ readline.setPrompt(REPL_PROMPT);
603
+ }
604
+ if (!allowed) {
605
+ write("Git diff review denied; no process started.\n");
606
+ return;
607
+ }
608
+ try {
609
+ const diff = await (options.gitDiffReader ?? readLiveGitDiff)(options.session.cwd, mode);
610
+ if (diff.trim().length === 0) {
611
+ write(`No ${mode} tracked Git changes found.\n`);
612
+ return;
613
+ }
614
+ await inspectPatch(diff, `live Git diff (${mode})`);
615
+ }
616
+ catch (error) {
617
+ write(`${error instanceof Error ? error.message : "Git diff unavailable"}\n`);
618
+ }
619
+ };
620
+ const signInAfterProviderSelection = async () => {
621
+ if (!options.tui || options.signIn === undefined)
622
+ return;
623
+ let missingKey;
624
+ try {
625
+ missingKey = options.signIn.needsApiKey === undefined
626
+ ? !options.signIn.ready()
627
+ : await options.signIn.needsApiKey();
628
+ }
629
+ catch (error) {
630
+ write(`Could not check saved API key: ${error instanceof Error ? error.message : String(error)}\n`);
631
+ return;
632
+ }
633
+ if (!missingKey)
634
+ return;
635
+ signingIn = true;
636
+ try {
637
+ await runSignIn(readline, readNextLine, write, options.signIn);
638
+ }
639
+ finally {
640
+ signingIn = false;
641
+ }
642
+ };
228
643
  try {
229
644
  for (;;) {
230
- const line = await readNextLine();
645
+ let line = options.inlineComposer && tuiInput !== undefined
646
+ ? await collectEditor(true)
647
+ : await readNextLine();
231
648
  if (line === undefined) {
232
- break;
649
+ if (inputClosed)
650
+ break;
651
+ continue;
233
652
  }
234
653
  if (line.trim().length === 0) {
235
654
  showPrompt();
236
655
  continue;
237
656
  }
238
- const outcome = handleSlashCommand(line, {
657
+ const fromComposer = line.trim() === "/compose";
658
+ const fromEditor = line.trim() === "/editor";
659
+ if (line.trim() === "/review") {
660
+ await inspectPatch();
661
+ if (inputClosed)
662
+ break;
663
+ showPrompt();
664
+ continue;
665
+ }
666
+ if (line.trim().startsWith("/gitdiff")) {
667
+ await inspectLiveGitDiff(line.trim());
668
+ if (inputClosed)
669
+ break;
670
+ showPrompt();
671
+ continue;
672
+ }
673
+ if (fromComposer || fromEditor) {
674
+ const draft = fromEditor ? await collectEditor() : await collectMultiline();
675
+ if (draft === undefined) {
676
+ if (inputClosed)
677
+ break;
678
+ showPrompt();
679
+ continue;
680
+ }
681
+ line = draft;
682
+ }
683
+ const outcome = fromComposer || fromEditor ? "not-a-command" : handleSlashCommand(line, {
239
684
  session: options.session,
240
685
  write,
686
+ width: terminalWidth({ columns: options.output.columns }),
687
+ color: options.tui === true && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb",
241
688
  ...(options.sessionFile === undefined
242
689
  ? {}
243
690
  : { sessionFile: options.sessionFile }),
@@ -245,13 +692,46 @@ export async function runRepl(options) {
245
692
  ? {}
246
693
  : { extensionCommands: options.extensionCommands }),
247
694
  ...(options.signIn === undefined ? {} : { signIn: options.signIn }),
695
+ ...(options.providers === undefined ? {} : { providers: options.providers }),
248
696
  });
249
697
  if (outcome === "exit") {
250
698
  break;
251
699
  }
252
700
  if (typeof outcome === "object" && outcome.kind === "login") {
701
+ if (outcome.providerArgument.length > 0) {
702
+ if (options.providers === undefined) {
703
+ write("Provider switching is unavailable in this session.\n");
704
+ showPrompt();
705
+ continue;
706
+ }
707
+ const selected = await options.providers.select(outcome.providerArgument);
708
+ write(selected.message);
709
+ if (!selected.ok) {
710
+ showPrompt();
711
+ continue;
712
+ }
713
+ }
253
714
  if (options.signIn !== undefined) {
254
- await runSignIn(readline, readNextLine, write, options.signIn);
715
+ signingIn = true;
716
+ try {
717
+ await runSignIn(readline, readNextLine, write, options.signIn);
718
+ }
719
+ finally {
720
+ signingIn = false;
721
+ }
722
+ }
723
+ showPrompt();
724
+ continue;
725
+ }
726
+ if (typeof outcome === "object" && (outcome.kind === "provider" || outcome.kind === "model" || outcome.kind === "endpoint")) {
727
+ if (options.providers !== undefined) {
728
+ const selected = outcome.kind === "provider"
729
+ ? await options.providers.select(outcome.argument)
730
+ : outcome.kind === "model" ? await options.providers.changeModel(outcome.argument)
731
+ : await options.providers.changeEndpoint(outcome.argument);
732
+ write(selected.message);
733
+ if (selected.ok && outcome.kind === "provider")
734
+ await signInAfterProviderSelection();
255
735
  }
256
736
  showPrompt();
257
737
  continue;
@@ -280,7 +760,7 @@ export async function runRepl(options) {
280
760
  if (options.signIn !== undefined && !options.signIn.ready()) {
281
761
  // Saying this before the run starts is clearer than letting the Provider
282
762
  // call fail and reporting it as an agent failure.
283
- write("No API key yet. Run /login to add one.\n");
763
+ write(`${options.providers?.unavailableMessage?.() ?? `No API key for ${options.signIn.provider}. Run /login or use /provider to switch.`}\n`);
284
764
  showPrompt();
285
765
  continue;
286
766
  }
@@ -306,9 +786,10 @@ export async function runRepl(options) {
306
786
  }
307
787
  }
308
788
  finally {
789
+ options.shellApproval?.setHandler(undefined);
309
790
  readline.close();
310
791
  tuiInput?.dispose();
311
792
  }
312
- return 0;
793
+ return interrupted ? 130 : 0;
313
794
  }
314
795
  //# sourceMappingURL=repl.js.map