code-ollama 0.23.0 → 0.23.2

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.
@@ -3,8 +3,8 @@ import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { exec } from "node:child_process";
6
- import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
7
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { Box, Static, Text, render, useApp, useInput, usePaste, useStdout } from "ink";
7
+ import { memo, useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { ProgressBar, Select, Spinner } from "@inkjs/ui";
10
10
  import { Marked } from "marked";
@@ -747,36 +747,46 @@ function ToolApproval({ toolCall, onDecision, theme = getTheme() }) {
747
747
  //#region src/components/TextInput/TextInput.tsx
748
748
  function buildLineSegments(displayValue, cursorPosition, width) {
749
749
  const safeWidth = Math.max(1, width);
750
- const cursorChar = displayValue[cursorPosition] || " ";
751
- const renderValue = displayValue.slice(0, cursorPosition) + cursorChar + displayValue.slice(cursorPosition + 1);
752
- const totalLength = Math.max(1, renderValue.length);
753
750
  const lines = [];
754
- for (let start = 0; start < totalLength; start += safeWidth) {
755
- const end = start + safeWidth;
756
- const text = renderValue.slice(start, end);
757
- const hasCursor = cursorPosition >= start && cursorPosition < end;
758
- if (!hasCursor) {
751
+ const logicalLines = displayValue.split("\n");
752
+ let lineStart = 0;
753
+ for (const [lineIndex, logicalLine] of logicalLines.entries()) {
754
+ const lineEnd = lineStart + logicalLine.length;
755
+ const hasCursorOnLine = cursorPosition >= lineStart && cursorPosition <= lineEnd;
756
+ const cursorOffset = cursorPosition - lineStart;
757
+ const renderValue = hasCursorOnLine && cursorOffset === logicalLine.length ? `${logicalLine} ` : logicalLine;
758
+ const totalLength = Math.max(1, renderValue.length);
759
+ for (let start = 0; start < totalLength; start += safeWidth) {
760
+ const end = start + safeWidth;
761
+ const text = renderValue.slice(start, end);
762
+ const hasCursor = hasCursorOnLine && cursorOffset >= start && cursorOffset < end;
763
+ if (!hasCursor) {
764
+ lines.push({
765
+ text,
766
+ hasCursor,
767
+ beforeCursor: "",
768
+ cursorChar: " ",
769
+ afterCursor: ""
770
+ });
771
+ continue;
772
+ }
773
+ const offset = cursorOffset - start;
759
774
  lines.push({
760
775
  text,
761
776
  hasCursor,
762
- beforeCursor: "",
763
- cursorChar: " ",
764
- afterCursor: ""
777
+ beforeCursor: text.slice(0, offset),
778
+ cursorChar: text[offset],
779
+ afterCursor: text.slice(offset + 1)
765
780
  });
766
- continue;
767
781
  }
768
- const offset = cursorPosition - start;
769
- lines.push({
770
- text,
771
- hasCursor,
772
- beforeCursor: text.slice(0, offset),
773
- cursorChar: text[offset] || " ",
774
- afterCursor: text.slice(offset + 1)
775
- });
782
+ lineStart = lineEnd + (lineIndex < logicalLines.length - 1 ? 1 : 0);
776
783
  }
777
784
  return lines;
778
785
  }
779
- function TextInput({ value, isDisabled = false, placeholder, cursorPosition: externalCursorPosition, wrapIndent = 0, onChange, onSubmit }) {
786
+ function normalizePastedText(input) {
787
+ return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
788
+ }
789
+ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: externalCursorPosition, allowMultilinePaste = false, wrapIndent = 0, onChange, onSubmit }) {
780
790
  const { stdout } = useStdout();
781
791
  const [cursorPosition, setCursorPosition] = useState(value.length);
782
792
  const prevValueRef = useRef(value);
@@ -798,9 +808,24 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
798
808
  cursorPosition,
799
809
  externalCursorPosition
800
810
  ]);
811
+ const insertText = useCallback((text) => {
812
+ onChange(value.slice(0, cursorPosition) + text + value.slice(cursorPosition));
813
+ setCursorPosition(cursorPosition + text.length);
814
+ }, [
815
+ cursorPosition,
816
+ onChange,
817
+ value
818
+ ]);
819
+ usePaste((text) => {
820
+ insertText(normalizePastedText(text));
821
+ }, { isActive: allowMultilinePaste && !isDisabled });
801
822
  useInput((input, key) => {
802
823
  // v8 ignore next
803
824
  if (isDisabled) return;
825
+ if (allowMultilinePaste && input.length > 1 && /[\r\n]/.test(input)) {
826
+ insertText(normalizePastedText(input));
827
+ return;
828
+ }
804
829
  if (key.return) {
805
830
  onSubmit(value);
806
831
  setCursorPosition(0);
@@ -845,10 +870,7 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
845
870
  }
846
871
  if (key.ctrl) return;
847
872
  // v8 ignore start
848
- if (input) {
849
- onChange(value.slice(0, cursorPosition) + input + value.slice(cursorPosition));
850
- setCursorPosition(cursorPosition + input.length);
851
- }
873
+ if (input) insertText(input);
852
874
  // v8 ignore stop
853
875
  }, { isActive: !isDisabled });
854
876
  const displayValue = value || (placeholder ?? "");
@@ -948,25 +970,6 @@ function extractImageAttachments(input) {
948
970
  };
949
971
  }
950
972
  //#endregion
951
- //#region src/components/Chat/CommandMenu.tsx
952
- function getMatchingCommands(input) {
953
- const normalizedInput = input.trim().toLowerCase();
954
- if (!normalizedInput.startsWith("/")) return [];
955
- return LIST.filter(({ name }) => name.toLowerCase().startsWith(normalizedInput)).map(({ name, description }) => ({
956
- label: `${name} - ${description}`,
957
- value: name
958
- }));
959
- }
960
- function CommandMenu({ input, onSubmit }) {
961
- const commandOptions = useMemo(() => getMatchingCommands(input), [input]);
962
- if (!commandOptions.length) return null;
963
- return /* @__PURE__ */ jsx(SelectPrompt, {
964
- highlightText: input,
965
- onChange: onSubmit,
966
- options: commandOptions
967
- });
968
- }
969
- //#endregion
970
973
  //#region src/components/Suggestions/Suggestions.tsx
971
974
  var DEFAULT_MAX_VISIBLE_OPTIONS = 5;
972
975
  function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_MAX_VISIBLE_OPTIONS, resetKey, onHighlight, onSelect }) {
@@ -999,7 +1002,7 @@ function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_
999
1002
  setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
1000
1003
  return;
1001
1004
  }
