ideacode 1.3.2 → 1.3.3

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.
Files changed (2) hide show
  1. package/dist/repl.js +113 -61
  2. package/package.json +1 -1
package/dist/repl.js CHANGED
@@ -50,15 +50,14 @@ const PARALLEL_SAFE_TOOLS = new Set([
50
50
  ]);
51
51
  const LOADING_TICK_MS = 80;
52
52
  const MAX_EMPTY_ASSISTANT_RETRIES = 3;
53
- const TYPING_LAYOUT_FREEZE_MS = 180;
53
+ const TYPING_LAYOUT_FREEZE_MS = 120;
54
+ const INPUT_COMMIT_INTERVAL_MS = 45;
54
55
  const TRUNCATE_NOTE = "\n\n(Output truncated to save context. Use read with offset/limit, grep with a specific pattern, or tail with fewer lines to get more.)";
55
56
  function truncateToolResult(content) {
56
57
  if (content.length <= MAX_TOOL_RESULT_CHARS)
57
58
  return content;
58
59
  return content.slice(0, MAX_TOOL_RESULT_CHARS) + TRUNCATE_NOTE;
59
60
  }
60
- const isMac = process.platform === "darwin";
61
- const pasteShortcut = isMac ? "Cmd+V" : "Ctrl+V";
62
61
  function listFilesWithFilter(cwd, filter) {
63
62
  try {
64
63
  const pattern = path.join(cwd, "**", "*").replace(/\\/g, "/");
@@ -404,6 +403,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
404
403
  const [showHelpModal, setShowHelpModal] = useState(false);
405
404
  const [slashSuggestionIndex, setSlashSuggestionIndex] = useState(0);
406
405
  const [inputCursor, setInputCursor] = useState(0);
406
+ const inputValueRef = useRef(inputValue);
407
+ const inputCursorRef = useRef(inputCursor);
408
+ const inputCommitTimerRef = useRef(null);
409
+ const [layoutInputLineCount, setLayoutInputLineCount] = useState(1);
410
+ const shrinkLineTimerRef = useRef(null);
407
411
  const [isInputLayoutFrozen, setIsInputLayoutFrozen] = useState(false);
408
412
  const [frozenFooterLines, setFrozenFooterLines] = useState(2);
409
413
  const skipNextSubmitRef = useRef(false);
@@ -412,7 +416,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
412
416
  const [logScrollOffset, setLogScrollOffset] = useState(0);
413
417
  const scrollBoundsRef = useRef({ maxLogScrollOffset: 0, logViewportHeight: 1 });
414
418
  const prevEscRef = useRef(false);
415
- const inputChangeMountedRef = useRef(false);
416
419
  const typingFreezeTimerRef = useRef(null);
417
420
  useEffect(() => {
418
421
  // Enable SGR mouse + basic tracking so trackpad wheel scrolling works.
@@ -488,21 +491,69 @@ export function Repl({ apiKey, cwd, onQuit }) {
488
491
  const lines = inputValue.split("\n");
489
492
  return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
490
493
  }, [inputValue, wrapWidth]);
491
- const [stableInputLineCount, setStableInputLineCount] = useState(inputLineCount);
492
494
  useEffect(() => {
493
- if (inputLineCount <= 1) {
494
- setStableInputLineCount(1);
495
+ if (inputLineCount >= layoutInputLineCount) {
496
+ if (shrinkLineTimerRef.current) {
497
+ clearTimeout(shrinkLineTimerRef.current);
498
+ shrinkLineTimerRef.current = null;
499
+ }
500
+ setLayoutInputLineCount(inputLineCount);
495
501
  return;
496
502
  }
497
- const t = setTimeout(() => setStableInputLineCount(inputLineCount), 90);
498
- return () => clearTimeout(t);
499
- }, [inputLineCount]);
503
+ if (shrinkLineTimerRef.current) {
504
+ clearTimeout(shrinkLineTimerRef.current);
505
+ }
506
+ shrinkLineTimerRef.current = setTimeout(() => {
507
+ setLayoutInputLineCount(inputLineCount);
508
+ shrinkLineTimerRef.current = null;
509
+ }, 120);
510
+ return () => {
511
+ if (shrinkLineTimerRef.current) {
512
+ clearTimeout(shrinkLineTimerRef.current);
513
+ }
514
+ };
515
+ }, [inputLineCount, layoutInputLineCount]);
516
+ useEffect(() => {
517
+ inputValueRef.current = inputValue;
518
+ }, [inputValue]);
519
+ useEffect(() => {
520
+ inputCursorRef.current = inputCursor;
521
+ }, [inputCursor]);
522
+ const commitInputState = useCallback((immediate = false) => {
523
+ const flush = () => {
524
+ inputCommitTimerRef.current = null;
525
+ setInputValue(inputValueRef.current);
526
+ setInputCursor(inputCursorRef.current);
527
+ };
528
+ if (immediate) {
529
+ if (inputCommitTimerRef.current) {
530
+ clearTimeout(inputCommitTimerRef.current);
531
+ inputCommitTimerRef.current = null;
532
+ }
533
+ flush();
534
+ return;
535
+ }
536
+ if (inputCommitTimerRef.current)
537
+ return;
538
+ inputCommitTimerRef.current = setTimeout(flush, INPUT_COMMIT_INTERVAL_MS);
539
+ }, []);
540
+ const applyInputMutation = useCallback((nextValue, nextCursor, immediate = false) => {
541
+ inputValueRef.current = nextValue;
542
+ inputCursorRef.current = Math.max(0, Math.min(nextCursor, nextValue.length));
543
+ // Keep cursor/text rendering strictly in sync while typing.
544
+ void immediate;
545
+ commitInputState(true);
546
+ }, [commitInputState]);
500
547
  useEffect(() => {
501
548
  setInputCursor((c) => Math.min(c, Math.max(0, inputValue.length)));
502
549
  }, [inputValue.length]);
503
550
  useEffect(() => {
504
- if (!inputChangeMountedRef.current) {
505
- inputChangeMountedRef.current = true;
551
+ if (inputLineCount > layoutInputLineCount) {
552
+ setIsInputLayoutFrozen(false);
553
+ if (typingFreezeTimerRef.current) {
554
+ clearTimeout(typingFreezeTimerRef.current);
555
+ typingFreezeTimerRef.current = null;
556
+ }
506
557
  return;
507
558
  }
508
559
  setIsInputLayoutFrozen(true);
@@ -513,13 +564,21 @@ export function Repl({ apiKey, cwd, onQuit }) {
513
564
  setIsInputLayoutFrozen(false);
514
565
  typingFreezeTimerRef.current = null;
515
566
  }, TYPING_LAYOUT_FREEZE_MS);
516
- }, [inputValue, inputCursor]);
567
+ }, [inputValue, inputCursor, inputLineCount, layoutInputLineCount]);
517
568
  useEffect(() => {
518
569
  return () => {
519
570
  if (typingFreezeTimerRef.current) {
520
571
  clearTimeout(typingFreezeTimerRef.current);
521
572
  typingFreezeTimerRef.current = null;
522
573
  }
574
+ if (shrinkLineTimerRef.current) {
575
+ clearTimeout(shrinkLineTimerRef.current);
576
+ shrinkLineTimerRef.current = null;
577
+ }
578
+ if (inputCommitTimerRef.current) {
579
+ clearTimeout(inputCommitTimerRef.current);
580
+ inputCommitTimerRef.current = null;
581
+ }
523
582
  };
524
583
  }, []);
