@pentoshi/clai 2.0.27 → 2.0.29
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/dist/agent/confirm-port.d.ts +30 -0
- package/dist/agent/confirm-port.js +89 -0
- package/dist/agent/confirm-port.js.map +1 -0
- package/dist/agent/context-manager.d.ts +10 -0
- package/dist/agent/context-manager.js +16 -2
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/plan-tool.d.ts +26 -0
- package/dist/agent/plan-tool.js +188 -0
- package/dist/agent/plan-tool.js.map +1 -0
- package/dist/agent/runner.d.ts +14 -165
- package/dist/agent/runner.js +37 -1585
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-policy.d.ts +19 -0
- package/dist/agent/session-policy.js +40 -0
- package/dist/agent/session-policy.js.map +1 -0
- package/dist/agent/stop-summary.d.ts +8 -0
- package/dist/agent/stop-summary.js +53 -0
- package/dist/agent/stop-summary.js.map +1 -0
- package/dist/agent/tool-call-parser.d.ts +155 -0
- package/dist/agent/tool-call-parser.js +1107 -0
- package/dist/agent/tool-call-parser.js.map +1 -0
- package/dist/agent/tool-output-formatting.d.ts +7 -0
- package/dist/agent/tool-output-formatting.js +127 -0
- package/dist/agent/tool-output-formatting.js.map +1 -0
- package/dist/commands/update.js +1 -1
- package/dist/modes/agent.d.ts +1 -0
- package/dist/modes/agent.js.map +1 -1
- package/dist/prompts/index.d.ts +2 -2
- package/dist/prompts/index.js +103 -59
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl/prompt-line.d.ts +19 -0
- package/dist/repl/prompt-line.js +555 -0
- package/dist/repl/prompt-line.js.map +1 -0
- package/dist/repl/slash-commands.d.ts +24 -0
- package/dist/repl/slash-commands.js +317 -0
- package/dist/repl/slash-commands.js.map +1 -0
- package/dist/repl.d.ts +2 -25
- package/dist/repl.js +41 -898
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +1 -1
- package/dist/safety/classifier.js.map +1 -1
- package/dist/store/keys.js +0 -4
- package/dist/store/keys.js.map +1 -1
- package/dist/store/plan.js +1 -1
- package/dist/store/plan.js.map +1 -1
- package/dist/tools/nmap-runner.d.ts +19 -0
- package/dist/tools/nmap-runner.js +177 -0
- package/dist/tools/nmap-runner.js.map +1 -0
- package/dist/tools/package-binary.d.ts +1 -0
- package/dist/tools/package-binary.js +30 -0
- package/dist/tools/package-binary.js.map +1 -0
- package/dist/tools/pdf.js +2 -2
- package/dist/tools/pdf.js.map +1 -1
- package/dist/tools/registry.d.ts +2 -11
- package/dist/tools/registry.js +3 -203
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/tool-types.d.ts +12 -0
- package/dist/tools/tool-types.js +2 -0
- package/dist/tools/tool-types.js.map +1 -0
- package/dist/tools/web/audit.d.ts +7 -29
- package/dist/tools/web/audit.js +7 -29
- package/dist/tools/web/audit.js.map +1 -1
- package/dist/tools/web/budget.d.ts +5 -28
- package/dist/tools/web/budget.js +5 -28
- package/dist/tools/web/budget.js.map +1 -1
- package/dist/tools/web/capture.d.ts +8 -35
- package/dist/tools/web/capture.js +8 -35
- package/dist/tools/web/capture.js.map +1 -1
- package/dist/tools/web/fetch-core.d.ts +6 -26
- package/dist/tools/web/fetch-core.js +6 -26
- package/dist/tools/web/fetch-core.js.map +1 -1
- package/dist/tools/web/readable.d.ts +7 -13
- package/dist/tools/web/readable.js +7 -13
- package/dist/tools/web/readable.js.map +1 -1
- package/dist/tools/web/search.d.ts +7 -20
- package/dist/tools/web/search.js +7 -20
- package/dist/tools/web/search.js.map +1 -1
- package/dist/tools/web/ssrf-guard.d.ts +8 -27
- package/dist/tools/web/ssrf-guard.js +8 -27
- package/dist/tools/web/ssrf-guard.js.map +1 -1
- package/dist/tui/App.js +19 -88
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/hooks/useAgentRunner.js +17 -1
- package/dist/tui/hooks/useAgentRunner.js.map +1 -1
- package/dist/tui/key-intent.d.ts +36 -0
- package/dist/tui/key-intent.js +54 -0
- package/dist/tui/key-intent.js.map +1 -0
- package/dist/tui/render-lines.js +125 -37
- package/dist/tui/render-lines.js.map +1 -1
- package/dist/tui/state.js +2 -2
- package/dist/tui/state.js.map +1 -1
- package/dist/tui/text-format.d.ts +11 -0
- package/dist/tui/text-format.js +85 -0
- package/dist/tui/text-format.js.map +1 -0
- package/dist/ui/ansi-box.d.ts +7 -0
- package/dist/ui/ansi-box.js +18 -0
- package/dist/ui/ansi-box.js.map +1 -0
- package/dist/ui/banner.d.ts +0 -9
- package/dist/ui/banner.js +1 -43
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/intro-card.d.ts +10 -0
- package/dist/ui/intro-card.js +39 -0
- package/dist/ui/intro-card.js.map +1 -0
- package/dist/ui/markdown.js +1 -1
- package/dist/ui/markdown.js.map +1 -1
- package/dist/ui/output-pane.js.map +1 -1
- package/dist/ui/spinner.js +6 -17
- package/dist/ui/spinner.js.map +1 -1
- package/dist/ui/wordmark.d.ts +7 -0
- package/dist/ui/wordmark.js +42 -0
- package/dist/ui/wordmark.js.map +1 -0
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearLine, cursorTo, emitKeypressEvents, moveCursor
|
|
1
|
+
import { clearLine, cursorTo, emitKeypressEvents, moveCursor } from "node:readline";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { confirm } from "@inquirer/prompts";
|
|
@@ -11,9 +11,9 @@ import { getConfig, getProviderModel, setDefaultMode, setProviderModel, setThink
|
|
|
11
11
|
import { listSessions, saveSession, clearAllHistory, getSession, } from "./store/history.js";
|
|
12
12
|
import { assertProvider, defaultModels, getProviderInfoText } from "./llm/provider.js";
|
|
13
13
|
import { getProvider, providerAuth } from "./llm/router.js";
|
|
14
|
-
import { nvidiaFallbackModels } from "./llm/nvidia.js";
|
|
15
14
|
import { runUpdate, checkForUpdateSilent, getCurrentVersion, } from "./commands/update.js";
|
|
16
|
-
import {
|
|
15
|
+
import { renderModeSwitch, renderProviderSwitch, PROMPT, } from "./ui/banner.js";
|
|
16
|
+
import { renderIntroCard } from "./ui/intro-card.js";
|
|
17
17
|
import { clearThinking, createThinkingStreamParser, getLastThinking, rememberThinkingFromText, renderAllThinking, renderThinkingSummary, renderThinkingToggleMessage, } from "./ui/thinking.js";
|
|
18
18
|
import { createMarkdownStreamWriter, renderMarkdown } from "./ui/markdown.js";
|
|
19
19
|
import { startThinkingSpinner } from "./ui/spinner.js";
|
|
@@ -24,280 +24,34 @@ import { renderPlanDocument, renderPlanChecklist } from "./ui/plan-pane.js";
|
|
|
24
24
|
import { safeCwd, cwdIsBroken, recoverCwd } from "./os/cwd.js";
|
|
25
25
|
import { compactMessages, estimateMessagesTokens, } from "./agent/context-manager.js";
|
|
26
26
|
import { isCtrlC, isCtrlO, isCtrlP, isCtrlT, isEscape } from "./ui/keys.js";
|
|
27
|
-
import {
|
|
27
|
+
import { expandMentions, loadImageAttachments, imageAttachmentPaths, } from "./ui/mentions.js";
|
|
28
28
|
import { imageOcr } from "./tools/image.js";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
description: "open searchable picker (type/↑/↓ + Enter), or pass a name/number",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
command: "/provider",
|
|
40
|
-
usage: "[name]",
|
|
41
|
-
description: "switch provider or open picker",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
command: "/use",
|
|
45
|
-
usage: "<provider>",
|
|
46
|
-
description: "alias for /provider <name>",
|
|
47
|
-
},
|
|
48
|
-
{ command: "/set", usage: "[provider] [key]", description: "store API key or open picker" },
|
|
49
|
-
{ command: "/unset", usage: "[provider]", description: "remove key or open picker" },
|
|
50
|
-
{ command: "/keys", description: "list configured providers" },
|
|
51
|
-
{
|
|
52
|
-
command: "/info",
|
|
53
|
-
usage: "[provider]",
|
|
54
|
-
description: "show info for the active or specified provider",
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
command: "/search",
|
|
58
|
-
usage: "[provider]",
|
|
59
|
-
description: "switch web.search provider or open picker",
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
command: "/search-provider",
|
|
63
|
-
usage: "[provider]",
|
|
64
|
-
description: "alias for /search",
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
command: "/mouse",
|
|
68
|
-
usage: "[on|off]",
|
|
69
|
-
description: "toggle touchpad transcript scrolling vs native selection",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
command: "/variants",
|
|
73
|
-
usage: "[on|off|none|minimal|low|medium|high|xhigh]",
|
|
74
|
-
description: "toggle thinking/effort (interactive picker if no arg)",
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
command: "/reasoning",
|
|
78
|
-
usage: "[on|off|none|minimal|low|medium|high|xhigh]",
|
|
79
|
-
description: "alias for /variants",
|
|
80
|
-
},
|
|
81
|
-
{ command: "/clear", description: "clear context" },
|
|
82
|
-
{
|
|
83
|
-
command: "/new",
|
|
84
|
-
description: "start a fresh session (clear context, no history carryover)",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
command: "/history",
|
|
88
|
-
description: "browse & resume past sessions (interactive picker)",
|
|
89
|
-
},
|
|
90
|
-
{ command: "/save", usage: "<name>", description: "save session" },
|
|
91
|
-
{ command: "/reset", description: "clear all saved history" },
|
|
92
|
-
{ command: "/cwd", usage: "<path>", description: "change working directory" },
|
|
93
|
-
{
|
|
94
|
-
command: "/allow",
|
|
95
|
-
usage: "<tool>|list",
|
|
96
|
-
description: "allow a tool for this session (not persisted)",
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
command: "/disallow",
|
|
100
|
-
usage: "<tool>",
|
|
101
|
-
description: "revoke a session allow",
|
|
102
|
-
},
|
|
103
|
-
{ command: "/think", description: "show thinking from last response" },
|
|
104
|
-
{ command: "/thinking", description: "alias for /think" },
|
|
105
|
-
{
|
|
106
|
-
command: "/output",
|
|
107
|
-
usage: "[last|<id>|list]",
|
|
108
|
-
description: "open full tool output (Ctrl+O); q closes the pager",
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
command: "/freeonly",
|
|
112
|
-
usage: "[on|off]",
|
|
113
|
-
description: "skip paid providers when fallback is enabled",
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
command: "/fallback",
|
|
117
|
-
usage: "[on|off]",
|
|
118
|
-
description: "try other configured providers after a failure (off by default)",
|
|
119
|
-
},
|
|
120
|
-
{ command: "/compact", description: "compact session history now" },
|
|
121
|
-
{ command: "/context", description: "show estimated context size" },
|
|
122
|
-
{
|
|
123
|
-
command: "/plan",
|
|
124
|
-
description: "view the current session plan (also Ctrl+P)",
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
command: "/implement",
|
|
128
|
-
description: "approve the current plan and have clai execute it",
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
command: "/discard",
|
|
132
|
-
description: "discard the current plan so later messages ignore it",
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
command: "/scope",
|
|
136
|
-
usage: "[show|clear|new|add <targets>]",
|
|
137
|
-
description: "manage pentest engagement scope",
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
command: "/privacy",
|
|
141
|
-
usage: "[status|clear-history|clear-logs|clear-artifacts|clear-all|on|off]",
|
|
142
|
-
description: "control retention and private mode (in-memory only)",
|
|
143
|
-
},
|
|
144
|
-
{ command: "/update", description: "check for updates" },
|
|
145
|
-
{
|
|
146
|
-
command: "/clean",
|
|
147
|
-
description: "clear screen and reset chat (fresh start)",
|
|
148
|
-
},
|
|
149
|
-
{ command: "/exit", description: "quit" },
|
|
150
|
-
{ command: "/quit", description: "alias for /exit" },
|
|
151
|
-
{ command: "/help", description: "list commands" },
|
|
152
|
-
];
|
|
153
|
-
// ── Well-known models per provider (refreshed May 2026) ───────────────────
|
|
154
|
-
/** Curated model choices used by both frontends. */
|
|
155
|
-
export const knownModels = {
|
|
156
|
-
groq: [
|
|
157
|
-
"openai/gpt-oss-20b",
|
|
158
|
-
"openai/gpt-oss-120b",
|
|
159
|
-
"openai/gpt-oss-safeguard-20b",
|
|
160
|
-
"qwen/qwen3-32b",
|
|
161
|
-
"llama-3.3-70b-versatile",
|
|
162
|
-
"llama-3.1-8b-instant",
|
|
163
|
-
"meta-llama/llama-4-scout-17b-16e-instruct",
|
|
164
|
-
"groq/compound-mini",
|
|
165
|
-
"groq/compound",
|
|
166
|
-
],
|
|
167
|
-
gemini: [
|
|
168
|
-
"gemini-3.5-flash",
|
|
169
|
-
"gemini-3.1-pro-preview",
|
|
170
|
-
"gemini-3-flash-preview",
|
|
171
|
-
"gemini-3.1-flash-lite",
|
|
172
|
-
"gemini-3-pro-preview",
|
|
173
|
-
"gemini-2.5-pro",
|
|
174
|
-
"gemini-2.5-flash",
|
|
175
|
-
"gemini-2.5-flash-lite",
|
|
176
|
-
"gemini-2.0-flash",
|
|
177
|
-
"gemini-2.0-flash-lite",
|
|
178
|
-
],
|
|
179
|
-
openrouter: [
|
|
180
|
-
"meta-llama/llama-3.3-70b-instruct:free",
|
|
181
|
-
"deepseek/deepseek-v4-flash:free",
|
|
182
|
-
"openai/gpt-oss-20b:free",
|
|
183
|
-
"qwen/qwen3-coder:free",
|
|
184
|
-
"qwen/qwen3-next-80b-a3b-instruct:free",
|
|
185
|
-
"google/gemma-4-31b-it:free",
|
|
186
|
-
"nvidia/nemotron-3-nano-30b-a3b:free",
|
|
187
|
-
"z-ai/glm-4.5-air:free",
|
|
188
|
-
"moonshotai/kimi-k2.6",
|
|
189
|
-
"meta-llama/llama-4-maverick",
|
|
190
|
-
"google/gemini-2.5-flash",
|
|
191
|
-
],
|
|
192
|
-
openai: [
|
|
193
|
-
"gpt-5.5",
|
|
194
|
-
"gpt-5.5-pro",
|
|
195
|
-
"gpt-5.4",
|
|
196
|
-
"gpt-5.4-pro",
|
|
197
|
-
"gpt-5.4-mini",
|
|
198
|
-
"gpt-5.4-nano",
|
|
199
|
-
"gpt-rosalind",
|
|
200
|
-
"gpt-realtime-2",
|
|
201
|
-
"openai/gpt-oss-20b",
|
|
202
|
-
"openai/gpt-oss-120b",
|
|
203
|
-
"gpt-5",
|
|
204
|
-
"gpt-5-mini",
|
|
205
|
-
"gpt-5-nano",
|
|
206
|
-
"gpt-4o-mini",
|
|
207
|
-
"gpt-4o",
|
|
208
|
-
],
|
|
209
|
-
anthropic: [
|
|
210
|
-
"claude-opus-4-7",
|
|
211
|
-
"claude-sonnet-4-6",
|
|
212
|
-
"claude-haiku-4-5",
|
|
213
|
-
"claude-opus-4-6",
|
|
214
|
-
"claude-sonnet-4-5",
|
|
215
|
-
"claude-3-5-sonnet-latest",
|
|
216
|
-
"claude-3-5-haiku-latest",
|
|
217
|
-
],
|
|
218
|
-
nvidia: nvidiaFallbackModels,
|
|
219
|
-
ollama: [
|
|
220
|
-
"llama3.3:70b",
|
|
221
|
-
"llama3.2:3b",
|
|
222
|
-
"llama3.1:8b",
|
|
223
|
-
"qwen2.5:7b",
|
|
224
|
-
"qwen2.5-coder:7b",
|
|
225
|
-
"deepseek-r1:7b",
|
|
226
|
-
"mistral:7b",
|
|
227
|
-
"gemma3:9b",
|
|
228
|
-
"phi4:14b",
|
|
229
|
-
"codellama:7b",
|
|
230
|
-
],
|
|
231
|
-
agentrouter: [
|
|
232
|
-
"claude-opus-4-6",
|
|
233
|
-
"claude-opus-4-7",
|
|
234
|
-
"claude-opus-4-8",
|
|
235
|
-
"glm-5.2",
|
|
236
|
-
"gpt-5.5",
|
|
237
|
-
],
|
|
238
|
-
kimchi: [
|
|
239
|
-
"kimi-k2.6",
|
|
240
|
-
"minimax-m2.7",
|
|
241
|
-
"nemotron-3-super-fp4",
|
|
242
|
-
],
|
|
243
|
-
"aws-mantle": [],
|
|
244
|
-
bynara: [
|
|
245
|
-
// Free tier models
|
|
246
|
-
"mimo-v2.5-free",
|
|
247
|
-
"mimo-v2.5-pro-free",
|
|
248
|
-
"mistral-large",
|
|
249
|
-
"mistral-medium-3-5",
|
|
250
|
-
// Pay-as-you-go / subscription models (from https://router.bynara.id/pricing)
|
|
251
|
-
"mimo-v2.5",
|
|
252
|
-
"mimo-v2.5-pro",
|
|
253
|
-
"mimo-v2.5-hermes",
|
|
254
|
-
"mimo-v2.5-pro-hermes",
|
|
255
|
-
"mimo-v2.5-pro-ultraspeed",
|
|
256
|
-
"claude-opus-4.7",
|
|
257
|
-
"claude-opus-4.8",
|
|
258
|
-
"claude-sonnet-4.6",
|
|
259
|
-
"claude-sonnet-4.5",
|
|
260
|
-
"claude-haiku-4.5",
|
|
261
|
-
"deepseek-v4-flash",
|
|
262
|
-
"deepseek-v4-pro",
|
|
263
|
-
"gemini-3-flash",
|
|
264
|
-
"gemini-3.1-pro",
|
|
265
|
-
"gemini-3.5-flash",
|
|
266
|
-
"gemini-2.5-flash",
|
|
267
|
-
"glm-5.1",
|
|
268
|
-
"glm-5.2",
|
|
269
|
-
"gpt-5.4",
|
|
270
|
-
"gpt-5.5",
|
|
271
|
-
"kimi-k2.6",
|
|
272
|
-
"kimi-k2.7-code",
|
|
273
|
-
"minimax-m3",
|
|
274
|
-
"qwen-3.7-max",
|
|
275
|
-
"qwen-3.7-plus",
|
|
276
|
-
"qwen-3.7-plus-1m",
|
|
277
|
-
"bynara-max",
|
|
278
|
-
],
|
|
279
|
-
};
|
|
280
|
-
export function getKnownModels(provider) {
|
|
281
|
-
return [...(knownModels[provider] ?? [])];
|
|
282
|
-
}
|
|
283
|
-
// ── Abort controller for streaming cancellation ─────────────────────────────
|
|
284
|
-
let currentAbortController = null;
|
|
285
|
-
class AbortRunError extends Error {
|
|
286
|
-
constructor() {
|
|
287
|
-
super("Aborted.");
|
|
288
|
-
this.name = "AbortRunError";
|
|
289
|
-
}
|
|
290
|
-
}
|
|
29
|
+
import { readPromptLine, stripAnsi, isPrintableSequence, } from "./repl/prompt-line.js";
|
|
30
|
+
import { slashCommands, knownModels, looksLikeSlashCommand, slashCommandLabel, } from "./repl/slash-commands.js";
|
|
31
|
+
// Re-exported so existing imports of these names from "./repl.js" keep
|
|
32
|
+
// working unchanged — the command catalogue, model lists, and the line
|
|
33
|
+
// editor now live in their own modules under "./repl/".
|
|
34
|
+
export { slashCommands, knownModels, getKnownModels, looksLikeSlashCommand, getSlashCommandSuggestions, isKnownSlashCommand, } from "./repl/slash-commands.js";
|
|
35
|
+
export { renderSlashCommandMenu, renderFileMentionMenu, } from "./repl/prompt-line.js";
|
|
291
36
|
function splitCommand(line) {
|
|
292
37
|
return (line
|
|
293
38
|
.match(/(?:[^\s"]+|"[^"]*")+/g)
|
|
294
39
|
?.map((part) => part.replace(/^"|"$/g, "")) ?? []);
|
|
295
40
|
}
|
|
296
|
-
function
|
|
297
|
-
|
|
41
|
+
function isAbortLikeError(error) {
|
|
42
|
+
if (!error)
|
|
43
|
+
return false;
|
|
44
|
+
if (typeof error === "object") {
|
|
45
|
+
const err = error;
|
|
46
|
+
if (err.name === "AbortError")
|
|
47
|
+
return true;
|
|
48
|
+
if (err.code === "ABORT_ERR")
|
|
49
|
+
return true;
|
|
50
|
+
if (typeof err.message === "string" && /abort/i.test(err.message))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
298
54
|
}
|
|
299
|
-
/** Set of known slash-command names (without the leading "/"). */
|
|
300
|
-
const knownSlashNames = new Set(slashCommands.map((c) => c.command.slice(1).toLowerCase()));
|
|
301
55
|
/**
|
|
302
56
|
* Build an OCR text layer for attached images. Some providers/proxies accept
|
|
303
57
|
* multimodal `image_url` parts but silently ignore the bytes upstream — the
|
|
@@ -335,622 +89,13 @@ async function buildImageOcrGrounding(line, baseDir) {
|
|
|
335
89
|
sections.join("\n\n") +
|
|
336
90
|
"\n</image-ocr>");
|
|
337
91
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
* so we route it to the normal prompt path where expandMentions() turns it
|
|
345
|
-
* into a file attachment.
|
|
346
|
-
*/
|
|
347
|
-
export function looksLikeSlashCommand(line) {
|
|
348
|
-
if (!line.startsWith("/") || line.length < 2)
|
|
349
|
-
return false;
|
|
350
|
-
// First whitespace-delimited token, minus the leading slash.
|
|
351
|
-
const firstToken = line.slice(1).split(/\s/)[0] ?? "";
|
|
352
|
-
// A path-like first token (contains another "/" or a backslash escape, or
|
|
353
|
-
// looks like a filename with an extension) is never a command.
|
|
354
|
-
if (firstToken.includes("/") || firstToken.includes("\\"))
|
|
355
|
-
return false;
|
|
356
|
-
const name = firstToken.toLowerCase();
|
|
357
|
-
// Exact match against a known command, or a unique prefix of one (so
|
|
358
|
-
// partial typing like "/imp" still routes to the command handler, which
|
|
359
|
-
// already resolves abbreviations). Unknown words like a single-segment
|
|
360
|
-
// path token still fall through to handleSlash's "unknown command" help,
|
|
361
|
-
// which is the historical behavior for genuine typos.
|
|
362
|
-
if (knownSlashNames.has(name))
|
|
363
|
-
return true;
|
|
364
|
-
// Only treat as a (mistyped) command when it has no path/extension shape.
|
|
365
|
-
// "Users" alone (from "/Users") would be caught above by the "/" check,
|
|
366
|
-
// so here we accept bare alpha words as command attempts.
|
|
367
|
-
return /^[a-z][a-z0-9-]*$/i.test(firstToken);
|
|
368
|
-
}
|
|
369
|
-
function isAbortLikeError(error) {
|
|
370
|
-
if (!error)
|
|
371
|
-
return false;
|
|
372
|
-
if (typeof error === "object") {
|
|
373
|
-
const err = error;
|
|
374
|
-
if (err.name === "AbortError")
|
|
375
|
-
return true;
|
|
376
|
-
if (err.code === "ABORT_ERR")
|
|
377
|
-
return true;
|
|
378
|
-
if (typeof err.message === "string" && /abort/i.test(err.message))
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
function slashCommandLabel(command) {
|
|
384
|
-
return command.usage
|
|
385
|
-
? `${command.command} ${command.usage}`
|
|
386
|
-
: command.command;
|
|
387
|
-
}
|
|
388
|
-
function slashCommandFilter(line) {
|
|
389
|
-
// Show the menu immediately on '/' so the user can see available commands,
|
|
390
|
-
// but let Enter submit a raw '/' unless they explicitly navigate the menu.
|
|
391
|
-
if (!line.startsWith("/") || line.length < 1 || /\s/.test(line))
|
|
392
|
-
return null;
|
|
393
|
-
// Don't show the command menu for an absolute path the user is typing or
|
|
394
|
-
// drag-dropped (e.g. "/Users/me/file.png"): a path's first token has more
|
|
395
|
-
// "/" or backslash escapes in it. Those go to the normal prompt path.
|
|
396
|
-
const firstToken = line.slice(1).split(/\s/)[0] ?? "";
|
|
397
|
-
if (firstToken.includes("/") || firstToken.includes("\\"))
|
|
398
|
-
return null;
|
|
399
|
-
return line.slice(1).toLowerCase();
|
|
400
|
-
}
|
|
401
|
-
export function getSlashCommandSuggestions(line) {
|
|
402
|
-
const filter = slashCommandFilter(line);
|
|
403
|
-
if (filter === null)
|
|
404
|
-
return [];
|
|
405
|
-
return slashCommands.filter((command) => command.command.slice(1).toLowerCase().startsWith(filter));
|
|
406
|
-
}
|
|
407
|
-
export function isKnownSlashCommand(command) {
|
|
408
|
-
const normalized = command.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
409
|
-
return slashCommands.some((item) => item.command === normalized);
|
|
410
|
-
}
|
|
411
|
-
function fitPlain(text, maxWidth) {
|
|
412
|
-
if (maxWidth <= 0)
|
|
413
|
-
return "";
|
|
414
|
-
if (text.length <= maxWidth)
|
|
415
|
-
return text;
|
|
416
|
-
if (maxWidth === 1)
|
|
417
|
-
return "…";
|
|
418
|
-
return `${text.slice(0, maxWidth - 1)}…`;
|
|
419
|
-
}
|
|
420
|
-
export function renderSlashCommandMenu(line, suggestions, selectedIndex) {
|
|
421
|
-
const cols = terminalColumns();
|
|
422
|
-
// Stay one column short to avoid terminal autowrap when the cursor lands in
|
|
423
|
-
// the final column. refresh() depends on each menu item occupying one row.
|
|
424
|
-
const maxWidth = Math.max(1, cols - 1);
|
|
425
|
-
if (suggestions.length === 0) {
|
|
426
|
-
return [chalk.dim(fitPlain(` no commands matching ${line}`, maxWidth))];
|
|
427
|
-
}
|
|
428
|
-
// Cap visible items to leave room in the terminal
|
|
429
|
-
const termRows = process.stdout.rows || 24;
|
|
430
|
-
const maxVisible = Math.max(5, termRows - 4);
|
|
431
|
-
const visible = suggestions.slice(0, maxVisible);
|
|
432
|
-
const maxCommandLength = Math.max(...visible.map((command) => slashCommandLabel(command).length));
|
|
433
|
-
const items = visible.map((command, index) => {
|
|
434
|
-
const markerPlain = index === selectedIndex ? "›" : " ";
|
|
435
|
-
const marker = index === selectedIndex ? chalk.magenta("›") : " ";
|
|
436
|
-
const prefix = ` ${markerPlain} `;
|
|
437
|
-
const labelBudget = Math.max(1, maxWidth - prefix.length);
|
|
438
|
-
const labelWidth = Math.min(maxCommandLength + 2, labelBudget);
|
|
439
|
-
const label = fitPlain(slashCommandLabel(command), labelWidth).padEnd(labelWidth);
|
|
440
|
-
const descWidth = Math.max(0, maxWidth - prefix.length - label.length);
|
|
441
|
-
const desc = fitPlain(command.description, descWidth);
|
|
442
|
-
return ` ${marker} ${chalk.cyan(label)}${chalk.dim(desc)}`;
|
|
443
|
-
});
|
|
444
|
-
if (suggestions.length > maxVisible) {
|
|
445
|
-
items.push(chalk.dim(fitPlain(` … ${suggestions.length - maxVisible} more`, maxWidth)));
|
|
446
|
-
}
|
|
447
|
-
return items;
|
|
448
|
-
}
|
|
449
|
-
export function renderFileMentionMenu(query, suggestions, selectedIndex) {
|
|
450
|
-
const cols = terminalColumns();
|
|
451
|
-
const maxWidth = Math.max(1, cols - 1);
|
|
452
|
-
if (suggestions.length === 0) {
|
|
453
|
-
return [chalk.dim(fitPlain(` no files matching @${query}`, maxWidth))];
|
|
454
|
-
}
|
|
455
|
-
const termRows = process.stdout.rows || 24;
|
|
456
|
-
const maxVisible = Math.max(5, termRows - 4);
|
|
457
|
-
const visible = suggestions.slice(0, maxVisible);
|
|
458
|
-
const items = visible.map((suggestion, index) => {
|
|
459
|
-
const markerPlain = index === selectedIndex ? "›" : " ";
|
|
460
|
-
const marker = index === selectedIndex ? chalk.magenta("›") : " ";
|
|
461
|
-
const prefix = ` ${markerPlain} `;
|
|
462
|
-
const labelBudget = Math.max(1, maxWidth - prefix.length);
|
|
463
|
-
const label = fitPlain(suggestion.label, labelBudget);
|
|
464
|
-
const colored = suggestion.isDir ? chalk.cyan(label) : chalk.white(label);
|
|
465
|
-
return ` ${marker} ${colored}`;
|
|
466
|
-
});
|
|
467
|
-
if (suggestions.length > maxVisible) {
|
|
468
|
-
items.push(chalk.dim(fitPlain(` … ${suggestions.length - maxVisible} more`, maxWidth)));
|
|
469
|
-
}
|
|
470
|
-
return items;
|
|
471
|
-
}
|
|
472
|
-
function isPrintableSequence(sequence) {
|
|
473
|
-
return sequence !== undefined && /^[^\x00-\x1f\x7f]+$/u.test(sequence);
|
|
474
|
-
}
|
|
475
|
-
function terminalColumns() {
|
|
476
|
-
return Math.max(1, process.stdout.columns || 80);
|
|
477
|
-
}
|
|
478
|
-
function promptCursorPosition(line, cursor, columns) {
|
|
479
|
-
const cols = Math.max(1, columns);
|
|
480
|
-
const promptCols = promptColumnsForRender();
|
|
481
|
-
// Walk the buffer up to the cursor, advancing visual row/col. Newlines
|
|
482
|
-
// (from a multi-line paste) move to the next row at column 0; otherwise we
|
|
483
|
-
// wrap when a row fills. The prompt only offsets the very first row.
|
|
484
|
-
let row = 0;
|
|
485
|
-
let col = promptCols < cols ? promptCols : 0;
|
|
486
|
-
const end = Math.max(0, Math.min(cursor, line.length));
|
|
487
|
-
for (let i = 0; i < end; i += 1) {
|
|
488
|
-
if (line[i] === "\n") {
|
|
489
|
-
row += 1;
|
|
490
|
-
col = 0;
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
col += 1;
|
|
494
|
-
if (col >= cols) {
|
|
495
|
-
row += 1;
|
|
496
|
-
col = 0;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return { row, col };
|
|
500
|
-
}
|
|
501
|
-
function promptColumnsForRender() {
|
|
502
|
-
return stripAnsi(PROMPT).length;
|
|
503
|
-
}
|
|
504
|
-
function buildPromptRows(line, columns, includeCursorRow) {
|
|
505
|
-
const cols = Math.max(1, columns);
|
|
506
|
-
const promptCols = promptColumnsForRender();
|
|
507
|
-
const rows = [];
|
|
508
|
-
// Split on explicit newlines first (multi-line paste), then wrap each
|
|
509
|
-
// logical line by the terminal width. Only the first logical line carries
|
|
510
|
-
// the prompt prefix; continuation lines start at column 0.
|
|
511
|
-
const logicalLines = line.split("\n");
|
|
512
|
-
logicalLines.forEach((logical, li) => {
|
|
513
|
-
const prefix = li === 0 ? PROMPT : "";
|
|
514
|
-
const prefixCols = li === 0 ? promptCols : 0;
|
|
515
|
-
if (prefixCols >= cols) {
|
|
516
|
-
rows.push(prefix);
|
|
517
|
-
for (let i = 0; i < logical.length; i += cols) {
|
|
518
|
-
rows.push(logical.slice(i, i + cols));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
const firstRowCapacity = cols - prefixCols;
|
|
523
|
-
rows.push(prefix + logical.slice(0, firstRowCapacity));
|
|
524
|
-
for (let i = firstRowCapacity; i < logical.length; i += cols) {
|
|
525
|
-
rows.push(logical.slice(i, i + cols));
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
if (includeCursorRow) {
|
|
530
|
-
// Pad so the row the cursor will sit on exists even at exact width
|
|
531
|
-
// boundaries or after a trailing newline.
|
|
532
|
-
const cursorRows = promptCursorPosition(line, line.length, cols).row + 1;
|
|
533
|
-
while (rows.length < cursorRows)
|
|
534
|
-
rows.push("");
|
|
92
|
+
// Abort controller for streaming cancellation
|
|
93
|
+
let currentAbortController = null;
|
|
94
|
+
class AbortRunError extends Error {
|
|
95
|
+
constructor() {
|
|
96
|
+
super("Aborted.");
|
|
97
|
+
this.name = "AbortRunError";
|
|
535
98
|
}
|
|
536
|
-
return rows;
|
|
537
|
-
}
|
|
538
|
-
async function readPromptLine(options) {
|
|
539
|
-
return new Promise((resolve) => {
|
|
540
|
-
let line = "";
|
|
541
|
-
let cursor = 0;
|
|
542
|
-
let selectedIndex = 0;
|
|
543
|
-
let menuNavigated = false;
|
|
544
|
-
let dismissedSlashLine = null;
|
|
545
|
-
let mentionDismissed = false;
|
|
546
|
-
let historyIndex = null;
|
|
547
|
-
let historyDraft = "";
|
|
548
|
-
let lastCtrlCAt = 0;
|
|
549
|
-
// Bracketed-paste state. When the terminal wraps a paste in
|
|
550
|
-
// paste-start/paste-end markers, we buffer the whole paste (including
|
|
551
|
-
// its embedded newlines) and insert it as literal text — so a multi-line
|
|
552
|
-
// prompt is captured in full instead of the first newline submitting and
|
|
553
|
-
// dropping the rest.
|
|
554
|
-
let pasting = false;
|
|
555
|
-
let pasteBuffer = "";
|
|
556
|
-
// Track which row (relative to prompt start) the cursor is on.
|
|
557
|
-
// Needed to move back up to prompt start when text wraps across rows.
|
|
558
|
-
let promptCursorRow = 0;
|
|
559
|
-
const getMenuState = () => {
|
|
560
|
-
const filter = slashCommandFilter(line);
|
|
561
|
-
if (filter === null || dismissedSlashLine === line) {
|
|
562
|
-
return { visible: false, suggestions: [] };
|
|
563
|
-
}
|
|
564
|
-
const suggestions = getSlashCommandSuggestions(line);
|
|
565
|
-
if (selectedIndex >= suggestions.length)
|
|
566
|
-
selectedIndex = 0;
|
|
567
|
-
return { visible: true, suggestions };
|
|
568
|
-
};
|
|
569
|
-
// File @-mention autocomplete: active when the cursor sits inside an
|
|
570
|
-
// `@partial/path` token. Mutually exclusive with the slash menu (slash
|
|
571
|
-
// requires the line to start with "/" and contain no whitespace).
|
|
572
|
-
const getMentionState = () => {
|
|
573
|
-
if (mentionDismissed || line.startsWith("/")) {
|
|
574
|
-
return { visible: false, query: "", start: 0, suggestions: [] };
|
|
575
|
-
}
|
|
576
|
-
const q = getMentionQuery(line, cursor);
|
|
577
|
-
if (!q)
|
|
578
|
-
return { visible: false, query: "", start: 0, suggestions: [] };
|
|
579
|
-
const suggestions = findFileSuggestions(q.query);
|
|
580
|
-
if (suggestions.length === 0) {
|
|
581
|
-
return { visible: false, query: q.query, start: q.start, suggestions };
|
|
582
|
-
}
|
|
583
|
-
if (selectedIndex >= suggestions.length)
|
|
584
|
-
selectedIndex = 0;
|
|
585
|
-
return { visible: true, query: q.query, start: q.start, suggestions };
|
|
586
|
-
};
|
|
587
|
-
const applyMention = (suggestion, start) => {
|
|
588
|
-
const before = line.slice(0, start);
|
|
589
|
-
const after = line.slice(cursor);
|
|
590
|
-
let insert = `@${suggestion.value}`;
|
|
591
|
-
let newCursor = before.length + insert.length;
|
|
592
|
-
if (!suggestion.isDir) {
|
|
593
|
-
// Completed a file — add a trailing space and close the menu so the
|
|
594
|
-
// user can keep typing their request.
|
|
595
|
-
insert += " ";
|
|
596
|
-
newCursor = before.length + insert.length;
|
|
597
|
-
mentionDismissed = true;
|
|
598
|
-
}
|
|
599
|
-
else {
|
|
600
|
-
// Completed a directory — keep the menu open so the user drills in.
|
|
601
|
-
mentionDismissed = false;
|
|
602
|
-
}
|
|
603
|
-
line = before + insert + after;
|
|
604
|
-
cursor = newCursor;
|
|
605
|
-
selectedIndex = 0;
|
|
606
|
-
menuNavigated = false;
|
|
607
|
-
refresh();
|
|
608
|
-
};
|
|
609
|
-
const refresh = () => {
|
|
610
|
-
const cols = terminalColumns();
|
|
611
|
-
const menu = getMenuState();
|
|
612
|
-
const mention = menu.visible
|
|
613
|
-
? {
|
|
614
|
-
visible: false,
|
|
615
|
-
query: "",
|
|
616
|
-
start: 0,
|
|
617
|
-
suggestions: [],
|
|
618
|
-
}
|
|
619
|
-
: getMentionState();
|
|
620
|
-
const menuLines = menu.visible
|
|
621
|
-
? renderSlashCommandMenu(line, menu.suggestions, selectedIndex)
|
|
622
|
-
: mention.visible
|
|
623
|
-
? renderFileMentionMenu(mention.query, mention.suggestions, selectedIndex)
|
|
624
|
-
: [];
|
|
625
|
-
const promptRows = buildPromptRows(line, cols, true);
|
|
626
|
-
const target = promptCursorPosition(line, cursor, cols);
|
|
627
|
-
const blockRows = [...promptRows, ...menuLines];
|
|
628
|
-
// Always redraw the whole prompt block from its anchor. Partial row
|
|
629
|
-
// clearing is fragile once terminal autowrap, slash menus, and cursor
|
|
630
|
-
// movement mix; clearing to the end of screen leaves no stale wrapped
|
|
631
|
-
// prompt/menu rows behind and keeps the cursor anchor stable.
|
|
632
|
-
if (promptCursorRow > 0) {
|
|
633
|
-
moveCursor(output, 0, -promptCursorRow);
|
|
634
|
-
}
|
|
635
|
-
cursorTo(output, 0);
|
|
636
|
-
output.write("\x1b[J");
|
|
637
|
-
output.write(blockRows.join("\n"));
|
|
638
|
-
const currentRow = Math.max(0, blockRows.length - 1);
|
|
639
|
-
const rowDelta = target.row - currentRow;
|
|
640
|
-
if (rowDelta !== 0) {
|
|
641
|
-
moveCursor(output, 0, rowDelta);
|
|
642
|
-
}
|
|
643
|
-
cursorTo(output, target.col);
|
|
644
|
-
promptCursorRow = target.row;
|
|
645
|
-
};
|
|
646
|
-
const editLine = (nextLine, nextCursor) => {
|
|
647
|
-
line = nextLine;
|
|
648
|
-
cursor = Math.max(0, Math.min(nextCursor, line.length));
|
|
649
|
-
selectedIndex = 0;
|
|
650
|
-
menuNavigated = false;
|
|
651
|
-
dismissedSlashLine = null;
|
|
652
|
-
mentionDismissed = false;
|
|
653
|
-
historyIndex = null;
|
|
654
|
-
refresh();
|
|
655
|
-
};
|
|
656
|
-
// Insert literal text (e.g. a multi-line paste) at the cursor, preserving
|
|
657
|
-
// newlines so the full pasted prompt is kept and later submitted intact.
|
|
658
|
-
const insertText = (text) => {
|
|
659
|
-
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
660
|
-
if (!normalized)
|
|
661
|
-
return;
|
|
662
|
-
line = line.slice(0, cursor) + normalized + line.slice(cursor);
|
|
663
|
-
cursor += normalized.length;
|
|
664
|
-
selectedIndex = 0;
|
|
665
|
-
menuNavigated = false;
|
|
666
|
-
dismissedSlashLine = null;
|
|
667
|
-
mentionDismissed = false;
|
|
668
|
-
historyIndex = null;
|
|
669
|
-
refresh();
|
|
670
|
-
};
|
|
671
|
-
const cleanup = (restoreInput = false) => {
|
|
672
|
-
input.off("keypress", handleKeypress);
|
|
673
|
-
// Leave bracketed-paste mode so the terminal isn't left in a special
|
|
674
|
-
// state during tool execution or after exit.
|
|
675
|
-
if (input.isTTY)
|
|
676
|
-
output.write("\x1b[?2004l");
|
|
677
|
-
if (restoreInput) {
|
|
678
|
-
input.pause();
|
|
679
|
-
if (input.isTTY)
|
|
680
|
-
input.setRawMode(false);
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
const clearPromptDisplay = () => {
|
|
684
|
-
// Move back to prompt start
|
|
685
|
-
if (promptCursorRow > 0) {
|
|
686
|
-
moveCursor(output, 0, -promptCursorRow);
|
|
687
|
-
}
|
|
688
|
-
cursorTo(output, 0);
|
|
689
|
-
output.write("\x1b[J");
|
|
690
|
-
promptCursorRow = 0;
|
|
691
|
-
};
|
|
692
|
-
const submit = (submittedLine) => {
|
|
693
|
-
line = submittedLine;
|
|
694
|
-
cursor = line.length;
|
|
695
|
-
// Move back to prompt start
|
|
696
|
-
if (promptCursorRow > 0) {
|
|
697
|
-
moveCursor(output, 0, -promptCursorRow);
|
|
698
|
-
}
|
|
699
|
-
cursorTo(output, 0);
|
|
700
|
-
output.write("\x1b[J");
|
|
701
|
-
// Write final prompt using the same wrapping rules as refresh(), then
|
|
702
|
-
// move to the next line. Do not include the extra cursor-only row used
|
|
703
|
-
// for interactive editing at exact terminal-width boundaries.
|
|
704
|
-
output.write(buildPromptRows(line, terminalColumns(), false).join("\n"));
|
|
705
|
-
output.write("\n");
|
|
706
|
-
promptCursorRow = 0;
|
|
707
|
-
cleanup();
|
|
708
|
-
resolve(submittedLine);
|
|
709
|
-
};
|
|
710
|
-
function handleKeypress(sequence, key) {
|
|
711
|
-
// The alt-screen pager owns the terminal while open; ignore everything
|
|
712
|
-
// here so the user's navigation keys don't bleed into the input line.
|
|
713
|
-
if (isPagerActive())
|
|
714
|
-
return;
|
|
715
|
-
// ── Bracketed paste ───────────────────────────────────────────────
|
|
716
|
-
// The terminal brackets a paste with paste-start / paste-end markers.
|
|
717
|
-
// Buffer everything in between (including embedded newlines) and insert
|
|
718
|
-
// it literally, so a long multi-line prompt is captured in full and the
|
|
719
|
-
// first newline does not submit early.
|
|
720
|
-
if (key?.name === "paste-start") {
|
|
721
|
-
pasting = true;
|
|
722
|
-
pasteBuffer = "";
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
if (key?.name === "paste-end") {
|
|
726
|
-
pasting = false;
|
|
727
|
-
const pasted = pasteBuffer;
|
|
728
|
-
pasteBuffer = "";
|
|
729
|
-
if (pasted)
|
|
730
|
-
insertText(pasted);
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (pasting) {
|
|
734
|
-
if (key?.name === "return" || key?.name === "enter") {
|
|
735
|
-
pasteBuffer += "\n";
|
|
736
|
-
}
|
|
737
|
-
else if (key?.name === "tab") {
|
|
738
|
-
pasteBuffer += "\t";
|
|
739
|
-
}
|
|
740
|
-
else if (isPrintableSequence(sequence)) {
|
|
741
|
-
pasteBuffer += sequence;
|
|
742
|
-
}
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
const menu = getMenuState();
|
|
746
|
-
const mention = menu.visible
|
|
747
|
-
? {
|
|
748
|
-
visible: false,
|
|
749
|
-
query: "",
|
|
750
|
-
start: 0,
|
|
751
|
-
suggestions: [],
|
|
752
|
-
}
|
|
753
|
-
: getMentionState();
|
|
754
|
-
// Cmd+C on macOS terminals is handled by the OS (it never reaches us),
|
|
755
|
-
// but some Linux terminals forward Meta+C. Treat that as a no-op so
|
|
756
|
-
// selecting + copying never breaks the REPL.
|
|
757
|
-
if (key.meta && !key.ctrl && key.name === "c")
|
|
758
|
-
return;
|
|
759
|
-
if (isCtrlC(key)) {
|
|
760
|
-
// First press: clear the current line. Second press within 1s: exit.
|
|
761
|
-
// This mirrors bash / Claude Code and avoids killing the REPL by
|
|
762
|
-
// accident when users habitually press Ctrl+C to copy in some
|
|
763
|
-
// terminals.
|
|
764
|
-
const now = Date.now();
|
|
765
|
-
if (line.length > 0) {
|
|
766
|
-
editLine("", 0);
|
|
767
|
-
lastCtrlCAt = now;
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
if (now - lastCtrlCAt < 1_000) {
|
|
771
|
-
cleanup(true);
|
|
772
|
-
output.write("\n");
|
|
773
|
-
process.exit(0);
|
|
774
|
-
}
|
|
775
|
-
lastCtrlCAt = now;
|
|
776
|
-
output.write("\n");
|
|
777
|
-
output.write(chalk.dim(" (press Ctrl+C again to exit)\n"));
|
|
778
|
-
output.write(PROMPT);
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
if (isCtrlT(key)) {
|
|
782
|
-
options.onThinkingShortcut();
|
|
783
|
-
refresh();
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
if (isCtrlO(key)) {
|
|
787
|
-
clearPromptDisplay();
|
|
788
|
-
output.write("\n");
|
|
789
|
-
void options.onOutputShortcut().finally(refresh);
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
if (isCtrlP(key)) {
|
|
793
|
-
clearPromptDisplay();
|
|
794
|
-
output.write("\n");
|
|
795
|
-
void options.onPlanShortcut().finally(refresh);
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
if (key.name === "return" || key.name === "enter") {
|
|
799
|
-
if (mention.visible && mention.suggestions.length > 0) {
|
|
800
|
-
applyMention(mention.suggestions[selectedIndex] ?? mention.suggestions[0], mention.start);
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
const useSelection = menu.visible && (line !== "/" || menuNavigated);
|
|
804
|
-
const selectedCommand = useSelection
|
|
805
|
-
? menu.suggestions[selectedIndex]
|
|
806
|
-
: undefined;
|
|
807
|
-
submit(selectedCommand?.command ?? line);
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
if (key.name === "tab") {
|
|
811
|
-
if (mention.visible && mention.suggestions.length > 0) {
|
|
812
|
-
applyMention(mention.suggestions[selectedIndex] ?? mention.suggestions[0], mention.start);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
if (menu.visible && menu.suggestions.length > 0) {
|
|
816
|
-
const target = menu.suggestions[selectedIndex] ?? menu.suggestions[0];
|
|
817
|
-
editLine(target.command, target.command.length);
|
|
818
|
-
}
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
if (isEscape(key)) {
|
|
822
|
-
if (mention.visible) {
|
|
823
|
-
mentionDismissed = true;
|
|
824
|
-
refresh();
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
if (menu.visible) {
|
|
828
|
-
dismissedSlashLine = line;
|
|
829
|
-
refresh();
|
|
830
|
-
}
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
if (key.name === "up") {
|
|
834
|
-
if (mention.visible && mention.suggestions.length > 0) {
|
|
835
|
-
selectedIndex =
|
|
836
|
-
(selectedIndex - 1 + mention.suggestions.length) %
|
|
837
|
-
mention.suggestions.length;
|
|
838
|
-
menuNavigated = true;
|
|
839
|
-
refresh();
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
if (menu.visible && menu.suggestions.length > 0) {
|
|
843
|
-
selectedIndex =
|
|
844
|
-
(selectedIndex - 1 + menu.suggestions.length) %
|
|
845
|
-
menu.suggestions.length;
|
|
846
|
-
menuNavigated = true;
|
|
847
|
-
refresh();
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
if (options.history.length > 0) {
|
|
851
|
-
if (historyIndex === null) {
|
|
852
|
-
historyDraft = line;
|
|
853
|
-
historyIndex = options.history.length - 1;
|
|
854
|
-
}
|
|
855
|
-
else {
|
|
856
|
-
historyIndex = Math.max(0, historyIndex - 1);
|
|
857
|
-
}
|
|
858
|
-
line = options.history[historyIndex] ?? "";
|
|
859
|
-
cursor = line.length;
|
|
860
|
-
selectedIndex = 0;
|
|
861
|
-
dismissedSlashLine = null;
|
|
862
|
-
refresh();
|
|
863
|
-
}
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
if (key.name === "down") {
|
|
867
|
-
if (mention.visible && mention.suggestions.length > 0) {
|
|
868
|
-
selectedIndex = (selectedIndex + 1) % mention.suggestions.length;
|
|
869
|
-
menuNavigated = true;
|
|
870
|
-
refresh();
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
if (menu.visible && menu.suggestions.length > 0) {
|
|
874
|
-
selectedIndex = (selectedIndex + 1) % menu.suggestions.length;
|
|
875
|
-
menuNavigated = true;
|
|
876
|
-
refresh();
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
if (historyIndex !== null) {
|
|
880
|
-
if (historyIndex < options.history.length - 1) {
|
|
881
|
-
historyIndex += 1;
|
|
882
|
-
line = options.history[historyIndex] ?? "";
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
historyIndex = null;
|
|
886
|
-
line = historyDraft;
|
|
887
|
-
}
|
|
888
|
-
cursor = line.length;
|
|
889
|
-
selectedIndex = 0;
|
|
890
|
-
dismissedSlashLine = null;
|
|
891
|
-
refresh();
|
|
892
|
-
}
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
if (key.name === "left") {
|
|
896
|
-
if (cursor > 0) {
|
|
897
|
-
cursor -= 1;
|
|
898
|
-
refresh();
|
|
899
|
-
}
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (key.name === "right") {
|
|
903
|
-
if (cursor < line.length) {
|
|
904
|
-
cursor += 1;
|
|
905
|
-
refresh();
|
|
906
|
-
}
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
if (key.name === "home" || (key.ctrl && key.name === "a")) {
|
|
910
|
-
cursor = 0;
|
|
911
|
-
refresh();
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
if (key.name === "end" || (key.ctrl && key.name === "e")) {
|
|
915
|
-
cursor = line.length;
|
|
916
|
-
refresh();
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (key.name === "backspace") {
|
|
920
|
-
if (cursor > 0) {
|
|
921
|
-
editLine(line.slice(0, cursor - 1) + line.slice(cursor), cursor - 1);
|
|
922
|
-
}
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
if (key.name === "delete") {
|
|
926
|
-
if (cursor < line.length) {
|
|
927
|
-
editLine(line.slice(0, cursor) + line.slice(cursor + 1), cursor);
|
|
928
|
-
}
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
if (key.ctrl && key.name === "u") {
|
|
932
|
-
editLine(line.slice(cursor), 0);
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
if (key.ctrl && key.name === "k") {
|
|
936
|
-
editLine(line.slice(0, cursor), cursor);
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
if (isPrintableSequence(sequence) && !key.ctrl && !key.meta) {
|
|
940
|
-
editLine(line.slice(0, cursor) + sequence + line.slice(cursor), cursor + sequence.length);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
output.write(PROMPT);
|
|
944
|
-
if (input.isTTY) {
|
|
945
|
-
input.setRawMode(true);
|
|
946
|
-
// Enable bracketed paste so multi-line pastes arrive as one chunk
|
|
947
|
-
// (wrapped in paste-start/paste-end) instead of submitting at the
|
|
948
|
-
// first embedded newline.
|
|
949
|
-
output.write("\x1b[?2004h");
|
|
950
|
-
}
|
|
951
|
-
input.resume();
|
|
952
|
-
input.on("keypress", handleKeypress);
|
|
953
|
-
});
|
|
954
99
|
}
|
|
955
100
|
// Build a concise, width-bounded spinner label for ask-mode research so the
|
|
956
101
|
// user sees what's being searched/fetched without the line overflowing.
|
|
@@ -2051,16 +1196,15 @@ async function handleSlash(line, state) {
|
|
|
2051
1196
|
clearThinking();
|
|
2052
1197
|
// Clear the entire screen and move cursor to top
|
|
2053
1198
|
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2054
|
-
// Re-render the startup
|
|
2055
|
-
console.log(
|
|
2056
|
-
|
|
1199
|
+
// Re-render the startup intro card
|
|
1200
|
+
console.log(renderIntroCard({
|
|
1201
|
+
version: getCurrentVersion(),
|
|
2057
1202
|
workdir: safeCwd(),
|
|
2058
1203
|
model: state.model,
|
|
2059
1204
|
provider: state.provider,
|
|
2060
1205
|
mode: state.mode,
|
|
2061
1206
|
}));
|
|
2062
|
-
console.log(
|
|
2063
|
-
console.log(chalk.dim(" ESC abort │ Ctrl+C clears input │ @ to attach files │ Ctrl+T thinking │ Ctrl+O tool output │ Ctrl+P plan (q to close)\n"));
|
|
1207
|
+
console.log();
|
|
2064
1208
|
return true;
|
|
2065
1209
|
}
|
|
2066
1210
|
case "/update":
|
|
@@ -2111,7 +1255,7 @@ export async function startRepl(options = {}) {
|
|
|
2111
1255
|
};
|
|
2112
1256
|
process.on("unhandledRejection", handleUnhandledRejection);
|
|
2113
1257
|
process.on("uncaughtException", handleUncaughtException);
|
|
2114
|
-
//
|
|
1258
|
+
// ESC / Ctrl+C abort; Ctrl+T toggles hidden thinking
|
|
2115
1259
|
if (process.stdin.isTTY) {
|
|
2116
1260
|
process.stdin.setRawMode(false);
|
|
2117
1261
|
}
|
|
@@ -2243,16 +1387,15 @@ export async function startRepl(options = {}) {
|
|
|
2243
1387
|
console.log(chalk.dim("\n (press Ctrl+C again to exit)"));
|
|
2244
1388
|
};
|
|
2245
1389
|
process.on("SIGINT", handleSigint);
|
|
2246
|
-
//
|
|
2247
|
-
console.log(
|
|
2248
|
-
|
|
1390
|
+
// Startup intro card
|
|
1391
|
+
console.log(renderIntroCard({
|
|
1392
|
+
version: getCurrentVersion(),
|
|
2249
1393
|
workdir: safeCwd(),
|
|
2250
1394
|
model: state.model,
|
|
2251
1395
|
provider: state.provider,
|
|
2252
1396
|
mode: state.mode,
|
|
2253
1397
|
}));
|
|
2254
|
-
console.log(
|
|
2255
|
-
console.log(chalk.dim(" ESC abort │ Ctrl+C clears input │ @ to attach files │ Ctrl+T thinking │ Ctrl+O tool output │ Ctrl+P plan (q to close)\n"));
|
|
1398
|
+
console.log();
|
|
2256
1399
|
// Hint thinking-capable users that the toggle exists. We default it to
|
|
2257
1400
|
// off for speed, since on NIM many models route through a much slower
|
|
2258
1401
|
// chat-template path when reasoning is enabled.
|
|
@@ -2279,7 +1422,7 @@ export async function startRepl(options = {}) {
|
|
|
2279
1422
|
isReadingPrompt = false;
|
|
2280
1423
|
if (!line)
|
|
2281
1424
|
continue;
|
|
2282
|
-
//
|
|
1425
|
+
// /implement — approve the active plan and execute it
|
|
2283
1426
|
// Handled here (not in handleSlash) because it must trigger a full
|
|
2284
1427
|
// agent run with the plan marked approved, not just print something.
|
|
2285
1428
|
let implementApproved = false;
|
|
@@ -2310,7 +1453,7 @@ export async function startRepl(options = {}) {
|
|
|
2310
1453
|
"Do NOT call web.search — you already know everything needed. " +
|
|
2311
1454
|
"Run real commands (installs, servers, verification) — do not claim anything ran without a successful tool call.";
|
|
2312
1455
|
}
|
|
2313
|
-
//
|
|
1456
|
+
// Continuation detection
|
|
2314
1457
|
// When the user types "continue" after the agent paused at a step limit,
|
|
2315
1458
|
// augment the bare keyword with rich resumption context so the model
|
|
2316
1459
|
// picks up from where it left off instead of restarting.
|