1002
- if (key.tab || key.return || input === " " || input === "\r") onSelect(options[focusedIndex]);
1005
+ if (key.tab || key.return) onSelect(options[focusedIndex]);
1003
1006
  });
1004
1007
  if (!options.length) return null;
1005
1008
  const visibleStart = Math.min(Math.max(0, focusedIndex - maxVisibleOptions + 1), Math.max(0, options.length - maxVisibleOptions));
@@ -1017,6 +1020,26 @@ function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_
1017
1020
  });
1018
1021
  }
1019
1022
  //#endregion
1023
+ //#region src/components/Chat/CommandMenu.tsx
1024
+ function getMatchingCommands(input) {
1025
+ const normalizedInput = input.trim().toLowerCase();
1026
+ if (!normalizedInput.startsWith("/")) return [];
1027
+ return LIST.filter(({ name }) => name.toLowerCase().startsWith(normalizedInput)).map(({ name, description }) => ({
1028
+ label: `${name} - ${description}`,
1029
+ value: name
1030
+ }));
1031
+ }
1032
+ function CommandMenu({ input, onSubmit }) {
1033
+ const commandOptions = useMemo(() => getMatchingCommands(input), [input]);
1034
+ if (!commandOptions.length) return null;
1035
+ return /* @__PURE__ */ jsx(Suggestions, {
1036
+ onSelect: (option) => {
1037
+ onSubmit(option.value);
1038
+ },
1039
+ options: commandOptions
1040
+ });
1041
+ }
1042
+ //#endregion
1020
1043
  //#region src/components/Chat/FileSuggestions.tsx
1021
1044
  var MENTION_PATTERN = /(^|.)@(\S+)/;
1022
1045
  var RIPGREP_MAX_BUFFER = 10 * 1024 * 1024;
@@ -1263,7 +1286,8 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1263
1286
  onSubmit,
1264
1287
  resetInput
1265
1288
  ]);
1266
- const showCommandMenu = input.startsWith("/");
1289
+ const isMultilineInput = input.includes("\n");
1290
+ const showCommandMenu = input.startsWith("/") && !isMultilineInput;
1267
1291
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
1268
1292
  const handleHistoryNavigation = useCallback((direction) => {
1269
1293
  if (!history.length || showFileSuggestions || hasAttachments) return;
@@ -1305,7 +1329,7 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1305
1329
  showFileSuggestions
1306
1330
  ]);
