chivgent 1.1.1 → 1.1.3

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 (122) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +15 -1
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli-options.d.ts +6 -0
  7. package/dist/cli-options.d.ts.map +1 -1
  8. package/dist/cli-options.js +22 -0
  9. package/dist/cli-options.js.map +1 -1
  10. package/dist/cli-runtime.d.ts.map +1 -1
  11. package/dist/cli-runtime.js +30 -8
  12. package/dist/cli-runtime.js.map +1 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +11 -3
  15. package/dist/cli.js.map +1 -1
  16. package/dist/context/compaction.d.ts +10 -0
  17. package/dist/context/compaction.d.ts.map +1 -1
  18. package/dist/context/compaction.js +50 -10
  19. package/dist/context/compaction.js.map +1 -1
  20. package/dist/context/context-manager.d.ts +2 -5
  21. package/dist/context/context-manager.d.ts.map +1 -1
  22. package/dist/context/context-manager.js +47 -19
  23. package/dist/context/context-manager.js.map +1 -1
  24. package/dist/context/errors.d.ts +12 -0
  25. package/dist/context/errors.d.ts.map +1 -0
  26. package/dist/context/errors.js +19 -0
  27. package/dist/context/errors.js.map +1 -0
  28. package/dist/evals/report.d.ts.map +1 -1
  29. package/dist/evals/report.js +3 -0
  30. package/dist/evals/report.js.map +1 -1
  31. package/dist/evals/runner.d.ts +2 -0
  32. package/dist/evals/runner.d.ts.map +1 -1
  33. package/dist/evals/runner.js +13 -1
  34. package/dist/evals/runner.js.map +1 -1
  35. package/dist/evals/task.d.ts +2 -0
  36. package/dist/evals/task.d.ts.map +1 -1
  37. package/dist/evals/task.js +7 -0
  38. package/dist/evals/task.js.map +1 -1
  39. package/dist/events.d.ts +2 -0
  40. package/dist/events.d.ts.map +1 -1
  41. package/dist/events.js.map +1 -1
  42. package/dist/llm.d.ts +2 -0
  43. package/dist/llm.d.ts.map +1 -1
  44. package/dist/llm.js.map +1 -1
  45. package/dist/permissions.d.ts +6 -1
  46. package/dist/permissions.d.ts.map +1 -1
  47. package/dist/permissions.js +21 -4
  48. package/dist/permissions.js.map +1 -1
  49. package/dist/prompts.d.ts +4 -9
  50. package/dist/prompts.d.ts.map +1 -1
  51. package/dist/prompts.js +37 -9
  52. package/dist/prompts.js.map +1 -1
  53. package/dist/providers/definitions.js +1 -1
  54. package/dist/providers/definitions.js.map +1 -1
  55. package/dist/providers/openai-compatible-chat.d.ts.map +1 -1
  56. package/dist/providers/openai-compatible-chat.js +11 -4
  57. package/dist/providers/openai-compatible-chat.js.map +1 -1
  58. package/dist/providers/openai.js +2 -0
  59. package/dist/providers/openai.js.map +1 -1
  60. package/dist/repl.d.ts +12 -1
  61. package/dist/repl.d.ts.map +1 -1
  62. package/dist/repl.js +234 -60
  63. package/dist/repl.js.map +1 -1
  64. package/dist/session-store.d.ts +3 -0
  65. package/dist/session-store.d.ts.map +1 -1
  66. package/dist/session-store.js +5 -0
  67. package/dist/session-store.js.map +1 -1
  68. package/dist/session.d.ts +1 -0
  69. package/dist/session.d.ts.map +1 -1
  70. package/dist/session.js +33 -3
  71. package/dist/session.js.map +1 -1
  72. package/dist/shell/docker.d.ts.map +1 -1
  73. package/dist/shell/docker.js +1 -0
  74. package/dist/shell/docker.js.map +1 -1
  75. package/dist/shell/native.d.ts.map +1 -1
  76. package/dist/shell/native.js +1 -0
  77. package/dist/shell/native.js.map +1 -1
  78. package/dist/shell/types.d.ts +2 -0
  79. package/dist/shell/types.d.ts.map +1 -1
  80. package/dist/tools/bash.d.ts +16 -24
  81. package/dist/tools/bash.d.ts.map +1 -1
  82. package/dist/tools/bash.js +94 -30
  83. package/dist/tools/bash.js.map +1 -1
  84. package/dist/tools/target.d.ts.map +1 -1
  85. package/dist/tools/target.js +3 -1
  86. package/dist/tools/target.js.map +1 -1
  87. package/dist/tools/web-search.d.ts +42 -0
  88. package/dist/tools/web-search.d.ts.map +1 -0
  89. package/dist/tools/web-search.js +154 -0
  90. package/dist/tools/web-search.js.map +1 -0
  91. package/dist/tui/activity.d.ts +3 -1
  92. package/dist/tui/activity.d.ts.map +1 -1
  93. package/dist/tui/activity.js +46 -6
  94. package/dist/tui/activity.js.map +1 -1
  95. package/dist/tui/editor-input.d.ts +10 -0
  96. package/dist/tui/editor-input.d.ts.map +1 -1
  97. package/dist/tui/editor-input.js +126 -33
  98. package/dist/tui/editor-input.js.map +1 -1
  99. package/dist/tui/editor.d.ts +1 -1
  100. package/dist/tui/editor.d.ts.map +1 -1
  101. package/dist/tui/editor.js +31 -5
  102. package/dist/tui/editor.js.map +1 -1
  103. package/dist/tui/live.d.ts.map +1 -1
  104. package/dist/tui/live.js +55 -4
  105. package/dist/tui/live.js.map +1 -1
  106. package/dist/tui/markdown.d.ts +13 -0
  107. package/dist/tui/markdown.d.ts.map +1 -1
  108. package/dist/tui/markdown.js +35 -26
  109. package/dist/tui/markdown.js.map +1 -1
  110. package/dist/tui/provider-picker.d.ts +28 -0
  111. package/dist/tui/provider-picker.d.ts.map +1 -0
  112. package/dist/tui/provider-picker.js +131 -0
  113. package/dist/tui/provider-picker.js.map +1 -0
  114. package/dist/tui/welcome.d.ts +5 -4
  115. package/dist/tui/welcome.d.ts.map +1 -1
  116. package/dist/tui/welcome.js +8 -9
  117. package/dist/tui/welcome.js.map +1 -1
  118. package/dist/websearch/backend.d.ts +83 -0
  119. package/dist/websearch/backend.d.ts.map +1 -0
  120. package/dist/websearch/backend.js +238 -0
  121. package/dist/websearch/backend.js.map +1 -0
  122. package/package.json +1 -1
