ideacode 1.0.0

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/repl.js ADDED
@@ -0,0 +1,897 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Main REPL UI: input, log viewport, slash/@ suggestions, modals (model picker, palette), API loop and tool dispatch.
4
+ */
5
+ import { useState, useCallback, useRef, useMemo, useEffect } from "react";
6
+ import { Box, Text, useInput, useStdout } from "ink";
7
+ import { globSync } from "glob";
8
+ import * as path from "node:path";
9
+ import gradient from "gradient-string";
10
+ // Custom matcha-themed gradient: matcha green → dark sepia
11
+ const matchaGradient = gradient(["#7F9A65", "#5C4033"]);
12
+ import { getModel, saveModel, saveBraveSearchApiKey, getBraveSearchApiKey } from "./config.js";
13
+ import { callApi, fetchModels } from "./api.js";
14
+ import { estimateTokens, ensureUnderBudget } from "./context.js";
15
+ import { runTool } from "./tools/index.js";
16
+ import { COMMANDS, matchCommand, resolveCommand } from "./commands.js";
17
+ import { colors, icons, separator, agentMessage, toolCallBox, toolResultLine, inkColors, } from "./ui/index.js";
18
+ function wordStartBackward(value, cursor) {
19
+ let i = cursor - 1;
20
+ while (i >= 0 && /\s/.test(value[i]))
21
+ i--;
22
+ while (i >= 0 && /[\w]/.test(value[i]))
23
+ i--;
24
+ return i + 1;
25
+ }
26
+ function wordEndForward(value, cursor) {
27
+ let i = cursor;
28
+ while (i < value.length && /[\w]/.test(value[i]))
29
+ i++;
30
+ return i;
31
+ }
32
+ const CONTEXT_WINDOW_K = 128;
33
+ const MAX_AT_SUGGESTIONS = 12;
34
+ const INITIAL_BANNER_LINES = 12;
35
+ const isMac = process.platform === "darwin";
36
+ const pasteShortcut = isMac ? "Cmd+V" : "Ctrl+V";
37
+ function listFilesWithFilter(cwd, filter) {
38
+ try {
39
+ const pattern = path.join(cwd, "**", "*").replace(/\\/g, "/");
40
+ const files = globSync(pattern, { cwd, nodir: true, dot: true });
41
+ const rel = files.map((f) => path.relative(cwd, f).replace(/\\/g, "/"));
42
+ const f = filter.toLowerCase();
43
+ if (!f)
44
+ return rel.slice(0, MAX_AT_SUGGESTIONS);
45
+ return rel.filter((p) => p.toLowerCase().includes(f)).slice(0, MAX_AT_SUGGESTIONS);
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ function parseAtSegments(value) {
52
+ const segments = [];
53
+ let pos = 0;
54
+ while (pos < value.length) {
55
+ const at = value.indexOf("@", pos);
56
+ if (at === -1) {
57
+ segments.push({ type: "normal", text: value.slice(pos) });
58
+ break;
59
+ }
60
+ segments.push({ type: "normal", text: value.slice(pos, at) });
61
+ let end = at + 1;
62
+ while (end < value.length && value[end] !== " " && value[end] !== "\n")
63
+ end++;
64
+ segments.push({ type: "path", text: value.slice(at, end) });
65
+ pos = end;
66
+ }
67
+ return segments;
68
+ }
69
+ const PROMPT_INDENT_LEN = 2;
70
+ function wrapLine(line, width) {
71
+ if (width < 1)
72
+ return [line];
73
+ const out = [];
74
+ for (let i = 0; i < line.length; i += width) {
75
+ out.push(line.slice(i, i + width));
76
+ }
77
+ return out.length > 0 ? out : [""];
78
+ }
79
+ function useTerminalSize() {
80
+ const { stdout } = useStdout();
81
+ const [size, setSize] = useState(() => ({
82
+ rows: stdout?.rows ?? 24,
83
+ columns: stdout?.columns ?? 80,
84
+ }));
85
+ useEffect(() => {
86
+ if (!stdout?.isTTY)
87
+ return;
88
+ const onResize = () => setSize({ rows: stdout.rows ?? 24, columns: stdout.columns ?? 80 });
89
+ stdout.on("resize", onResize);
90
+ onResize();
91
+ return () => {
92
+ stdout.off("resize", onResize);
93
+ };
94
+ }, [stdout]);
95
+ return size;
96
+ }
97
+ export function Repl({ apiKey, cwd, onQuit }) {
98
+ const { rows: termRows, columns: termColumns } = useTerminalSize();
99
+ // Big ASCII art logo for ideacode
100
+ const bigLogo = `
101
+ ██╗██████╗ ███████╗ █████╗ ██████╗ ██████╗ ██████╗ ███████╗
102
+ ██║██╔══██╗██╔════╝██╔══██╗██╔════╝██╔═══██╗██╔══██╗██╔════╝
103
+ ██║██║ ██║█████╗ ███████║██║ ██║ ██║██║ ██║█████╗
104
+ ██║██║ ██║██╔══╝ ██╔══██║██║ ██║ ██║██║ ██║██╔══╝
105
+ ██║██████╔╝███████╗██║ ██║╚██████╗╚██████╔╝██████╔╝███████╗
106
+ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
107
+ `;
108
+ const [logLines, setLogLines] = useState(() => {
109
+ const model = getModel();
110
+ return [
111
+ "",
112
+ matchaGradient(bigLogo),
113
+ colors.accent(` ${model}`) + colors.dim(" · ") + colors.accentPale("OpenRouter") + colors.dim(` · ${cwd}`),
114
+ colors.mutedDark(" / commands ! shell @ files · Ctrl+P palette · Ctrl+C or /q to quit"),
115
+ "",
116
+ ];
117
+ });
118
+ const [inputValue, setInputValue] = useState("");
119
+ const [currentModel, setCurrentModel] = useState(getModel);
120
+ const [messages, setMessages] = useState([]);
121
+ const [loading, setLoading] = useState(false);
122
+ const [showPalette, setShowPalette] = useState(false);
123
+ const [paletteIndex, setPaletteIndex] = useState(0);
124
+ const [showModelSelector, setShowModelSelector] = useState(false);
125
+ const [modelList, setModelList] = useState([]);
126
+ const [modelIndex, setModelIndex] = useState(0);
127
+ const [modelSearchFilter, setModelSearchFilter] = useState("");
128
+ const [showBraveKeyModal, setShowBraveKeyModal] = useState(false);
129
+ const [braveKeyInput, setBraveKeyInput] = useState("");
130
+ const [showHelpModal, setShowHelpModal] = useState(false);
131
+ const [slashSuggestionIndex, setSlashSuggestionIndex] = useState(0);
132
+ const [inputCursor, setInputCursor] = useState(0);
133
+ const skipNextSubmitRef = useRef(false);
134
+ const queuedMessageRef = useRef(null);
135
+ const lastUserMessageRef = useRef("");
136
+ const [lastUserPrompt, setLastUserPrompt] = useState("");
137
+ const [logScrollOffset, setLogScrollOffset] = useState(0);
138
+ const prevEscRef = useRef(false);
139
+ const [spinnerTick, setSpinnerTick] = useState(0);
140
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
141
+ const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
142
+ const contextWindowK = useMemo(() => {
143
+ const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
144
+ return ctx != null ? Math.round(ctx / 1024) : CONTEXT_WINDOW_K;
145
+ }, [modelList, currentModel]);
146
+ const tokenDisplay = `${Math.round(estimatedTokens / 1000)}K / ${contextWindowK}K`;
147
+ const filteredModelList = useMemo(() => {
148
+ const q = modelSearchFilter.trim().toLowerCase();
149
+ if (!q)
150
+ return modelList;
151
+ return modelList.filter((m) => m.id.toLowerCase().includes(q) ||
152
+ (m.name ?? "").toLowerCase().includes(q));
153
+ }, [modelList, modelSearchFilter]);
154
+ useEffect(() => {
155
+ setInputCursor((c) => Math.min(c, Math.max(0, inputValue.length)));
156
+ }, [inputValue.length]);
157
+ useEffect(() => {
158
+ if (apiKey)
159
+ fetchModels(apiKey).then(setModelList);
160
+ }, [apiKey]);
161
+ useEffect(() => {
162
+ if (showModelSelector) {
163
+ setModelSearchFilter("");
164
+ setModelIndex(0);
165
+ }
166
+ }, [showModelSelector]);
167
+ useEffect(() => {
168
+ if (showModelSelector && filteredModelList.length > 0)
169
+ setModelIndex((i) => Math.min(i, filteredModelList.length - 1));
170
+ }, [showModelSelector, filteredModelList.length]);
171
+ useEffect(() => {
172
+ if (!loading)
173
+ return;
174
+ const t = setInterval(() => setSpinnerTick((n) => n + 1), 80);
175
+ return () => clearInterval(t);
176
+ }, [loading]);
177
+ const showSlashSuggestions = inputValue.startsWith("/");
178
+ const filteredSlashCommands = useMemo(() => {
179
+ const filter = inputValue.slice(1).trim();
180
+ return COMMANDS.filter((c) => matchCommand(filter, c));
181
+ }, [inputValue]);
182
+ const clampedSlashIndex = Math.min(Math.max(0, slashSuggestionIndex), Math.max(0, filteredSlashCommands.length - 1));
183
+ useEffect(() => {
184
+ setSlashSuggestionIndex(0);
185
+ }, [inputValue]);
186
+ const lastAtIndex = inputValue.lastIndexOf("@");
187
+ const atPathEnd = lastAtIndex < 0
188
+ ? -1
189
+ : (() => {
190
+ let end = lastAtIndex + 1;
191
+ while (end < inputValue.length && inputValue[end] !== " " && inputValue[end] !== "\n")
192
+ end++;
193
+ return end;
194
+ })();
195
+ const cursorInAtSegment = lastAtIndex >= 0 &&
196
+ inputCursor >= lastAtIndex &&
197
+ inputCursor <= atPathEnd;
198
+ const hasCharsAfterAt = atPathEnd > lastAtIndex + 1;
199
+ const atFilter = cursorInAtSegment ? inputValue.slice(lastAtIndex + 1, inputCursor) : "";
200
+ const lastAtPath = lastAtIndex >= 0 && atPathEnd > lastAtIndex
201
+ ? inputValue.slice(lastAtIndex + 1, atPathEnd).trim()
202
+ : "";
203
+ const filteredFilePaths = useMemo(() => (cursorInAtSegment ? listFilesWithFilter(cwd, atFilter) : []), [cwd, cursorInAtSegment, atFilter]);
204
+ const lastAtPathMatches = useMemo(() => lastAtPath.length > 0 ? listFilesWithFilter(cwd, lastAtPath).includes(lastAtPath) : false, [cwd, lastAtPath]);
205
+ const showAtSuggestions = cursorInAtSegment && hasCharsAfterAt && filteredFilePaths.length > 0;
206
+ const [atSuggestionIndex, setAtSuggestionIndex] = useState(0);
207
+ const clampedAtFileIndex = Math.min(Math.max(0, atSuggestionIndex), Math.max(0, filteredFilePaths.length - 1));
208
+ useEffect(() => {
209
+ setAtSuggestionIndex(0);
210
+ }, [atFilter]);
211
+ const appendLog = useCallback((line) => {
212
+ const lines = line.split("\n");
213
+ if (lines.length > 1 && lines[0] === "")
214
+ lines.shift();
215
+ setLogLines((prev) => [...prev, ...lines]);
216
+ }, []);
217
+ const braveKeyHadExistingRef = useRef(false);
218
+ const BRAVE_KEY_PLACEHOLDER = "••••••••";
219
+ const openBraveKeyModal = useCallback(() => {
220
+ const existing = getBraveSearchApiKey();
221
+ braveKeyHadExistingRef.current = !!existing;
222
+ setBraveKeyInput(existing ? BRAVE_KEY_PLACEHOLDER : "");
223
+ setShowBraveKeyModal(true);
224
+ }, []);
225
+ const openHelpModal = useCallback(() => setShowHelpModal(true), []);
226
+ const openModelSelector = useCallback(async () => {
227
+ setShowPalette(false);
228
+ const models = await fetchModels(apiKey);
229
+ setModelList(models);
230
+ setModelIndex(0);
231
+ setShowModelSelector(true);
232
+ }, [apiKey]);
233
+ const processInput = useCallback(async (value) => {
234
+ const userInput = value.trim();
235
+ if (!userInput)
236
+ return true;
237
+ const isShellCommand = userInput[0] === "!" || userInput[0] === "\uFF01";
238
+ if (isShellCommand) {
239
+ const cmd = userInput.slice(1).trim();
240
+ if (!cmd)
241
+ return true;
242
+ appendLog(separator());
243
+ appendLog(colors.accent(icons.prompt) + " ! " + cmd);
244
+ appendLog(separator());
245
+ const output = await runTool("bash", { cmd });
246
+ const maxShellOutput = 2000;
247
+ const outPreview = output.length > maxShellOutput
248
+ ? output.slice(0, maxShellOutput) + "\n... (" + (output.length - maxShellOutput) + " more chars)"
249
+ : output;
250
+ for (const line of outPreview.split("\n")) {
251
+ appendLog(toolResultLine(line));
252
+ }
253
+ appendLog("");
254
+ setMessages((prev) => [
255
+ ...prev,
256
+ { role: "user", content: userInput + "\n---\n" + output },
257
+ ]);
258
+ return true;
259
+ }
260
+ const canonical = resolveCommand(userInput);
261
+ if (canonical === "/q")
262
+ return false;
263
+ if (canonical === "/clear") {
264
+ setMessages([]);
265
+ setLogScrollOffset(0);
266
+ appendLog(colors.success(`${icons.clear} Cleared conversation`));
267
+ appendLog("");
268
+ return true;
269
+ }
270
+ if (canonical === "/palette" || userInput === "/") {
271
+ setShowPalette(true);
272
+ return true;
273
+ }
274
+ if (canonical === "/models") {
275
+ await openModelSelector();
276
+ return true;
277
+ }
278
+ if (canonical === "/brave") {
279
+ openBraveKeyModal();
280
+ return true;
281
+ }
282
+ if (canonical === "/help") {
283
+ openHelpModal();
284
+ return true;
285
+ }
286
+ if (canonical === "/status") {
287
+ appendLog(colors.muted(` ${currentModel}`) +
288
+ colors.dim(" · ") +
289
+ colors.accent(cwd) +
290
+ colors.dim(` · ${messages.length} messages`));
291
+ appendLog("");
292
+ return true;
293
+ }
294
+ if (messages.length === 0) {
295
+ setLogLines([]);
296
+ setLogScrollOffset(0);
297
+ }
298
+ lastUserMessageRef.current = userInput;
299
+ setLastUserPrompt(userInput);
300
+ let state = [...messages, { role: "user", content: userInput }];
301
+ const systemPrompt = `Concise coding assistant. cwd: ${cwd}. Use focused greps (specific patterns, narrow paths) and read in chunks when files are large; avoid one huge grep or read that floods context. When exploring a dependency, set path to that package (e.g. node_modules/<pkg>) and list/read only what you need.`;
302
+ const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
303
+ const maxContextTokens = Math.floor((modelContext ?? CONTEXT_WINDOW_K * 1024) * 0.85);
304
+ const stateBeforeCompress = state;
305
+ state = await ensureUnderBudget(apiKey, state, systemPrompt, currentModel, {
306
+ maxTokens: maxContextTokens,
307
+ keepLast: 6,
308
+ });
309
+ if (state.length < stateBeforeCompress.length) {
310
+ appendLog(colors.muted(" (context compressed to stay under limit)\n"));
311
+ }
312
+ for (;;) {
313
+ setLoading(true);
314
+ const response = await callApi(apiKey, state, systemPrompt, currentModel);
315
+ setLoading(false);
316
+ const contentBlocks = response.content ?? [];
317
+ const toolResults = [];
318
+ for (let bi = 0; bi < contentBlocks.length; bi++) {
319
+ const block = contentBlocks[bi];
320
+ if (block.type === "text" && block.text) {
321
+ appendLog("");
322
+ appendLog(agentMessage(block.text).trimEnd());
323
+ }
324
+ if (block.type === "tool_use" && block.name && block.input) {
325
+ const toolName = block.name;
326
+ const toolArgs = block.input;
327
+ const firstVal = Object.values(toolArgs)[0];
328
+ const argPreview = String(firstVal ?? "").slice(0, 50);
329
+ const result = await runTool(toolName, toolArgs);
330
+ const ok = !result.startsWith("error:");
331
+ appendLog(toolCallBox(toolName, argPreview, ok));
332
+ const resultLines = result.split("\n");
333
+ let preview = resultLines[0]?.slice(0, 60) ?? "";
334
+ if (resultLines.length > 1)
335
+ preview += ` ... +${resultLines.length - 1} lines`;
336
+ else if (preview.length > 60)
337
+ preview += "...";
338
+ appendLog(toolResultLine(preview, ok));
339
+ if (block.id) {
340
+ toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result });
341
+ }
342
+ }
343
+ }
344
+ state = [...state, { role: "assistant", content: contentBlocks }];
345
+ if (toolResults.length === 0) {
346
+ setMessages(state);
347
+ break;
348
+ }
349
+ state = [...state, { role: "user", content: toolResults }];
350
+ setMessages(state);
351
+ }
352
+ return true;
353
+ }, [apiKey, cwd, currentModel, messages, modelList, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
354
+ const handleSubmit = useCallback(async (value) => {
355
+ if (skipNextSubmitRef.current) {
356
+ skipNextSubmitRef.current = false;
357
+ return;
358
+ }
359
+ const trimmed = value.trim();
360
+ if (trimmed === "/models") {
361
+ setInputValue("");
362
+ setInputCursor(0);
363
+ openModelSelector();
364
+ return;
365
+ }
366
+ if (trimmed === "/brave") {
367
+ setInputValue("");
368
+ setInputCursor(0);
369
+ openBraveKeyModal();
370
+ return;
371
+ }
372
+ if (trimmed === "/help" || trimmed === "/?") {
373
+ setInputValue("");
374
+ setInputCursor(0);
375
+ openHelpModal();
376
+ return;
377
+ }
378
+ setInputValue("");
379
+ setInputCursor(0);
380
+ try {
381
+ const cont = await processInput(value);
382
+ if (!cont) {
383
+ onQuit();
384
+ return;
385
+ }
386
+ const queued = queuedMessageRef.current;
387
+ if (queued) {
388
+ queuedMessageRef.current = null;
389
+ await processInput(queued);
390
+ }
391
+ }
392
+ catch (err) {
393
+ appendLog(colors.error(`${icons.error} ${err instanceof Error ? err.message : String(err)}`));
394
+ appendLog("");
395
+ }
396
+ }, [processInput, onQuit, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
397
+ useInput((input, key) => {
398
+ if (showHelpModal) {
399
+ setShowHelpModal(false);
400
+ return;
401
+ }
402
+ if (showBraveKeyModal) {
403
+ if (key.return) {
404
+ const isPlaceholder = braveKeyInput === BRAVE_KEY_PLACEHOLDER;
405
+ const isEmpty = !braveKeyInput.trim();
406
+ const unchanged = isPlaceholder || (braveKeyHadExistingRef.current && isEmpty);
407
+ const keyToSave = unchanged ? "" : braveKeyInput.trim();
408
+ if (keyToSave)
409
+ saveBraveSearchApiKey(keyToSave);
410
+ setShowBraveKeyModal(false);
411
+ setBraveKeyInput("");
412
+ appendLog(keyToSave ? colors.success("Brave Search API key saved. web_search is now available.") : colors.muted("Brave key unchanged."));
413
+ appendLog("");
414
+ }
415
+ else if (key.escape) {
416
+ setShowBraveKeyModal(false);
417
+ setBraveKeyInput("");
418
+ }
419
+ else if (key.backspace || key.delete) {
420
+ setBraveKeyInput((prev) => (prev === BRAVE_KEY_PLACEHOLDER ? "" : prev.slice(0, -1)));
421
+ }
422
+ else if (input && !key.ctrl && !key.meta && input !== "\b" && input !== "\x7f") {
423
+ setBraveKeyInput((prev) => (prev === BRAVE_KEY_PLACEHOLDER ? input : prev + input));
424
+ }
425
+ return;
426
+ }
427
+ if (showModelSelector) {
428
+ if (key.upArrow)
429
+ setModelIndex((i) => Math.max(0, i - 1));
430
+ else if (key.downArrow)
431
+ setModelIndex((i) => Math.min(filteredModelList.length - 1, i + 1));
432
+ else if (key.return) {
433
+ const selected = filteredModelList[modelIndex]?.id;
434
+ if (selected) {
435
+ saveModel(selected);
436
+ setCurrentModel(selected);
437
+ appendLog(colors.success(`Model set to ${selected}`));
438
+ appendLog("");
439
+ }
440
+ setShowModelSelector(false);
441
+ }
442
+ else if (key.escape)
443
+ setShowModelSelector(false);
444
+ else if (key.backspace || key.delete) {
445
+ setModelSearchFilter((prev) => prev.slice(0, -1));
446
+ }
447
+ else if (input && !key.ctrl && !key.meta && input !== "\b" && input !== "\x7f") {
448
+ setModelSearchFilter((prev) => prev + input);
449
+ setModelIndex(0);
450
+ }
451
+ return;
452
+ }
453
+ if (showPalette) {
454
+ const paletteCount = COMMANDS.length + 1;
455
+ if (key.upArrow)
456
+ setPaletteIndex((i) => Math.max(0, i - 1));
457
+ else if (key.downArrow)
458
+ setPaletteIndex((i) => Math.min(paletteCount - 1, i + 1));
459
+ else if (key.return) {
460
+ if (paletteIndex < COMMANDS.length) {
461
+ const selected = COMMANDS[paletteIndex];
462
+ if (selected) {
463
+ setShowPalette(false);
464
+ processInput(selected.cmd).then((cont) => {
465
+ if (!cont)
466
+ onQuit();
467
+ }).catch((err) => {
468
+ appendLog(colors.error(`${icons.error} ${err instanceof Error ? err.message : String(err)}`));
469
+ appendLog("");
470
+ });
471
+ }
472
+ }
473
+ setShowPalette(false);
474
+ return;
475
+ }
476
+ else if (key.escape)
477
+ setShowPalette(false);
478
+ return;
479
+ }
480
+ if (showSlashSuggestions && filteredSlashCommands.length > 0) {
481
+ if (key.upArrow) {
482
+ setSlashSuggestionIndex((i) => Math.max(0, i - 1));
483
+ return;
484
+ }
485
+ if (key.downArrow) {
486
+ setSlashSuggestionIndex((i) => Math.min(filteredSlashCommands.length - 1, i + 1));
487
+ return;
488
+ }
489
+ if (key.tab) {
490
+ const selected = filteredSlashCommands[clampedSlashIndex];
491
+ if (selected) {
492
+ setInputValue(selected.cmd);
493
+ setInputCursor(selected.cmd.length);
494
+ }
495
+ return;
496
+ }
497
+ if (key.return) {
498
+ const selected = filteredSlashCommands[clampedSlashIndex];
499
+ if (selected) {
500
+ skipNextSubmitRef.current = true;
501
+ setInputValue("");
502
+ setInputCursor(0);
503
+ if (selected.cmd === "/models") {
504
+ openModelSelector();
505
+ return;
506
+ }
507
+ if (selected.cmd === "/brave") {
508
+ openBraveKeyModal();
509
+ return;
510
+ }
511
+ if (selected.cmd === "/help") {
512
+ openHelpModal();
513
+ return;
514
+ }
515
+ processInput(selected.cmd).then((cont) => {
516
+ if (!cont)
517
+ onQuit();
518
+ }).catch((err) => {
519
+ appendLog(colors.error(`${icons.error} ${err instanceof Error ? err.message : String(err)}`));
520
+ appendLog("");
521
+ });
522
+ return;
523
+ }
524
+ }
525
+ if (key.escape) {
526
+ setInputValue("");
527
+ setInputCursor(0);
528
+ return;
529
+ }
530
+ }
531
+ if (cursorInAtSegment) {
532
+ if (key.escape) {
533
+ setInputValue((prev) => {
534
+ const lastAt = prev.lastIndexOf("@");
535
+ return lastAt >= 0 ? prev.slice(0, lastAt) + prev.slice(inputCursor) : prev;
536
+ });
537
+ setInputCursor(lastAtIndex);
538
+ return;
539
+ }
540
+ if (filteredFilePaths.length > 0) {
541
+ if (key.upArrow) {
542
+ setAtSuggestionIndex((i) => Math.max(0, i - 1));
543
+ return;
544
+ }
545
+ if (key.downArrow) {
546
+ setAtSuggestionIndex((i) => Math.min(filteredFilePaths.length - 1, i + 1));
547
+ return;
548
+ }
549
+ if (key.tab) {
550
+ const selected = filteredFilePaths[clampedAtFileIndex];
551
+ if (selected !== undefined) {
552
+ const cur = inputCursor;
553
+ const lastAt = inputValue.lastIndexOf("@");
554
+ const replacement = "@" + selected;
555
+ setInputValue((prev) => prev.slice(0, lastAt) + replacement + " " + prev.slice(cur));
556
+ setInputCursor(lastAt + replacement.length + 1);
557
+ }
558
+ return;
559
+ }
560
+ }
561
+ }
562
+ if (!showModelSelector && !showPalette) {
563
+ const withModifier = key.ctrl || key.meta || key.shift;
564
+ const scrollUp = key.pageUp ||
565
+ (key.upArrow && (withModifier || !inputValue.trim()));
566
+ const scrollDown = key.pageDown ||
567
+ (key.downArrow && (withModifier || !inputValue.trim()));
568
+ if (scrollUp) {
569
+ setLogScrollOffset((prev) => Math.min(maxLogScrollOffset, prev + logViewportHeight));
570
+ return;
571
+ }
572
+ if (scrollDown) {
573
+ setLogScrollOffset((prev) => Math.max(0, prev - logViewportHeight));
574
+ return;
575
+ }
576
+ if (!key.escape)
577
+ prevEscRef.current = false;
578
+ const len = inputValue.length;
579
+ const cur = inputCursor;
580
+ if (key.tab) {
581
+ if (inputValue.trim()) {
582
+ queuedMessageRef.current = inputValue;
583
+ setInputValue("");
584
+ setInputCursor(0);
585
+ appendLog(colors.muted(" Message queued. Send to run after this turn."));
586
+ appendLog("");
587
+ }
588
+ return;
589
+ }
590
+ if (key.escape) {
591
+ if (inputValue.length === 0) {
592
+ if (prevEscRef.current) {
593
+ prevEscRef.current = false;
594
+ const last = lastUserMessageRef.current;
595
+ if (last) {
596
+ setInputValue(last);
597
+ setInputCursor(last.length);
598
+ setMessages((prev) => (prev.length > 0 && prev[prev.length - 1]?.role === "user" ? prev.slice(0, -1) : prev));
599
+ appendLog(colors.muted(" Editing previous message. Submit to replace."));
600
+ appendLog("");
601
+ }
602
+ }
603
+ else
604
+ prevEscRef.current = true;
605
+ }
606
+ else {
607
+ setInputValue("");
608
+ setInputCursor(0);
609
+ prevEscRef.current = false;
610
+ }
611
+ return;
612
+ }
613
+ if (key.return) {
614
+ handleSubmit(inputValue);
615
+ setInputValue("");
616
+ setInputCursor(0);
617
+ return;
618
+ }
619
+ if (key.ctrl && input === "u") {
620
+ setInputValue((prev) => prev.slice(cur));
621
+ setInputCursor(0);
622
+ return;
623
+ }
624
+ if (key.ctrl && input === "k") {
625
+ setInputValue((prev) => prev.slice(0, cur));
626
+ return;
627
+ }
628
+ const killWordBefore = (key.ctrl && input === "w") ||
629
+ (key.meta && key.backspace) ||
630
+ (key.meta && key.delete && cur > 0);
631
+ if (killWordBefore) {
632
+ const start = wordStartBackward(inputValue, cur);
633
+ if (start < cur) {
634
+ setInputValue((prev) => prev.slice(0, start) + prev.slice(cur));
635
+ setInputCursor(start);
636
+ }
637
+ return;
638
+ }
639
+ if (key.meta && input === "d") {
640
+ const end = wordEndForward(inputValue, cur);
641
+ if (end > cur) {
642
+ setInputValue((prev) => prev.slice(0, cur) + prev.slice(end));
643
+ }
644
+ return;
645
+ }
646
+ if ((key.meta && key.leftArrow) || (key.ctrl && key.leftArrow)) {
647
+ setInputCursor(wordStartBackward(inputValue, cur));
648
+ return;
649
+ }
650
+ if ((key.meta && key.rightArrow) || (key.ctrl && key.rightArrow)) {
651
+ setInputCursor(wordEndForward(inputValue, cur));
652
+ return;
653
+ }
654
+ if (key.ctrl && input === "j") {
655
+ setInputValue((prev) => prev.slice(0, cur) + "\n" + prev.slice(cur));
656
+ setInputCursor(cur + 1);
657
+ return;
658
+ }
659
+ if (key.ctrl && input === "a") {
660
+ setInputCursor(0);
661
+ return;
662
+ }
663
+ if (key.ctrl && input === "e") {
664
+ setInputCursor(len);
665
+ return;
666
+ }
667
+ if (key.ctrl && input === "h") {
668
+ if (cur > 0) {
669
+ setInputValue((prev) => prev.slice(0, cur - 1) + prev.slice(cur));
670
+ setInputCursor(cur - 1);
671
+ }
672
+ return;
673
+ }
674
+ if (key.ctrl && input === "d") {
675
+ if (cur < len) {
676
+ setInputValue((prev) => prev.slice(0, cur) + prev.slice(cur + 1));
677
+ }
678
+ return;
679
+ }
680
+ if (key.backspace || (key.delete && cur > 0)) {
681
+ if (cur > 0) {
682
+ setInputValue((prev) => prev.slice(0, cur - 1) + prev.slice(cur));
683
+ setInputCursor(cur - 1);
684
+ }
685
+ return;
686
+ }
687
+ if (key.delete && cur < len) {
688
+ setInputValue((prev) => prev.slice(0, cur) + prev.slice(cur + 1));
689
+ return;
690
+ }
691
+ if (key.leftArrow) {
692
+ setInputCursor(Math.max(0, cur - 1));
693
+ return;
694
+ }
695
+ if (key.rightArrow) {
696
+ setInputCursor(Math.min(len, cur + 1));
697
+ return;
698
+ }
699
+ if (input === "?" && !inputValue.trim()) {
700
+ setShowHelpModal(true);
701
+ return;
702
+ }
703
+ if (input && !key.ctrl && !key.meta) {
704
+ setInputValue((prev) => prev.slice(0, cur) + input + prev.slice(cur));
705
+ setInputCursor(cur + input.length);
706
+ return;
707
+ }
708
+ }
709
+ if (key.ctrl && input === "p") {
710
+ setShowPalette(true);
711
+ }
712
+ if (key.ctrl && input === "c") {
713
+ onQuit();
714
+ }
715
+ });
716
+ if (showModelSelector) {
717
+ const modelModalMaxHeight = 18;
718
+ const modelModalWidth = 108;
719
+ const modelModalHeight = Math.min(filteredModelList.length + 4, modelModalMaxHeight);
720
+ const topPad = Math.max(0, Math.floor((termRows - modelModalHeight) / 2));
721
+ const leftPad = Math.max(0, Math.floor((termColumns - modelModalWidth) / 2));
722
+ const visibleModelCount = Math.min(filteredModelList.length, modelModalHeight - 4);
723
+ const modelScrollOffset = Math.max(0, Math.min(modelIndex - Math.floor(visibleModelCount / 2), filteredModelList.length - visibleModelCount));
724
+ const visibleModels = filteredModelList.slice(modelScrollOffset, modelScrollOffset + visibleModelCount);
725
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: topPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: leftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: modelModalWidth, minHeight: modelModalHeight, children: [_jsx(Text, { bold: true, children: " Select model " }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: " Filter: " }), _jsx(Text, { children: modelSearchFilter || " " }), modelSearchFilter.length > 0 && (_jsxs(Text, { dimColor: true, color: "gray", children: [" ", "(", filteredModelList.length, " match", filteredModelList.length !== 1 ? "es" : "", ")"] }))] }), visibleModels.length === 0 ? (_jsx(Text, { color: "gray", children: " No match \u2014 type to search by id or name " })) : (visibleModels.map((m, i) => {
726
+ const actualIndex = modelScrollOffset + i;
727
+ return (_jsxs(Text, { color: actualIndex === modelIndex ? inkColors.primary : undefined, children: [actualIndex === modelIndex ? "› " : " ", m.name ? `${m.id} — ${m.name}` : m.id] }, m.id));
728
+ })), _jsx(Text, { color: "gray", children: " \u2191/\u2193 select Enter confirm Esc cancel Type to filter " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
729
+ }
730
+ const slashSuggestionBoxLines = showSlashSuggestions
731
+ ? 3 + Math.max(1, filteredSlashCommands.length)
732
+ : 0;
733
+ const atSuggestionBoxLines = cursorInAtSegment
734
+ ? 4 + Math.max(1, filteredFilePaths.length)
735
+ : 0;
736
+ const suggestionBoxLines = slashSuggestionBoxLines || atSuggestionBoxLines;
737
+ const wrapWidth = Math.max(10, termColumns - PROMPT_INDENT_LEN - 2);
738
+ const inputLineCount = (() => {
739
+ const lines = inputValue.split("\n");
740
+ return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
741
+ })();
742
+ const lastPromptLineCount = lastUserPrompt ? lastUserPrompt.split("\n").length : 0;
743
+ const lastPromptLines = lastUserPrompt
744
+ ? (lastPromptLineCount > 3 ? 4 : lastPromptLineCount)
745
+ : 0;
746
+ const reservedLines = 1 + lastPromptLines + inputLineCount + (loading ? 2 : 1);
747
+ const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
748
+ const maxLogScrollOffset = Math.max(0, logLines.length - logViewportHeight);
749
+ const logStartIndex = Math.max(0, logLines.length - logViewportHeight - Math.min(logScrollOffset, maxLogScrollOffset));
750
+ const visibleLogLines = logLines.slice(logStartIndex, logStartIndex + logViewportHeight);
751
+ if (showHelpModal) {
752
+ const helpModalWidth = 56;
753
+ const helpTopPad = Math.max(0, Math.floor((termRows - 20) / 2));
754
+ const helpLeftPad = Math.max(0, Math.floor((termColumns - helpModalWidth) / 2));
755
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: helpTopPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: helpLeftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: helpModalWidth, children: [_jsx(Text, { bold: true, children: " Help " }), _jsx(Text, { color: "gray", children: " What you can do " }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " Message " }), _jsx(Text, { color: "gray", children: " Type and Enter to send to the agent. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " / " }), _jsx(Text, { color: "gray", children: " Commands. Type / then pick: /models, /brave, /help, /clear, /status, /q. Ctrl+P palette. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " @ " }), _jsx(Text, { color: "gray", children: " Attach files. Type @ then path; Tab to complete. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " ! " }), _jsx(Text, { color: "gray", children: " Run a shell command. Type ! then the command. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " Scroll " }), _jsx(Text, { color: "gray", children: " \u2191/\u2193 when input empty, or Ctrl/Opt+\u2191/\u2193 to scroll chat. " })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: " Press any key to close " }) })] })] }), _jsx(Box, { flexGrow: 1 })] }));
756
+ }
757
+ if (showBraveKeyModal) {
758
+ const braveModalWidth = 52;
759
+ const topPad = Math.max(0, Math.floor((termRows - 6) / 2));
760
+ const leftPad = Math.max(0, Math.floor((termColumns - braveModalWidth) / 2));
761
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: topPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: leftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: braveModalWidth, children: [_jsx(Text, { bold: true, children: " Brave Search API key " }), _jsx(Text, { color: "gray", children: " Get one at https://brave.com/search/api " }), braveKeyInput === BRAVE_KEY_PLACEHOLDER && (_jsx(Text, { color: "gray", children: " Key already set. Type or paste to replace. " })), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " Key: " }), _jsx(Text, { children: braveKeyInput || "\u00A0" })] }), _jsx(Text, { color: "gray", children: " Enter to save, Esc to cancel " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
762
+ }
763
+ if (showPalette) {
764
+ const paletteModalHeight = COMMANDS.length + 4;
765
+ const paletteModalWidth = 52;
766
+ const topPad = Math.max(0, Math.floor((termRows - paletteModalHeight) / 2));
767
+ const leftPad = Math.max(0, Math.floor((termColumns - paletteModalWidth) / 2));
768
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: topPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: leftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: paletteModalWidth, minHeight: paletteModalHeight, children: [_jsx(Text, { bold: true, children: " Command palette " }), COMMANDS.map((c, i) => (_jsxs(Text, { color: i === paletteIndex ? inkColors.primary : undefined, children: [i === paletteIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: "gray", children: [" \u2014 ", c.desc] })] }, c.cmd))), _jsxs(Text, { color: paletteIndex === COMMANDS.length ? inkColors.primary : undefined, children: [paletteIndex === COMMANDS.length ? "› " : " ", "Cancel (Esc)"] }), _jsx(Text, { color: "gray", children: " \u2191/\u2193 select, Enter confirm, Esc close " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
769
+ }
770
+ const footerLines = suggestionBoxLines + 1 + inputLineCount;
771
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [lastUserPrompt ? (_jsx(Box, { flexDirection: "column", children: (() => {
772
+ const lines = lastUserPrompt.split("\n");
773
+ const showLines = lines.slice(0, 3);
774
+ const hasMore = lines.length > 3;
775
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, dimColor: true, children: [icons.prompt, " Last:", " "] }), _jsx(Text, { dimColor: true, color: "gray", children: showLines[0] ?? "" })] }), showLines.slice(1).map((ln, i) => (_jsx(Box, { flexDirection: "row", paddingLeft: 8, children: _jsx(Text, { dimColor: true, color: "gray", children: ln }) }, i))), hasMore && (_jsx(Box, { flexDirection: "row", paddingLeft: 8, children: _jsx(Text, { dimColor: true, color: "gray", children: "\u2026" }) }))] }));
776
+ })() })) : null, _jsx(Box, { flexDirection: "column", height: logViewportHeight, overflow: "hidden", children: visibleLogLines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line }, logLines.length - visibleLogLines.length + i))) }), loading && (_jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsxs(Text, { color: "gray", children: [" ", SPINNER[spinnerTick % SPINNER.length], " Thinking\u2026"] }) }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: "gray", children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: "gray", children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
777
+ const i = filteredSlashCommands.length - 1 - rev;
778
+ return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: "gray", children: [" \u2014 ", c.desc] })] }, c.cmd));
779
+ })), _jsx(Text, { color: "gray", children: " Commands (\u2191/\u2193 select, Enter run, Esc clear) " })] })), cursorInAtSegment && !showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: "gray", children: [filteredFilePaths.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", hasCharsAfterAt ? "No match" : "Type to search files", " "] })) : ([...filteredFilePaths].reverse().map((p, rev) => {
780
+ const i = filteredFilePaths.length - 1 - rev;
781
+ return (_jsxs(Text, { color: i === clampedAtFileIndex ? inkColors.primary : undefined, children: [i === clampedAtFileIndex ? "› " : " ", p] }, p));
782
+ })), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color: "gray", children: " Files (\u2191/\u2193 select, Enter/Tab complete, Esc clear) " }) })] })), _jsxs(Box, { flexDirection: "row", marginTop: 0, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: "gray", dimColor: true, children: ` · / ! @ ↑/↓ scroll (when input empty) or Ctrl/Opt+↑/↓ Ctrl+J newline Tab queue Esc Esc edit ${pasteShortcut} paste Ctrl+C exit` })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: inputValue.length === 0 ? (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] }), _jsx(Text, { inverse: true, color: inkColors.primary, children: " " }), _jsx(Text, { color: "gray", children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
783
+ const lines = inputValue.split("\n");
784
+ let lineStart = 0;
785
+ return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
786
+ const lineEnd = lineStart + lineText.length;
787
+ const cursorOnThisLine = inputCursor >= lineStart && inputCursor <= lineEnd;
788
+ const cursorOffsetInLine = cursorOnThisLine ? inputCursor - lineStart : -1;
789
+ const currentLineStart = lineStart;
790
+ lineStart = lineEnd + 1;
791
+ const segments = parseAtSegments(lineText);
792
+ let runIdx = 0;
793
+ const segmentsWithStyle = [];
794
+ segments.forEach((seg) => {
795
+ const start = runIdx;
796
+ const end = runIdx + seg.text.length;
797
+ runIdx = end;
798
+ const segmentStartInInput = currentLineStart + start;
799
+ const segmentEndInInput = currentLineStart + end;
800
+ const isCurrentAtSegment = seg.type === "path" &&
801
+ cursorOnThisLine &&
802
+ cursorOffsetInLine >= start &&
803
+ cursorOffsetInLine <= end;
804
+ const segmentContainsLastAt = lastAtIndex >= segmentStartInInput && lastAtIndex <= segmentEndInInput;
805
+ const pathPart = seg.text.slice(1).trim();
806
+ const completedPathMatches = segmentContainsLastAt &&
807
+ pathPart === lastAtPath &&
808
+ lastAtPathMatches;
809
+ const usePathStyle = seg.type === "path" &&
810
+ seg.text.length > 1 &&
811
+ ((isCurrentAtSegment && filteredFilePaths.length > 0) ||
812
+ completedPathMatches);
813
+ if (seg.type === "normal" &&
814
+ start === 0 &&
815
+ seg.text.startsWith("/") &&
816
+ filteredSlashCommands.length > 0) {
817
+ const slashEnd = seg.text.indexOf(" ") === -1 ? seg.text.length : seg.text.indexOf(" ");
818
+ segmentsWithStyle.push({
819
+ start: 0,
820
+ end: slashEnd,
821
+ style: { bold: true, color: inkColors.path },
822
+ });
823
+ if (slashEnd < seg.text.length) {
824
+ segmentsWithStyle.push({
825
+ start: slashEnd,
826
+ end,
827
+ style: {},
828
+ });
829
+ }
830
+ }
831
+ else {
832
+ segmentsWithStyle.push({
833
+ start,
834
+ end,
835
+ style: usePathStyle ? { bold: true, color: inkColors.path } : {},
836
+ });
837
+ }
838
+ });
839
+ const visualLines = wrapLine(lineText, wrapWidth);
840
+ return visualLines.map((visualChunk, v) => {
841
+ const visualStart = v * wrapWidth;
842
+ const visualEnd = Math.min((v + 1) * wrapWidth, lineText.length);
843
+ const isLastVisualOfThisLine = v === visualLines.length - 1;
844
+ const cursorAtEndOfVisual = isLastVisualOfThisLine && cursorOffsetInLine === visualEnd;
845
+ const cursorPosInVisual = cursorOnThisLine &&
846
+ cursorOffsetInLine >= visualStart &&
847
+ (cursorOffsetInLine < visualEnd || cursorAtEndOfVisual)
848
+ ? cursorOffsetInLine < visualEnd
849
+ ? cursorOffsetInLine - visualStart
850
+ : visualEnd - visualStart
851
+ : -1;
852
+ const lineNodes = [];
853
+ if (lineText === "" && v === 0 && cursorOnThisLine) {
854
+ lineNodes.push(_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor"));
855
+ }
856
+ else {
857
+ let cursorRendered = false;
858
+ segmentsWithStyle.forEach((seg, segIdx) => {
859
+ const oStart = Math.max(visualStart, seg.start);
860
+ const oEnd = Math.min(visualEnd, seg.end);
861
+ if (oEnd <= oStart)
862
+ return;
863
+ const text = lineText.slice(oStart, oEnd);
864
+ if (cursorPosInVisual >= 0) {
865
+ const cursorInSeg = cursorPosInVisual >= oStart - visualStart &&
866
+ cursorPosInVisual < oEnd - visualStart;
867
+ if (cursorInSeg) {
868
+ const segRel = cursorPosInVisual - (oStart - visualStart);
869
+ const before = text.slice(0, segRel);
870
+ const curChar = text[segRel] ?? "\u00A0";
871
+ const after = text.slice(segRel + 1);
872
+ const usePath = "color" in seg.style && !!seg.style.color;
873
+ lineNodes.push(_jsx(Text, { ...seg.style, children: before }, `${segIdx}-a`));
874
+ lineNodes.push(_jsx(Text, { inverse: true, color: usePath ? inkColors.path : inkColors.primary, bold: "bold" in seg.style && !!seg.style.bold, children: curChar }, `${segIdx}-b`));
875
+ lineNodes.push(_jsx(Text, { ...seg.style, children: after }, `${segIdx}-c`));
876
+ cursorRendered = true;
877
+ }
878
+ else {
879
+ lineNodes.push(_jsx(Text, { ...seg.style, children: text }, segIdx));
880
+ }
881
+ }
882
+ else {
883
+ lineNodes.push(_jsx(Text, { ...seg.style, children: text }, segIdx));
884
+ }
885
+ });
886
+ if (cursorPosInVisual >= 0 && !cursorRendered) {
887
+ lineNodes.push(_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor-end"));
888
+ }
889
+ }
890
+ const isFirstRow = lineIdx === 0 && v === 0;
891
+ const isLastLogicalLine = lineIdx === lines.length - 1;
892
+ const isLastVisualOfLine = v === visualLines.length - 1;
893
+ return (_jsxs(Box, { flexDirection: "row", children: [isFirstRow ? (_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] })) : (_jsx(Text, { children: " ".repeat(PROMPT_INDENT_LEN) })), lineNodes, isLastLogicalLine && isLastVisualOfLine && inputValue.startsWith("!") && (_jsxs(Text, { dimColor: true, color: "gray", children: [" — ", "type a shell command to run"] }))] }, `${lineIdx}-${v}`));
894
+ });
895
+ }) }));
896
+ })()) })] })] }));
897
+ }