1307
1331
  const handleSubmitText = useCallback((value) => {
1308
- if (value.startsWith("/")) return;
1332
+ if (value.startsWith("/") && !value.includes("\n")) return;
1309
1333
  if (hasFileSuggestionQuery(value)) {
1310
1334
  if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
1311
1335
  return;
@@ -1365,6 +1389,7 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1365
1389
  value: input,
1366
1390
  isDisabled,
1367
1391
  cursorPosition,
1392
+ allowMultilinePaste: true,
1368
1393
  wrapIndent,
1369
1394
  onChange: handleInputChange,
1370
1395
  onSubmit: handleSubmitText,
@@ -1389,6 +1414,21 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1389
1414
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1390
1415
  var PLAN_CHECKLIST_REMINDER = "Then display the plan using either the Plan Needs Input or Proposed Plan Markdown template";
1391
1416
  var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1417
+ var ChatActionType = /* @__PURE__ */ function(ChatActionType) {
1418
+ ChatActionType["AppendMessage"] = "append-message";
1419
+ ChatActionType["ClearPendingPlan"] = "clear-pending-plan";
1420
+ ChatActionType["ClearPendingToolCall"] = "clear-pending-tool-call";
1421
+ ChatActionType["CommitMessages"] = "commit-messages";
1422
+ ChatActionType["Interrupt"] = "interrupt";
1423
+ ChatActionType["RequestPlanReview"] = "request-plan-review";
1424
+ ChatActionType["RequestToolApproval"] = "request-tool-approval";
1425
+ ChatActionType["ResetSession"] = "reset-session";
1426
+ ChatActionType["SetLoading"] = "set-loading";
1427
+ ChatActionType["SetStreamingMessage"] = "set-streaming-message";
1428
+ ChatActionType["StartTurn"] = "start-turn";
1429
+ ChatActionType["ToolRejected"] = "tool-rejected";
1430
+ return ChatActionType;
1431
+ }({});
1392
1432
  var InterruptReason = /* @__PURE__ */ function(InterruptReason) {
1393
1433
  InterruptReason["Interrupted"] = "interrupted";
1394
1434
  InterruptReason["Rejected"] = "rejected";
@@ -1406,27 +1446,91 @@ function hasExecutablePlan(content) {
1406
1446
  return lines.slice(executionStepsIndex + 1, nextSectionIndex === -1 ? void 0 : nextSectionIndex).some((line) => /^(?:[-*]|\d+[.)])\s+\S/.test(line.trim()));
1407
1447
  }
1408
1448
  //#endregion
1449
+ //#region src/components/Chat/reducer.ts
1450
+ function createInitialChatState(messages = []) {
1451
+ return {
1452
+ messages,
1453
+ streamingMessage: null,
1454
+ isLoading: false,
1455
+ pendingToolCall: null,
1456
+ pendingPlan: null,
1457
+ interruptReason: null
1458
+ };
1459
+ }
1460
+ function chatReducer(state, action) {
1461
+ switch (action.type) {
1462
+ case ChatActionType.AppendMessage: return {
1463
+ ...state,
1464
+ messages: [...state.messages, action.message]
1465
+ };
1466
+ case ChatActionType.ClearPendingPlan: return {
1467
+ ...state,
1468
+ pendingPlan: null
1469
+ };
1470
+ case ChatActionType.ClearPendingToolCall: return {
1471
+ ...state,
1472
+ pendingToolCall: null
1473
+ };
1474
+ case ChatActionType.CommitMessages: return {
1475
+ ...state,
1476
+ messages: action.messages
1477
+ };
1478
+ case ChatActionType.Interrupt: return {
1479
+ ...state,
1480
+ messages: [...state.messages, action.message],
1481
+ streamingMessage: null,
1482
+ isLoading: false,
1483
+ interruptReason: InterruptReason.Interrupted
1484
+ };
1485
+ case ChatActionType.RequestPlanReview: return {
1486
+ ...state,
1487
+ pendingPlan: action.pendingPlan,
1488
+ isLoading: false
1489
+ };
1490
+ case ChatActionType.RequestToolApproval: return {
1491
+ ...state,
1492
+ pendingToolCall: action.pendingToolCall,
1493
+ isLoading: false
1494
+ };
1495
+ case ChatActionType.ResetSession: return createInitialChatState(action.messages);
1496
+ case ChatActionType.SetLoading: return {
1497
+ ...state,
1498
+ isLoading: action.isLoading
1499
+ };
1500
+ case ChatActionType.SetStreamingMessage: return {
1501
+ ...state,
1502
+ streamingMessage: action.message
1503
+ };
1504
+ case ChatActionType.StartTurn: return {
1505
+ ...state,
1506
+ messages: [...state.messages, action.message],
1507
+ isLoading: true,
1508
+ interruptReason: null
1509
+ };
1510
+ case ChatActionType.ToolRejected: return {
1511
+ ...state,
1512
+ messages: action.messages,
1513
+ isLoading: false,
1514
+ interruptReason: InterruptReason.Rejected
1515
+ };
1516
+ }
1517
+ }
1518
+ //#endregion
1409
1519
  //#region src/components/Chat/Chat.tsx
1410
1520
  var MAX_TOOL_TURNS = 25;
1411
1521
  var MAX_TOOL_INTENT_CORRECTIONS = 2;
1412
1522
  function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1413
1523
  const sessionMessages = initialMessages ?? [];
1414
1524
  const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
1415
- const [messages, setMessages] = useState(sessionMessages);
1416
- const [streamingMessage, setStreamingMessage] = useState(null);
1417
- const [isLoading, setIsLoading] = useState(false);
1418
- const [pendingToolCall, setPendingToolCall] = useState(null);
1419
- const [pendingPlan, setPendingPlan] = useState(null);
1420
- const [interruptReason, setInterruptReason] = useState(null);
1525
+ const [state, dispatch] = useReducer(chatReducer, sessionMessages, createInitialChatState);
1526
+ const { messages, streamingMessage, isLoading, pendingToolCall, pendingPlan, interruptReason } = state;
1421
1527
  const abortControllerRef = useRef(null);
1422
1528
  const persistedSnapshotRef = useRef("");
1423
1529
  useEffect(() => {
1424
- setMessages(sessionMessages);
1425
- setStreamingMessage(null);
1426
- setIsLoading(false);
1427
- setPendingToolCall(null);
1428
- setPendingPlan(null);
1429
- setInterruptReason(null);
1530
+ dispatch({
1531
+ type: ChatActionType.ResetSession,
1532
+ messages: sessionMessages
1533
+ });
1430
1534
  persistedSnapshotRef.current = JSON.stringify(sessionMessages);
1431
1535
  }, [sessionId]);
1432
1536
  useEffect(() => {
@@ -1467,13 +1571,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1467
1571
  const handleInterrupt = useCallback(() => {
1468
1572
  abortControllerRef.current?.abort();
1469
1573
  abortControllerRef.current = null;
1470
- setIsLoading(false);
1471
- setStreamingMessage(null);
1472
- setInterruptReason(InterruptReason.Interrupted);
1473
- setMessages((prev) => [...prev, {
1474
- role: USER,
1475
- content: TURN_ABORTED_MESSAGE
1476
- }]);
1574
+ dispatch({
1575
+ type: ChatActionType.Interrupt,
1576
+ message: {
1577
+ role: USER,
1578
+ content: TURN_ABORTED_MESSAGE
1579
+ }
1580
+ });
1477
1581
  }, []);
1478
1582
  const processStream = useCallback(async (currentMessages, executionMode = mode) => {
1479
1583
  const modelName = model;
@@ -1498,27 +1602,45 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1498
1602
  if (assistantCommitted) {
1499
1603
  if (committedMessages.at(-1)?.role === "assistant") {
1500
1604
  committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
1501
- setMessages(committedMessages);
1605
+ dispatch({
1606
+ type: ChatActionType.CommitMessages,
1607
+ messages: committedMessages
1608
+ });
1502
1609
  }
1503
1610
  return committedMessages;
1504
1611
  }
1505
1612
  // v8 ignore stop
1506
1613
  assistantCommitted = true;
1507
- setStreamingMessage(null);
1614
+ dispatch({
1615
+ type: ChatActionType.SetStreamingMessage,
1616
+ message: null
1617
+ });
1508
1618
  if (!assistantMessage.content) {
1509
- setMessages(committedMessages);
1619
+ dispatch({
1620
+ type: ChatActionType.CommitMessages,
1621
+ messages: committedMessages
1622
+ });
1510
1623
  return committedMessages;
1511
1624
  }
1512
1625
  committedMessages = [...committedMessages, { ...assistantMessage }];
1513
- setMessages(committedMessages);
1626
+ dispatch({
1627
+ type: ChatActionType.CommitMessages,
1628
+ messages: committedMessages
1629
+ });
1514
1630
  return committedMessages;
1515
1631
  };
1516
- setStreamingMessage(assistantMessage);
1632
+ dispatch({
1633
+ type: ChatActionType.SetStreamingMessage,
1634
+ message: assistantMessage
1635
+ });
1517
1636
  let nextMessages = null;
1518
1637
  for await (const chunk of streamChat(withSystemMessage(activeMessages), modelName, TOOLS, controller.signal)) {
1519
1638
  if (chunk.type === "content") {
1520
1639
  assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1521
- setStreamingMessage({ ...assistantMessage });
1640
+ dispatch({
1641
+ type: ChatActionType.SetStreamingMessage,
1642
+ message: { ...assistantMessage }
1643
+ });
1522
1644
  continue;
1523
1645
  }
1524
1646
  if (chunk.tool_calls.length === 0) continue;
@@ -1527,12 +1649,14 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1527
1649
  for (const toolCall of chunk.tool_calls) try {
1528
1650
  const normalized = normalizeToolCall(toolCall);
1529
1651
  if (executionMode === "safe" && normalized.requiresApproval) {
1530
- setPendingToolCall({
1531
- toolCall,
1532
- messages: [...updatedMessages, ...toolResultMessages],
1533
- executionMode
1652
+ dispatch({
1653
+ type: ChatActionType.RequestToolApproval,
1654
+ pendingToolCall: {
1655
+ toolCall,
1656
+ messages: [...updatedMessages, ...toolResultMessages],
1657
+ executionMode
1658
+ }
1534
1659
  });
1535
- setIsLoading(false);
1536
1660
  return;
1537
1661
  }
1538
1662
  // v8 ignore next
@@ -1547,7 +1671,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1547
1671
  }));
1548
1672
  }
1549
1673
  nextMessages = [...updatedMessages, ...toolResultMessages];
1550
- setMessages(nextMessages);
1674
+ dispatch({
1675
+ type: ChatActionType.CommitMessages,
1676
+ messages: nextMessages
1677
+ });
1551
1678
  break;
1552
1679
  }
1553
1680
  if (!nextMessages) {
@@ -1559,7 +1686,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1559
1686
  role: SYSTEM,
1560
1687
  content: TOOL_INTENT_CORRECTION
1561
1688
  }];
1562
- setMessages(activeMessages);
1689
+ dispatch({
1690
+ type: ChatActionType.CommitMessages,
1691
+ messages: activeMessages
1692
+ });
1563
1693
  continue;
1564
1694
  }
1565
1695
  return;
@@ -1568,14 +1698,18 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1568
1698
  toolIntentCorrections = 0;
1569
1699
  /* v8 ignore start */
1570
1700
  if (toolTurns >= MAX_TOOL_TURNS) {
1571
- setMessages([...nextMessages, {
1701
+ const stoppedMessages = [...nextMessages, {
1572
1702
  role: SYSTEM,
1573
1703
  content: [
1574
1704
  "Tool execution stopped because the maximum tool turn limit was reached",
1575
1705
  ACTION_NOT_PERFORMED,
1576
1706
  "Summarize completed work and explain what remains without calling more tools."
1577
1707
  ].join("\n")
1578
- }]);
1708
+ }];
1709
+ dispatch({
1710
+ type: ChatActionType.CommitMessages,
1711
+ messages: stoppedMessages
1712
+ });
1579
1713
  return;
1580
1714
  }
1581
1715
  /* v8 ignore stop */
@@ -1589,13 +1723,22 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1589
1723
  content: `Error: ${error instanceof Error ? error.message : String(error)}`
1590
1724
  };
1591
1725
  await prewarmCodeBlocks(errorMessage.content, theme);
1592
- setStreamingMessage(null);
1593
- setMessages([...activeMessages, errorMessage]);
1726
+ dispatch({
1727
+ type: ChatActionType.SetStreamingMessage,
1728
+ message: null
1729
+ });
1730
+ dispatch({
1731
+ type: ChatActionType.CommitMessages,
1732
+ messages: [...activeMessages, errorMessage]
1733
+ });
1594
1734
  }
1595
1735
  } finally {
1596
1736
  // v8 ignore next
1597
1737
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
1598
- setIsLoading(false);
1738
+ dispatch({
1739
+ type: ChatActionType.SetLoading,
1740
+ isLoading: false
1741
+ });
1599
1742
  }