525
584
  useEffect(() => {
@@ -1017,11 +1076,13 @@ export function Repl({ apiKey, cwd, onQuit }) {
1017
1076
  }
1018
1077
  }
1019
1078
  if (!showModelSelector && !showPalette) {
1079
+ const inputNow = inputValueRef.current;
1080
+ const cursorNow = inputCursorRef.current;
1020
1081
  const withModifier = key.ctrl || key.meta || key.shift;
1021
1082
  const scrollUp = key.pageUp ||
1022
- (key.upArrow && (withModifier || !inputValue.trim()));
1083
+ (key.upArrow && (withModifier || !inputNow.trim()));
1023
1084
  const scrollDown = key.pageDown ||
1024
- (key.downArrow && (withModifier || !inputValue.trim()));
1085
+ (key.downArrow && (withModifier || !inputNow.trim()));
1025
1086
  if (scrollUp) {
1026
1087
  setLogScrollOffset((prev) => Math.min(maxLogScrollOffset, prev + logViewportHeight));
1027
1088
  return;
@@ -1032,26 +1093,24 @@ export function Repl({ apiKey, cwd, onQuit }) {
1032
1093
  }
1033
1094
  if (!key.escape)
1034
1095
  prevEscRef.current = false;
1035
- const len = inputValue.length;
1036
- const cur = inputCursor;
1096
+ const len = inputNow.length;
1097
+ const cur = cursorNow;
1037
1098
  if (key.tab) {
1038
- if (inputValue.trim()) {
1039
- queuedMessageRef.current = inputValue;
1040
- setInputValue("");
1041
- setInputCursor(0);
1099
+ if (inputNow.trim()) {
1100
+ queuedMessageRef.current = inputNow;
1101
+ applyInputMutation("", 0, true);
1042
1102
  appendLog(colors.muted(" Message queued. Send to run after this turn."));
1043
1103
  appendLog("");
1044
1104
  }
1045
1105
  return;
1046
1106
  }
1047
1107
  if (key.escape) {
1048
- if (inputValue.length === 0) {
1108
+ if (inputNow.length === 0) {
1049
1109
  if (prevEscRef.current) {
1050
1110
  prevEscRef.current = false;
1051
1111
  const last = lastUserMessageRef.current;
1052
1112
  if (last) {
1053
- setInputValue(last);
1054
- setInputCursor(last.length);
1113
+ applyInputMutation(last, last.length, true);
1055
1114
  setMessages((prev) => (prev.length > 0 && prev[prev.length - 1]?.role === "user" ? prev.slice(0, -1) : prev));
1056
1115
  appendLog(colors.muted(" Editing previous message. Submit to replace."));
1057
1116
  appendLog("");
@@ -1061,113 +1120,107 @@ export function Repl({ apiKey, cwd, onQuit }) {
1061
1120
  prevEscRef.current = true;
1062
1121
  }
1063
1122
  else {
1064
- setInputValue("");
1065
- setInputCursor(0);
1123
+ applyInputMutation("", 0, true);
1066
1124
  prevEscRef.current = false;
1067
1125
  }
1068
1126
  return;
1069
1127
  }
1070
1128
  if (key.return) {
1071
- handleSubmit(inputValue);
1072
- setInputValue("");
1073
- setInputCursor(0);
1129
+ commitInputState(true);
1130
+ handleSubmit(inputValueRef.current);
1131
+ applyInputMutation("", 0, true);
1074
1132
  return;
1075
1133
  }
1076
1134
  if (key.ctrl && input === "u") {
1077
- setInputValue((prev) => prev.slice(cur));
1078
- setInputCursor(0);
1135
+ applyInputMutation(inputNow.slice(cur), 0);
1079
1136
  return;
1080
1137
  }
1081
1138
  if (key.ctrl && input === "k") {
1082
- setInputValue((prev) => prev.slice(0, cur));
1139
+ applyInputMutation(inputNow.slice(0, cur), cur);
1083
1140
  return;
1084
1141
  }
1085
1142
  const killWordBefore = (key.ctrl && input === "w") ||
1086
1143
  (key.meta && key.backspace) ||
1087
1144
  (key.meta && key.delete && cur > 0);
1088
1145
  if (killWordBefore) {
1089
- const start = wordStartBackward(inputValue, cur);
1146
+ const start = wordStartBackward(inputNow, cur);
1090
1147
  if (start < cur) {
1091
- setInputValue((prev) => prev.slice(0, start) + prev.slice(cur));
1092
- setInputCursor(start);
1148
+ applyInputMutation(inputNow.slice(0, start) + inputNow.slice(cur), start);
1093
1149
  }
1094
1150
  return;
1095
1151
  }
1096
1152
  if (key.meta && input === "d") {
1097
- const end = wordEndForward(inputValue, cur);
1153
+ const end = wordEndForward(inputNow, cur);
1098
1154
  if (end > cur) {
1099
- setInputValue((prev) => prev.slice(0, cur) + prev.slice(end));
1155
+ applyInputMutation(inputNow.slice(0, cur) + inputNow.slice(end), cur);
1100
1156
  }
1101
1157
  return;
1102
1158
  }
1103
1159
  if ((key.meta && key.leftArrow) || (key.ctrl && key.leftArrow)) {
1104
- setInputCursor(wordStartBackward(inputValue, cur));
1160
+ applyInputMutation(inputNow, wordStartBackward(inputNow, cur));
1105
1161
  return;
1106
1162
  }
1107
1163
  if ((key.meta && key.rightArrow) || (key.ctrl && key.rightArrow)) {
1108
- setInputCursor(wordEndForward(inputValue, cur));
1164
+ applyInputMutation(inputNow, wordEndForward(inputNow, cur));
1109
1165
  return;
1110
1166
  }
1111
1167
  if (key.meta && (input === "b" || input === "f")) {
1112
1168
  if (input === "b")
1113
- setInputCursor(wordStartBackward(inputValue, cur));
1169
+ applyInputMutation(inputNow, wordStartBackward(inputNow, cur));
1114
1170
  else
1115
- setInputCursor(wordEndForward(inputValue, cur));
1171
+ applyInputMutation(inputNow, wordEndForward(inputNow, cur));
1116
1172
  return;
1117
1173
  }
1118
1174
  if (key.ctrl && (input === "f" || input === "b")) {
1119
1175
  if (input === "f")
1120
- setInputCursor(Math.min(len, cur + 1));
1176
+ applyInputMutation(inputNow, Math.min(len, cur + 1));
1121
1177
  else
1122
- setInputCursor(Math.max(0, cur - 1));
1178
+ applyInputMutation(inputNow, Math.max(0, cur - 1));
1123
1179
  return;
1124
1180
  }
1125
1181
  if (key.ctrl && input === "j") {
1126
- setInputValue((prev) => prev.slice(0, cur) + "\n" + prev.slice(cur));
1127
- setInputCursor(cur + 1);
1182
+ applyInputMutation(inputNow.slice(0, cur) + "\n" + inputNow.slice(cur), cur + 1);
1128
1183
  return;
1129
1184
  }
1130
1185
  if (key.ctrl && input === "a") {
1131
- setInputCursor(0);
1186
+ applyInputMutation(inputNow, 0);
1132
1187
  return;
1133
1188
  }
1134
1189
  if (key.ctrl && input === "e") {
1135
- setInputCursor(len);
1190
+ applyInputMutation(inputNow, len);
1136
1191
  return;
1137
1192
  }
1138
1193
  if (key.ctrl && input === "h") {
1139
1194
  if (cur > 0) {
1140
- setInputValue((prev) => prev.slice(0, cur - 1) + prev.slice(cur));
1141
- setInputCursor(cur - 1);
1195
+ applyInputMutation(inputNow.slice(0, cur - 1) + inputNow.slice(cur), cur - 1);
1142
1196
  }
1143
1197
  return;
1144
1198
  }
1145
1199
  if (key.ctrl && input === "d") {
1146
1200
  if (cur < len) {
1147
- setInputValue((prev) => prev.slice(0, cur) + prev.slice(cur + 1));
1201
+ applyInputMutation(inputNow.slice(0, cur) + inputNow.slice(cur + 1), cur);
1148
1202
  }
1149
1203
  return;
1150
1204
  }
1151
1205
  if (key.backspace || (key.delete && cur > 0)) {
1152
1206
  if (cur > 0) {
1153
- setInputValue((prev) => prev.slice(0, cur - 1) + prev.slice(cur));
1154
- setInputCursor(cur - 1);
1207
+ applyInputMutation(inputNow.slice(0, cur - 1) + inputNow.slice(cur), cur - 1);
1155
1208
  }
1156
1209
  return;
1157
1210
  }
1158
1211
  if (key.delete && cur < len) {
1159
- setInputValue((prev) => prev.slice(0, cur) + prev.slice(cur + 1));
1212
+ applyInputMutation(inputNow.slice(0, cur) + inputNow.slice(cur + 1), cur);
1160
1213
  return;
1161
1214
  }
1162
1215
  if (key.leftArrow) {
1163
- setInputCursor(Math.max(0, cur - 1));
1216
+ applyInputMutation(inputNow, Math.max(0, cur - 1));
1164
1217
  return;
1165
1218
  }
1166
1219
  if (key.rightArrow) {
1167
- setInputCursor(Math.min(len, cur + 1));
1220
+ applyInputMutation(inputNow, Math.min(len, cur + 1));
1168
1221
  return;
1169
1222
  }
1170
- if (input === "?" && !inputValue.trim()) {
1223
+ if (input === "?" && !inputNow.trim()) {
1171
1224
  setShowHelpModal(true);
1172
1225
  return;
1173
1226
  }
@@ -1175,8 +1228,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
1175
1228
  return;
1176
1229
  }
1177
1230
  if (input && !key.ctrl && !key.meta) {
1178
- setInputValue((prev) => prev.slice(0, cur) + input + prev.slice(cur));
1179
- setInputCursor(cur + input.length);
1231
+ applyInputMutation(inputNow.slice(0, cur) + input + inputNow.slice(cur), cur + input.length);
1180
1232
  return;
1181
1233
  }
1182
1234
  }
@@ -1209,7 +1261,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
1209
1261
  : 0;
1210
1262
  const suggestionBoxLines = slashSuggestionBoxLines || atSuggestionBoxLines;
1211
1263
  // Keep a fixed loading row reserved to avoid viewport jumps/flicker when loading starts/stops.
1212
- const reservedLines = 1 + stableInputLineCount + 2;
1264
+ const reservedLines = 1 + layoutInputLineCount + 2;
1213
1265
  const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
1214
1266
  const effectiveLogLines = logLines;
1215
1267
  const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
@@ -1243,7 +1295,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
1243
1295
  const leftPad = Math.max(0, Math.floor((termColumns - paletteModalWidth) / 2));
1244
1296
  return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: topPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: leftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: paletteModalWidth, minHeight: paletteModalHeight, children: [_jsx(Text, { bold: true, children: " Command palette " }), COMMANDS.map((c, i) => (_jsxs(Text, { color: i === paletteIndex ? inkColors.primary : undefined, children: [i === paletteIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: inkColors.textSecondary, children: [" \u2014 ", c.desc] })] }, c.cmd))), _jsxs(Text, { color: paletteIndex === COMMANDS.length ? inkColors.primary : undefined, children: [paletteIndex === COMMANDS.length ? "› " : " ", "Cancel (Esc)"] }), _jsx(Text, { color: inkColors.textSecondary, children: " \u2191/\u2193 select, Enter confirm, Esc close " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
1245
1297
  }
1246
- const calculatedFooterLines = suggestionBoxLines + 1 + stableInputLineCount;
1298
+ const calculatedFooterLines = suggestionBoxLines + 1 + layoutInputLineCount;
1247
1299
  useEffect(() => {
1248
1300
  if (!isInputLayoutFrozen) {
1249
1301
  setFrozenFooterLines(calculatedFooterLines);
@@ -1257,7 +1309,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
1257
1309
  })), _jsx(Text, { color: inkColors.textSecondary, children: " Commands (\u2191/\u2193 select, Enter run, Esc clear) " })] })), cursorInAtSegment && !showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, children: [filteredFilePaths.length === 0 ? (_jsxs(Text, { color: inkColors.textSecondary, children: [" ", hasCharsAfterAt ? "No match" : "Type to search files", " "] })) : ([...filteredFilePaths].reverse().map((p, rev) => {
1258
1310
  const i = filteredFilePaths.length - 1 - rev;
1259
1311
  return (_jsxs(Text, { color: i === clampedAtFileIndex ? inkColors.primary : undefined, children: [i === clampedAtFileIndex ? "› " : " ", p] }, p));
1260
- })), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color: inkColors.textSecondary, children: " Files (\u2191/\u2193 select, Enter/Tab complete, Esc clear) " }) })] })), _jsxs(Box, { flexDirection: "row", marginTop: 0, children: [_jsxs(Text, { color: "gray", children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: "gray", children: ` · / ! @ trackpad/↑/↓ scroll Opt/Fn+select Ctrl+J newline Tab queue Esc Esc edit ${pasteShortcut} paste Ctrl+C exit` })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: inputValue.length === 0 ? (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] }), cursorBlinkOn ? (_jsx(Text, { inverse: true, color: inkColors.primary, children: " " })) : (_jsx(Text, { color: inkColors.primary, children: " " })), _jsx(Text, { color: inkColors.textSecondary, children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
1312
+ })), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color: inkColors.textSecondary, children: " Files (\u2191/\u2193 select, Enter/Tab complete, Esc clear) " }) })] })), _jsxs(Box, { flexDirection: "row", marginTop: 0, children: [_jsxs(Text, { color: inkColors.mutedDark, children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: inkColors.mutedDark, children: ` · / ! @ trackpad/↑/↓ scroll Ctrl+J newline Tab queue Esc Esc edit` })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: inputValue.length === 0 ? (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] }), cursorBlinkOn ? (_jsx(Text, { inverse: true, color: inkColors.primary, children: " " })) : (_jsx(Text, { color: inkColors.primary, children: " " })), _jsx(Text, { color: inkColors.textSecondary, children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
1261
1313
  const lines = inputValue.split("\n");
1262
1314
  let lineStart = 0;
1263
1315
  return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideacode",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "CLI TUI for AI agents via OpenRouter — agentic loop, tools, markdown",
5
5
  "type": "module",
6
6
  "repository": {