ideacode 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/repl.js CHANGED
@@ -50,15 +50,15 @@ 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;
55
+ const SLASH_SUGGESTION_ROWS = Math.max(1, COMMANDS.length);
54
56
  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
57
  function truncateToolResult(content) {
56
58
  if (content.length <= MAX_TOOL_RESULT_CHARS)
57
59
  return content;
58
60
  return content.slice(0, MAX_TOOL_RESULT_CHARS) + TRUNCATE_NOTE;
59
61
  }
60
- const isMac = process.platform === "darwin";
61
- const pasteShortcut = isMac ? "Cmd+V" : "Ctrl+V";
62
62
  function listFilesWithFilter(cwd, filter) {
63
63
  try {
64
64
  const pattern = path.join(cwd, "**", "*").replace(/\\/g, "/");
@@ -404,6 +404,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
404
404
  const [showHelpModal, setShowHelpModal] = useState(false);
405
405
  const [slashSuggestionIndex, setSlashSuggestionIndex] = useState(0);
406
406
  const [inputCursor, setInputCursor] = useState(0);
407
+ const inputValueRef = useRef(inputValue);
408
+ const inputCursorRef = useRef(inputCursor);
409
+ const inputCommitTimerRef = useRef(null);
410
+ const [layoutInputLineCount, setLayoutInputLineCount] = useState(1);
411
+ const shrinkLineTimerRef = useRef(null);
407
412
  const [isInputLayoutFrozen, setIsInputLayoutFrozen] = useState(false);
408
413
  const [frozenFooterLines, setFrozenFooterLines] = useState(2);
409
414
  const skipNextSubmitRef = useRef(false);
@@ -412,7 +417,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
412
417
  const [logScrollOffset, setLogScrollOffset] = useState(0);
413
418
  const scrollBoundsRef = useRef({ maxLogScrollOffset: 0, logViewportHeight: 1 });
414
419
  const prevEscRef = useRef(false);
415
- const inputChangeMountedRef = useRef(false);
416
420
  const typingFreezeTimerRef = useRef(null);
417
421
  useEffect(() => {
418
422
  // Enable SGR mouse + basic tracking so trackpad wheel scrolling works.
@@ -488,21 +492,69 @@ export function Repl({ apiKey, cwd, onQuit }) {
488
492
  const lines = inputValue.split("\n");
489
493
  return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
490
494
  }, [inputValue, wrapWidth]);
491
- const [stableInputLineCount, setStableInputLineCount] = useState(inputLineCount);
492
495
  useEffect(() => {
493
- if (inputLineCount <= 1) {
494
- setStableInputLineCount(1);
496
+ if (inputLineCount >= layoutInputLineCount) {
497
+ if (shrinkLineTimerRef.current) {
498
+ clearTimeout(shrinkLineTimerRef.current);
499
+ shrinkLineTimerRef.current = null;
500
+ }
501
+ setLayoutInputLineCount(inputLineCount);
495
502
  return;
496
503
  }
497
- const t = setTimeout(() => setStableInputLineCount(inputLineCount), 90);
498
- return () => clearTimeout(t);
499
- }, [inputLineCount]);
504
+ if (shrinkLineTimerRef.current) {
505
+ clearTimeout(shrinkLineTimerRef.current);
506
+ }
507
+ shrinkLineTimerRef.current = setTimeout(() => {
508
+ setLayoutInputLineCount(inputLineCount);
509
+ shrinkLineTimerRef.current = null;
510
+ }, 120);
511
+ return () => {
512
+ if (shrinkLineTimerRef.current) {
513
+ clearTimeout(shrinkLineTimerRef.current);
514
+ }
515
+ };
516
+ }, [inputLineCount, layoutInputLineCount]);
517
+ useEffect(() => {
518
+ inputValueRef.current = inputValue;
519
+ }, [inputValue]);
520
+ useEffect(() => {
521
+ inputCursorRef.current = inputCursor;
522
+ }, [inputCursor]);
523
+ const commitInputState = useCallback((immediate = false) => {
524
+ const flush = () => {
525
+ inputCommitTimerRef.current = null;
526
+ setInputValue(inputValueRef.current);
527
+ setInputCursor(inputCursorRef.current);
528
+ };
529
+ if (immediate) {
530
+ if (inputCommitTimerRef.current) {
531
+ clearTimeout(inputCommitTimerRef.current);
532
+ inputCommitTimerRef.current = null;
533
+ }
534
+ flush();
535
+ return;
536
+ }
537
+ if (inputCommitTimerRef.current)
538
+ return;
539
+ inputCommitTimerRef.current = setTimeout(flush, INPUT_COMMIT_INTERVAL_MS);
540
+ }, []);
541
+ const applyInputMutation = useCallback((nextValue, nextCursor, immediate = false) => {
542
+ inputValueRef.current = nextValue;
543
+ inputCursorRef.current = Math.max(0, Math.min(nextCursor, nextValue.length));
544
+ // Keep cursor/text rendering strictly in sync while typing.
545
+ void immediate;
546
+ commitInputState(true);
547
+ }, [commitInputState]);
500
548
  useEffect(() => {
501
549
  setInputCursor((c) => Math.min(c, Math.max(0, inputValue.length)));
502
550
  }, [inputValue.length]);
503
551
  useEffect(() => {
504
- if (!inputChangeMountedRef.current) {
505
- inputChangeMountedRef.current = true;
552
+ if (inputLineCount > layoutInputLineCount) {
553
+ setIsInputLayoutFrozen(false);
554
+ if (typingFreezeTimerRef.current) {
555
+ clearTimeout(typingFreezeTimerRef.current);
556
+ typingFreezeTimerRef.current = null;
557
+ }
506
558
  return;
507
559
  }
508
560
  setIsInputLayoutFrozen(true);
@@ -513,13 +565,21 @@ export function Repl({ apiKey, cwd, onQuit }) {
513
565
  setIsInputLayoutFrozen(false);
514
566
  typingFreezeTimerRef.current = null;
515
567
  }, TYPING_LAYOUT_FREEZE_MS);
516
- }, [inputValue, inputCursor]);
568
+ }, [inputValue, inputCursor, inputLineCount, layoutInputLineCount]);
517
569
  useEffect(() => {
518
570
  return () => {
519
571
  if (typingFreezeTimerRef.current) {
520
572
  clearTimeout(typingFreezeTimerRef.current);
521
573
  typingFreezeTimerRef.current = null;
522
574
  }
575
+ if (shrinkLineTimerRef.current) {
576
+ clearTimeout(shrinkLineTimerRef.current);
577
+ shrinkLineTimerRef.current = null;
578
+ }
579
+ if (inputCommitTimerRef.current) {
580
+ clearTimeout(inputCommitTimerRef.current);
581
+ inputCommitTimerRef.current = null;
582
+ }
523
583
  };
524
584
  }, []);