1600
1743
  }, [
1601
1744
  buildToolResultMessage,
@@ -1625,22 +1768,37 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1625
1768
  if (assistantCommitted) {
1626
1769
  if (committedMessages.at(-1)?.role === "assistant") {
1627
1770
  committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
1628
- setMessages(committedMessages);
1771
+ dispatch({
1772
+ type: ChatActionType.CommitMessages,
1773
+ messages: committedMessages
1774
+ });
1629
1775
  }
1630
1776
  return committedMessages;
1631
1777
  }
1632
1778
  /* v8 ignore stop */
1633
1779
  assistantCommitted = true;
1634
- setStreamingMessage(null);
1780
+ dispatch({
1781
+ type: ChatActionType.SetStreamingMessage,
1782
+ message: null
1783
+ });
1635
1784
  if (!assistantMessage.content) {
1636
- setMessages(committedMessages);
1785
+ dispatch({
1786
+ type: ChatActionType.CommitMessages,
1787
+ messages: committedMessages
1788
+ });
1637
1789
  return committedMessages;
1638
1790
  }
1639
1791
  committedMessages = [...committedMessages, { ...assistantMessage }];
1640
- setMessages(committedMessages);
1792
+ dispatch({
1793
+ type: ChatActionType.CommitMessages,
1794
+ messages: committedMessages
1795
+ });
1641
1796
  return committedMessages;
1642
1797
  };
