code-ollama 0.15.0 → 0.16.0

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.
@@ -169,7 +169,7 @@ var inlineMathExtension = {
169
169
  }
170
170
  };
171
171
  //#endregion
172
- //#region src/components/Markdown/Markdown.tsx
172
+ //#region src/components/Markdown/render.ts
173
173
  var HR_PLACEHOLDER = "__CODE_OLLAMA_HR_PLACEHOLDER__";
174
174
  function renderMarkdown(content, hrWidth) {
175
175
  const hr = "─".repeat(Math.max(1, hrWidth));
@@ -196,10 +196,12 @@ function renderMarkdown(content, hrWidth) {
196
196
  const result = markdown.parse(content);
197
197
  return (typeof result === "string" ? result.trim() : content).replaceAll(HR_PLACEHOLDER, hr);
198
198
  } catch {
199
+ // v8 ignore next
199
200
  return content;
200
201
  }
201
- // v8 ignore stop
202
202
  }
203
+ //#endregion
204
+ //#region src/components/Markdown/Markdown.tsx
203
205
  var Markdown = memo(function Markdown({ content, color, dimColor }) {
204
206
  const { stdout } = useStdout();
205
207
  const availableWidth = stdout.columns - 4;
@@ -217,7 +219,72 @@ var TURN_ABORTED_MESSAGE = [
217
219
  "</turn_aborted>"
218
220
  ].join("\n");
219
221
  //#endregion
220
- //#region src/components/Messages/utils.ts
222
+ //#region src/components/Messages/layout.ts
223
+ var ANSI_REGEX = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
224
+ var CODE_BLOCK_MARGIN_Y = 2;
225
+ var CODE_BLOCK_BORDER_Y = 2;
226
+ var CODE_BLOCK_CHROME_X = 4;
227
+ function stripAnsi(value) {
228
+ return value.replaceAll(ANSI_REGEX, "");
229
+ }
230
+ function countLineWidth(value) {
231
+ return Array.from(stripAnsi(value)).length;
232
+ }
233
+ /**
234
+ * Counts the number of wrapped lines for a given content and width.
235
+ *
236
+ * This function splits the content by newlines and calculates how many lines
237
+ * each segment would wrap to based on the available width.
238
+ *
239
+ * @param content The text content to wrap.
240
+ * @param width The available width for wrapping.
241
+ * @returns The number of wrapped lines.
242
+ */
243
+ function countWrappedLines(content, width) {
244
+ const safeWidth = Math.max(1, width);
245
+ return content.split("\n").reduce((lineCount, line) => {
246
+ const visibleWidth = countLineWidth(line);
247
+ return lineCount + Math.max(1, Math.ceil(visibleWidth / safeWidth));
248
+ }, 0);
249
+ }
250
+ /**
251
+ * Calculates the height of a code block based on its content and width.
252
+ *
253
+ * This function accounts for margins, borders, and wrapped lines to determine
254
+ * the total height required for displaying a code block.
255
+ *
256
+ * @param content The code block content to render.
257
+ * @param width The available width for the code block.
258
+ * @returns The total height in lines.
259
+ */
260
+ function getCodeBlockHeight(content, width) {
261
+ const contentWidth = Math.max(1, width - CODE_BLOCK_CHROME_X);
262
+ return CODE_BLOCK_MARGIN_Y + CODE_BLOCK_BORDER_Y + countWrappedLines(content, contentWidth);
263
+ }
264
+ /**
265
+ * Calculates the total height of streaming text content based on wrapped lines.
266
+ *
267
+ * @param textParts Array of text parts with their content and type.
268
+ * @param width The available width for wrapping text.
269
+ * @returns The total height in lines.
270
+ */
271
+ function getStreamingTextHeight(textParts, width) {
272
+ return textParts.reduce((height, part) => {
273
+ const renderMarkdown$1 = renderMarkdown;
274
+ return height + countWrappedLines(part.type === "markdown" ? renderMarkdown$1(part.content, width) : part.content, width);
275
+ }, 0);
276
+ }
277
+ /**
278
+ * Calculates the available width for assistant content after accounting for margins.
279
+ *
280
+ * @param columns The total number of columns in the terminal.
281
+ * @returns The available width for content (always at least 1).
282
+ */
283
+ function getAssistantContentWidth(columns) {
284
+ return Math.max(1, columns - 4);
285
+ }
286
+ //#endregion
287
+ //#region src/components/Messages/parsing.ts
221
288
  var FENCE_LINE_REGEX = /^(?<indent>[ \t]*)(?<fence>`{3,})(?<language>\w+)?[ \t]*$/;
222
289
  function flushTextSegment(segments, textLines) {
223
290
  const textContent = textLines.join("\n").trim();
@@ -303,14 +370,8 @@ function parseContent(content) {
303
370
  flushTextSegment(segments, textLines);
304
371
  return segments;
305
372
  }
306
- function getMessageColor(role) {
307
- switch (role) {
308
- case USER: return "black";
309
- case ASSISTANT: return "cyan";
310
- case SYSTEM: return "gray";
311
- default: return;
312
- }
313
- }
373
+ //#endregion
374
+ //#region src/components/Messages/streaming.ts
314
375
  function isWordCharacter(char) {
315
376
  return char !== void 0 && /[A-Za-z0-9]/.test(char);
316
377
  }
@@ -405,11 +466,27 @@ function splitStreamingInlineContent(content) {
405
466
  return parts;
406
467
  }
407
468
  //#endregion
469
+ //#region src/components/Messages/styles.ts
470
+ function getMessageColor(role) {
471
+ switch (role) {
472
+ case USER: return "black";
473
+ case ASSISTANT: return "cyan";
474
+ case SYSTEM: return "gray";
475
+ default: return;
476
+ }
477
+ }
478
+ //#endregion
408
479
  //#region src/components/Messages/Messages.tsx
409
480
  function Message({ message, isStreaming = false }) {
481
+ const { stdout } = useStdout();
410
482
  const messageColor = getMessageColor(message.role);
411
483
  const isSystem = message.role === SYSTEM;
412
484
  const isUser = message.role === USER;
485
+ const isStreamingAssistant = isStreaming && !isUser && !isSystem;
486
+ const stickyHeightRef = useRef({
487
+ columns: stdout.columns,
488
+ maxHeight: 0
489
+ });
413
490
  if (isSystem) return /* @__PURE__ */ jsx(Box, {
414
491
  flexDirection: "column",
415
492
  marginBottom: 1,
@@ -420,10 +497,23 @@ function Message({ message, isStreaming = false }) {
420
497
  children: message.content
421
498
  })
422
499
  });
423
- return /* @__PURE__ */ jsx(Box, {
500
+ const segments = parseContent(message.content);
501
+ const availableWidth = getAssistantContentWidth(stdout.columns);
502
+ if (stickyHeightRef.current.columns !== stdout.columns) stickyHeightRef.current = {
503
+ columns: stdout.columns,
504
+ maxHeight: 0
505
+ };
506
+ const streamingHeight = isStreamingAssistant ? segments.reduce((height, segment) => {
507
+ if (segment.type === "code") return height + getCodeBlockHeight(segment.content, availableWidth);
508
+ if (segment.type === "raw") return height + getCodeBlockHeight(unwrapRawMarkdownFence(segment.content) ?? segment.content, availableWidth);
509
+ return height + getStreamingTextHeight(splitStreamingInlineContent(segment.content), availableWidth);
510
+ }, 0) : 0;
511
+ if (isStreamingAssistant) stickyHeightRef.current.maxHeight = Math.max(stickyHeightRef.current.maxHeight, streamingHeight);
512
+ const stickyPaddingLines = isStreamingAssistant ? stickyHeightRef.current.maxHeight - streamingHeight : 0;
513
+ return /* @__PURE__ */ jsxs(Box, {
424
514
  flexDirection: "column",
425
515
  marginBottom: 1,
426
- children: parseContent(message.content).map((segment, index) => {
516
+ children: [segments.map((segment, index) => {
427
517
  const prefix = isUser && index === 0 ? "> " : "";
428
518
  if (segment.type === "code") return isUser ? /* @__PURE__ */ jsx(Text, {
429
519
  color: messageColor,
@@ -465,7 +555,7 @@ function Message({ message, isStreaming = false }) {
465
555
  color: messageColor
466
556
  }, partIndex))
467
557
  }, index);
468
- })
558
+ }), Array.from({ length: stickyPaddingLines }, (_, index) => /* @__PURE__ */ jsx(Text, { children: " " }, "padding-" + String(index)))]
469
559
  });
470
560
  }
471
561
  function Messages({ messages, isLoading, sessionId, streamingMessage }) {
@@ -644,16 +734,6 @@ function ToolApproval({ toolCall, onDecision }) {
644
734
  });
645
735
  }
646
736
  //#endregion
647
- //#region src/components/Chat/constants.ts
648
- var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
649
- var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
650
- var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
651
- var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
652
- INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
653
- INTERRUPT_REASON["REJECTED"] = "rejected";
654
- return INTERRUPT_REASON;
655
- }({});
656
- //#endregion
657
737
  //#region src/components/TextInput/TextInput.tsx
658
738
  function buildLineSegments(displayValue, cursorPosition, width) {
659
739
  const safeWidth = Math.max(1, width);
@@ -953,17 +1033,28 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
953
1033
  });
954
1034
  }
955
1035
  //#endregion
956
- //#region src/components/Chat/Input.tsx
1036
+ //#region src/components/Chat/ChatInput.tsx
957
1037
  function hasFileSuggestionQuery(input) {
958
1038
  return /(^|.)@\S+/.test(input);
959
1039
  }
960
- function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1040
+ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, onSubmit }) {
961
1041
  const { exit } = useApp();
1042
+ const [history, setHistory] = useState(sessionHistory);
1043
+ const [historyIndex, setHistoryIndex] = useState(null);
962
1044
  const [input, setInput] = useState("");
963
1045
  const [cursorPosition, setCursorPosition] = useState(void 0);
964
1046
  const fileSuggestionRef = useRef(null);
1047
+ useEffect(() => {
1048
+ setHistory(sessionHistory);
1049
+ setHistoryIndex(null);
1050
+ setInput("");
1051
+ setCursorPosition(void 0);
1052
+ fileSuggestionRef.current = null;
1053
+ }, [sessionHistory]);
965
1054
  const resetInput = useCallback(() => {
966
1055
  setInput("");
1056
+ setCursorPosition(void 0);
1057
+ setHistoryIndex(null);
967
1058
  }, []);
968
1059
  const handleSelectFileSuggestion = useCallback((nextInput) => {
969
1060
  setInput(nextInput.value);
@@ -987,15 +1078,58 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
987
1078
  };
988
1079
  } else fileSuggestionRef.current = null;
989
1080
  }, [input]);
1081
+ const handleInputChange = useCallback((nextInput) => {
1082
+ setInput(nextInput);
1083
+ setHistoryIndex(null);
1084
+ }, []);
990
1085
  const submitAndReset = useCallback((input) => {
991
1086
  const trimmedInput = input.trim();
992
1087
  if (!trimmedInput) return;
993
1088
  onSubmit(trimmedInput);
1089
+ if (!trimmedInput.startsWith("/")) setHistory((previousHistory) => [...previousHistory, trimmedInput]);
994
1090
  resetInput();
995
1091
  fileSuggestionRef.current = null;
996
1092
  }, [onSubmit, resetInput]);
997
1093
  const showCommandMenu = input.startsWith("/");
998
1094
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
1095
+ const handleHistoryNavigation = useCallback((direction) => {
1096
+ if (!history.length || showFileSuggestions) return;
1097
+ if (direction === "up") {
1098
+ if (historyIndex === null) {
1099
+ if (input) return;
1100
+ const nextIndex = history.length - 1;
1101
+ const nextInput = history[nextIndex];
1102
+ setHistoryIndex(nextIndex);
1103
+ setInput(nextInput);
1104
+ setCursorPosition(nextInput.length);
1105
+ return;
1106
+ }
1107
+ if (historyIndex === 0) return;
1108
+ const nextIndex = historyIndex - 1;
1109
+ const nextInput = history[nextIndex];
1110
+ setHistoryIndex(nextIndex);
1111
+ setInput(nextInput);
1112
+ setCursorPosition(nextInput.length);
1113
+ return;
1114
+ }
1115
+ if (historyIndex === null) return;
1116
+ if (historyIndex === history.length - 1) {
1117
+ setHistoryIndex(null);
1118
+ setInput("");
1119
+ setCursorPosition(0);
1120
+ return;
1121
+ }
1122
+ const nextIndex = historyIndex + 1;
1123
+ const nextInput = history[nextIndex];
1124
+ setHistoryIndex(nextIndex);
1125
+ setInput(nextInput);
1126
+ setCursorPosition(nextInput.length);
1127
+ }, [
1128
+ history,
1129
+ historyIndex,
1130
+ input,
1131
+ showFileSuggestions
1132
+ ]);
999
1133
  const handleSubmitText = useCallback((input) => {
1000
1134
  if (input.startsWith("/")) return;
1001
1135
  if (hasFileSuggestionQuery(input)) {
@@ -1007,8 +1141,8 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1007
1141
  const handleSubmitCommand = useCallback((input) => {
1008
1142
  if (LIST.find(({ name }) => name === input)) submitAndReset(input);
1009
1143
  }, [submitAndReset]);
1010
- useInput((_input, key) => {
1011
- const isCtrlC = key.ctrl && _input === "c";
1144
+ useInput((inputKey, key) => {
1145
+ const isCtrlC = key.ctrl && inputKey === "c";
1012
1146
  if (isDisabled) {
1013
1147
  if (key.escape || isCtrlC) onInterrupt?.();
1014
1148
  return;
@@ -1020,6 +1154,11 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1020
1154
  }
1021
1155
  exit();
1022
1156
  }
1157
+ if (key.upArrow) {
1158
+ handleHistoryNavigation("up");
1159
+ return;
1160
+ }
1161
+ if (key.downArrow) handleHistoryNavigation("down");
1023
1162
  });
1024
1163
  return /* @__PURE__ */ jsxs(Box, {
1025
1164
  flexDirection: "column",
@@ -1029,7 +1168,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1029
1168
  isDisabled,
1030
1169
  cursorPosition,
1031
1170
  wrapIndent: 2,
1032
- onChange: setInput,
1171
+ onChange: handleInputChange,
1033
1172
  onSubmit: handleSubmitText,
1034
1173
  placeholder: "Ask anything... (/ commands, @ files)"
1035
1174
  })] }),
@@ -1047,6 +1186,16 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
1047
1186
  });
1048
1187
  }
1049
1188
  //#endregion
1189
+ //#region src/components/Chat/constants.ts
1190
+ var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1191
+ var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
1192
+ var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1193
+ var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
1194
+ INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
1195
+ INTERRUPT_REASON["REJECTED"] = "rejected";
1196
+ return INTERRUPT_REASON;
1197
+ }({});
1198
+ //#endregion
1050
1199
  //#region src/components/Chat/plan.ts
1051
1200
  function hasExecutablePlan(content) {
1052
1201
  return content.split("\n").some((line) => {
@@ -1058,6 +1207,7 @@ function hasExecutablePlan(content) {
1058
1207
  //#region src/components/Chat/Chat.tsx
1059
1208
  function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
1060
1209
  const sessionMessages = initialMessages ?? [];
1210
+ const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
1061
1211
  const [messages, setMessages] = useState(sessionMessages);
1062
1212
  const [streamingMessage, setStreamingMessage] = useState(null);
1063
1213
  const [isLoading, setIsLoading] = useState(false);
@@ -1406,7 +1556,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1406
1556
  }),
1407
1557
  !pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Box, {
1408
1558
  marginTop: 1,
1409
- children: /* @__PURE__ */ jsx(Input, {
1559
+ children: /* @__PURE__ */ jsx(ChatInput, {
1560
+ history,
1410
1561
  isDisabled: isLoading,
1411
1562
  onInterrupt: handleInterrupt,
1412
1563
  onSubmit: handleSubmit
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ var LIST = [
33
33
  //#endregion
34
34
  //#region package.json
35
35
  var name = "code-ollama";
36
- var version = "0.15.0";
36
+ var version = "0.16.0";
37
37
  //#endregion
38
38
  //#region src/constants/package.ts
39
39
  var NAME = name;
@@ -931,7 +931,7 @@ async function main(args = process.argv.slice(2)) {
931
931
  else await launchTui();
932
932
  }
933
933
  async function launchTui(sessionId) {
934
- const { renderApp } = await import("./assets/tui-DPx5MGHZ.js");
934
+ const { renderApp } = await import("./assets/tui-85A3pZD2.js");
935
935
  reset();
936
936
  renderApp(sessionId);
937
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Ollama coding agent that runs in your terminal",
5
5
  "author": "Mark <mark@remarkablemark.org> (https://remarkablemark.org)",
6
6
  "type": "module",
@@ -57,7 +57,7 @@
57
57
  "@types/node": "25.8.0",
58
58
  "@types/react": "19.2.14",
59
59
  "@vitest/coverage-v8": "4.1.6",
60
- "eslint": "10.3.0",
60
+ "eslint": "10.4.0",
61
61
  "eslint-plugin-prettier": "5.5.5",
62
62
  "eslint-plugin-simple-import-sort": "13.0.0",
63
63
  "globals": "17.6.0",