525
585
  useEffect(() => {
@@ -937,11 +997,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
937
997
  }
938
998
  if (showSlashSuggestions && filteredSlashCommands.length > 0) {
939
999
  if (key.upArrow) {
940
- setSlashSuggestionIndex((i) => Math.max(0, i - 1));
1000
+ setSlashSuggestionIndex((i) => Math.min(filteredSlashCommands.length - 1, i + 1));
941
1001
  return;
942
1002
  }
943
1003
  if (key.downArrow) {
944
- setSlashSuggestionIndex((i) => Math.min(filteredSlashCommands.length - 1, i + 1));
1004
+ setSlashSuggestionIndex((i) => Math.max(0, i - 1));
945
1005
  return;
946
1006
  }
947
1007
  if (key.tab) {
@@ -1017,11 +1077,13 @@ export function Repl({ apiKey, cwd, onQuit }) {
1017
1077
  }
1018
1078
  }
1019
1079
  if (!showModelSelector && !showPalette) {
1080
+ const inputNow = inputValueRef.current;
1081
+ const cursorNow = inputCursorRef.current;
1020
1082
  const withModifier = key.ctrl || key.meta || key.shift;
1021
1083
  const scrollUp = key.pageUp ||
1022
- (key.upArrow && (withModifier || !inputValue.trim()));
1084
+ (key.upArrow && (withModifier || !inputNow.trim()));
1023
1085
  const scrollDown = key.pageDown ||
1024
- (key.downArrow && (withModifier || !inputValue.trim()));
1086
+ (key.downArrow && (withModifier || !inputNow.trim()));
1025
1087
  if (scrollUp) {
1026
1088
  setLogScrollOffset((prev) => Math.min(maxLogScrollOffset, prev + logViewportHeight));
1027
1089
  return;
@@ -1032,26 +1094,24 @@ export function Repl({ apiKey, cwd, onQuit }) {
1032
1094
  }
1033
1095
  if (!key.escape)
1034
1096
  prevEscRef.current = false;
1035
- const len = inputValue.length;
1036
- const cur = inputCursor;
1097
+ const len = inputNow.length;
1098
+ const cur = cursorNow;
1037
1099
  if (key.tab) {
1038
- if (inputValue.trim()) {
1039
- queuedMessageRef.current = inputValue;
1040
- setInputValue("");
1041
- setInputCursor(0);
1100
+ if (inputNow.trim()) {
1101
+ queuedMessageRef.current = inputNow;
1102
+ applyInputMutation("", 0, true);
1042
1103
  appendLog(colors.muted(" Message queued. Send to run after this turn."));
1043
1104
  appendLog("");
1044
1105
  }
1045
1106
  return;
1046
1107
  }
1047
1108
  if (key.escape) {
1048
- if (inputValue.length === 0) {
1109
+ if (inputNow.length === 0) {
1049
1110
  if (prevEscRef.current) {
1050
1111
  prevEscRef.current = false;
1051
1112
  const last = lastUserMessageRef.current;
1052
1113
  if (last) {
1053
- setInputValue(last);
1054
- setInputCursor(last.length);
1114
+ applyInputMutation(last, last.length, true);
1055
1115
  setMessages((prev) => (prev.length > 0 && prev[prev.length - 1]?.role === "user" ? prev.slice(0, -1) : prev));
1056
1116
  appendLog(colors.muted(" Editing previous message. Submit to replace."));
1057
1117
  appendLog("");
@@ -1061,113 +1121,107 @@ export function Repl({ apiKey, cwd, onQuit }) {
1061
1121
  prevEscRef.current = true;
1062
1122
  }
1063
1123
  else {
1064
- setInputValue("");
1065
- setInputCursor(0);
1124
+ applyInputMutation("", 0, true);
1066
1125
  prevEscRef.current = false;
1067
1126
  }
1068
1127
  return;
1069
1128
  }
1070
1129
  if (key.return) {
1071
- handleSubmit(inputValue);
1072
- setInputValue("");
1073
- setInputCursor(0);
1130
+ commitInputState(true);
1131
+ handleSubmit(inputValueRef.current);
1132
+ applyInputMutation("", 0, true);
1074
1133
  return;
1075
1134
  }
1076
1135
  if (key.ctrl && input === "u") {
1077
- setInputValue((prev) => prev.slice(cur));
1078
- setInputCursor(0);
1136
+ applyInputMutation(inputNow.slice(cur), 0);
1079
1137
  return;
1080
1138
  }
1081
1139
  if (key.ctrl && input === "k") {
1082
- setInputValue((prev) => prev.slice(0, cur));
1140
+ applyInputMutation(inputNow.slice(0, cur), cur);
1083
1141
  return;
1084
1142
  }
1085
1143
  const killWordBefore = (key.ctrl && input === "w") ||
1086
1144
  (key.meta && key.backspace) ||
1087
1145
  (key.meta && key.delete && cur > 0);
1088
1146
  if (killWordBefore) {
1089
- const start = wordStartBackward(inputValue, cur);
1147
+ const start = wordStartBackward(inputNow, cur);
1090
1148
  if (start < cur) {
1091
- setInputValue((prev) => prev.slice(0, start) + prev.slice(cur));
1092
- setInputCursor(start);
1149
+ applyInputMutation(inputNow.slice(0, start) + inputNow.slice(cur), start);
1093
1150
  }
1094
1151
  return;
1095
1152
  }
1096
1153
  if (key.meta && input === "d") {
1097
- const end = wordEndForward(inputValue, cur);
1154
+ const end = wordEndForward(inputNow, cur);
1098
1155
  if (end > cur) {
1099
- setInputValue((prev) => prev.slice(0, cur) + prev.slice(end));
1156
+ applyInputMutation(inputNow.slice(0, cur) + inputNow.slice(end), cur);
1100
1157
  }
1101
1158
  return;
1102
1159
  }
1103
1160
  if ((key.meta && key.leftArrow) || (key.ctrl && key.leftArrow)) {
1104
- setInputCursor(wordStartBackward(inputValue, cur));
1161
+ applyInputMutation(inputNow, wordStartBackward(inputNow, cur));
1105
1162
  return;
1106
1163
  }
1107
1164
  if ((key.meta && key.rightArrow) || (key.ctrl && key.rightArrow)) {
1108
- setInputCursor(wordEndForward(inputValue, cur));
1165
+ applyInputMutation(inputNow, wordEndForward(inputNow, cur));
1109
1166
  return;
1110
1167
  }
1111
1168
  if (key.meta && (input === "b" || input === "f")) {
1112
1169
  if (input === "b")
1113
- setInputCursor(wordStartBackward(inputValue, cur));
1170
+ applyInputMutation(inputNow, wordStartBackward(inputNow, cur));
1114
1171
  else
1115
- setInputCursor(wordEndForward(inputValue, cur));
1172
+ applyInputMutation(inputNow, wordEndForward(inputNow, cur));
1116
1173
  return;
1117
1174
  }
1118
1175
  if (key.ctrl && (input === "f" || input === "b")) {
1119
1176
  if (input === "f")
1120
- setInputCursor(Math.min(len, cur + 1));
1177
+ applyInputMutation(inputNow, Math.min(len, cur + 1));
1121
1178
  else
1122
- setInputCursor(Math.max(0, cur - 1));
1179
+ applyInputMutation(inputNow, Math.max(0, cur - 1));
1123
1180
  return;
1124
1181
  }
1125
1182
  if (key.ctrl && input === "j") {
1126
- setInputValue((prev) => prev.slice(0, cur) + "\n" + prev.slice(cur));
1127
- setInputCursor(cur + 1);
1183
+ applyInputMutation(inputNow.slice(0, cur) + "\n" + inputNow.slice(cur), cur + 1);
1128
1184
  return;
1129
1185
  }
1130
1186
  if (key.ctrl && input === "a") {
1131
- setInputCursor(0);
1187
+ applyInputMutation(inputNow, 0);
1132
1188
  return;
1133
1189
  }
1134
1190
  if (key.ctrl && input === "e") {
1135
- setInputCursor(len);
1191
+ applyInputMutation(inputNow, len);
1136
1192
  return;
1137
1193
  }
1138
1194
  if (key.ctrl && input === "h") {
1139
1195
  if (cur > 0) {
1140
- setInputValue((prev) => prev.slice(0, cur - 1) + prev.slice(cur));
1141
- setInputCursor(cur - 1);
1196
+ applyInputMutation(inputNow.slice(0, cur - 1) + inputNow.slice(cur), cur - 1);
1142
1197
  }
1143
1198
  return;
1144
1199
  }
1145
1200
  if (key.ctrl && input === "d") {
1146
1201
  if (cur < len) {
1147
- setInputValue((prev) => prev.slice(0, cur) + prev.slice(cur + 1));
1202
+ applyInputMutation(inputNow.slice(0, cur) + inputNow.slice(cur + 1), cur);
1148
1203
  }
1149
1204
  return;
1150
1205
  }
1151
1206
  if (key.backspace || (key.delete && cur > 0)) {
1152
1207
  if (cur > 0) {
1153
- setInputValue((prev) => prev.slice(0, cur - 1) + prev.slice(cur));
1154
- setInputCursor(cur - 1);
1208
+ applyInputMutation(inputNow.slice(0, cur - 1) + inputNow.slice(cur), cur - 1);
1155
1209
  }
1156
1210
  return;
1157
1211
  }
1158
1212
  if (key.delete && cur < len) {
1159
- setInputValue((prev) => prev.slice(0, cur) + prev.slice(cur + 1));
1213
+ applyInputMutation(inputNow.slice(0, cur) + inputNow.slice(cur + 1), cur);
1160
1214
  return;
1161
1215
  }
1162
1216
  if (key.leftArrow) {
1163
- setInputCursor(Math.max(0, cur - 1));
1217
+ applyInputMutation(inputNow, Math.max(0, cur - 1));
1164
1218
  return;
1165
1219
  }
1166
1220
  if (key.rightArrow) {
1167
- setInputCursor(Math.min(len, cur + 1));
1221
+ applyInputMutation(inputNow, Math.min(len, cur + 1));
1168
1222
  return;
1169
1223
  }
1170
- if (input === "?" && !inputValue.trim()) {
1224
+ if (input === "?" && !inputNow.trim()) {
1171
1225
  setShowHelpModal(true);
1172
1226
  return;
1173
1227
  }
@@ -1175,8 +1229,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
1175
1229
  return;
1176
1230
  }
1177
1231
  if (input && !key.ctrl && !key.meta) {
1178
- setInputValue((prev) => prev.slice(0, cur) + input + prev.slice(cur));
1179
- setInputCursor(cur + input.length);
1232
+ applyInputMutation(inputNow.slice(0, cur) + input + inputNow.slice(cur), cur + input.length);
1180
1233
  return;
1181
1234
  }
1182
1235
  }
@@ -1187,29 +1240,15 @@ export function Repl({ apiKey, cwd, onQuit }) {
1187
1240
  handleQuit();
1188
1241
  }
1189
1242
  });
1190
- if (showModelSelector) {
1191
- const modelModalMaxHeight = 18;
1192
- const modelModalWidth = 108;
1193
- const modelModalHeight = Math.min(filteredModelList.length + 4, modelModalMaxHeight);
1194
- const topPad = Math.max(0, Math.floor((termRows - modelModalHeight) / 2));
1195
- const leftPad = Math.max(0, Math.floor((termColumns - modelModalWidth) / 2));
1196
- const visibleModelCount = Math.min(filteredModelList.length, modelModalHeight - 4);
1197
- const modelScrollOffset = Math.max(0, Math.min(modelIndex - Math.floor(visibleModelCount / 2), filteredModelList.length - visibleModelCount));
1198
- const visibleModels = filteredModelList.slice(modelScrollOffset, modelScrollOffset + visibleModelCount);
1199
- 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: modelModalWidth, minHeight: modelModalHeight, children: [_jsx(Text, { bold: true, children: " Select model " }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: inkColors.textSecondary, children: " Filter: " }), _jsx(Text, { children: modelSearchFilter || " " }), modelSearchFilter.length > 0 && (_jsxs(Text, { color: inkColors.textDisabled, children: [" ", "(", filteredModelList.length, " match", filteredModelList.length !== 1 ? "es" : "", ")"] }))] }), visibleModels.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match \u2014 type to search by id or name " })) : (visibleModels.map((m, i) => {
1200
- const actualIndex = modelScrollOffset + i;
1201
- return (_jsxs(Text, { color: actualIndex === modelIndex ? inkColors.primary : undefined, children: [actualIndex === modelIndex ? "› " : " ", m.name ? `${m.id} — ${m.name}` : m.id] }, m.id));
1202
- })), _jsx(Text, { color: inkColors.textSecondary, children: " \u2191/\u2193 select Enter confirm Esc cancel Type to filter " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
1203
- }
1204
1243
  const slashSuggestionBoxLines = showSlashSuggestions
1205
- ? 3 + Math.max(1, filteredSlashCommands.length)
1244
+ ? 3 + SLASH_SUGGESTION_ROWS
1206
1245
  : 0;
1207
1246
  const atSuggestionBoxLines = cursorInAtSegment
1208
1247
  ? 4 + Math.max(1, filteredFilePaths.length)
1209
1248
  : 0;
1210
1249
  const suggestionBoxLines = slashSuggestionBoxLines || atSuggestionBoxLines;
1211
1250
  // Keep a fixed loading row reserved to avoid viewport jumps/flicker when loading starts/stops.
1212
- const reservedLines = 1 + stableInputLineCount + 2;
1251
+ const reservedLines = 1 + layoutInputLineCount + 2;
1213
1252
  const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
1214
1253
  const effectiveLogLines = logLines;
1215
1254
  const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
@@ -1221,6 +1260,28 @@ export function Repl({ apiKey, cwd, onQuit }) {
1221
1260
  const sliceEnd = logStartIndex + logViewportHeight;
1222
1261
  const visibleLogLines = useMemo(() => effectiveLogLines.slice(logStartIndex, sliceEnd), [effectiveLogLines, logStartIndex, sliceEnd]);
1223
1262
  const useSimpleInputRenderer = inputLineCount > 1;
1263
+ const calculatedFooterLines = suggestionBoxLines + 1 + layoutInputLineCount;
1264
+ useEffect(() => {
1265
+ if (!isInputLayoutFrozen) {
1266
+ setFrozenFooterLines(calculatedFooterLines);
1267
+ }
1268
+ }, [isInputLayoutFrozen, calculatedFooterLines]);
1269
+ const footerLines = isInputLayoutFrozen ? frozenFooterLines : calculatedFooterLines;
1270
+ loadingFooterLinesRef.current = footerLines;
1271
+ if (showModelSelector) {
1272
+ const modelModalMaxHeight = 18;
1273
+ const modelModalWidth = 108;
1274
+ const modelModalHeight = Math.min(filteredModelList.length + 4, modelModalMaxHeight);
1275
+ const topPad = Math.max(0, Math.floor((termRows - modelModalHeight) / 2));
1276
+ const leftPad = Math.max(0, Math.floor((termColumns - modelModalWidth) / 2));
1277
+ const visibleModelCount = Math.min(filteredModelList.length, modelModalHeight - 4);
1278
+ const modelScrollOffset = Math.max(0, Math.min(modelIndex - Math.floor(visibleModelCount / 2), filteredModelList.length - visibleModelCount));
1279
+ const visibleModels = filteredModelList.slice(modelScrollOffset, modelScrollOffset + visibleModelCount);
1280
+ 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: modelModalWidth, minHeight: modelModalHeight, children: [_jsx(Text, { bold: true, children: " Select model " }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: inkColors.textSecondary, children: " Filter: " }), _jsx(Text, { children: modelSearchFilter || " " }), modelSearchFilter.length > 0 && (_jsxs(Text, { color: inkColors.textDisabled, children: [" ", "(", filteredModelList.length, " match", filteredModelList.length !== 1 ? "es" : "", ")"] }))] }), visibleModels.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match \u2014 type to search by id or name " })) : (visibleModels.map((m, i) => {
1281
+ const actualIndex = modelScrollOffset + i;
1282
+ return (_jsxs(Text, { color: actualIndex === modelIndex ? inkColors.primary : undefined, children: [actualIndex === modelIndex ? "› " : " ", m.name ? `${m.id} — ${m.name}` : m.id] }, m.id));
1283
+ })), _jsx(Text, { color: inkColors.textSecondary, children: " \u2191/\u2193 select Enter confirm Esc cancel Type to filter " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
1284
+ }
1224
1285
  if (showHelpModal) {
1225
1286
  const helpModalWidth = Math.min(88, Math.max(80, termColumns - 4));
1226
1287
  const helpContentRows = 20;
@@ -1243,21 +1304,15 @@ export function Repl({ apiKey, cwd, onQuit }) {
1243
1304
  const leftPad = Math.max(0, Math.floor((termColumns - paletteModalWidth) / 2));
1244
1305
  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
1306
  }
1246
- const calculatedFooterLines = suggestionBoxLines + 1 + stableInputLineCount;
1247
- useEffect(() => {
1248
- if (!isInputLayoutFrozen) {
1249
- setFrozenFooterLines(calculatedFooterLines);
1250
- }
1251
- }, [isInputLayoutFrozen, calculatedFooterLines]);
1252
- const footerLines = isInputLayoutFrozen ? frozenFooterLines : calculatedFooterLines;
1253
- loadingFooterLinesRef.current = footerLines;
1254
- return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(LogViewport, { lines: visibleLogLines, startIndex: logStartIndex, height: logViewportHeight }), _jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: "\u00A0" }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
1255
- const i = filteredSlashCommands.length - 1 - rev;
1307
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(LogViewport, { lines: visibleLogLines, startIndex: logStartIndex, height: logViewportHeight }), _jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: "\u00A0" }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, height: slashSuggestionBoxLines, children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match " })) : ([...filteredSlashCommands.slice(0, SLASH_SUGGESTION_ROWS)].reverse().map((c, rev) => {
1308
+ const i = Math.min(filteredSlashCommands.length, SLASH_SUGGESTION_ROWS) - 1 - rev;
1256
1309
  return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: inkColors.textSecondary, children: [" \u2014 ", c.desc] })] }, c.cmd));