1643
- setStreamingMessage(emptyAssistantMessage);
1798
+ dispatch({
1799
+ type: ChatActionType.SetStreamingMessage,
1800
+ message: emptyAssistantMessage
1801
+ });
1644
1802
  try {
1645
1803
  const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
1646
1804
  const planResearchMessages = [...currentMessages, {
@@ -1652,18 +1810,30 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1652
1810
  if (controller.signal.aborted) return;
1653
1811
  if (chunk.type === "content") {
1654
1812
  assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1655
- setStreamingMessage({ ...assistantMessage });
1813
+ dispatch({
1814
+ type: ChatActionType.SetStreamingMessage,
1815
+ message: { ...assistantMessage }
1816
+ });
1656
1817
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
1657
1818
  const toolName = toolCall.function.name;
1658
1819
  if (!READ_TOOLS.has(toolName)) {
1659
1820
  const correctionMessage = buildPlanModeCorrectionMessage(toolName);
1660
- setStreamingMessage(null);
1821
+ dispatch({
1822
+ type: ChatActionType.SetStreamingMessage,
1823
+ message: null
1824
+ });
1661
1825
  const newMessages = [...committedMessages, correctionMessage];
1662
- setMessages(newMessages);
1826
+ dispatch({
1827
+ type: ChatActionType.CommitMessages,
1828
+ messages: newMessages
1829
+ });
1663
1830
  await processStreamReadOnly(newMessages);
1664
1831
  return;
1665
1832
  }
1666
- setStreamingMessage(emptyAssistantMessage);
1833
+ dispatch({
1834
+ type: ChatActionType.SetStreamingMessage,
1835
+ message: emptyAssistantMessage
1836
+ });
1667
1837
  assistantMessage.content = "";
1668
1838
  const updatedMessages = committedMessages;
1669
1839
  let normalized;
@@ -1676,14 +1846,20 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1676
1846
  error: error instanceof Error ? error.message : String(error)
1677
1847
  });
1678
1848
  const newMessages = [...updatedMessages, toolResultMessage];
1679
- setMessages(newMessages);
1849
+ dispatch({
1850
+ type: ChatActionType.CommitMessages,
1851
+ messages: newMessages
1852
+ });
1680
1853
  await processStreamReadOnly(newMessages);
1681
1854
  return;
1682
1855
  }
1683
1856
  const result = await executeTool(normalized.name, normalized.arguments, { allowedTools: READ_TOOLS });
1684
1857
  const toolResultMessage = buildToolResultMessage(normalized.name, result, normalized.arguments);
1685
1858
  const newMessages = [...updatedMessages, toolResultMessage];
1686
- setMessages(newMessages);
1859
+ dispatch({
1860
+ type: ChatActionType.CommitMessages,
1861
+ messages: newMessages
1862
+ });
1687
1863
  await processStreamReadOnly(newMessages);
1688
1864
  return;
1689
1865
  }