package/dist/repl.js CHANGED
@@ -4,63 +4,74 @@ import { readLiveGitDiff } from "./tui/git-diff.js";
4
4
  import { terminalText } from "./tui/text.js";
5
5
  import { terminalColorEnabled } from "./tui/selection.js";
6
6
  import { PatchReviewController } from "./tui/review.js";
7
+ import { ProviderPickerController } from "./tui/provider-picker.js";
7
8
  import { InputMenu } from "./tui/menu.js";
8
9
  import { createInterface, emitKeypressEvents } from "node:readline";
9
10
  import { formatTokens } from "./providers/usage.js";
10
11
  import { approvalPreview, commandReviewable } from "./tui/activity.js";
11
12
  import { terminalWidth } from "./tui/live.js";
12
13
  import { formatDiffPage, mostRecentSuccessfulPatch } from "./tui/diff.js";
13
- import { describePermissionMode, parsePermissionMode } from "./permissions.js";
14
+ import { describePermissionMode, parsePermissionMode, WEB_SEARCH_APPROVAL_PREFIX } from "./permissions.js";
14
15
  import { HeuristicTokenEstimator } from "./context/token-estimator.js";
15
16
  export const REPL_PROMPT = "› ";
16
17
  export const BUILT_IN_COMMANDS = [
17
18
  "help",
18
- "session",
19
19
  "status",
20
- "compact",
21
- "plan",
22
- "permissions",
23
20
  "new",
24
21
  "resume",
25
22
  "rename",
26
- "tools",
27
23
  "diff",
24
+ "clear",
25
+ "compact",
26
+ "permissions",
27
+ "provider",
28
+ "model",
29
+ "endpoint",
30
+ "login",
31
+ "exit",
32
+ ];
33
+ export const LEGACY_COMMAND_ALIASES = [
34
+ "session",
35
+ "plan",
36
+ "tools",
28
37
  "compose",
29
38
  "editor",
30
39
  "review",
31
40
  "gitdiff",
32
- "clear",
33
- "exit",
34
41
  "quit",
35
- "login",
36
42
  "logout",
37
- "provider",
38
- "model",
39
- "endpoint",
40
43
  ];
