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.
- package/README.md +136 -886
- package/README.zh-CN.md +71 -29
- package/dist/agent.d.ts +14 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +195 -28
- package/dist/agent.js.map +1 -1
- package/dist/cli-options.d.ts +2 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +12 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli-runtime.d.ts +24 -0
- package/dist/cli-runtime.d.ts.map +1 -0
- package/dist/cli-runtime.js +64 -0
- package/dist/cli-runtime.js.map +1 -0
- package/dist/cli.js +29 -97
- package/dist/cli.js.map +1 -1
- package/dist/context/compaction.d.ts +5 -2
- package/dist/context/compaction.d.ts.map +1 -1
- package/dist/context/compaction.js +58 -32
- package/dist/context/compaction.js.map +1 -1
- package/dist/context/context-manager.d.ts +2 -0
- package/dist/context/context-manager.d.ts.map +1 -1
- package/dist/context/context-manager.js +4 -3
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/project-instructions.d.ts +17 -0
- package/dist/context/project-instructions.d.ts.map +1 -0
- package/dist/context/project-instructions.js +191 -0
- package/dist/context/project-instructions.js.map +1 -0
- package/dist/evals/baseline.d.ts +41 -0
- package/dist/evals/baseline.d.ts.map +1 -0
- package/dist/evals/baseline.js +121 -0
- package/dist/evals/baseline.js.map +1 -0
- package/dist/evals/cli.js +27 -1
- package/dist/evals/cli.js.map +1 -1
- package/dist/evals/parse-args.d.ts +2 -0
- package/dist/evals/parse-args.d.ts.map +1 -1
- package/dist/evals/parse-args.js +5 -0
- package/dist/evals/parse-args.js.map +1 -1
- package/dist/evals/report.d.ts +2 -0
- package/dist/evals/report.d.ts.map +1 -1
- package/dist/evals/report.js +12 -8
- package/dist/evals/report.js.map +1 -1
- package/dist/evals/runner.d.ts +3 -0
- package/dist/evals/runner.d.ts.map +1 -1
- package/dist/evals/runner.js +5 -1
- package/dist/evals/runner.js.map +1 -1
- package/dist/evals/task.d.ts.map +1 -1
- package/dist/evals/task.js +1 -0
- package/dist/evals/task.js.map +1 -1
- package/dist/evals/tools.d.ts +4 -1
- package/dist/evals/tools.d.ts.map +1 -1
- package/dist/evals/tools.js +11 -4
- package/dist/evals/tools.js.map +1 -1
- package/dist/events.d.ts +7 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/patch/parse.d.ts +26 -0
- package/dist/patch/parse.d.ts.map +1 -0
- package/dist/patch/parse.js +117 -0
- package/dist/patch/parse.js.map +1 -0
- package/dist/prompts.d.ts +2 -2
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +4 -3
- package/dist/prompts.js.map +1 -1
- package/dist/providers/deferred-client.d.ts +2 -1
- package/dist/providers/deferred-client.d.ts.map +1 -1
- package/dist/providers/deferred-client.js +4 -0
- package/dist/providers/deferred-client.js.map +1 -1
- package/dist/providers/interactive.d.ts +31 -0
- package/dist/providers/interactive.d.ts.map +1 -0
- package/dist/providers/interactive.js +184 -0
- package/dist/providers/interactive.js.map +1 -0
- package/dist/remote/repl.d.ts.map +1 -1
- package/dist/remote/repl.js +6 -5
- package/dist/remote/repl.js.map +1 -1
- package/dist/repl.d.ts +43 -4
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +513 -32
- package/dist/repl.js.map +1 -1
- package/dist/session-store.d.ts +6 -2
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +5 -1
- package/dist/session-store.js.map +1 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +38 -12
- package/dist/session.js.map +1 -1
- package/dist/shell/approval.d.ts +15 -0
- package/dist/shell/approval.d.ts.map +1 -0
- package/dist/shell/approval.js +25 -0
- package/dist/shell/approval.js.map +1 -0
- package/dist/shell/docker.d.ts +18 -0
- package/dist/shell/docker.d.ts.map +1 -0
- package/dist/shell/docker.js +83 -0
- package/dist/shell/docker.js.map +1 -0
- package/dist/shell/local.d.ts +1 -1
- package/dist/shell/local.d.ts.map +1 -1
- package/dist/shell/local.js +1 -1
- package/dist/shell/local.js.map +1 -1
- package/dist/tools/apply-patch.d.ts +18 -0
- package/dist/tools/apply-patch.d.ts.map +1 -0
- package/dist/tools/apply-patch.js +53 -0
- package/dist/tools/apply-patch.js.map +1 -0
- package/dist/tools/bash.d.ts +4 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +28 -3
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/parallel.d.ts +14 -0
- package/dist/tools/parallel.d.ts.map +1 -0
- package/dist/tools/parallel.js +20 -0
- package/dist/tools/parallel.js.map +1 -0
- package/dist/tools/tool.d.ts +7 -0
- package/dist/tools/tool.d.ts.map +1 -1
- package/dist/tools/tool.js +7 -1
- package/dist/tools/tool.js.map +1 -1
- package/dist/tools/update-plan.d.ts +57 -0
- package/dist/tools/update-plan.d.ts.map +1 -0
- package/dist/tools/update-plan.js +99 -0
- package/dist/tools/update-plan.js.map +1 -0
- package/dist/tui/activity.d.ts +17 -0
- package/dist/tui/activity.d.ts.map +1 -0
- package/dist/tui/activity.js +188 -0
- package/dist/tui/activity.js.map +1 -0
- package/dist/tui/diff.d.ts +14 -0
- package/dist/tui/diff.d.ts.map +1 -0
- package/dist/tui/diff.js +57 -0
- package/dist/tui/diff.js.map +1 -0
- package/dist/tui/editor-input.d.ts +53 -0
- package/dist/tui/editor-input.d.ts.map +1 -0
- package/dist/tui/editor-input.js +353 -0
- package/dist/tui/editor-input.js.map +1 -0
- package/dist/tui/editor.d.ts +72 -0
- package/dist/tui/editor.d.ts.map +1 -0
- package/dist/tui/editor.js +321 -0
- package/dist/tui/editor.js.map +1 -0
- package/dist/tui/git-diff.d.ts +7 -0
- package/dist/tui/git-diff.d.ts.map +1 -0
- package/dist/tui/git-diff.js +48 -0
- package/dist/tui/git-diff.js.map +1 -0
- package/dist/tui/input.d.ts +20 -0
- package/dist/tui/input.d.ts.map +1 -1
- package/dist/tui/input.js +175 -13
- package/dist/tui/input.js.map +1 -1
- package/dist/tui/live.d.ts +8 -0
- package/dist/tui/live.d.ts.map +1 -1
- package/dist/tui/live.js +37 -5
- package/dist/tui/live.js.map +1 -1
- package/dist/tui/markdown.d.ts +7 -0
- package/dist/tui/markdown.d.ts.map +1 -0
- package/dist/tui/markdown.js +55 -0
- package/dist/tui/markdown.js.map +1 -0
- package/dist/tui/menu.d.ts +26 -0
- package/dist/tui/menu.d.ts.map +1 -0
- package/dist/tui/menu.js +75 -0
- package/dist/tui/menu.js.map +1 -0
- package/dist/tui/review.d.ts +27 -0
- package/dist/tui/review.d.ts.map +1 -0
- package/dist/tui/review.js +114 -0
- package/dist/tui/review.js.map +1 -0
- package/dist/tui/slash.d.ts +11 -0
- package/dist/tui/slash.d.ts.map +1 -0
- package/dist/tui/slash.js +13 -0
- package/dist/tui/slash.js.map +1 -0
- package/dist/tui/state.d.ts +5 -0
- package/dist/tui/state.d.ts.map +1 -1
- package/dist/tui/state.js +10 -1
- package/dist/tui/state.js.map +1 -1
- package/dist/tui/syntax.d.ts +6 -0
- package/dist/tui/syntax.d.ts.map +1 -0
- package/dist/tui/syntax.js +48 -0
- package/dist/tui/syntax.js.map +1 -0
- package/dist/tui/text.d.ts +2 -0
- package/dist/tui/text.d.ts.map +1 -1
- package/dist/tui/text.js +19 -0
- package/dist/tui/text.js.map +1 -1
- package/dist/tui/view.d.ts +1 -1
- package/dist/tui/view.d.ts.map +1 -1
- package/dist/tui/view.js +77 -31
- package/dist/tui/view.js.map +1 -1
- package/dist/tui/welcome.d.ts +7 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +40 -23
- package/dist/tui/welcome.js.map +1 -1
- package/dist/workspace/patch.d.ts +11 -0
- package/dist/workspace/patch.d.ts.map +1 -0
- package/dist/workspace/patch.js +124 -0
- package/dist/workspace/patch.js.map +1 -0
- package/dist/workspace/types.d.ts +7 -0
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/types.js.map +1 -1
- package/dist/workspace.d.ts +2 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +5 -0
- package/dist/workspace.js.map +1 -1
- 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
|
-
/
|
|
20
|
-
/
|
|
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
|
|
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(
|
|
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.
|
|
160
|
-
*
|
|
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
|
-
//
|
|
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("
|
|
393
|
+
options.output.write("\n");
|
|
212
394
|
}
|
|
213
|
-
|
|
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
|
-
|
|
645
|
+
let line = options.inlineComposer && tuiInput !== undefined
|
|
646
|
+
? await collectEditor(true)
|
|
647
|
+
: await readNextLine();
|
|
231
648
|
if (line === undefined) {
|
|
232
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|