@@ -1691,11 +1867,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1691
1867
  await prewarmCodeBlocks(assistantMessage.content, theme);
1692
1868
  const researchMessages = commitAssistantMessage();
1693
1869
  if (hasExecutablePlan(assistantMessage.content)) {
1694
- setPendingPlan({
1695
- planContent: assistantMessage.content,
1696
- messages: researchMessages
1870
+ dispatch({
1871
+ type: ChatActionType.RequestPlanReview,
1872
+ pendingPlan: {
1873
+ planContent: assistantMessage.content,
1874
+ messages: researchMessages
1875
+ }
1697
1876
  });
1698
- setIsLoading(false);
1699
1877
  return;
1700
1878
  }
1701
1879
  const planInstruction = {
@@ -1707,32 +1885,60 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1707
1885
  role: ASSISTANT,
1708
1886
  content: ""
1709
1887
  };
1710
- setStreamingMessage(emptyAssistantMessage);
1888
+ dispatch({
1889
+ type: ChatActionType.SetStreamingMessage,
1890
+ message: emptyAssistantMessage
1891
+ });
1711
1892
  try {
1712
1893
  for await (const chunk of streamChat(withSystemMessage(planMessages), modelName, [], controller.signal)) {
1713
1894
  // v8 ignore next 3
1714
1895
  if (controller.signal.aborted) return;
1715
1896
  if (chunk.type === "content") {
1716
1897
  planAssistantMessage.content = sanitizeAssistantContent(planAssistantMessage.content + chunk.content);
1717
- setStreamingMessage({ ...planAssistantMessage });
1898
+ dispatch({
1899
+ type: ChatActionType.SetStreamingMessage,
1900
+ message: { ...planAssistantMessage }
1901
+ });
1718
1902
  }
1719
1903
  }
1720
1904
  } catch (error) {
1721
1905
  // v8 ignore next
1722
1906
  planAssistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1723
- setMessages([...planMessages, { ...planAssistantMessage }]);
1724
- setStreamingMessage(null);
1725
- setIsLoading(false);
1907
+ const errorPlanMessages = [...planMessages, { ...planAssistantMessage }];
1908
+ dispatch({
1909
+ type: ChatActionType.CommitMessages,
1910
+ messages: errorPlanMessages
1911
+ });
1912
+ dispatch({
1913
+ type: ChatActionType.SetStreamingMessage,
1914
+ message: null
1915
+ });
1916
+ dispatch({
1917
+ type: ChatActionType.SetLoading,
1918
+ isLoading: false
1919
+ });
1726
1920
  return;
1727
1921
  }
1728
1922
  const finalPlanMessages = [...planMessages, { ...planAssistantMessage }];
1729
- setMessages(finalPlanMessages);
1730
- setStreamingMessage(null);
1731
- if (hasExecutablePlan(planAssistantMessage.content)) setPendingPlan({
1732
- planContent: planAssistantMessage.content,
1923
+ dispatch({
1924
+ type: ChatActionType.CommitMessages,
1733
1925
  messages: finalPlanMessages
1734
1926
  });
1735
- setIsLoading(false);
1927
+ dispatch({
1928
+ type: ChatActionType.SetStreamingMessage,
1929
+ message: null
1930
+ });
1931
+ if (hasExecutablePlan(planAssistantMessage.content)) dispatch({
1932
+ type: ChatActionType.RequestPlanReview,
1933
+ pendingPlan: {
1934
+ planContent: planAssistantMessage.content,
1935
+ messages: finalPlanMessages
1936
+ }
1937
+ });
1938
+ dispatch({
1939
+ type: ChatActionType.SetLoading,
1940
+ isLoading: false
1941
+ });
1736
1942
  } catch (error) {
1737
1943
  // v8 ignore next
1738
1944
  if (!controller.signal.aborted) {
@@ -1742,7 +1948,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1742
1948
  }
1743
1949
  } finally {
1744
1950
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
1745
- setIsLoading(false);
1951
+ dispatch({
1952
+ type: ChatActionType.SetLoading,
1953
+ isLoading: false
1954
+ });
1746
1955
  }
1747
1956
  }, [
1748
1957
  buildPlanModeCorrectionMessage,
@@ -1754,19 +1963,25 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1754
1963
  // v8 ignore next
1755
1964
  if (!pendingPlan) return;
1756
1965
  const { messages: planMessages } = pendingPlan;
1757
- setPendingPlan(null);
1966
+ dispatch({ type: ChatActionType.ClearPendingPlan });
1758
1967
  if (mode === "plan") {
1759
1968
  onModeChange(PLAN);
1760
1969
  const cancelMessage = {
1761
1970
  role: SYSTEM,
1762
1971
  content: "Continuing in Plan mode. No tools were executed."
1763
1972
  };
1764
- setMessages((previousMessages) => [...previousMessages, cancelMessage]);
1973
+ dispatch({
1974
+ type: ChatActionType.AppendMessage,
1975
+ message: cancelMessage
1976
+ });
1765
1977
  return;
1766
1978
  }
1767
1979
  const selectedMode = mode === "auto" ? AUTO : SAFE;
1768
1980
  onModeChange(selectedMode);
1769
- setIsLoading(true);
1981
+ dispatch({
1982
+ type: ChatActionType.SetLoading,
1983
+ isLoading: true
1984
+ });
1770
1985
  const executeInstruction = {
1771
1986
  role: SYSTEM,
1772
1987
  content: mode === "auto" ? "Execute the plan above. Use tools as needed without asking for further confirmation." : "Execute the plan above one step at a time. Wait for user approval before each tool call that modifies files or runs commands."
@@ -1781,14 +1996,20 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1781
1996
  // v8 ignore next
1782
1997
  if (!pendingToolCall) return;
1783
1998
  const { executionMode, messages: approvedMessages, toolCall } = pendingToolCall;
1784
- setPendingToolCall(null);
1785
- setIsLoading(true);
1999
+ dispatch({ type: ChatActionType.ClearPendingToolCall });
2000
+ dispatch({
2001
+ type: ChatActionType.SetLoading,
2002
+ isLoading: true
2003
+ });
1786
2004
  switch (decision) {
1787
2005
  case APPROVE: {
1788
2006
  const result = await executeToolCall(toolCall);
1789
2007
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, result, toolCall.function.arguments);
1790
2008
  const newMessages = [...approvedMessages, toolResultMessage];
1791
- setMessages(newMessages);
2009
+ dispatch({
2010
+ type: ChatActionType.CommitMessages,
2011
+ messages: newMessages
2012
+ });
1792
2013
  await processStream(newMessages, executionMode);
1793
2014
  break;
1794
2015
  }
@@ -1800,9 +2021,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1800
2021
  error: "Tool call rejected by user"
1801
2022
  }, toolCall.function.arguments)
1802
2023
  };
1803
- setMessages([...approvedMessages, toolResultMessage]);
1804
- setIsLoading(false);
1805
- setInterruptReason(InterruptReason.Rejected);
2024
+ dispatch({
2025
+ type: ChatActionType.ToolRejected,
2026
+ messages: [...approvedMessages, toolResultMessage]
2027
+ });
1806
2028
  break;
1807
2029
  }
