ideacode 1.2.2 → 1.2.4
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/api.js +27 -12
- package/dist/context.js +59 -9
- package/dist/index.js +40 -2
- package/dist/repl.js +232 -51
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -9,12 +9,25 @@ export async function fetchModels(apiKey) {
|
|
|
9
9
|
const json = (await res.json());
|
|
10
10
|
return json.data ?? [];
|
|
11
11
|
}
|
|
12
|
-
const MAX_RETRIES =
|
|
13
|
-
const INITIAL_BACKOFF_MS =
|
|
12
|
+
const MAX_RETRIES = 8;
|
|
13
|
+
const INITIAL_BACKOFF_MS = 1200;
|
|
14
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
15
|
+
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
14
16
|
function sleep(ms) {
|
|
15
17
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
18
|
}
|
|
17
|
-
|
|
19
|
+
function computeRetryDelayMs(attempt, retryAfterHeader) {
|
|
20
|
+
if (retryAfterHeader) {
|
|
21
|
+
const retrySeconds = Number.parseFloat(retryAfterHeader);
|
|
22
|
+
if (Number.isFinite(retrySeconds) && retrySeconds > 0) {
|
|
23
|
+
return Math.max(1000, Math.min(MAX_BACKOFF_MS, Math.round(retrySeconds * 1000)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const base = Math.min(MAX_BACKOFF_MS, INITIAL_BACKOFF_MS * Math.pow(2, attempt));
|
|
27
|
+
const jitter = 0.75 + Math.random() * 0.5; // 0.75x .. 1.25x
|
|
28
|
+
return Math.max(1000, Math.round(base * jitter));
|
|
29
|
+
}
|
|
30
|
+
export async function callApi(apiKey, messages, systemPrompt, model, callbacks) {
|
|
18
31
|
const body = {
|
|
19
32
|
model,
|
|
20
33
|
max_tokens: 8192,
|
|
@@ -36,11 +49,15 @@ export async function callApi(apiKey, messages, systemPrompt, model) {
|
|
|
36
49
|
return res.json();
|
|
37
50
|
const text = await res.text();
|
|
38
51
|
lastError = new Error(`API ${res.status}: ${text}`);
|
|
39
|
-
if ((res.status
|
|
52
|
+
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES) {
|
|
40
53
|
const retryAfter = res.headers.get("retry-after");
|
|
41
|
-
const waitMs = retryAfter
|
|
42
|
-
|
|
43
|
-
:
|
|
54
|
+
const waitMs = computeRetryDelayMs(attempt, retryAfter);
|
|
55
|
+
callbacks?.onRetry?.({
|
|
56
|
+
attempt: attempt + 1,
|
|
57
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
58
|
+
waitMs,
|
|
59
|
+
status: res.status,
|
|
60
|
+
});
|
|
44
61
|
await sleep(waitMs);
|
|
45
62
|
continue;
|
|
46
63
|
}
|
|
@@ -187,7 +204,7 @@ export async function callApiStream(apiKey, messages, systemPrompt, model, callb
|
|
|
187
204
|
await flushRemainingToolCalls();
|
|
188
205
|
return finish();
|
|
189
206
|
}
|
|
190
|
-
const SUMMARIZE_SYSTEM = "You are a summarizer.
|
|
207
|
+
const SUMMARIZE_SYSTEM = "You are a summarizer. Compress the conversation while preserving fidelity. Output plain text with these exact sections: Goal, Constraints, Decisions, Open Questions, Next Actions, Critical Facts. Keep literals (paths, model ids, command names, env vars, URLs, numbers, error messages) whenever available. Include key tool results in concise form. Do not add preamble or commentary.";
|
|
191
208
|
export async function callSummarize(apiKey, messages, model) {
|
|
192
209
|
const body = {
|
|
193
210
|
model,
|
|
@@ -213,11 +230,9 @@ export async function callSummarize(apiKey, messages, model) {
|
|
|
213
230
|
}
|
|
214
231
|
const text = await res.text();
|
|
215
232
|
lastError = new Error(`Summarize API ${res.status}: ${text}`);
|
|
216
|
-
if ((res.status
|
|
233
|
+
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES) {
|
|
217
234
|
const retryAfter = res.headers.get("retry-after");
|
|
218
|
-
const waitMs = retryAfter
|
|
219
|
-
? Math.max(1000, parseInt(retryAfter, 10) * 1000)
|
|
220
|
-
: INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
235
|
+
const waitMs = computeRetryDelayMs(attempt, retryAfter);
|
|
221
236
|
await sleep(waitMs);
|
|
222
237
|
continue;
|
|
223
238
|
}
|
package/dist/context.js
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
import { callSummarize } from "./api.js";
|
|
2
|
+
const MAX_PINNED_FACTS = 28;
|
|
3
|
+
const MAX_PINNED_FACT_CHARS = 200;
|
|
4
|
+
function messageContentToText(content) {
|
|
5
|
+
if (typeof content === "string")
|
|
6
|
+
return content;
|
|
7
|
+
if (Array.isArray(content)) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const item of content) {
|
|
10
|
+
if (item && typeof item === "object") {
|
|
11
|
+
const maybe = item;
|
|
12
|
+
if (typeof maybe.content === "string")
|
|
13
|
+
parts.push(maybe.content);
|
|
14
|
+
if (maybe.type === "tool_use" && typeof maybe.name === "string") {
|
|
15
|
+
parts.push(`tool_use ${maybe.name} ${JSON.stringify(maybe.input ?? {})}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parts.join("\n");
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify(content);
|
|
22
|
+
}
|
|
23
|
+
function extractPinnedFacts(messages) {
|
|
24
|
+
const allLines = messages
|
|
25
|
+
.flatMap((m) => messageContentToText(m.content).split("\n"))
|
|
26
|
+
.map((l) => l.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const facts = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const importantPattern = /(\/[A-Za-z0-9._/-]{2,}|https?:\/\/\S+|\b[A-Z][A-Z0-9_]{2,}\b|\b(error|failed|must|never|always|todo|fixme|constraint)\b)/i;
|
|
31
|
+
for (const line of allLines) {
|
|
32
|
+
if (!importantPattern.test(line))
|
|
33
|
+
continue;
|
|
34
|
+
const cleaned = line.replace(/\s+/g, " ").slice(0, MAX_PINNED_FACT_CHARS);
|
|
35
|
+
if (!cleaned || seen.has(cleaned))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(cleaned);
|
|
38
|
+
facts.push(cleaned);
|
|
39
|
+
if (facts.length >= MAX_PINNED_FACTS)
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return facts;
|
|
43
|
+
}
|
|
44
|
+
function buildSummaryEnvelope(summary, pinnedFacts) {
|
|
45
|
+
const factBlock = pinnedFacts.length > 0
|
|
46
|
+
? `Pinned facts (verbatim, highest priority):\n${pinnedFacts.map((f) => `- ${f}`).join("\n")}\n\n`
|
|
47
|
+
: "";
|
|
48
|
+
return `${factBlock}Conversation summary:\n${summary}`.trim();
|
|
49
|
+
}
|
|
2
50
|
export function estimateTokens(messages, systemPrompt) {
|
|
3
51
|
let chars = 0;
|
|
4
52
|
for (const m of messages) {
|
|
@@ -21,9 +69,10 @@ export async function compressState(apiKey, state, systemPrompt, model, options)
|
|
|
21
69
|
const toSummarize = state.slice(0, state.length - keepLast);
|
|
22
70
|
const recent = state.slice(-keepLast);
|
|
23
71
|
const summary = await callSummarize(apiKey, toSummarize, model);
|
|
72
|
+
const pinnedFacts = extractPinnedFacts(toSummarize);
|
|
24
73
|
const summaryMessage = {
|
|
25
|
-
role: "
|
|
26
|
-
content:
|
|
74
|
+
role: "assistant",
|
|
75
|
+
content: buildSummaryEnvelope(summary, pinnedFacts),
|
|
27
76
|
};
|
|
28
77
|
return [summaryMessage, ...recent];
|
|
29
78
|
}
|
|
@@ -31,12 +80,13 @@ export async function ensureUnderBudget(apiKey, state, systemPrompt, model, opti
|
|
|
31
80
|
const { maxTokens, keepLast } = options;
|
|
32
81
|
if (estimateTokens(state, systemPrompt) <= maxTokens)
|
|
33
82
|
return state;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
83
|
+
let working = state;
|
|
84
|
+
if (working.length > keepLast) {
|
|
85
|
+
working = await compressState(apiKey, working, systemPrompt, model, { keepLast });
|
|
86
|
+
}
|
|
87
|
+
// If still over budget, trim oldest messages as a hard fallback.
|
|
88
|
+
while (working.length > 1 && estimateTokens(working, systemPrompt) > maxTokens) {
|
|
89
|
+
working = working.slice(1);
|
|
40
90
|
}
|
|
41
|
-
return
|
|
91
|
+
return working;
|
|
42
92
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import "dotenv/config";
|
|
4
|
+
import { writeSync } from "node:fs";
|
|
4
5
|
import { render } from "ink";
|
|
5
6
|
import { getApiKey } from "./config.js";
|
|
6
7
|
import { getVersion } from "./version.js";
|
|
7
8
|
import { runOnboarding } from "./onboarding.js";
|
|
8
9
|
import { Repl } from "./repl.js";
|
|
10
|
+
const PRINT_TRANSCRIPT_ON_EXIT = process.env.IDEACODE_TRANSCRIPT_ON_EXIT === "1";
|
|
11
|
+
const CLEAR_SCREEN_ON_EXIT = process.env.IDEACODE_CLEAR_ON_EXIT !== "0";
|
|
12
|
+
const TERMINAL_RESTORE_SEQ = "\x1b[?2004l\x1b[?1004l\x1b[?1007l\x1b[?1015l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[0m";
|
|
13
|
+
function restoreTerminalState() {
|
|
14
|
+
try {
|
|
15
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
16
|
+
process.stdin.setRawMode(false);
|
|
17
|
+
}
|
|
18
|
+
if (process.stdout.isTTY) {
|
|
19
|
+
writeSync(process.stdout.fd, TERMINAL_RESTORE_SEQ);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore restore failures during process teardown.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
process.on("exit", restoreTerminalState);
|
|
27
|
+
process.on("SIGINT", () => {
|
|
28
|
+
restoreTerminalState();
|
|
29
|
+
process.exit(130);
|
|
30
|
+
});
|
|
31
|
+
process.on("SIGTERM", () => {
|
|
32
|
+
restoreTerminalState();
|
|
33
|
+
process.exit(143);
|
|
34
|
+
});
|
|
9
35
|
async function main() {
|
|
10
36
|
const args = process.argv.slice(2);
|
|
11
37
|
if (args.includes("-v") || args.includes("--version")) {
|
|
@@ -20,7 +46,19 @@ async function main() {
|
|
|
20
46
|
if (!apiKey) {
|
|
21
47
|
process.exit(1);
|
|
22
48
|
}
|
|
23
|
-
|
|
24
|
-
|
|
49
|
+
let app;
|
|
50
|
+
let transcriptLines = [];
|
|
51
|
+
app = render(_jsx(Repl, { apiKey: apiKey, cwd: process.cwd(), onQuit: (lines) => {
|
|
52
|
+
transcriptLines = lines;
|
|
53
|
+
app.unmount();
|
|
54
|
+
} }));
|
|
55
|
+
await app.waitUntilExit();
|
|
56
|
+
if (!PRINT_TRANSCRIPT_ON_EXIT && CLEAR_SCREEN_ON_EXIT && process.stdout.isTTY) {
|
|
57
|
+
// Clear lingering in-place TUI frame from primary screen buffer.
|
|
58
|
+
writeSync(process.stdout.fd, "\x1b[2J\x1b[H");
|
|
59
|
+
}
|
|
60
|
+
if (PRINT_TRANSCRIPT_ON_EXIT && transcriptLines.length > 0) {
|
|
61
|
+
process.stdout.write("\n" + transcriptLines.join("\n") + "\n");
|
|
62
|
+
}
|
|
25
63
|
}
|
|
26
64
|
main();
|
package/dist/repl.js
CHANGED
|
@@ -6,6 +6,7 @@ import React, { useState, useCallback, useRef, useMemo, useEffect } from "react"
|
|
|
6
6
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
7
7
|
import { globSync } from "glob";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { writeSync } from "node:fs";
|
|
9
10
|
import gradient from "gradient-string";
|
|
10
11
|
// Custom matcha-themed gradient: matcha green → dark sepia
|
|
11
12
|
const matchaGradient = gradient(["#7F9A65", "#5C4033"]);
|
|
@@ -39,7 +40,8 @@ const MAX_AT_SUGGESTIONS = 12;
|
|
|
39
40
|
const INITIAL_BANNER_LINES = 12;
|
|
40
41
|
const ENABLE_PARALLEL_TOOL_CALLS = process.env.IDEACODE_PARALLEL_TOOL_CALLS !== "0";
|
|
41
42
|
const PARALLEL_SAFE_TOOLS = new Set(["read", "glob", "grep", "web_fetch", "web_search"]);
|
|
42
|
-
const LOADING_TICK_MS =
|
|
43
|
+
const LOADING_TICK_MS = 80;
|
|
44
|
+
const MAX_EMPTY_ASSISTANT_RETRIES = 3;
|
|
43
45
|
const TRUNCATE_NOTE = "\n\n(Output truncated to save context. Use read with offset/limit, grep with a specific pattern, or tail with fewer lines to get more.)";
|
|
44
46
|
function truncateToolResult(content) {
|
|
45
47
|
if (content.length <= MAX_TOOL_RESULT_CHARS)
|
|
@@ -62,19 +64,90 @@ function listFilesWithFilter(cwd, filter) {
|
|
|
62
64
|
return [];
|
|
63
65
|
}
|
|
64
66
|
}
|
|
67
|
+
function stripHeredocBodies(cmdRaw) {
|
|
68
|
+
const lines = cmdRaw.replace(/\r\n/g, "\n").split("\n");
|
|
69
|
+
const out = [];
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < lines.length) {
|
|
72
|
+
const line = lines[i] ?? "";
|
|
73
|
+
out.push(line);
|
|
74
|
+
const markerMatch = line.match(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/);
|
|
75
|
+
if (!markerMatch) {
|
|
76
|
+
i += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const marker = markerMatch[2] ?? "";
|
|
80
|
+
i += 1;
|
|
81
|
+
while (i < lines.length) {
|
|
82
|
+
const bodyLine = lines[i] ?? "";
|
|
83
|
+
if (bodyLine.trim() === marker) {
|
|
84
|
+
out.push(bodyLine);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
i += 1;
|
|
90
|
+
}
|
|
91
|
+
return out.join("\n");
|
|
92
|
+
}
|
|
65
93
|
function summarizeBashCommand(cmdRaw) {
|
|
66
|
-
const
|
|
67
|
-
|
|
94
|
+
const sanitized = stripHeredocBodies(cmdRaw);
|
|
95
|
+
const parts = sanitized
|
|
96
|
+
.split(/\n|&&|\|\||;|\|/g)
|
|
68
97
|
.map((s) => s.trim())
|
|
69
98
|
.filter(Boolean);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
const skipTokens = new Set([
|
|
100
|
+
"if",
|
|
101
|
+
"then",
|
|
102
|
+
"else",
|
|
103
|
+
"elif",
|
|
104
|
+
"fi",
|
|
105
|
+
"for",
|
|
106
|
+
"while",
|
|
107
|
+
"do",
|
|
108
|
+
"done",
|
|
109
|
+
"case",
|
|
110
|
+
"esac",
|
|
111
|
+
"in",
|
|
112
|
+
"function",
|
|
113
|
+
"{",
|
|
114
|
+
"}",
|
|
115
|
+
"(",
|
|
116
|
+
")",
|
|
117
|
+
]);
|
|
118
|
+
const commands = [];
|
|
119
|
+
for (const part of parts) {
|
|
120
|
+
let s = part.replace(/^\(+/, "").trim();
|
|
121
|
+
if (!s || s.toUpperCase() === "EOF")
|
|
122
|
+
continue;
|
|
123
|
+
// Strip simple environment assignments at the front: FOO=bar CMD
|
|
124
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(s)) {
|
|
125
|
+
const idx = s.indexOf(" ");
|
|
126
|
+
if (idx === -1) {
|
|
127
|
+
s = "";
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
s = s.slice(idx + 1).trim();
|
|
131
|
+
}
|
|
132
|
+
if (!s)
|
|
133
|
+
continue;
|
|
134
|
+
const token = (s.split(/\s+/)[0] ?? "").replace(/^['"]|['"]$/g, "").toLowerCase();
|
|
135
|
+
if (!/^[A-Za-z0-9_./-]+$/.test(token))
|
|
136
|
+
continue;
|
|
137
|
+
if (skipTokens.has(token))
|
|
138
|
+
continue;
|
|
139
|
+
if (token === "echo")
|
|
140
|
+
continue;
|
|
141
|
+
if (token === "cat" && /<<\s*['"]?EOF/i.test(s))
|
|
142
|
+
continue;
|
|
143
|
+
if (!commands.includes(token))
|
|
144
|
+
commands.push(token);
|
|
145
|
+
}
|
|
146
|
+
if (commands.length === 0)
|
|
147
|
+
return "bash";
|
|
148
|
+
const shown = commands.slice(0, 5);
|
|
149
|
+
const suffix = commands.length > 5 ? `, +${commands.length - 5}` : "";
|
|
150
|
+
return (shown.join(", ") + suffix).slice(0, 140);
|
|
78
151
|
}
|
|
79
152
|
function toolArgPreview(toolName, toolArgs) {
|
|
80
153
|
if (toolName === "bash") {
|
|
@@ -196,36 +269,41 @@ function useTerminalSize() {
|
|
|
196
269
|
}, [stdout]);
|
|
197
270
|
return size;
|
|
198
271
|
}
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
out += inWindow ? colors.gray(label[i] ?? "") : colors.mutedDark(label[i] ?? "");
|
|
211
|
-
}
|
|
212
|
-
return out;
|
|
272
|
+
const LogViewport = React.memo(function LogViewport({ lines, startIndex, height, }) {
|
|
273
|
+
return (_jsx(Box, { flexDirection: "column", height: height, overflow: "hidden", children: lines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line }, startIndex + i))) }));
|
|
274
|
+
});
|
|
275
|
+
function orbitDots(frame) {
|
|
276
|
+
const phase = frame % 6;
|
|
277
|
+
const activeIndex = phase <= 3 ? phase : 6 - phase;
|
|
278
|
+
const slots = ["·", "·", "·", "·"];
|
|
279
|
+
slots[activeIndex] = "●";
|
|
280
|
+
return slots
|
|
281
|
+
.map((ch, i) => (i === activeIndex ? colors.gray(ch) : colors.mutedDark(ch)))
|
|
282
|
+
.join("");
|
|
213
283
|
}
|
|
214
284
|
const LoadingStatus = React.memo(function LoadingStatus({ active, label, }) {
|
|
215
285
|
const [frame, setFrame] = useState(0);
|
|
216
|
-
const
|
|
286
|
+
const startedAtRef = useRef(null);
|
|
217
287
|
useEffect(() => {
|
|
218
|
-
if (!active)
|
|
288
|
+
if (!active) {
|
|
289
|
+
startedAtRef.current = null;
|
|
219
290
|
return;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
291
|
+
}
|
|
292
|
+
if (startedAtRef.current == null) {
|
|
293
|
+
startedAtRef.current = Date.now();
|
|
294
|
+
setFrame(0);
|
|
295
|
+
}
|
|
296
|
+
const anim = setInterval(() => setFrame((n) => n + 1), LOADING_TICK_MS);
|
|
297
|
+
return () => {
|
|
298
|
+
clearInterval(anim);
|
|
299
|
+
};
|
|
300
|
+
}, [active]);
|
|
225
301
|
if (!active)
|
|
226
302
|
return _jsx(Text, { color: inkColors.textSecondary, children: "\u00A0" });
|
|
227
|
-
const
|
|
228
|
-
|
|
303
|
+
const startedAt = startedAtRef.current ?? Date.now();
|
|
304
|
+
const elapsedSeconds = Math.max(0, (Date.now() - startedAt) / 1000);
|
|
305
|
+
const elapsedText = elapsedSeconds < 10 ? `${elapsedSeconds.toFixed(1)}s` : `${Math.floor(elapsedSeconds)}s`;
|
|
306
|
+
return (_jsxs(Text, { color: inkColors.textSecondary, children: [" ", orbitDots(frame), " ", colors.gray(label), " ", colors.gray(elapsedText)] }));
|
|
229
307
|
});
|
|
230
308
|
export function Repl({ apiKey, cwd, onQuit }) {
|
|
231
309
|
const { rows: termRows, columns: termColumns } = useTerminalSize();
|
|
@@ -258,6 +336,10 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
258
336
|
}
|
|
259
337
|
return banner;
|
|
260
338
|
});
|
|
339
|
+
const logLinesRef = useRef(logLines);
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
logLinesRef.current = logLines;
|
|
342
|
+
}, [logLines]);
|
|
261
343
|
const [inputValue, setInputValue] = useState("");
|
|
262
344
|
const [currentModel, setCurrentModel] = useState(getModel);
|
|
263
345
|
const [messages, setMessages] = useState(() => loadConversation(cwd));
|
|
@@ -305,10 +387,23 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
305
387
|
if (saveDebounceRef.current)
|
|
306
388
|
clearTimeout(saveDebounceRef.current);
|
|
307
389
|
saveConversation(cwd, messagesRef.current);
|
|
308
|
-
|
|
390
|
+
// Best-effort terminal mode reset in case process exits before React cleanup runs.
|
|
391
|
+
try {
|
|
392
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
393
|
+
process.stdin.setRawMode(false);
|
|
394
|
+
}
|
|
395
|
+
if (process.stdout.isTTY) {
|
|
396
|
+
writeSync(process.stdout.fd, "\x1b[?2004l\x1b[?1004l\x1b[?1007l\x1b[?1015l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[0m");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Ignore restore failures during teardown.
|
|
401
|
+
}
|
|
402
|
+
onQuit(logLinesRef.current);
|
|
309
403
|
}, [cwd, onQuit]);
|
|
310
404
|
const [loading, setLoading] = useState(false);
|
|
311
405
|
const [loadingLabel, setLoadingLabel] = useState("Thinking…");
|
|
406
|
+
const cursorBlinkOn = true;
|
|
312
407
|
const [showPalette, setShowPalette] = useState(false);
|
|
313
408
|
const [paletteIndex, setPaletteIndex] = useState(0);
|
|
314
409
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
@@ -327,6 +422,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
327
422
|
const scrollBoundsRef = useRef({ maxLogScrollOffset: 0, logViewportHeight: 1 });
|
|
328
423
|
const prevEscRef = useRef(false);
|
|
329
424
|
useEffect(() => {
|
|
425
|
+
// Enable SGR mouse + basic tracking so trackpad wheel scrolling works.
|
|
330
426
|
process.stdout.write("\x1b[?1006h\x1b[?1000h");
|
|
331
427
|
return () => {
|
|
332
428
|
process.stdout.write("\x1b[?1006l\x1b[?1000l");
|
|
@@ -345,6 +441,20 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
345
441
|
return modelList.filter((m) => m.id.toLowerCase().includes(q) ||
|
|
346
442
|
(m.name ?? "").toLowerCase().includes(q));
|
|
347
443
|
}, [modelList, modelSearchFilter]);
|
|
444
|
+
const wrapWidth = Math.max(10, termColumns - PROMPT_INDENT_LEN - 2);
|
|
445
|
+
const inputLineCount = useMemo(() => {
|
|
446
|
+
const lines = inputValue.split("\n");
|
|
447
|
+
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
|
|
448
|
+
}, [inputValue, wrapWidth]);
|
|
449
|
+
const [stableInputLineCount, setStableInputLineCount] = useState(inputLineCount);
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
if (inputLineCount <= 1) {
|
|
452
|
+
setStableInputLineCount(1);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const t = setTimeout(() => setStableInputLineCount(inputLineCount), 90);
|
|
456
|
+
return () => clearTimeout(t);
|
|
457
|
+
}, [inputLineCount]);
|
|
348
458
|
useEffect(() => {
|
|
349
459
|
setInputCursor((c) => Math.min(c, Math.max(0, inputValue.length)));
|
|
350
460
|
}, [inputValue.length]);
|
|
@@ -512,15 +622,36 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
512
622
|
setLoading(true);
|
|
513
623
|
state = await ensureUnderBudget(apiKey, state, systemPrompt, currentModel, {
|
|
514
624
|
maxTokens: maxContextTokens,
|
|
515
|
-
keepLast:
|
|
625
|
+
keepLast: 8,
|
|
516
626
|
});
|
|
517
627
|
if (state.length < stateBeforeCompress.length) {
|
|
518
628
|
appendLog(colors.muted(" (context compressed to stay under limit)\n"));
|
|
519
629
|
}
|
|
630
|
+
setLoadingLabel("Thinking…");
|
|
631
|
+
let emptyAssistantRetries = 0;
|
|
520
632
|
for (;;) {
|
|
633
|
+
setLoading(true);
|
|
521
634
|
setLoadingLabel("Thinking…");
|
|
522
|
-
const response = await callApi(apiKey, state, systemPrompt, currentModel
|
|
635
|
+
const response = await callApi(apiKey, state, systemPrompt, currentModel, {
|
|
636
|
+
onRetry: ({ attempt, maxAttempts, waitMs, status }) => {
|
|
637
|
+
setLoadingLabel(`Rate limited (${status}), retry ${attempt}/${maxAttempts} in ${(waitMs / 1000).toFixed(1)}s…`);
|
|
638
|
+
},
|
|
639
|
+
});
|
|
523
640
|
const contentBlocks = response.content ?? [];
|
|
641
|
+
const hasMeaningfulAssistantOutput = contentBlocks.some((block) => block.type === "tool_use" || (block.type === "text" && !!block.text?.trim()));
|
|
642
|
+
if (!hasMeaningfulAssistantOutput) {
|
|
643
|
+
emptyAssistantRetries += 1;
|
|
644
|
+
if (emptyAssistantRetries <= MAX_EMPTY_ASSISTANT_RETRIES) {
|
|
645
|
+
setLoadingLabel(`No output yet, retrying ${emptyAssistantRetries}/${MAX_EMPTY_ASSISTANT_RETRIES}…`);
|
|
646
|
+
appendLog(colors.muted(` ${icons.tool} model returned an empty turn, retrying (${emptyAssistantRetries}/${MAX_EMPTY_ASSISTANT_RETRIES})…`));
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
appendLog(colors.error(`${icons.error} model returned empty output repeatedly. Stopping this turn; you can submit "continue" to resume.`));
|
|
650
|
+
appendLog("");
|
|
651
|
+
setMessages(state);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
emptyAssistantRetries = 0;
|
|
524
655
|
const toolResults = [];
|
|
525
656
|
const renderToolOutcome = (planned, result, extraIndent = 0) => {
|
|
526
657
|
const ok = !result.startsWith("error:");
|
|
@@ -535,6 +666,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
535
666
|
const runParallelBatch = async (batch) => {
|
|
536
667
|
if (batch.length === 0)
|
|
537
668
|
return;
|
|
669
|
+
setLoadingLabel(`Running ${batch.length} tools in parallel…`);
|
|
538
670
|
const started = Date.now();
|
|
539
671
|
const groupedTools = Array.from(batch.reduce((acc, planned) => {
|
|
540
672
|
acc.set(planned.toolName, (acc.get(planned.toolName) ?? 0) + 1);
|
|
@@ -574,11 +706,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
574
706
|
}
|
|
575
707
|
await runParallelBatch(parallelBatch);
|
|
576
708
|
parallelBatch = [];
|
|
709
|
+
setLoadingLabel(`Running ${planned.toolName}…`);
|
|
577
710
|
const result = await runTool(planned.toolName, planned.toolArgs);
|
|
578
711
|
renderToolOutcome(planned, result);
|
|
579
712
|
}
|
|
580
713
|
await runParallelBatch(parallelBatch);
|
|
581
|
-
setLoading(false);
|
|
582
714
|
state = [...state, { role: "assistant", content: contentBlocks }];
|
|
583
715
|
if (toolResults.length === 0) {
|
|
584
716
|
setMessages(state);
|
|
@@ -587,6 +719,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
587
719
|
state = [...state, { role: "user", content: toolResults }];
|
|
588
720
|
setMessages(state);
|
|
589
721
|
}
|
|
722
|
+
setLoading(false);
|
|
590
723
|
return true;
|
|
591
724
|
}, [apiKey, cwd, currentModel, messages, modelList, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
|
|
592
725
|
const handleSubmit = useCallback(async (value) => {
|
|
@@ -1004,13 +1137,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1004
1137
|
? 4 + Math.max(1, filteredFilePaths.length)
|
|
1005
1138
|
: 0;
|
|
1006
1139
|
const suggestionBoxLines = slashSuggestionBoxLines || atSuggestionBoxLines;
|
|
1007
|
-
const wrapWidth = Math.max(10, termColumns - PROMPT_INDENT_LEN - 2);
|
|
1008
|
-
const inputLineCount = (() => {
|
|
1009
|
-
const lines = inputValue.split("\n");
|
|
1010
|
-
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
|
|
1011
|
-
})();
|
|
1012
1140
|
// Keep a fixed loading row reserved to avoid viewport jumps/flicker when loading starts/stops.
|
|
1013
|
-
const reservedLines = 1 +
|
|
1141
|
+
const reservedLines = 1 + stableInputLineCount + 2;
|
|
1014
1142
|
const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
|
|
1015
1143
|
const effectiveLogLines = logLines;
|
|
1016
1144
|
const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
|
|
@@ -1020,7 +1148,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1020
1148
|
logStartIndex = 0;
|
|
1021
1149
|
}
|
|
1022
1150
|
const sliceEnd = logStartIndex + logViewportHeight;
|
|
1023
|
-
const visibleLogLines = effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
1151
|
+
const visibleLogLines = useMemo(() => effectiveLogLines.slice(logStartIndex, sliceEnd), [effectiveLogLines, logStartIndex, sliceEnd]);
|
|
1152
|
+
const useSimpleInputRenderer = inputLineCount > 1;
|
|
1024
1153
|
if (showHelpModal) {
|
|
1025
1154
|
const helpModalWidth = Math.min(88, Math.max(80, termColumns - 4));
|
|
1026
1155
|
const helpContentRows = 20;
|
|
@@ -1043,14 +1172,14 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1043
1172
|
const leftPad = Math.max(0, Math.floor((termColumns - paletteModalWidth) / 2));
|
|
1044
1173
|
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: inkColors.textSecondary, children: [" \u2014 ", c.desc] })] }, c.cmd))), _jsxs(Text, { color: paletteIndex === COMMANDS.length ? inkColors.primary : undefined, children: [paletteIndex === COMMANDS.length ? "› " : " ", "Cancel (Esc)"] }), _jsx(Text, { color: inkColors.textSecondary, children: " \u2191/\u2193 select, Enter confirm, Esc close " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
|
|
1045
1174
|
}
|
|
1046
|
-
const footerLines = suggestionBoxLines + 1 +
|
|
1047
|
-
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(
|
|
1175
|
+
const footerLines = suggestionBoxLines + 1 + stableInputLineCount;
|
|
1176
|
+
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(LogViewport, { lines: visibleLogLines, startIndex: logStartIndex, height: logViewportHeight }), _jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsx(LoadingStatus, { active: loading, label: loadingLabel }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
|
|
1048
1177
|
const i = filteredSlashCommands.length - 1 - rev;
|
|
1049
1178
|
return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: inkColors.textSecondary, children: [" \u2014 ", c.desc] })] }, c.cmd));
|
|
1050
1179
|
})), _jsx(Text, { color: inkColors.textSecondary, children: " Commands (\u2191/\u2193 select, Enter run, Esc clear) " })] })), cursorInAtSegment && !showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, children: [filteredFilePaths.length === 0 ? (_jsxs(Text, { color: inkColors.textSecondary, children: [" ", hasCharsAfterAt ? "No match" : "Type to search files", " "] })) : ([...filteredFilePaths].reverse().map((p, rev) => {
|
|
1051
1180
|
const i = filteredFilePaths.length - 1 - rev;
|
|
1052
1181
|
return (_jsxs(Text, { color: i === clampedAtFileIndex ? inkColors.primary : undefined, children: [i === clampedAtFileIndex ? "› " : " ", p] }, p));
|
|
1053
|
-
})), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color: inkColors.textSecondary, children: " Files (\u2191/\u2193 select, Enter/Tab complete, Esc clear) " }) })] })), _jsxs(Box, { flexDirection: "row", marginTop: 0, children: [_jsxs(Text, { color: "gray", children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: "gray", children: ` · / ! @ trackpad/↑/↓ scroll Opt/Fn+select 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: inkColors.textSecondary, children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
|
|
1182
|
+
})), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color: inkColors.textSecondary, children: " Files (\u2191/\u2193 select, Enter/Tab complete, Esc clear) " }) })] })), _jsxs(Box, { flexDirection: "row", marginTop: 0, children: [_jsxs(Text, { color: "gray", children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: "gray", children: ` · / ! @ trackpad/↑/↓ scroll Opt/Fn+select 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, " "] }), cursorBlinkOn ? (_jsx(Text, { inverse: true, color: inkColors.primary, children: " " })) : (_jsx(Text, { color: inkColors.primary, children: " " })), _jsx(Text, { color: inkColors.textSecondary, children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
|
|
1054
1183
|
const lines = inputValue.split("\n");
|
|
1055
1184
|
let lineStart = 0;
|
|
1056
1185
|
return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
|
|
@@ -1059,6 +1188,49 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1059
1188
|
const cursorOffsetInLine = cursorOnThisLine ? inputCursor - lineStart : -1;
|
|
1060
1189
|
const currentLineStart = lineStart;
|
|
1061
1190
|
lineStart = lineEnd + 1;
|
|
1191
|
+
if (useSimpleInputRenderer) {
|
|
1192
|
+
const visualLines = wrapLine(lineText, wrapWidth);
|
|
1193
|
+
return visualLines.map((visualChunk, v) => {
|
|
1194
|
+
const visualStart = v * wrapWidth;
|
|
1195
|
+
const visualEnd = Math.min((v + 1) * wrapWidth, lineText.length);
|
|
1196
|
+
const isLastVisualOfThisLine = v === visualLines.length - 1;
|
|
1197
|
+
const cursorAtEndOfVisual = isLastVisualOfThisLine && cursorOffsetInLine === visualEnd;
|
|
1198
|
+
const cursorPosInVisual = cursorOnThisLine &&
|
|
1199
|
+
cursorOffsetInLine >= visualStart &&
|
|
1200
|
+
(cursorOffsetInLine < visualEnd || cursorAtEndOfVisual)
|
|
1201
|
+
? cursorOffsetInLine < visualEnd
|
|
1202
|
+
? cursorOffsetInLine - visualStart
|
|
1203
|
+
: visualEnd - visualStart
|
|
1204
|
+
: -1;
|
|
1205
|
+
const isFirstRow = lineIdx === 0 && v === 0;
|
|
1206
|
+
const isLastLogicalLine = lineIdx === lines.length - 1;
|
|
1207
|
+
const isLastVisualOfLine = v === visualLines.length - 1;
|
|
1208
|
+
const rowNodes = [];
|
|
1209
|
+
if (lineText === "" && v === 0 && cursorOnThisLine) {
|
|
1210
|
+
rowNodes.push(cursorBlinkOn
|
|
1211
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor-empty-on"))
|
|
1212
|
+
: (_jsx(Text, { color: inkColors.primary, children: "\u00A0" }, "cursor-empty-off")));
|
|
1213
|
+
}
|
|
1214
|
+
else if (cursorPosInVisual >= 0) {
|
|
1215
|
+
const before = visualChunk.slice(0, cursorPosInVisual);
|
|
1216
|
+
const curChar = cursorPosInVisual < visualChunk.length
|
|
1217
|
+
? visualChunk[cursorPosInVisual]
|
|
1218
|
+
: "\u00A0";
|
|
1219
|
+
const after = cursorPosInVisual < visualChunk.length
|
|
1220
|
+
? visualChunk.slice(cursorPosInVisual + 1)
|
|
1221
|
+
: "";
|
|
1222
|
+
rowNodes.push(_jsx(Text, { children: before }, "plain-before"));
|
|
1223
|
+
rowNodes.push(cursorBlinkOn
|
|
1224
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: curChar }, "plain-caret-on"))
|
|
1225
|
+
: (_jsx(Text, { children: curChar }, "plain-caret-off")));
|
|
1226
|
+
rowNodes.push(_jsx(Text, { children: after }, "plain-after"));
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
rowNodes.push(_jsx(Text, { children: visualChunk }, "plain"));
|
|
1230
|
+
}
|
|
1231
|
+
return (_jsxs(Box, { flexDirection: "row", children: [isFirstRow ? (_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] })) : (_jsx(Text, { children: " ".repeat(PROMPT_INDENT_LEN) })), rowNodes, isLastLogicalLine && isLastVisualOfLine && inputValue.startsWith("!") && (_jsxs(Text, { color: inkColors.textDisabled, children: [" — ", "type a shell command to run"] }))] }, `simple-${lineIdx}-${v}`));
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1062
1234
|
const segments = parseAtSegments(lineText);
|
|
1063
1235
|
let runIdx = 0;
|
|
1064
1236
|
const segmentsWithStyle = [];
|
|
@@ -1122,7 +1294,9 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1122
1294
|
: -1;
|
|
1123
1295
|
const lineNodes = [];
|
|
1124
1296
|
if (lineText === "" && v === 0 && cursorOnThisLine) {
|
|
1125
|
-
lineNodes.push(
|
|
1297
|
+
lineNodes.push(cursorBlinkOn
|
|
1298
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor-on"))
|
|
1299
|
+
: (_jsx(Text, { color: inkColors.primary, children: "\u00A0" }, "cursor-off")));
|
|
1126
1300
|
}
|
|
1127
1301
|
else {
|
|
1128
1302
|
let cursorRendered = false;
|
|
@@ -1142,7 +1316,12 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1142
1316
|
const after = text.slice(segRel + 1);
|
|
1143
1317
|
const usePath = "color" in seg.style && !!seg.style.color;
|
|
1144
1318
|
lineNodes.push(_jsx(Text, { ...seg.style, children: before }, `${segIdx}-a`));
|
|
1145
|
-
|
|
1319
|
+
if (cursorBlinkOn) {
|
|
1320
|
+
lineNodes.push(_jsx(Text, { inverse: true, color: usePath ? inkColors.path : inkColors.primary, bold: "bold" in seg.style && !!seg.style.bold, children: curChar }, `${segIdx}-b-on`));
|
|
1321
|
+
}
|
|
1322
|
+
else {
|
|
1323
|
+
lineNodes.push(_jsx(Text, { ...seg.style, children: curChar }, `${segIdx}-b-off`));
|
|
1324
|
+
}
|
|
1146
1325
|
lineNodes.push(_jsx(Text, { ...seg.style, children: after }, `${segIdx}-c`));
|
|
1147
1326
|
cursorRendered = true;
|
|
1148
1327
|
}
|
|
@@ -1155,7 +1334,9 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1155
1334
|
}
|
|
1156
1335
|
});
|
|
1157
1336
|
if (cursorPosInVisual >= 0 && !cursorRendered) {
|
|
1158
|
-
lineNodes.push(
|
|
1337
|
+
lineNodes.push(cursorBlinkOn
|
|
1338
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor-end-on"))
|
|
1339
|
+
: (_jsx(Text, { color: inkColors.primary, children: "\u00A0" }, "cursor-end-off")));
|
|
1159
1340
|
}
|
|
1160
1341
|
}
|
|
1161
1342
|
const isFirstRow = lineIdx === 0 && v === 0;
|