ideacode 1.1.4 → 1.1.6
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 +139 -0
- package/dist/buffered-stdin.js +95 -0
- package/dist/config.js +1 -0
- package/dist/repl.js +112 -38
- package/dist/stdin-transform.js +78 -0
- package/dist/tools/index.js +7 -3
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -29,6 +29,145 @@ export async function callApi(apiKey, messages, systemPrompt, model) {
|
|
|
29
29
|
throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
30
30
|
return res.json();
|
|
31
31
|
}
|
|
32
|
+
function parseStreamChunk(line) {
|
|
33
|
+
if (line.startsWith("data: ")) {
|
|
34
|
+
const data = line.slice(6).trim();
|
|
35
|
+
if (data === "[DONE]")
|
|
36
|
+
return { __done: true };
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(data);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
export async function callApiStream(apiKey, messages, systemPrompt, model, callbacks, signal) {
|
|
47
|
+
const chatMessages = [
|
|
48
|
+
{ role: "system", content: systemPrompt },
|
|
49
|
+
...messages,
|
|
50
|
+
];
|
|
51
|
+
const body = {
|
|
52
|
+
model,
|
|
53
|
+
max_tokens: 8192,
|
|
54
|
+
messages: chatMessages,
|
|
55
|
+
tools: makeSchema(),
|
|
56
|
+
stream: true,
|
|
57
|
+
};
|
|
58
|
+
const res = await fetch(config.chatCompletionsUrl, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
Authorization: `Bearer ${apiKey}`,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
signal,
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
69
|
+
const reader = res.body?.getReader();
|
|
70
|
+
if (!reader)
|
|
71
|
+
throw new Error("No response body");
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
let buffer = "";
|
|
74
|
+
const contentBlocks = [];
|
|
75
|
+
let textAccum = "";
|
|
76
|
+
let textBlockIndex = -1;
|
|
77
|
+
const toolAccum = [];
|
|
78
|
+
const tryEmitToolCall = async (index) => {
|
|
79
|
+
const t = toolAccum[index];
|
|
80
|
+
if (!t?.id || !t?.name)
|
|
81
|
+
return false;
|
|
82
|
+
let args = {};
|
|
83
|
+
if (t.arguments.trim()) {
|
|
84
|
+
try {
|
|
85
|
+
args = JSON.parse(t.arguments);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
await callbacks.onToolCall({ id: t.id, name: t.name, input: args });
|
|
92
|
+
contentBlocks.push({ type: "tool_use", id: t.id, name: t.name, input: args });
|
|
93
|
+
toolAccum[index] = { arguments: "" };
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
const flushRemainingToolCalls = async () => {
|
|
97
|
+
for (let i = 0; i < toolAccum.length; i++) {
|
|
98
|
+
if (toolAccum[i]?.id && toolAccum[i]?.name)
|
|
99
|
+
await tryEmitToolCall(i);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const finish = () => {
|
|
103
|
+
if (textBlockIndex >= 0)
|
|
104
|
+
contentBlocks[textBlockIndex].text = textAccum;
|
|
105
|
+
else if (textAccum.trim())
|
|
106
|
+
contentBlocks.unshift({ type: "text", text: textAccum });
|
|
107
|
+
return contentBlocks;
|
|
108
|
+
};
|
|
109
|
+
while (true) {
|
|
110
|
+
const { done, value } = await reader.read();
|
|
111
|
+
if (done)
|
|
112
|
+
break;
|
|
113
|
+
buffer += decoder.decode(value, { stream: true });
|
|
114
|
+
const lines = buffer.split("\n");
|
|
115
|
+
buffer = lines.pop() ?? "";
|
|
116
|
+
let batch = "";
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const parsed = parseStreamChunk(line);
|
|
119
|
+
if (!parsed)
|
|
120
|
+
continue;
|
|
121
|
+
if ("__done" in parsed && parsed.__done === true) {
|
|
122
|
+
if (batch)
|
|
123
|
+
callbacks.onTextDelta(batch);
|
|
124
|
+
await flushRemainingToolCalls();
|
|
125
|
+
return finish();
|
|
126
|
+
}
|
|
127
|
+
const choices = parsed.choices;
|
|
128
|
+
const delta = choices?.[0]?.delta;
|
|
129
|
+
const finishReason = choices?.[0]?.finish_reason;
|
|
130
|
+
if (typeof delta?.content === "string" && delta.content) {
|
|
131
|
+
textAccum += delta.content;
|
|
132
|
+
batch += delta.content;
|
|
133
|
+
if (textBlockIndex < 0) {
|
|
134
|
+
contentBlocks.push({ type: "text", text: textAccum });
|
|
135
|
+
textBlockIndex = contentBlocks.length - 1;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
contentBlocks[textBlockIndex].text = textAccum;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const tc = delta?.tool_calls;
|
|
142
|
+
if (Array.isArray(tc)) {
|
|
143
|
+
for (const d of tc) {
|
|
144
|
+
const i = d.index ?? 0;
|
|
145
|
+
if (!toolAccum[i])
|
|
146
|
+
toolAccum[i] = { arguments: "" };
|
|
147
|
+
if (d.id)
|
|
148
|
+
toolAccum[i].id = d.id;
|
|
149
|
+
if (d.name)
|
|
150
|
+
toolAccum[i].name = d.name;
|
|
151
|
+
if (typeof d.arguments === "string")
|
|
152
|
+
toolAccum[i].arguments += d.arguments;
|
|
153
|
+
}
|
|
154
|
+
for (let i = 0; i < toolAccum.length; i++) {
|
|
155
|
+
await tryEmitToolCall(i);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (finishReason) {
|
|
159
|
+
if (batch)
|
|
160
|
+
callbacks.onTextDelta(batch);
|
|
161
|
+
await flushRemainingToolCalls();
|
|
162
|
+
return finish();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (batch)
|
|
166
|
+
callbacks.onTextDelta(batch);
|
|
167
|
+
}
|
|
168
|
+
await flushRemainingToolCalls();
|
|
169
|
+
return finish();
|
|
170
|
+
}
|
|
32
171
|
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
172
|
export async function callSummarize(apiKey, messages, model) {
|
|
34
173
|
const body = {
|
|
@@ -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;
|
|
@@ -91,7 +93,7 @@ function replayMessagesToLogLines(messages) {
|
|
|
91
93
|
const msg = messages[i];
|
|
92
94
|
if (msg.role === "user") {
|
|
93
95
|
if (typeof msg.content === "string") {
|
|
94
|
-
lines.push(...userPromptBox(msg.content).split("\n"), "");
|
|
96
|
+
lines.push("", ...userPromptBox(msg.content).split("\n"), "");
|
|
95
97
|
}
|
|
96
98
|
else if (Array.isArray(msg.content)) {
|
|
97
99
|
const prev = messages[i - 1];
|
|
@@ -102,11 +104,12 @@ function replayMessagesToLogLines(messages) {
|
|
|
102
104
|
for (const tr of toolResults) {
|
|
103
105
|
const block = toolUses.find((b) => b.id === tr.tool_use_id);
|
|
104
106
|
if (block?.name) {
|
|
107
|
+
const name = block.name.trim().toLowerCase();
|
|
105
108
|
const firstVal = block.input && typeof block.input === "object" ? Object.values(block.input)[0] : undefined;
|
|
106
109
|
const argPreview = String(firstVal ?? "").slice(0, 50) || "—";
|
|
107
110
|
const content = tr.content ?? "";
|
|
108
111
|
const ok = !content.startsWith("error:");
|
|
109
|
-
lines.push(toolCallBox(
|
|
112
|
+
lines.push(toolCallBox(name, argPreview, ok));
|
|
110
113
|
const preview = content.split("\n")[0]?.slice(0, 60) ?? "";
|
|
111
114
|
lines.push(toolResultLine(preview, ok));
|
|
112
115
|
const tokens = estimateTokensForString(content);
|
|
@@ -120,7 +123,8 @@ function replayMessagesToLogLines(messages) {
|
|
|
120
123
|
const blocks = msg.content;
|
|
121
124
|
for (const block of blocks) {
|
|
122
125
|
if (block.type === "text" && block.text?.trim()) {
|
|
123
|
-
lines.
|
|
126
|
+
if (lines[lines.length - 1] !== "")
|
|
127
|
+
lines.push("");
|
|
124
128
|
lines.push(...agentMessage(block.text).trimEnd().split("\n"));
|
|
125
129
|
}
|
|
126
130
|
}
|
|
@@ -157,40 +161,55 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
157
161
|
██║██████╔╝███████╗██║ ██║╚██████╗╚██████╔╝██████╔╝███████╗
|
|
158
162
|
╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
159
163
|
`;
|
|
164
|
+
const hasRestoredLogRef = useRef(false);
|
|
165
|
+
const restoredCwdRef = useRef(null);
|
|
160
166
|
const [logLines, setLogLines] = useState(() => {
|
|
161
167
|
const model = getModel();
|
|
162
168
|
const version = getVersion();
|
|
163
|
-
|
|
169
|
+
const banner = [
|
|
164
170
|
"",
|
|
165
171
|
matchaGradient(bigLogo),
|
|
166
172
|
colors.accent(` ideacode v${version}`) + colors.dim(" · ") + colors.accentPale(model) + colors.dim(" · ") + colors.bold("OpenRouter") + colors.dim(` · ${cwd}`),
|
|
167
173
|
colors.mutedDark(" / commands ! shell @ files · Ctrl+P palette · Ctrl+C or /q to quit"),
|
|
168
174
|
"",
|
|
169
175
|
];
|
|
176
|
+
const loaded = loadConversation(cwd);
|
|
177
|
+
if (loaded.length > 0) {
|
|
178
|
+
hasRestoredLogRef.current = true;
|
|
179
|
+
restoredCwdRef.current = cwd;
|
|
180
|
+
return [...banner, ...replayMessagesToLogLines(loaded)];
|
|
181
|
+
}
|
|
182
|
+
return banner;
|
|
170
183
|
});
|
|
171
184
|
const [inputValue, setInputValue] = useState("");
|
|
172
185
|
const [currentModel, setCurrentModel] = useState(getModel);
|
|
173
186
|
const [messages, setMessages] = useState(() => loadConversation(cwd));
|
|
174
187
|
const messagesRef = useRef(messages);
|
|
175
|
-
const hasRestoredLogRef = useRef(false);
|
|
176
188
|
useEffect(() => {
|
|
177
189
|
messagesRef.current = messages;
|
|
178
190
|
}, [messages]);
|
|
179
191
|
useEffect(() => {
|
|
192
|
+
if (hasRestoredLogRef.current && restoredCwdRef.current === cwd)
|
|
193
|
+
return;
|
|
180
194
|
const loaded = loadConversation(cwd);
|
|
181
|
-
|
|
195
|
+
const model = getModel();
|
|
196
|
+
const version = getVersion();
|
|
197
|
+
const banner = [
|
|
198
|
+
"",
|
|
199
|
+
matchaGradient(bigLogo),
|
|
200
|
+
colors.accent(` ideacode v${version}`) + colors.dim(" · ") + colors.accent(model) + colors.dim(" · ") + colors.accentPale("OpenRouter") + colors.dim(` · ${cwd}`),
|
|
201
|
+
colors.mutedDark(" / commands ! shell @ files · Ctrl+P palette · Ctrl+C or /q to quit"),
|
|
202
|
+
"",
|
|
203
|
+
];
|
|
204
|
+
if (loaded.length > 0) {
|
|
182
205
|
hasRestoredLogRef.current = true;
|
|
183
|
-
const model = getModel();
|
|
184
|
-
const version = getVersion();
|
|
185
|
-
const banner = [
|
|
186
|
-
"",
|
|
187
|
-
matchaGradient(bigLogo),
|
|
188
|
-
colors.accent(` ideacode v${version}`) + colors.dim(" · ") + colors.accent(model) + colors.dim(" · ") + colors.accentPale("OpenRouter") + colors.dim(` · ${cwd}`),
|
|
189
|
-
colors.mutedDark(" / commands ! shell @ files · Ctrl+P palette · Ctrl+C or /q to quit"),
|
|
190
|
-
"",
|
|
191
|
-
];
|
|
192
206
|
setLogLines([...banner, ...replayMessagesToLogLines(loaded)]);
|
|
193
207
|
}
|
|
208
|
+
else {
|
|
209
|
+
hasRestoredLogRef.current = false;
|
|
210
|
+
setLogLines(banner);
|
|
211
|
+
}
|
|
212
|
+
restoredCwdRef.current = cwd;
|
|
194
213
|
}, [cwd]);
|
|
195
214
|
const saveDebounceRef = useRef(null);
|
|
196
215
|
useEffect(() => {
|
|
@@ -227,7 +246,14 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
227
246
|
const queuedMessageRef = useRef(null);
|
|
228
247
|
const lastUserMessageRef = useRef("");
|
|
229
248
|
const [logScrollOffset, setLogScrollOffset] = useState(0);
|
|
249
|
+
const scrollBoundsRef = useRef({ maxLogScrollOffset: 0, logViewportHeight: 1 });
|
|
230
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
|
+
}, []);
|
|
231
257
|
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
232
258
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
233
259
|
const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
|
|
@@ -300,11 +326,16 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
300
326
|
useEffect(() => {
|
|
301
327
|
setAtSuggestionIndex(0);
|
|
302
328
|
}, [atFilter]);
|
|
329
|
+
const lastLogLineRef = useRef("");
|
|
303
330
|
const appendLog = useCallback((line) => {
|
|
304
331
|
const lines = line.split("\n");
|
|
305
332
|
if (lines.length > 1 && lines[0] === "")
|
|
306
333
|
lines.shift();
|
|
307
|
-
setLogLines((prev) =>
|
|
334
|
+
setLogLines((prev) => {
|
|
335
|
+
const next = [...prev, ...lines];
|
|
336
|
+
lastLogLineRef.current = next[next.length - 1] ?? "";
|
|
337
|
+
return next;
|
|
338
|
+
});
|
|
308
339
|
}, []);
|
|
309
340
|
useEffect(() => {
|
|
310
341
|
const version = getVersion();
|
|
@@ -362,8 +393,16 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
362
393
|
if (canonical === "/clear") {
|
|
363
394
|
setMessages([]);
|
|
364
395
|
setLogScrollOffset(0);
|
|
365
|
-
|
|
366
|
-
|
|
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);
|
|
367
406
|
return true;
|
|
368
407
|
}
|
|
369
408
|
if (canonical === "/palette" || userInput === "/") {
|
|
@@ -390,11 +429,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
390
429
|
appendLog("");
|
|
391
430
|
return true;
|
|
392
431
|
}
|
|
393
|
-
if (messages.length === 0) {
|
|
394
|
-
setLogLines([]);
|
|
395
|
-
setLogScrollOffset(0);
|
|
396
|
-
}
|
|
397
432
|
lastUserMessageRef.current = userInput;
|
|
433
|
+
appendLog("");
|
|
398
434
|
appendLog(userPromptBox(userInput));
|
|
399
435
|
appendLog("");
|
|
400
436
|
let state = [...messages, { role: "user", content: userInput }];
|
|
@@ -412,17 +448,16 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
412
448
|
for (;;) {
|
|
413
449
|
setLoading(true);
|
|
414
450
|
const response = await callApi(apiKey, state, systemPrompt, currentModel);
|
|
415
|
-
setLoading(false);
|
|
416
451
|
const contentBlocks = response.content ?? [];
|
|
417
452
|
const toolResults = [];
|
|
418
|
-
for (
|
|
419
|
-
const block = contentBlocks[bi];
|
|
453
|
+
for (const block of contentBlocks) {
|
|
420
454
|
if (block.type === "text" && block.text?.trim()) {
|
|
421
|
-
|
|
455
|
+
if (lastLogLineRef.current !== "")
|
|
456
|
+
appendLog("");
|
|
422
457
|
appendLog(agentMessage(block.text).trimEnd());
|
|
423
458
|
}
|
|
424
459
|
if (block.type === "tool_use" && block.name && block.input) {
|
|
425
|
-
const toolName = block.name;
|
|
460
|
+
const toolName = block.name.trim().toLowerCase();
|
|
426
461
|
const toolArgs = block.input;
|
|
427
462
|
const firstVal = Object.values(toolArgs)[0];
|
|
428
463
|
const argPreview = String(firstVal ?? "").slice(0, 100) || "—";
|
|
@@ -439,11 +474,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
439
474
|
const contentForApi = truncateToolResult(result);
|
|
440
475
|
const tokens = estimateTokensForString(contentForApi);
|
|
441
476
|
appendLog(toolResultTokenLine(tokens, ok));
|
|
442
|
-
if (block.id)
|
|
477
|
+
if (block.id)
|
|
443
478
|
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: contentForApi });
|
|
444
|
-
}
|
|
445
479
|
}
|
|
446
480
|
}
|
|
481
|
+
setLoading(false);
|
|
447
482
|
state = [...state, { role: "assistant", content: contentBlocks }];
|
|
448
483
|
if (toolResults.length === 0) {
|
|
449
484
|
setMessages(state);
|
|
@@ -498,6 +533,21 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
498
533
|
}
|
|
499
534
|
}, [processInput, handleQuit, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
|
|
500
535
|
useInput((input, key) => {
|
|
536
|
+
if (typeof input === "string" && /\[<\d+;\d+;\d+[Mm]/.test(input)) {
|
|
537
|
+
const isWheelUp = input.includes("<64;");
|
|
538
|
+
const isWheelDown = input.includes("<65;");
|
|
539
|
+
if (isWheelUp || isWheelDown) {
|
|
540
|
+
const step = 3;
|
|
541
|
+
const { maxLogScrollOffset: maxOff } = scrollBoundsRef.current;
|
|
542
|
+
if (isWheelUp) {
|
|
543
|
+
setLogScrollOffset((prev) => Math.min(maxOff, prev + step));
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
setLogScrollOffset((prev) => Math.max(0, prev - step));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
501
551
|
if (showHelpModal) {
|
|
502
552
|
setShowHelpModal(false);
|
|
503
553
|
return;
|
|
@@ -600,7 +650,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
600
650
|
if (key.return) {
|
|
601
651
|
const selected = filteredSlashCommands[clampedSlashIndex];
|
|
602
652
|
if (selected) {
|
|
603
|
-
skipNextSubmitRef.current = true;
|
|
604
653
|
setInputValue("");
|
|
605
654
|
setInputCursor(0);
|
|
606
655
|
if (selected.cmd === "/models") {
|
|
@@ -754,6 +803,20 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
754
803
|
setInputCursor(wordEndForward(inputValue, cur));
|
|
755
804
|
return;
|
|
756
805
|
}
|
|
806
|
+
if (key.meta && (input === "b" || input === "f")) {
|
|
807
|
+
if (input === "b")
|
|
808
|
+
setInputCursor(wordStartBackward(inputValue, cur));
|
|
809
|
+
else
|
|
810
|
+
setInputCursor(wordEndForward(inputValue, cur));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (key.ctrl && (input === "f" || input === "b")) {
|
|
814
|
+
if (input === "f")
|
|
815
|
+
setInputCursor(Math.min(len, cur + 1));
|
|
816
|
+
else
|
|
817
|
+
setInputCursor(Math.max(0, cur - 1));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
757
820
|
if (key.ctrl && input === "j") {
|
|
758
821
|
setInputValue((prev) => prev.slice(0, cur) + "\n" + prev.slice(cur));
|
|
759
822
|
setInputCursor(cur + 1);
|
|
@@ -844,14 +907,25 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
844
907
|
})();
|
|
845
908
|
const reservedLines = 1 + inputLineCount + (loading ? 2 : 1);
|
|
846
909
|
const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
|
|
847
|
-
const
|
|
848
|
-
const
|
|
849
|
-
|
|
910
|
+
const effectiveLogLines = logLines;
|
|
911
|
+
const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
|
|
912
|
+
scrollBoundsRef.current = { maxLogScrollOffset, logViewportHeight };
|
|
913
|
+
let logStartIndex = Math.max(0, effectiveLogLines.length - logViewportHeight - Math.min(logScrollOffset, maxLogScrollOffset));
|
|
914
|
+
if (logScrollOffset >= maxLogScrollOffset - 1 && maxLogScrollOffset > 0) {
|
|
915
|
+
logStartIndex = 0;
|
|
916
|
+
}
|
|
917
|
+
const sliceEnd = logStartIndex + logViewportHeight;
|
|
918
|
+
const visibleLogLines = logStartIndex === 0 && effectiveLogLines.length > 0
|
|
919
|
+
? ["", ...effectiveLogLines.slice(0, logViewportHeight - 1)]
|
|
920
|
+
: effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
850
921
|
if (showHelpModal) {
|
|
851
|
-
const helpModalWidth =
|
|
852
|
-
const
|
|
922
|
+
const helpModalWidth = Math.min(88, Math.max(80, termColumns - 4));
|
|
923
|
+
const helpContentRows = 20;
|
|
924
|
+
const helpTopPad = Math.max(0, Math.floor((termRows - helpContentRows) / 2));
|
|
853
925
|
const helpLeftPad = Math.max(0, Math.floor((termColumns - helpModalWidth) / 2));
|
|
854
|
-
|
|
926
|
+
const labelWidth = 20;
|
|
927
|
+
const descWidth = helpModalWidth - (2 * 2) - labelWidth - 2;
|
|
928
|
+
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 })] }));
|
|
855
929
|
}
|
|
856
930
|
if (showBraveKeyModal) {
|
|
857
931
|
const braveModalWidth = 52;
|
|
@@ -867,13 +941,13 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
867
941
|
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 })] }));
|
|
868
942
|
}
|
|
869
943
|
const footerLines = suggestionBoxLines + 1 + inputLineCount;
|
|
870
|
-
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 },
|
|
944
|
+
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) => {
|
|
871
945
|
const i = filteredSlashCommands.length - 1 - rev;
|
|
872
946
|
return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: "gray", children: [" \u2014 ", c.desc] })] }, c.cmd));
|
|
873
947
|
})), _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) => {
|
|
874
948
|
const i = filteredFilePaths.length - 1 - rev;
|
|
875
949
|
return (_jsxs(Text, { color: i === clampedAtFileIndex ? inkColors.primary : undefined, children: [i === clampedAtFileIndex ? "› " : " ", p] }, p));
|
|
876
|
-
})), _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: ` · / ! @
|
|
950
|
+
})), _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..." })] })) : ((() => {
|
|
877
951
|
const lines = inputValue.split("\n");
|
|
878
952
|
let lineStart = 0;
|
|
879
953
|
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 };
|
package/dist/tools/index.js
CHANGED
|
@@ -48,14 +48,18 @@ function getTools() {
|
|
|
48
48
|
const { web_search: _, ...rest } = TOOLS;
|
|
49
49
|
return rest;
|
|
50
50
|
}
|
|
51
|
+
function normalizeToolName(name) {
|
|
52
|
+
return name.trim().toLowerCase();
|
|
53
|
+
}
|
|
51
54
|
export async function runTool(name, args) {
|
|
52
|
-
|
|
55
|
+
const canonical = normalizeToolName(name);
|
|
56
|
+
if (canonical === "web_search" && !getBraveSearchApiKey()) {
|
|
53
57
|
return "error: Brave Search API key not set. Use /brave or set BRAVE_API_KEY to enable web search.";
|
|
54
58
|
}
|
|
55
59
|
const tools = getTools();
|
|
56
|
-
const def = tools[
|
|
60
|
+
const def = tools[canonical];
|
|
57
61
|
if (!def)
|
|
58
|
-
return `error: Unknown tool: ${
|
|
62
|
+
return `error: Unknown tool: ${canonical}`;
|
|
59
63
|
try {
|
|
60
64
|
const result = await def[2](args);
|
|
61
65
|
return result;
|