44
+ export const BUILT_IN_DESCRIPTIONS = {
45
+ help: "show this help",
46
+ status: "show session, model, tokens & permissions",
47
+ new: "save and start a fresh session",
48
+ resume: "switch to a saved session in workspace",
49
+ rename: "set current session title",
50
+ diff: "review workspace Git changes (/diff patch for patch)",
51
+ clear: "clear transcript in current session",
52
+ compact: "summarise older history for the model",
53
+ permissions: "inspect or change permission policy",
54
+ provider: "choose a Provider",
55
+ model: "show or change current model",
56
+ endpoint: "set URL for OpenAI-compatible Provider",
57
+ login: "store API key or sign in with subscription",
58
+ exit: "leave chivgent",
59
+ };
41
60
  const HELP = `Commands:
42
61
  /help Show this help
43
- /session Show the current session id, workspace, and size
44
- /status Show live session, model, permissions, plan state, and usage
62
+ /status Show live session, model, permissions, plan state, and usage (alias: /session)
45
63
  /compact Summarise older history for the model; keeps the full transcript
46
64
  /plan [on|off] Toggle restricted read-only Plan Mode
47
- /permissions [ask-first|workspace|unrestricted] Inspect or change this process's policy
65
+ /permissions [ask-first|workspace|unrestricted|plan] Inspect or change this process's policy
48
66
  /new [title] Save this session and start a fresh session
49
67
  /resume [id] Switch to a saved session in this workspace
50
68
  /rename TITLE Set the current session title
51
- /tools List the tools available to the model
52
- /diff [N] Review the latest successful apply_patch, page N
53
- /compose Write a multi-line prompt (/send to submit, /cancel to discard)
54
- /editor Open the cursor-editable multiline composer (Ctrl+S sends)
55
- /review Browse last applied patch with arrows, q to close
56
- /gitdiff [--staged] Review actual workspace Git changes after explicit approval
69
+ /diff [--staged|patch] Review workspace Git changes (or latest patch)
57
70
  /clear Start a new transcript in the same session
58
71
  /provider Choose a Provider (↑↓ and Enter in the TUI)
59
72
  /model Show or change the current model: /model MODEL
60
73
  /endpoint Set the URL for an OpenAI-compatible Provider
61
- /login Store a key for the current Provider; /login NAME switches first
62
- /login oauth [openai|xai] Sign in with a subscription
63
- /logout Sign out of the current OAuth subscription
74
+ /login Store API key or sign in with subscription (/login logout to sign out)
64
75
  /exit Leave chivgent
65
76
 
66
77
  Anything else is sent to the model. Ctrl+C stops a running answer.
@@ -95,8 +106,6 @@ export function handleSlashCommand(line, context) {
95
106
  context.write(helpText(context));
96
107
  return "handled";
97
108
  case "/session":
98
- context.write(describeSession(context));
99
- return "handled";
100
109
  case "/status":
101
110
  context.write(describeStatus(context));
102
111
  return "handled";
@@ -123,6 +132,12 @@ export function handleSlashCommand(line, context) {
123
132
  }
124
133
  case "/permissions": {
125
134
  const argument = trimmed.slice("/permissions".length).trim().toLowerCase();
135
+ if (argument === "plan" || argument === "plan on") {
136
+ return { kind: "plan", enabled: true };
137
+ }
138
+ if (argument === "plan off") {
139
+ return { kind: "plan", enabled: false };
140
+ }
126
141
  if (context.permissionPolicy === undefined) {
127
142
  context.write("Named permission modes are unavailable because this session uses legacy capability flags. Start with --permission-mode to enable them.\n");
128
143
  }
@@ -134,7 +149,7 @@ export function handleSlashCommand(line, context) {
134
149
  return { kind: "permission-mode", mode: parsePermissionMode(argument) };
135
150
  }
136
151
  catch {
137
- context.write("Usage: /permissions [ask-first|workspace|unrestricted]\n");
152
+ context.write("Usage: /permissions [ask-first|workspace|unrestricted|plan]\n");
138
153
  }
139
154
  }
140
155
  return "handled";
@@ -162,13 +177,28 @@ export function handleSlashCommand(line, context) {
162
177
  return "handled";
163
178
  case "/diff": {
164
179
  const argument = trimmed.slice("/diff".length).trim();
165
- if (argument.length > 0 && !/^[1-9]\d*$/.test(argument)) {
166
- context.write("Usage: /diff [positive page number]\n");
180
+ if (argument === "patch" || argument.startsWith("patch ")) {
181
+ const pageArg = argument.slice("patch".length).trim();
182
+ if (pageArg.length > 0 && !/^[1-9]\d*$/.test(pageArg)) {
183
+ context.write("Usage: /diff patch [positive page number]\n");
184
+ return "handled";
185
+ }
186
+ const patch = mostRecentSuccessfulPatch(context.session.messages);
187
+ if (patch === undefined) {
188
+ context.write("No successfully applied patch found in this session.\n");
189
+ return "handled";
190
+ }
191
+ const page = pageArg.length === 0 ? 1 : Number(pageArg);
192
+ context.write(formatDiffPage(patch, page, context.width ?? 80, context.color === true).text);
193
+ return "handled";
194
+ }
195
+ if (argument.length > 0 && !/^[1-9]\d*$/.test(argument) && argument !== "--staged") {
196
+ context.write("Usage: /diff [--staged|patch [positive page number]]\n");
167
197
  return "handled";
168
198
  }
169
199
  const patch = mostRecentSuccessfulPatch(context.session.messages);
170
200
  if (patch === undefined) {
171
- context.write("No successfully applied patch found in this session.\n");
201
+ context.write("No successfully applied patch found in this session. In interactive TUI, use /diff to review workspace Git changes.\n");
172
202
  return "handled";
173
203
  }
174
204
  const page = argument.length === 0 ? 1 : Number(argument);
@@ -176,10 +206,14 @@ export function handleSlashCommand(line, context) {
176
206
  return "handled";
177
207
  }
178
208
  case "/compose":
179
- context.write("Use /compose at the interactive prompt to enter multi-line mode.\n");
209
+ context.write(context.inlineComposer
210
+ ? "The multiline editor is already active in this prompt. Enter sends; Ctrl+O inserts a newline.\n"
211
+ : "Use /compose at the interactive prompt to enter multi-line mode.\n");
180
212
  return "handled";
181
213
  case "/editor":
182
- context.write("Use /editor in the rich interactive TUI to open the multiline editor.\n");
214
+ context.write(context.inlineComposer
215
+ ? "The cursor-addressable multiline editor is already active in this prompt. Enter sends; Ctrl+O inserts a newline.\n"
216
+ : "Use /editor in the rich interactive TUI to open the multiline editor.\n");
183
217
  return "handled";
184
218
  case "/review":
185
219
  context.write("Use /review in an interactive TUI to browse the most recent successful patch.\n");
@@ -197,6 +231,9 @@ export function handleSlashCommand(line, context) {
197
231
  return "handled";
198
232
  }
199
233
  if (trimmed === "/provider") {
234
+ if (context.interactivePicker === true) {
235
+ return { kind: "provider", argument: "" };
236
+ }
200
237
  context.write(context.providers.describe());
201
238
  return "handled";
202
239
  }
@@ -221,12 +258,21 @@ export function handleSlashCommand(line, context) {
221
258
  return "handled";
222
259
  }
223
260
  return { kind: "endpoint", argument: trimmed.slice("/endpoint".length).trim() };
224
- case "/login":
261
+ case "/login": {
262
+ const argument = trimmed.slice("/login".length).trim();
263
+ if (argument === "logout" || argument === "--logout") {
264
+ if (context.signIn?.signOut === undefined) {
265
+ context.write("OAuth sign-out is unavailable in this session.\n");
266
+ return "handled";
267
+ }
268
+ return { kind: "login", providerArgument: "__logout__" };
269
+ }
225
270
  if (context.signIn === undefined) {
226
271
  context.write("This session cannot store a key; it is attached to a server that already has one.\n");
227
272
  return "handled";
228
273
  }
229
- return { kind: "login", providerArgument: trimmed.slice("/login".length).trim() };
274
+ return { kind: "login", providerArgument: argument };
275
+ }
230
276
  case "/logout":
231
277
  if (context.signIn?.signOut === undefined) {
232
278
  context.write("OAuth sign-out is unavailable in this session.\n");
@@ -266,20 +312,6 @@ function describeUsage(context) {
266
312
  `tokens: ${parts.join(", ")}${usage.complete ? "" : " (a turn reported none)"}`,
267
313
  ];
268
314
  }
269
- function describeSession(context) {
270
- const lines = [
271
- `id: ${context.session.id}`,
272
- `workspace: ${context.session.cwd}`,
273
- `prompts: ${context.session.turns}`,
274
- `messages: ${context.session.messages.length}`,
275
- ...describeUsage(context),
276
- ];
277
- const sessionFile = context.sessionFileForSession?.(context.session) ?? context.sessionFile;
278
- if (sessionFile !== undefined) {
279
- lines.push(`log: ${sessionFile}`);
280
- }
281
- return `${lines.join("\n")}\n`;
282
- }
283
315
  function describeStatus(context) {
284
316
  const usage = describeUsage(context);
285
317
  let contextEstimate = "unavailable";
@@ -287,13 +319,19 @@ function describeStatus(context) {
287
319
  contextEstimate = `approximately ${new HeuristicTokenEstimator().estimateMessages(context.session.messages)} tokens (transcript only; excludes system prompt and tool schemas)`;
288
320
  }
289
321
  catch { /* inaccessible estimates are reported explicitly */ }
322
+ const sessionFile = context.sessionFileForSession?.(context.session) ?? context.sessionFile;
290
323
  const lines = [
291
324
  `provider: ${context.providers?.provider ?? context.providerName ?? "unavailable"}`,
292
325
  `model: ${context.providers === undefined ? context.modelName ?? "unavailable" : context.providers.model ?? "unavailable"}`,
293
- `cwd: ${context.session.cwd}`,
294
- `session: ${context.session.id}${context.session.title ? ` · ${context.session.title}` : ""}`,
326
+ `workspace: ${context.session.cwd}`,
327
+ `id: ${context.session.id}${context.session.title ? ` · ${context.session.title}` : ""}`,
328
+ `prompts: ${context.session.turns}`,
329
+ `messages: ${context.session.messages.length}`,
295
330
  `plan: ${context.session.planMode ? "on (writes and Shell blocked for this session)" : "off"}`,
296
331
  ...(context.permissionPolicy === undefined ? [] : [`permissions: ${context.permissionPolicy.mode} (${describePermissionMode(context.permissionPolicy.mode)})`]),
332
+ ...(context.webSearch === undefined ? [] : [
333
+ `web search: ${context.webSearch.provider}${context.permissionPolicy === undefined || context.permissionPolicy.mode === "unrestricted" ? "" : " (each query asks for approval)"}`,
334
+ ]),
297
335
  ...(context.permissionPolicy === undefined ? [
298
336
  `file write grant: ${context.permissions === undefined ? "unavailable" : context.permissions.includes("file-write") ? "enabled" : "disabled"}`,
299
337
  `Shell grant: ${context.permissions === undefined ? "unavailable" : context.permissions.includes("shell") ? "enabled" : "disabled"}`,
@@ -301,6 +339,7 @@ function describeStatus(context) {
301
339
  ] : []),
302
340
  ...usage,
303
341
  `context: ${contextEstimate}`,
342
+ ...(sessionFile !== undefined ? [`log: ${sessionFile}`] : []),
304
343
  ];
305
344
  return `${lines.join("\n")}\n`;
306
345
  }
@@ -325,6 +364,25 @@ async function withoutEcho(readline, read) {
325
364
  internals._writeToOutput = original;
326
365
  }
327
366
  }
367
+ /** Plain-REPL Provider chooser; only the API key itself is hidden. */
368
+ async function readProviderChoice(readNextLine, write, providerIds, active) {
369
+ write("Select a Provider for /login:\n");
370
+ providerIds.forEach((id, index) => {
371
+ write(` ${index + 1}. ${id}${id === active ? " (current)" : ""}\n`);
372
+ });
373
+ write("Provider (number or name, Enter cancels): ");
374
+ const answer = await readNextLine();
375
+ if (answer === undefined)
376
+ return undefined;
377
+ const choice = answer.trim();
378
+ if (choice.length === 0)
379
+ return undefined;
380
+ const number = Number(choice);
381
+ if (Number.isInteger(number) && number >= 1 && number <= providerIds.length) {
382
+ return providerIds[number - 1];
383
+ }
384
+ return providerIds.find((id) => id.toLowerCase() === choice.toLowerCase());
385
+ }
328
386
  async function runSignIn(readline, readNextLine, write, signIn) {
329
387
  write(`API key for ${signIn.provider} (hidden, saved locally; Enter skips): `);
330
388
  // The key is read from the same line source the loop uses. readline's
@@ -373,7 +431,7 @@ export async function runRepl(options) {
373
431
  const menu = tuiInput === undefined ? undefined : new InputMenu(readline, options.output);
374
432
  let dismissedLine;
375
433
  let refreshQueued = false;
376
- const menuPriority = ["provider", "model", "editor", "review", "help"];
434
+ const menuPriority = ["status", "diff", "provider", "model", "new", "resume", "help"];
377
435
  const commands = [...BUILT_IN_COMMANDS, ...(options.extensionCommands ?? []).map((command) => command.name)]
378
436
  .filter((name, index, all) => all.indexOf(name) === index)
379
437
  .sort((a, b) => {
@@ -384,10 +442,9 @@ export async function runRepl(options) {
384
442
  return rank(a) - rank(b);
385
443
  })
386
444
  .map((name) => {
387
- const description = options.extensionCommands?.find(command => command.name === name)?.description;
388
- const suffix = name === "provider" ? " · choose a Provider"
389
- : name === "model" ? " · change model"
390
- : description === undefined ? "" : ` · ${description}`;
445
+ const description = BUILT_IN_DESCRIPTIONS[name]
446
+ ?? options.extensionCommands?.find(command => command.name === name)?.description;
447
+ const suffix = description === undefined ? "" : ` · ${description}`;
391
448
  return { value: name, label: `/${name}${suffix}` };
392
449
  });
393
450
  const menuItems = (line) => {
@@ -541,8 +598,9 @@ export async function runRepl(options) {
541
598
  tuiInput?.setBusy(false);
542
599
  try {
543
600
  const width = terminalWidth({ columns: options.output.columns });
544
- write(approvalPreview(command, width, context ?? "Shell command"));
545
- if (!commandReviewable(command, width)) {
601
+ const wraps = context?.startsWith(WEB_SEARCH_APPROVAL_PREFIX) === true;
602
+ write(approvalPreview(command, width, context ?? "Shell command", wraps ? { wrap: true } : {}));
603
+ if (!wraps && !commandReviewable(command, width)) {
546
604
  write(" Denied: command cannot be fully reviewed at this width. Ask for a shorter command.\n");
547
605
  return false;
548
606
  }
@@ -637,12 +695,22 @@ export async function runRepl(options) {
637
695
  .map(message => message.content)
638
696
  .filter(message => message.length > 0)
639
697
  .slice(-100);
698
+ const footerIdentity = () => {
699
+ const provider = options.providers?.provider ?? options.providerName;
700
+ // A live Provider wins. Do not fall back to the startup model, and do
701
+ // not invent a name when the model or provider is still unset.
702
+ const model = options.providers === undefined ? options.modelName : options.providers.model;
703
+ if (provider === undefined || provider.length === 0 || model === undefined || model.length === 0)
704
+ return undefined;
705
+ return `${provider} · ${model}`;
706
+ };
640
707
  const editor = new EditorController(output, resolveDraft, {
641
708
  submitOnEnter: isDefault,
642
709
  history: recent,
643
710
  slashCommands: commands.map(item => ({ value: item.value, label: item.label })),
644
711
  mouse: isDefault && options.tui === true && output.isTTY === true,
645
712
  color: isDefault && terminalColorEnabled(output.isTTY === true, process.env),
713
+ ...(isDefault && options.tui ? { footerIdentity } : {}),
646
714
  ...(isDefault && options.tui && options.permissionPolicy !== undefined ? {
647
715
  footerStatus: () => {
648
716
  if (activeSession.planMode)
@@ -766,6 +834,40 @@ export async function runRepl(options) {
766
834
  write(`${error instanceof Error ? error.message : "Git diff unavailable"}\n`);
767
835
  }
768
836
  };
837
+ /** `/login` must name its target before any hidden key prompt. */
838
+ const chooseLoginProvider = async () => {
839
+ const providers = options.providers;
840
+ if (providers === undefined)
841
+ return undefined;
842
+ const ids = [...providers.providerIds()];
843
+ if (ids.length === 0)
844
+ return undefined;
845
+ if (tuiInput === undefined) {
846
+ return readProviderChoice(readNextLine, write, ids, providers.provider);
847
+ }
848
+ menu?.clear();
849
+ dismissedLine = undefined;
850
+ let resolveChoice;
851
+ const pending = new Promise(resolve => { resolveChoice = resolve; });
852
+ const output = options.output;
853
+ const picker = new ProviderPickerController(output, ids, providers.provider, resolveChoice);
854
+ const resized = () => { picker.resized(); };
855
+ const closed = () => { picker.cancel(); };
856
+ tuiInput.setRawHandler(chunk => { picker.receive(chunk); });
857
+ output.on("resize", resized);
858
+ readline.once("close", closed);
859
+ options.input.once("end", closed);
860
+ try {
861
+ picker.begin();
862
+ return await pending;
863
+ }
864
+ finally {
865
+ tuiInput.setRawHandler(undefined);
866
+ output.off("resize", resized);
867
+ readline.off("close", closed);
868
+ options.input.off("end", closed);
869
+ }
870
+ };
769
871
  const signInAfterProviderSelection = async () => {
770
872
  if (!options.tui || options.signIn === undefined)
771
873
  return;
@@ -805,21 +907,31 @@ export async function runRepl(options) {
805
907
  }
806
908
  const fromComposer = line.trim() === "/compose";
807
909
  const fromEditor = line.trim() === "/editor";
808
- if (line.trim() === "/review") {
910
+ if (line.trim() === "/review" || line.trim() === "/diff patch") {
809
911
  await inspectPatch();
810
912
  if (inputClosed)
811
913
  break;
812
914
  showPrompt();
813
915
  continue;
814
916
  }
815
- if (line.trim().startsWith("/gitdiff")) {
816
- await inspectLiveGitDiff(line.trim());
917
+ if (line.trim() === "/diff" || line.trim() === "/diff --staged" || line.trim().startsWith("/gitdiff")) {
918
+ const gitDiffLine = line.trim().startsWith("/gitdiff")
919
+ ? line.trim()
920
+ : `/gitdiff${line.trim().slice("/diff".length)}`;
921
+ await inspectLiveGitDiff(gitDiffLine);
817
922
  if (inputClosed)
818
923
  break;
819
924
  showPrompt();
820
925
  continue;
821
926
  }
822
927
  if (fromComposer || fromEditor) {
928
+ if (options.inlineComposer && tuiInput !== undefined) {
929
+ write("The cursor-addressable multiline editor is already active in this prompt. Enter sends; Ctrl+O inserts a newline.\n");
930
+ if (inputClosed)
931
+ break;
932
+ showPrompt();
933
+ continue;
934
+ }
823
935
  const draft = fromEditor ? await collectEditor() : await collectMultiline();
824
936
  if (draft === undefined) {
825
937
  if (inputClosed)
@@ -829,6 +941,14 @@ export async function runRepl(options) {
829
941
  }
830
942
  line = draft;
831
943
  }
944
+ if (options.inlineComposer && tuiInput !== undefined) {
945
+ const isOutputTTY = typeof options.output === "object" && options.output !== null && "isTTY" in options.output && options.output.isTTY === true;
946
+ const useColor = options.tui === true && terminalColorEnabled(isOutputTTY, process.env);
947
+ const prefix = useColor ? "\u001b[1m\u001b[2m›\u001b[0m " : "› ";
948
+ const lines = line.split("\n");
949
+ const formattedPrompt = lines.map((l, i) => `${i === 0 ? prefix : " "}${terminalText(l)}`).join("\n");
950
+ write(`\n${formattedPrompt}\n\n`);
951
+ }
832
952
  const outcome = fromComposer || fromEditor ? "not-a-command" : handleSlashCommand(line, {
833
953
  session: activeSession,
834
954
  write,
@@ -846,9 +966,11 @@ export async function runRepl(options) {
846
966
  ...(options.compactionEnabled === undefined ? {} : { compactionEnabled: options.compactionEnabled }),
847
967
  ...(options.providerName === undefined ? {} : { providerName: options.providerName }),
848
968
  ...(options.modelName === undefined ? {} : { modelName: options.modelName }),
969
+ ...(options.webSearch === undefined ? {} : { webSearch: options.webSearch }),
849
970
  ...(options.permissions === undefined ? {} : { permissions: options.permissions }),
850
971
  ...(options.permissionPolicy === undefined ? {} : { permissionPolicy: options.permissionPolicy }),
851
972
  ...(options.inlineComposer === true ? { inlineComposer: true } : {}),
973
+ ...(tuiInput !== undefined ? { interactivePicker: true } : {}),
852
974
  ...(options.sessionOptions === undefined ? {} : { sessionOptions: options.sessionOptions }),
853
975
  });
854
976
  if (outcome === "exit") {
@@ -906,6 +1028,31 @@ export async function runRepl(options) {
906
1028
  continue;
907
1029
  }
908
1030
  }
1031
+ else if (options.providers !== undefined) {
1032
+ signingIn = true;
1033
+ let chosen;
1034
+ try {
1035
+ chosen = await chooseLoginProvider();
1036
+ }
1037
+ finally {
1038
+ signingIn = false;
1039
+ }
1040
+ if (chosen === undefined) {
1041
+ write("No Provider selected; no key was stored.\n");
1042
+ if (inputClosed)
1043
+ break;
1044
+ showPrompt();
1045
+ continue;
1046
+ }
1047
+ const selected = await options.providers.select(chosen);
1048
+ write(selected.message);
1049
+ if (!selected.ok) {
1050
+ if (inputClosed)
1051
+ break;
1052
+ showPrompt();
1053
+ continue;
1054
+ }
1055
+ }
909
1056
  if (options.signIn !== undefined) {
910
1057
  signingIn = true;
911
1058
  try {
@@ -915,11 +1062,38 @@ export async function runRepl(options) {
915
1062
  signingIn = false;
916
1063
  }
917
1064
  }
1065
+ if (inputClosed)
1066
+ break;
918
1067
  showPrompt();
919
1068
  continue;
920
1069
  }
921
1070
  if (typeof outcome === "object" && (outcome.kind === "provider" || outcome.kind === "model" || outcome.kind === "endpoint")) {
922
1071
  if (options.providers !== undefined) {
1072
+ if (outcome.kind === "provider" && outcome.argument.length === 0 && tuiInput !== undefined) {
1073
+ signingIn = true;
1074
+ let chosen;
1075
+ try {
1076
+ chosen = await chooseLoginProvider();
1077
+ }
1078
+ finally {
1079
+ signingIn = false;
1080
+ }
1081
+ if (chosen === undefined) {
1082
+ write("No Provider selected; current Provider retained.\n");
1083
+ if (inputClosed)
1084
+ break;
1085
+ showPrompt();
1086
+ continue;
1087
+ }
1088
+ const selected = await options.providers.select(chosen);
1089
+ write(selected.message);
1090
+ if (selected.ok)
1091
+ await signInAfterProviderSelection();
1092
+ if (inputClosed)
1093
+ break;
1094
+ showPrompt();
1095
+ continue;
1096
+ }
923
1097
  const selected = outcome.kind === "provider"
924
1098
  ? await options.providers.select(outcome.argument)
925
1099
  : outcome.kind === "model" ? await options.providers.changeModel(outcome.argument)