1257
- })), _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) => {
1310
+ })), Array.from({
1311
+ length: Math.max(0, SLASH_SUGGESTION_ROWS - Math.min(filteredSlashCommands.length, SLASH_SUGGESTION_ROWS)),
1312
+ }).map((_, idx) => (_jsx(Text, { children: "\u00A0" }, `slash-pad-${idx}`))), _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
1313
  const i = filteredFilePaths.length - 1 - rev;
1259
1314
  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..." })] })) : ((() => {
1315
+ })), _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.footerHint, children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: inkColors.footerHint, 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
1316
  const lines = inputValue.split("\n");
1262
1317
  let lineStart = 0;
1263
1318
  return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
package/dist/ui/theme.js CHANGED
@@ -22,7 +22,7 @@ const THEME_DARK = {
22
22
  error: { main: "#f87171", dim: "#dc2626" },
23
23
  muted: { main: "#8a9a7a", dim: "#6a7a5a", dark: "#4a5a3a" },
24
24
  background: { dark: "#1a1a1a", darker: "#0f0f0f" },
25
- text: { primary: "#e2e8f0", secondary: "#98a08f", disabled: "#6f7867" },
25
+ text: { primary: "#e2e8f0", secondary: "#98a08f", disabled: "#6f7867", footerHint: "#635e4b" },
26
26
  },
27
27
  ui: {
28
28
  borderColor: "#7F9A65",
@@ -51,7 +51,7 @@ const THEME_LIGHT = {
51
51
  error: { main: "#dc2626", dim: "#b91c1c" },
52
52
  muted: { main: "#6b7a5a", dim: "#5a6a4a", dark: "#4a5a3a" },
53
53
  background: { dark: "#f1f5f9", darker: "#e2e8f0" },
54
- text: { primary: "#1e293b", secondary: "#5f6758", disabled: "#778070" },
54
+ text: { primary: "#1e293b", secondary: "#5f6758", disabled: "#778070", footerHint: "#635e4b" },
55
55
  },
56
56
  ui: {
57
57
  borderColor: "#5a7247",
@@ -125,5 +125,6 @@ export const inkColors = {
125
125
  textPrimary: theme.colors.text.primary,
126
126
  textSecondary: theme.colors.text.secondary,
127
127
  textDisabled: theme.colors.text.disabled,
128
+ footerHint: theme.colors.text.footerHint,
128
129
  };
129
130
  export const icons = theme.icons;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideacode",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "CLI TUI for AI agents via OpenRouter — agentic loop, tools, markdown",
5
5
  "type": "module",
6
6
  "repository": {