ideacode 1.1.5 → 1.1.7
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 +189 -16
- package/dist/buffered-stdin.js +95 -0
- package/dist/config.js +1 -0
- package/dist/repl.js +71 -20
- package/dist/stdin-transform.js +78 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -9,6 +9,11 @@ export async function fetchModels(apiKey) {
|
|
|
9
9
|
const json = (await res.json());
|
|
10
10
|
return json.data ?? [];
|
|
11
11
|
}
|
|
12
|
+
const MAX_RETRIES = 3;
|
|
13
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
14
|
+
function sleep(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
12
17
|
export async function callApi(apiKey, messages, systemPrompt, model) {
|
|
13
18
|
const body = {
|
|
14
19
|
model,
|
|
@@ -17,17 +22,170 @@ export async function callApi(apiKey, messages, systemPrompt, model) {
|
|
|
17
22
|
messages,
|
|
18
23
|
tools: makeSchema(),
|
|
19
24
|
};
|
|
20
|
-
|
|
25
|
+
let lastError = null;
|
|
26
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
27
|
+
const res = await fetch(config.apiUrl, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
Authorization: `Bearer ${apiKey}`,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
});
|
|
35
|
+
if (res.ok)
|
|
36
|
+
return res.json();
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
lastError = new Error(`API ${res.status}: ${text}`);
|
|
39
|
+
if ((res.status === 429 || res.status === 503) && attempt < MAX_RETRIES) {
|
|
40
|
+
const retryAfter = res.headers.get("retry-after");
|
|
41
|
+
const waitMs = retryAfter
|
|
42
|
+
? Math.max(1000, parseInt(retryAfter, 10) * 1000)
|
|
43
|
+
: INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
44
|
+
await sleep(waitMs);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw lastError;
|
|
48
|
+
}
|
|
49
|
+
throw lastError;
|
|
50
|
+
}
|
|
51
|
+
function parseStreamChunk(line) {
|
|
52
|
+
if (line.startsWith("data: ")) {
|
|
53
|
+
const data = line.slice(6).trim();
|
|
54
|
+
if (data === "[DONE]")
|
|
55
|
+
return { __done: true };
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(data);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
export async function callApiStream(apiKey, messages, systemPrompt, model, callbacks, signal) {
|
|
66
|
+
const chatMessages = [
|
|
67
|
+
{ role: "system", content: systemPrompt },
|
|
68
|
+
...messages,
|
|
69
|
+
];
|
|
70
|
+
const body = {
|
|
71
|
+
model,
|
|
72
|
+
max_tokens: 8192,
|
|
73
|
+
messages: chatMessages,
|
|
74
|
+
tools: makeSchema(),
|
|
75
|
+
stream: true,
|
|
76
|
+
};
|
|
77
|
+
const res = await fetch(config.chatCompletionsUrl, {
|
|
21
78
|
method: "POST",
|
|
22
79
|
headers: {
|
|
23
80
|
"Content-Type": "application/json",
|
|
24
81
|
Authorization: `Bearer ${apiKey}`,
|
|
25
82
|
},
|
|
26
83
|
body: JSON.stringify(body),
|
|
84
|
+
signal,
|
|
27
85
|
});
|
|
28
86
|
if (!res.ok)
|
|
29
87
|
throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
30
|
-
|
|
88
|
+
const reader = res.body?.getReader();
|
|
89
|
+
if (!reader)
|
|
90
|
+
throw new Error("No response body");
|
|
91
|
+
const decoder = new TextDecoder();
|
|
92
|
+
let buffer = "";
|
|
93
|
+
const contentBlocks = [];
|
|
94
|
+
let textAccum = "";
|
|
95
|
+
let textBlockIndex = -1;
|
|
96
|
+
const toolAccum = [];
|
|
97
|
+
const tryEmitToolCall = async (index) => {
|
|
98
|
+
const t = toolAccum[index];
|
|
99
|
+
if (!t?.id || !t?.name)
|
|
100
|
+
return false;
|
|
101
|
+
let args = {};
|
|
102
|
+
if (t.arguments.trim()) {
|
|
103
|
+
try {
|
|
104
|
+
args = JSON.parse(t.arguments);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await callbacks.onToolCall({ id: t.id, name: t.name, input: args });
|
|
111
|
+
contentBlocks.push({ type: "tool_use", id: t.id, name: t.name, input: args });
|
|
112
|
+
toolAccum[index] = { arguments: "" };
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
const flushRemainingToolCalls = async () => {
|
|
116
|
+
for (let i = 0; i < toolAccum.length; i++) {
|
|
117
|
+
if (toolAccum[i]?.id && toolAccum[i]?.name)
|
|
118
|
+
await tryEmitToolCall(i);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const finish = () => {
|
|
122
|
+
if (textBlockIndex >= 0)
|
|
123
|
+
contentBlocks[textBlockIndex].text = textAccum;
|
|
124
|
+
else if (textAccum.trim())
|
|
125
|
+
contentBlocks.unshift({ type: "text", text: textAccum });
|
|
126
|
+
return contentBlocks;
|
|
127
|
+
};
|
|
128
|
+
while (true) {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done)
|
|
131
|
+
break;
|
|
132
|
+
buffer += decoder.decode(value, { stream: true });
|
|
133
|
+
const lines = buffer.split("\n");
|
|
134
|
+
buffer = lines.pop() ?? "";
|
|
135
|
+
let batch = "";
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const parsed = parseStreamChunk(line);
|
|
138
|
+
if (!parsed)
|
|
139
|
+
continue;
|
|
140
|
+
if ("__done" in parsed && parsed.__done === true) {
|
|
141
|
+
if (batch)
|
|
142
|
+
callbacks.onTextDelta(batch);
|
|
143
|
+
await flushRemainingToolCalls();
|
|
144
|
+
return finish();
|
|
145
|
+
}
|
|
146
|
+
const choices = parsed.choices;
|
|
147
|
+
const delta = choices?.[0]?.delta;
|
|
148
|
+
const finishReason = choices?.[0]?.finish_reason;
|
|
149
|
+
if (typeof delta?.content === "string" && delta.content) {
|
|
150
|
+
textAccum += delta.content;
|
|
151
|
+
batch += delta.content;
|
|
152
|
+
if (textBlockIndex < 0) {
|
|
153
|
+
contentBlocks.push({ type: "text", text: textAccum });
|
|
154
|
+
textBlockIndex = contentBlocks.length - 1;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
contentBlocks[textBlockIndex].text = textAccum;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const tc = delta?.tool_calls;
|
|
161
|
+
if (Array.isArray(tc)) {
|
|
162
|
+
for (const d of tc) {
|
|
163
|
+
const i = d.index ?? 0;
|
|
164
|
+
if (!toolAccum[i])
|
|
165
|
+
toolAccum[i] = { arguments: "" };
|
|
166
|
+
if (d.id)
|
|
167
|
+
toolAccum[i].id = d.id;
|
|
168
|
+
if (d.name)
|
|
169
|
+
toolAccum[i].name = d.name;
|
|
170
|
+
if (typeof d.arguments === "string")
|
|
171
|
+
toolAccum[i].arguments += d.arguments;
|
|
172
|
+
}
|
|
173
|
+
for (let i = 0; i < toolAccum.length; i++) {
|
|
174
|
+
await tryEmitToolCall(i);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (finishReason) {
|
|
178
|
+
if (batch)
|
|
179
|
+
callbacks.onTextDelta(batch);
|
|
180
|
+
await flushRemainingToolCalls();
|
|
181
|
+
return finish();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (batch)
|
|
185
|
+
callbacks.onTextDelta(batch);
|
|
186
|
+
}
|
|
187
|
+
await flushRemainingToolCalls();
|
|
188
|
+
return finish();
|
|
31
189
|
}
|
|
32
190
|
const SUMMARIZE_SYSTEM = "You are a summarizer. Summarize the following conversation between user and assistant, including any tool use and results. Preserve the user's goal, key decisions, and important facts. Output only the summary, no preamble.";
|
|
33
191
|
export async function callSummarize(apiKey, messages, model) {
|
|
@@ -37,18 +195,33 @@ export async function callSummarize(apiKey, messages, model) {
|
|
|
37
195
|
system: SUMMARIZE_SYSTEM,
|
|
38
196
|
messages,
|
|
39
197
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
198
|
+
let lastError = null;
|
|
199
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
200
|
+
const res = await fetch(config.apiUrl, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
Authorization: `Bearer ${apiKey}`,
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify(body),
|
|
207
|
+
});
|
|
208
|
+
if (res.ok) {
|
|
209
|
+
const data = (await res.json());
|
|
210
|
+
const blocks = data.content ?? [];
|
|
211
|
+
const textBlock = blocks.find((b) => b.type === "text" && b.text);
|
|
212
|
+
return (textBlock?.text ?? "").trim();
|
|
213
|
+
}
|
|
214
|
+
const text = await res.text();
|
|
215
|
+
lastError = new Error(`Summarize API ${res.status}: ${text}`);
|
|
216
|
+
if ((res.status === 429 || res.status === 503) && attempt < MAX_RETRIES) {
|
|
217
|
+
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);
|
|
221
|
+
await sleep(waitMs);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
throw lastError;
|
|
225
|
+
}
|
|
226
|
+
throw lastError;
|
|
54
227
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps stdin and merges ESC-prefix (meta) sequences into a single chunk.
|
|
3
|
+
*
|
|
4
|
+
* With "Use option as meta key", Terminal.app sends Option+Left as two chunks:
|
|
5
|
+
* \x1b then \x1b[D. Ink's parser then sees lone Escape then plain Left, so
|
|
6
|
+
* word movement never gets Meta+arrow. Readline-style TUIs work because they
|
|
7
|
+
* buffer at the stream level: they wait for the next chunk after \x1b and
|
|
8
|
+
* merge before parsing. This wrapper does the same so Option+arrow and
|
|
9
|
+
* Opt+F/B work without terminal config.
|
|
10
|
+
*/
|
|
11
|
+
import { Readable } from "node:stream";
|
|
12
|
+
const ESC = "\x1b";
|
|
13
|
+
const META_MERGE_MS = 25;
|
|
14
|
+
export function createBufferedStdin(stdin, options = {}) {
|
|
15
|
+
const mergeMs = options.mergeMs ?? META_MERGE_MS;
|
|
16
|
+
let escapeBuffer = null;
|
|
17
|
+
let escapeTimer = null;
|
|
18
|
+
let readableAttached = false;
|
|
19
|
+
const stream = new Readable({
|
|
20
|
+
read() { },
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
});
|
|
23
|
+
const rawStdin = stdin;
|
|
24
|
+
function onReadable() {
|
|
25
|
+
let chunk;
|
|
26
|
+
while ((chunk = stdin.read()) !== null) {
|
|
27
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
28
|
+
if (escapeBuffer !== null) {
|
|
29
|
+
stream.push(escapeBuffer + s);
|
|
30
|
+
escapeBuffer = null;
|
|
31
|
+
if (escapeTimer) {
|
|
32
|
+
clearTimeout(escapeTimer);
|
|
33
|
+
escapeTimer = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (s === ESC) {
|
|
37
|
+
escapeBuffer = ESC;
|
|
38
|
+
escapeTimer = setTimeout(() => {
|
|
39
|
+
escapeTimer = null;
|
|
40
|
+
if (escapeBuffer !== null) {
|
|
41
|
+
stream.push(escapeBuffer);
|
|
42
|
+
escapeBuffer = null;
|
|
43
|
+
}
|
|
44
|
+
}, mergeMs);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
stream.push(s);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
stream.setRawMode = (enabled) => {
|
|
52
|
+
if (escapeTimer) {
|
|
53
|
+
clearTimeout(escapeTimer);
|
|
54
|
+
escapeTimer = null;
|
|
55
|
+
}
|
|
56
|
+
escapeBuffer = null;
|
|
57
|
+
if (rawStdin.setRawMode)
|
|
58
|
+
rawStdin.setRawMode(enabled);
|
|
59
|
+
if (enabled) {
|
|
60
|
+
if (!readableAttached) {
|
|
61
|
+
rawStdin.setEncoding?.("utf8");
|
|
62
|
+
stdin.on("readable", onReadable);
|
|
63
|
+
readableAttached = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (readableAttached) {
|
|
68
|
+
stdin.removeListener("readable", onReadable);
|
|
69
|
+
readableAttached = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
stream.setEncoding = (enc) => {
|
|
74
|
+
if (rawStdin.setEncoding)
|
|
75
|
+
rawStdin.setEncoding(enc);
|
|
76
|
+
return stream;
|
|
77
|
+
};
|
|
78
|
+
if (rawStdin.ref)
|
|
79
|
+
stream.ref = rawStdin.ref.bind(rawStdin);
|
|
80
|
+
if (rawStdin.unref)
|
|
81
|
+
stream.unref = rawStdin.unref.bind(rawStdin);
|
|
82
|
+
stream.isTTY = rawStdin.isTTY;
|
|
83
|
+
const origDestroy = stream.destroy.bind(stream);
|
|
84
|
+
stream.destroy = function (err) {
|
|
85
|
+
if (readableAttached) {
|
|
86
|
+
stdin.removeListener("readable", onReadable);
|
|
87
|
+
readableAttached = false;
|
|
88
|
+
}
|
|
89
|
+
if (escapeTimer)
|
|
90
|
+
clearTimeout(escapeTimer);
|
|
91
|
+
escapeBuffer = null;
|
|
92
|
+
return origDestroy(err);
|
|
93
|
+
};
|
|
94
|
+
return stream;
|
|
95
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -61,6 +61,7 @@ export function saveModel(model) {
|
|
|
61
61
|
}
|
|
62
62
|
export const config = {
|
|
63
63
|
apiUrl: "https://openrouter.ai/api/v1/messages",
|
|
64
|
+
chatCompletionsUrl: "https://openrouter.ai/api/v1/chat/completions",
|
|
64
65
|
modelsUrl: "https://openrouter.ai/api/v1/models",
|
|
65
66
|
get apiKey() {
|
|
66
67
|
return getApiKey();
|
package/dist/repl.js
CHANGED
|
@@ -27,6 +27,8 @@ function wordStartBackward(value, cursor) {
|
|
|
27
27
|
}
|
|
28
28
|
function wordEndForward(value, cursor) {
|
|
29
29
|
let i = cursor;
|
|
30
|
+
while (i < value.length && !/[\w]/.test(value[i]))
|
|
31
|
+
i++;
|
|
30
32
|
while (i < value.length && /[\w]/.test(value[i]))
|
|
31
33
|
i++;
|
|
32
34
|
return i;
|
|
@@ -244,7 +246,14 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
244
246
|
const queuedMessageRef = useRef(null);
|
|
245
247
|
const lastUserMessageRef = useRef("");
|
|
246
248
|
const [logScrollOffset, setLogScrollOffset] = useState(0);
|
|
249
|
+
const scrollBoundsRef = useRef({ maxLogScrollOffset: 0, logViewportHeight: 1 });
|
|
247
250
|
const prevEscRef = useRef(false);
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
process.stdout.write("\x1b[?1006h\x1b[?1000h");
|
|
253
|
+
return () => {
|
|
254
|
+
process.stdout.write("\x1b[?1006l\x1b[?1000l");
|
|
255
|
+
};
|
|
256
|
+
}, []);
|
|
248
257
|
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
249
258
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
250
259
|
const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
|
|
@@ -384,8 +393,16 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
384
393
|
if (canonical === "/clear") {
|
|
385
394
|
setMessages([]);
|
|
386
395
|
setLogScrollOffset(0);
|
|
387
|
-
|
|
388
|
-
|
|
396
|
+
const model = getModel();
|
|
397
|
+
const version = getVersion();
|
|
398
|
+
const banner = [
|
|
399
|
+
"",
|
|
400
|
+
matchaGradient(bigLogo),
|
|
401
|
+
colors.accent(` ideacode v${version}`) + colors.dim(" · ") + colors.accentPale(model) + colors.dim(" · ") + colors.bold("OpenRouter") + colors.dim(` · ${cwd}`),
|
|
402
|
+
colors.mutedDark(" / commands ! shell @ files · Ctrl+P palette · Ctrl+C or /q to quit"),
|
|
403
|
+
"",
|
|
404
|
+
];
|
|
405
|
+
setLogLines(banner);
|
|
389
406
|
return true;
|
|
390
407
|
}
|
|
391
408
|
if (canonical === "/palette" || userInput === "/") {
|
|
@@ -412,10 +429,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
412
429
|
appendLog("");
|
|
413
430
|
return true;
|
|
414
431
|
}
|
|
415
|
-
if (messages.length === 0) {
|
|
416
|
-
setLogLines([]);
|
|
417
|
-
setLogScrollOffset(0);
|
|
418
|
-
}
|
|
419
432
|
lastUserMessageRef.current = userInput;
|
|
420
433
|
appendLog("");
|
|
421
434
|
appendLog(userPromptBox(userInput));
|
|
@@ -435,11 +448,9 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
435
448
|
for (;;) {
|
|
436
449
|
setLoading(true);
|
|
437
450
|
const response = await callApi(apiKey, state, systemPrompt, currentModel);
|
|
438
|
-
setLoading(false);
|
|
439
451
|
const contentBlocks = response.content ?? [];
|
|
440
452
|
const toolResults = [];
|
|
441
|
-
for (
|
|
442
|
-
const block = contentBlocks[bi];
|
|
453
|
+
for (const block of contentBlocks) {
|
|
443
454
|
if (block.type === "text" && block.text?.trim()) {
|
|
444
455
|
if (lastLogLineRef.current !== "")
|
|
445
456
|
appendLog("");
|
|
@@ -463,11 +474,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
463
474
|
const contentForApi = truncateToolResult(result);
|
|
464
475
|
const tokens = estimateTokensForString(contentForApi);
|
|
465
476
|
appendLog(toolResultTokenLine(tokens, ok));
|
|
466
|
-
if (block.id)
|
|
477
|
+
if (block.id)
|
|
467
478
|
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: contentForApi });
|
|
468
|
-
}
|
|
469
479
|
}
|
|
470
480
|
}
|
|
481
|
+
setLoading(false);
|
|
471
482
|
state = [...state, { role: "assistant", content: contentBlocks }];
|
|
472
483
|
if (toolResults.length === 0) {
|
|
473
484
|
setMessages(state);
|
|
@@ -517,11 +528,27 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
517
528
|
}
|
|
518
529
|
}
|
|
519
530
|
catch (err) {
|
|
531
|
+
setLoading(false);
|
|
520
532
|
appendLog(colors.error(`${icons.error} ${err instanceof Error ? err.message : String(err)}`));
|
|
521
533
|
appendLog("");
|
|
522
534
|
}
|
|
523
535
|
}, [processInput, handleQuit, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
|
|
524
536
|
useInput((input, key) => {
|
|
537
|
+
if (typeof input === "string" && /\[<\d+;\d+;\d+[Mm]/.test(input)) {
|
|
538
|
+
const isWheelUp = input.includes("<64;");
|
|
539
|
+
const isWheelDown = input.includes("<65;");
|
|
540
|
+
if (isWheelUp || isWheelDown) {
|
|
541
|
+
const step = 3;
|
|
542
|
+
const { maxLogScrollOffset: maxOff } = scrollBoundsRef.current;
|
|
543
|
+
if (isWheelUp) {
|
|
544
|
+
setLogScrollOffset((prev) => Math.min(maxOff, prev + step));
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
setLogScrollOffset((prev) => Math.max(0, prev - step));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
525
552
|
if (showHelpModal) {
|
|
526
553
|
setShowHelpModal(false);
|
|
527
554
|
return;
|
|
@@ -624,7 +651,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
624
651
|
if (key.return) {
|
|
625
652
|
const selected = filteredSlashCommands[clampedSlashIndex];
|
|
626
653
|
if (selected) {
|
|
627
|
-
skipNextSubmitRef.current = true;
|
|
628
654
|
setInputValue("");
|
|
629
655
|
setInputCursor(0);
|
|
630
656
|
if (selected.cmd === "/models") {
|
|
@@ -778,6 +804,20 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
778
804
|
setInputCursor(wordEndForward(inputValue, cur));
|
|
779
805
|
return;
|
|
780
806
|
}
|
|
807
|
+
if (key.meta && (input === "b" || input === "f")) {
|
|
808
|
+
if (input === "b")
|
|
809
|
+
setInputCursor(wordStartBackward(inputValue, cur));
|
|
810
|
+
else
|
|
811
|
+
setInputCursor(wordEndForward(inputValue, cur));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (key.ctrl && (input === "f" || input === "b")) {
|
|
815
|
+
if (input === "f")
|
|
816
|
+
setInputCursor(Math.min(len, cur + 1));
|
|
817
|
+
else
|
|
818
|
+
setInputCursor(Math.max(0, cur - 1));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
781
821
|
if (key.ctrl && input === "j") {
|
|
782
822
|
setInputValue((prev) => prev.slice(0, cur) + "\n" + prev.slice(cur));
|
|
783
823
|
setInputCursor(cur + 1);
|
|
@@ -868,14 +908,25 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
868
908
|
})();
|
|
869
909
|
const reservedLines = 1 + inputLineCount + (loading ? 2 : 1);
|
|
870
910
|
const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
911
|
+
const effectiveLogLines = logLines;
|
|
912
|
+
const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
|
|
913
|
+
scrollBoundsRef.current = { maxLogScrollOffset, logViewportHeight };
|
|
914
|
+
let logStartIndex = Math.max(0, effectiveLogLines.length - logViewportHeight - Math.min(logScrollOffset, maxLogScrollOffset));
|
|
915
|
+
if (logScrollOffset >= maxLogScrollOffset - 1 && maxLogScrollOffset > 0) {
|
|
916
|
+
logStartIndex = 0;
|
|
917
|
+
}
|
|
918
|
+
const sliceEnd = logStartIndex + logViewportHeight;
|
|
919
|
+
const visibleLogLines = logStartIndex === 0 && effectiveLogLines.length > 0
|
|
920
|
+
? ["", ...effectiveLogLines.slice(0, logViewportHeight - 1)]
|
|
921
|
+
: effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
874
922
|
if (showHelpModal) {
|
|
875
|
-
const helpModalWidth =
|
|
876
|
-
const
|
|
923
|
+
const helpModalWidth = Math.min(88, Math.max(80, termColumns - 4));
|
|
924
|
+
const helpContentRows = 20;
|
|
925
|
+
const helpTopPad = Math.max(0, Math.floor((termRows - helpContentRows) / 2));
|
|
877
926
|
const helpLeftPad = Math.max(0, Math.floor((termColumns - helpModalWidth) / 2));
|
|
878
|
-
|
|
927
|
+
const labelWidth = 20;
|
|
928
|
+
const descWidth = helpModalWidth - (2 * 2) - labelWidth - 2;
|
|
929
|
+
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, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " Message " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: "gray", children: " Type and Enter to send to the agent. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " / " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: "gray", children: " Commands. Type / then pick: /models, /brave, /help, /clear, /status, /q. Ctrl+P palette. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " @ " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: "gray", children: " Attach files. Type @ then path; Tab to complete. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " ! " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: "gray", children: " Run a shell command. Type ! then the command. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " Word / char nav " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: "gray", children: " Ctrl+\u2190/\u2192 or Meta+\u2190/\u2192 word; Ctrl+F/B char (Emacs). Opt+\u2190/\u2192 needs terminal to send Meta (e.g. iTerm2: Esc+). " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " Scroll " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: "gray", children: " Trackpad/\u2191/\u2193 scroll. To select text: hold Option (iTerm2) or Fn (Terminal.app) or Shift (Windows/Linux). " }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: " Press any key to close " }) })] })] }), _jsx(Box, { flexGrow: 1 })] }));
|
|
879
930
|
}
|
|
880
931
|
if (showBraveKeyModal) {
|
|
881
932
|
const braveModalWidth = 52;
|
|
@@ -891,13 +942,13 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
891
942
|
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 })] }));
|
|
892
943
|
}
|
|
893
944
|
const footerLines = suggestionBoxLines + 1 + inputLineCount;
|
|
894
|
-
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", height: logViewportHeight, overflow: "hidden", children: visibleLogLines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line },
|
|
945
|
+
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", height: logViewportHeight, overflow: "hidden", children: visibleLogLines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line }, effectiveLogLines.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) => {
|
|
895
946
|
const i = filteredSlashCommands.length - 1 - rev;
|
|
896
947
|
return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: "gray", children: [" \u2014 ", c.desc] })] }, c.cmd));
|
|
897
948
|
})), _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) => {
|
|
898
949
|
const i = filteredFilePaths.length - 1 - rev;
|
|
899
950
|
return (_jsxs(Text, { color: i === clampedAtFileIndex ? inkColors.primary : undefined, children: [i === clampedAtFileIndex ? "› " : " ", p] }, p));
|
|
900
|
-
})), _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: ` · / ! @
|
|
951
|
+
})), _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: ` · / ! @ 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: "gray", children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
|
|
901
952
|
const lines = inputValue.split("\n");
|
|
902
953
|
let lineStart = 0;
|
|
903
954
|
return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps process.stdin and normalizes escape sequences so Option/Cmd key combos
|
|
3
|
+
* work out of the box on macOS without terminal key mapping.
|
|
4
|
+
*
|
|
5
|
+
* Many terminals send Esc+key (e.g. \x1b b, \x1b f) for Option+key. We translate
|
|
6
|
+
* those to CSI sequences that Ink's parser already understands (e.g. \x1b[1;3D
|
|
7
|
+
* for Option+Left). We also translate \x1b U and \x1b \x7f to Ctrl+U (delete
|
|
8
|
+
* line) so Cmd+Backspace can work when the terminal sends Esc+U or Esc+Backspace.
|
|
9
|
+
*/
|
|
10
|
+
import { Transform } from "node:stream";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
const ESC = "\x1b";
|
|
13
|
+
const CSI_OPT_LEFT = "\x1b[1;3D";
|
|
14
|
+
const CSI_OPT_RIGHT = "\x1b[1;3C";
|
|
15
|
+
const CTRL_U = "\x15";
|
|
16
|
+
function createStdinTransform() {
|
|
17
|
+
let buffer = "";
|
|
18
|
+
const transform = new Transform({
|
|
19
|
+
transform(chunk, _encoding, callback) {
|
|
20
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
21
|
+
const out = [];
|
|
22
|
+
while (buffer.length > 0) {
|
|
23
|
+
if (buffer[0] !== ESC) {
|
|
24
|
+
out.push(buffer[0]);
|
|
25
|
+
buffer = buffer.slice(1);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (buffer.length === 1) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const second = buffer[1];
|
|
32
|
+
if (second === "b") {
|
|
33
|
+
out.push(CSI_OPT_LEFT);
|
|
34
|
+
buffer = buffer.slice(2);
|
|
35
|
+
}
|
|
36
|
+
else if (second === "f") {
|
|
37
|
+
out.push(CSI_OPT_RIGHT);
|
|
38
|
+
buffer = buffer.slice(2);
|
|
39
|
+
}
|
|
40
|
+
else if (second === "U" || second === "\x7f") {
|
|
41
|
+
out.push(CTRL_U);
|
|
42
|
+
buffer = buffer.slice(2);
|
|
43
|
+
}
|
|
44
|
+
else if (second === "[") {
|
|
45
|
+
const idx = buffer.slice(2).search(/[A-Za-z~$^]/);
|
|
46
|
+
if (idx === -1)
|
|
47
|
+
break;
|
|
48
|
+
const len = 2 + idx + 1;
|
|
49
|
+
out.push(buffer.slice(0, len));
|
|
50
|
+
buffer = buffer.slice(len);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
out.push(buffer.slice(0, 2));
|
|
54
|
+
buffer = buffer.slice(2);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (out.length > 0) {
|
|
58
|
+
transform.push(out.join(""));
|
|
59
|
+
}
|
|
60
|
+
callback();
|
|
61
|
+
},
|
|
62
|
+
flush(callback) {
|
|
63
|
+
if (buffer.length > 0) {
|
|
64
|
+
transform.push(buffer);
|
|
65
|
+
buffer = "";
|
|
66
|
+
}
|
|
67
|
+
callback();
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const stdin = process.stdin;
|
|
71
|
+
transform.setRawMode = (mode) => stdin.setRawMode(mode);
|
|
72
|
+
transform.setEncoding = (enc) => stdin.setEncoding(enc);
|
|
73
|
+
transform.ref = () => stdin.ref();
|
|
74
|
+
transform.unref = () => stdin.unref();
|
|
75
|
+
transform.isTTY = stdin.isTTY ?? false;
|
|
76
|
+
return transform;
|
|
77
|
+
}
|
|
78
|
+
export { createStdinTransform };
|