ideacode 1.1.5 → 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 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;
@@ -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
- appendLog(colors.success(`${icons.clear} Cleared conversation`));
388
- appendLog("");
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 (let bi = 0; bi < contentBlocks.length; bi++) {
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);
@@ -522,6 +533,21 @@ export function Repl({ apiKey, cwd, onQuit }) {
522
533
  }
523
534
  }, [processInput, handleQuit, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
524
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
+ }
525
551
  if (showHelpModal) {
526
552
  setShowHelpModal(false);
527
553
  return;
@@ -624,7 +650,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
624
650
  if (key.return) {
625
651
  const selected = filteredSlashCommands[clampedSlashIndex];
626
652
  if (selected) {
627
- skipNextSubmitRef.current = true;
628
653
  setInputValue("");
629
654
  setInputCursor(0);
630
655
  if (selected.cmd === "/models") {
@@ -778,6 +803,20 @@ export function Repl({ apiKey, cwd, onQuit }) {
778
803
  setInputCursor(wordEndForward(inputValue, cur));
779
804
  return;
780
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
+ }
781
820
  if (key.ctrl && input === "j") {
782
821
  setInputValue((prev) => prev.slice(0, cur) + "\n" + prev.slice(cur));
783
822
  setInputCursor(cur + 1);
@@ -868,14 +907,25 @@ export function Repl({ apiKey, cwd, onQuit }) {
868
907
  })();
869
908
  const reservedLines = 1 + inputLineCount + (loading ? 2 : 1);
870
909
  const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
871
- const maxLogScrollOffset = Math.max(0, logLines.length - logViewportHeight);
872
- const logStartIndex = Math.max(0, logLines.length - logViewportHeight - Math.min(logScrollOffset, maxLogScrollOffset));
873
- const visibleLogLines = logLines.slice(logStartIndex, logStartIndex + logViewportHeight);
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);
874
921
  if (showHelpModal) {
875
- const helpModalWidth = 56;
876
- const helpTopPad = Math.max(0, Math.floor((termRows - 20) / 2));
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));
877
925
  const helpLeftPad = Math.max(0, Math.floor((termColumns - helpModalWidth) / 2));
878
- return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: helpTopPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: helpLeftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: helpModalWidth, children: [_jsx(Text, { bold: true, children: " Help " }), _jsx(Text, { color: "gray", children: " What you can do " }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " Message " }), _jsx(Text, { color: "gray", children: " Type and Enter to send to the agent. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " / " }), _jsx(Text, { color: "gray", children: " Commands. Type / then pick: /models, /brave, /help, /clear, /status, /q. Ctrl+P palette. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " @ " }), _jsx(Text, { color: "gray", children: " Attach files. Type @ then path; Tab to complete. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " ! " }), _jsx(Text, { color: "gray", children: " Run a shell command. Type ! then the command. " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " Scroll " }), _jsx(Text, { color: "gray", children: " \u2191/\u2193 when input empty, or Ctrl/Opt+\u2191/\u2193 to scroll chat. " })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: " Press any key to close " }) })] })] }), _jsx(Box, { flexGrow: 1 })] }));
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 })] }));
879
929
  }
880
930
  if (showBraveKeyModal) {
881
931
  const braveModalWidth = 52;
@@ -891,13 +941,13 @@ export function Repl({ apiKey, cwd, onQuit }) {
891
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 })] }));
892
942
  }
893
943
  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 }, logLines.length - visibleLogLines.length + i))) }), loading && (_jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsxs(Text, { color: "gray", children: [" ", SPINNER[spinnerTick % SPINNER.length], " Thinking\u2026"] }) }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: "gray", children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: "gray", children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
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) => {
895
945
  const i = filteredSlashCommands.length - 1 - rev;
896
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));
897
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) => {
898
948
  const i = filteredFilePaths.length - 1 - rev;
899
949
  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: ` · / ! @ ↑/↓ scroll (when input empty) or Ctrl/Opt+↑/↓ Ctrl+J newline Tab queue Esc Esc edit ${pasteShortcut} paste Ctrl+C exit` })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: inputValue.length === 0 ? (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] }), _jsx(Text, { inverse: true, color: inkColors.primary, children: " " }), _jsx(Text, { color: "gray", children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
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..." })] })) : ((() => {
901
951
  const lines = inputValue.split("\n");
902
952
  let lineStart = 0;
903
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideacode",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "CLI TUI for AI agents via OpenRouter — agentic loop, tools, markdown",
5
5
  "type": "module",
6
6
  "repository": {