1808
2030
  }
@@ -1812,21 +2034,22 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1812
2034
  processStream
1813
2035
  ]);
1814
2036
  const handleSubmit = useCallback(async ({ content, images }) => {
1815
- setInterruptReason(null);
1816
2037
  const userContent = content.trim();
1817
2038
  if (!userContent && !images?.length) return;
1818
2039
  if (userContent.startsWith("/")) {
1819
2040
  onCommand(userContent);
1820
2041
  return;
1821
2042
  }
1822
- setIsLoading(true);
1823
2043
  const userMessage = {
1824
2044
  role: USER,
1825
2045
  content: userContent,
1826
2046
  ...images?.length ? { images } : {}
1827
2047
  };
1828
2048
  const updatedMessages = [...messages, userMessage];
1829
- setMessages(updatedMessages);
2049
+ dispatch({
2050
+ type: ChatActionType.StartTurn,
2051
+ message: userMessage
2052
+ });
1830
2053
  if (mode === "plan") await processStreamReadOnly(updatedMessages);
1831
2054
  else await processStream(updatedMessages);
1832
2055
  }, [
package/dist/cli.js CHANGED
@@ -50,7 +50,7 @@ var LIST$1 = [
50
50
  //#endregion
51
51
  //#region package.json
52
52
  var name = "code-ollama";
53
- var version = "0.23.0";
53
+ var version = "0.23.2";
54
54
  //#endregion
55
55
  //#region src/constants/package.ts
56
56
  var NAME = name;
@@ -1029,11 +1029,15 @@ function writeFile(filePath, content) {
1029
1029
  */
1030
1030
  function editFile(filePath, oldText, newText) {
1031
1031
  try {
1032
- if (!existsSync(filePath)) return {
1033
- content: "",
1034
- error: `File not found: ${filePath}`
1035
- };
1036
- const content = readFileSync(filePath, "utf8");
1032
+ let content;
1033
+ try {
1034
+ content = readFileSync(filePath, "utf8");
1035
+ } catch {
1036
+ return {
1037
+ content: "",
1038
+ error: `File not found: ${filePath}`
1039
+ };
1040
+ }
1037
1041
  if (!content.includes(oldText)) return {
1038
1042
  content: "",
1039
1043
  error: `Exact text not found in file: ${filePath}`
@@ -1578,7 +1582,7 @@ async function main(args = process.argv.slice(2)) {
1578
1582
  else await launchTui();
1579
1583
  }
1580
1584
  async function launchTui(sessionId) {
1581
- const { renderApp } = await import("./assets/tui-iewVFcZW.js");
1585
+ const { renderApp } = await import("./assets/tui-DEmaVgHT.js");
1582
1586
  reset();
1583
1587
  renderApp(sessionId);
1584
1588
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
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",
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "@inkjs/ui": "2.0.0",
43
- "@shikijs/cli": "4.1.0",
43
+ "@shikijs/cli": "4.2.0",
44
44
  "cac": "7.0.0",
45
45
  "ink": "7.0.5",
46
46
  "marked": "15.0.12",