code-ollama 0.21.1 → 0.23.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.
@@ -1,4 +1,4 @@
1
- import { A as saveClipboardImage, B as PLAN_GENERATION_INSTRUCTION, C as deleteModel, D as loadConfig, E as streamChat, F as LIST$1, G as PLAN, H as CATALOG, I as getTheme, J as REJECT, K as SAFE, L as ASSISTANT, M as withSystemMessage, N as HEADER_PREFIX, O as saveConfig, P as WARNING, R as SYSTEM, S as checkHealth, T as pullModel, U as AUTO, V as BACK, W as LABEL, X as VERSION, Y as NAME, Z as LIST, _ as loadSession, a as normalizeToolCall, b as reset, c as WRITE_TOOLS, d as write, f as appendMessage, g as listSessions, h as deleteSessionIfEmpty, i as formatToolResultContent, j as resetSystemMessage, k as removeClipboardImage, l as tick, m as deleteSession, n as executeTool, o as READ_TOOLS, p as createSession, q as APPROVE, r as executeToolCall, s as TOOLS, t as checkForUpdate, u as color, v as updateSessionModel, w as listModels, x as setClearHandler, y as clear, z as USER } from "../cli.js";
1
+ import { $ as NAME, A as loadConfig, B as ASSISTANT, C as checkHealth, D as pullModel, E as listModels, F as withSystemMessage, G as BACK, H as USER, I as HEADER_PREFIX, J as LABEL, K as CATALOG, L as WARNING, M as removeClipboardImage, N as saveClipboardImage, O as sanitizeAssistantContent, P as resetSystemMessage, Q as REJECT, R as LIST$1, S as TOOL_INTENT_CORRECTION, T as hasUncalledToolIntent, U as PLAN_GENERATION_INSTRUCTION, V as SYSTEM, W as PLAN_INSTRUCTION, X as SAFE, Y as PLAN, Z as APPROVE, _ as loadSession, a as normalizeToolCall, b as reset, c as WRITE_TOOLS, d as write, et as VERSION, f as appendMessage, g as listSessions, h as deleteSessionIfEmpty, i as formatToolResultContent, j as saveConfig, k as streamChat, l as tick, m as deleteSession, n as executeTool, o as READ_TOOLS, p as createSession, q as AUTO, r as executeToolCall, s as TOOLS, t as checkForUpdate, tt as LIST, u as color, v as updateSessionModel, w as deleteModel, x as setClearHandler, y as clear, z as getTheme } from "../cli.js";
2
2
  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";
@@ -11,6 +11,16 @@ import { Marked } from "marked";
11
11
  import { markedTerminal } from "marked-terminal";
12
12
  //#region src/components/CodeBlock/CodeBlock.tsx
13
13
  var highlightCache = /* @__PURE__ */ new Map();
14
+ var DIFF_LANGUAGE = "diff";
15
+ function getDiffLineColor(line, isSystem, theme) {
16
+ switch (true) {
17
+ case isSystem: return theme.colors.messageSystem;
18
+ case line.startsWith("+") && !line.startsWith("+++"): return "green";
19
+ case line.startsWith("-") && !line.startsWith("---"): return "red";
20
+ case line.startsWith("@@"): return theme.colors.accent;
21
+ case line.startsWith("---") || line.startsWith("+++"): return theme.colors.secondary;
22
+ }
23
+ }
14
24
  var CODE_BLOCK_REGEX = /^(?<indent>[ \t]*)(`{3,})(\w+)?[ \t]*\n([\s\S]*?)^\k<indent>\2[ \t]*$/gm;
15
25
  function normalizeCodeBlockContent(content, indent = "") {
16
26
  if (!indent) return content.trim();
@@ -48,6 +58,7 @@ async function highlightCode(code, language = "text", codeTheme = getTheme().cod
48
58
  }
49
59
  }
50
60
  var CodeBlock = memo(function CodeBlock({ code, language, role, theme = getTheme() }) {
61
+ const isDiff = language === DIFF_LANGUAGE;
51
62
  const cacheKey = `${theme.codeTheme}:${language ?? ""}:${code}`;
52
63
  const [highlighted, setHighlighted] = useState(() => highlightCache.get(cacheKey) ?? code);
53
64
  useEffect(() => {
@@ -76,7 +87,13 @@ var CodeBlock = memo(function CodeBlock({ code, language, role, theme = getTheme
76
87
  borderColor: isSystem ? theme.colors.secondary : theme.colors.codeBorder,
77
88
  paddingX: 1,
78
89
  marginY: 1,
79
- children: /* @__PURE__ */ jsx(Text, {
90
+ children: isDiff ? code.split("\n").map((line, index) => {
91
+ return /* @__PURE__ */ jsx(Text, {
92
+ color: getDiffLineColor(line, isSystem, theme),
93
+ dimColor: isSystem,
94
+ children: line || " "
95
+ }, index);
96
+ }) : /* @__PURE__ */ jsx(Text, {
80
97
  color: isSystem ? theme.colors.messageSystem : void 0,
81
98
  dimColor: isSystem,
82
99
  children: highlighted
@@ -393,6 +410,24 @@ function renderStickyPaddingLines(count) {
393
410
  index
394
411
  ));
395
412
  }
413
+ function ToolResultMessage({ message, messageColor, theme }) {
414
+ const diffContent = message.toolResult?.diff?.visible;
415
+ return /* @__PURE__ */ jsxs(Box, {
416
+ flexDirection: "column",
417
+ marginBottom: 1,
418
+ marginX: 2,
419
+ children: [/* @__PURE__ */ jsx(Text, {
420
+ color: messageColor,
421
+ dimColor: true,
422
+ children: message.content
423
+ }), diffContent && /* @__PURE__ */ jsx(CodeBlock, {
424
+ code: diffContent,
425
+ language: "diff",
426
+ role: "assistant",
427
+ theme
428
+ })]
429
+ });
430
+ }
396
431
  function Message({ message, isStreaming = false, theme }) {
397
432
  const messageColor = getMessageColor(message.role, theme);
398
433
  const isSystem = message.role === SYSTEM;
@@ -403,16 +438,23 @@ function Message({ message, isStreaming = false, theme }) {
403
438
  columns: stdout.columns,
404
439
  maxHeight: 0
405
440
  });
406
- if (isSystem) return /* @__PURE__ */ jsx(Box, {
407
- flexDirection: "column",
408
- marginBottom: 1,
409
- marginX: 2,
410
- children: /* @__PURE__ */ jsx(Text, {
411
- color: messageColor,
412
- dimColor: true,
413
- children: message.content
414
- })
415
- });
441
+ if (isSystem) {
442
+ if (message.toolResult?.diff) return /* @__PURE__ */ jsx(ToolResultMessage, {
443
+ message,
444
+ messageColor,
445
+ theme
446
+ });
447
+ return /* @__PURE__ */ jsx(Box, {
448
+ flexDirection: "column",
449
+ marginBottom: 1,
450
+ marginX: 2,
451
+ children: /* @__PURE__ */ jsx(Text, {
452
+ color: messageColor,
453
+ dimColor: true,
454
+ children: message.content
455
+ })
456
+ });
457
+ }
416
458
  if (isUser) {
417
459
  // v8 ignore start
418
460
  const attachmentPrefix = (message.images ?? []).map((path) => `[${path.split(/[\\/]/).at(-1) ?? path}]`).join(" ");
@@ -491,29 +533,26 @@ function Message({ message, isStreaming = false, theme }) {
491
533
  //#endregion
492
534
  //#region src/components/Messages/Messages.tsx
493
535
  function Messages({ messages, isLoading, sessionId, streamingMessage, theme = getTheme() }) {
494
- return /* @__PURE__ */ jsxs(Box, {
495
- flexDirection: "column",
496
- children: [
497
- /* @__PURE__ */ jsx(Static, {
498
- items: messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE),
499
- children: (message, index) => /* @__PURE__ */ jsx(Message, {
500
- message,
501
- theme
502
- }, index)
503
- }, sessionId),
504
- streamingMessage && /* @__PURE__ */ jsx(Message, {
505
- isStreaming: true,
506
- message: streamingMessage,
536
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
537
+ /* @__PURE__ */ jsx(Static, {
538
+ items: messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE),
539
+ children: (message, index) => /* @__PURE__ */ jsx(Message, {
540
+ message,
507
541
  theme
508
- }),
509
- isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
510
- marginTop: -1,
511
- marginBottom: 1,
512
- marginX: 2,
513
- children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
514
- })
515
- ]
516
- });
542
+ }, index)
543
+ }, sessionId),
544
+ streamingMessage && /* @__PURE__ */ jsx(Message, {
545
+ isStreaming: true,
546
+ message: streamingMessage,
547
+ theme
548
+ }),
549
+ isLoading && streamingMessage && !streamingMessage.content && /* @__PURE__ */ jsx(Box, {
550
+ marginTop: -1,
551
+ marginBottom: 1,
552
+ marginX: 2,
553
+ children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
554
+ })
555
+ ] });
517
556
  }
518
557
  //#endregion
519
558
  //#region src/components/SelectPrompt/SelectPrompt.tsx
@@ -595,22 +634,22 @@ function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" })
595
634
  });
596
635
  }
597
636
  //#endregion
598
- //#region src/components/PlanApproval/PlanApproval.tsx
637
+ //#region src/components/PlanReview/PlanReview.tsx
599
638
  var options$1 = [
600
639
  {
601
- label: "Auto - Execute tools automatically",
640
+ label: "Approve in auto mode",
602
641
  value: AUTO
603
642
  },
604
643
  {
605
- label: "Safe - Approve each tool",
644
+ label: "Approve in safe mode",
606
645
  value: SAFE
607
646
  },
608
647
  {
609
- label: "Cancel - Continue planning",
648
+ label: "Continue planning",
610
649
  value: PLAN
611
650
  }
612
651
  ];
613
- function PlanApproval({ planContent, onModeChange, theme = getTheme() }) {
652
+ function PlanReview({ planContent, onModeChange, theme = getTheme() }) {
614
653
  return /* @__PURE__ */ jsx(Box, {
615
654
  marginX: 2,
616
655
  children: /* @__PURE__ */ jsx(SelectPrompt, {
@@ -628,13 +667,16 @@ function PlanApproval({ planContent, onModeChange, theme = getTheme() }) {
628
667
  /* @__PURE__ */ jsx(Text, {
629
668
  bold: true,
630
669
  color: theme.colors.accent,
631
- children: "Plan Generated - Choose execution mode:"
670
+ children: "Plan Review - Choose next step:"
632
671
  }),
633
672
  /* @__PURE__ */ jsx(Box, {
634
673
  marginY: 1,
635
- children: /* @__PURE__ */ jsx(Text, { children: planContent })
674
+ children: /* @__PURE__ */ jsx(Markdown, {
675
+ content: planContent,
676
+ theme
677
+ })
636
678
  }),
637
- /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select execution mode" })
679
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select review action" })
638
680
  ]
639
681
  })
640
682
  })
@@ -1345,7 +1387,7 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1345
1387
  //#endregion
1346
1388
  //#region src/components/Chat/constants.ts
1347
1389
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1348
- var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
1390
+ var PLAN_CHECKLIST_REMINDER = "Then display the plan using either the Plan Needs Input or Proposed Plan Markdown template";
1349
1391
  var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1350
1392
  var InterruptReason = /* @__PURE__ */ function(InterruptReason) {
1351
1393
  InterruptReason["Interrupted"] = "interrupted";
@@ -1355,14 +1397,18 @@ var InterruptReason = /* @__PURE__ */ function(InterruptReason) {
1355
1397
  //#endregion
1356
1398
  //#region src/components/Chat/plan.ts
1357
1399
  function hasExecutablePlan(content) {
1358
- return content.split("\n").some((line) => {
1359
- const trimmedLine = line.trim();
1360
- return /^- \[ \] (write_file|edit_file|run_shell)\(/.test(trimmedLine);
1361
- });
1400
+ const lines = content.split("\n");
1401
+ const proposedPlanIndex = lines.findIndex((line) => line.trim().toLowerCase() === "## proposed plan");
1402
+ if (proposedPlanIndex === -1) return false;
1403
+ const executionStepsIndex = lines.findIndex((line, index) => index > proposedPlanIndex && line.trim().toLowerCase() === "### execution steps");
1404
+ if (executionStepsIndex === -1) return false;
1405
+ const nextSectionIndex = lines.findIndex((line, index) => index > executionStepsIndex && /^#{1,6}\s+\S/.test(line.trim()));
1406
+ return lines.slice(executionStepsIndex + 1, nextSectionIndex === -1 ? void 0 : nextSectionIndex).some((line) => /^(?:[-*]|\d+[.)])\s+\S/.test(line.trim()));
1362
1407
  }
1363
1408
  //#endregion
1364
1409
  //#region src/components/Chat/Chat.tsx
1365
1410
  var MAX_TOOL_TURNS = 25;
1411
+ var MAX_TOOL_INTENT_CORRECTIONS = 2;
1366
1412
  function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1367
1413
  const sessionMessages = initialMessages ?? [];
1368
1414
  const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
@@ -1389,7 +1435,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1389
1435
  persistedSnapshotRef.current = snapshot;
1390
1436
  onMessagesChange?.(messages);
1391
1437
  }, [messages, onMessagesChange]);
1392
- const buildToolResultMessage = useCallback((toolName, result) => {
1438
+ const buildToolResultMessage = useCallback((toolName, result, args) => {
1393
1439
  if (result.error?.startsWith("Tool not allowed:")) return {
1394
1440
  role: SYSTEM,
1395
1441
  content: [
@@ -1401,7 +1447,11 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1401
1447
  };
1402
1448
  return {
1403
1449
  role: SYSTEM,
1404
- content: formatToolResultContent(toolName, result)
1450
+ content: formatToolResultContent(toolName, result, args),
1451
+ toolResult: {
1452
+ name: toolName,
1453
+ ...result.diff ? { diff: result.diff } : {}
1454
+ }
1405
1455
  };
1406
1456
  }, []);
1407
1457
  const buildPlanModeCorrectionMessage = useCallback((toolName) => ({
@@ -1433,6 +1483,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1433
1483
  abortControllerRef.current = controller;
1434
1484
  let activeMessages = currentMessages;
1435
1485
  let toolTurns = 0;
1486
+ let toolIntentCorrections = 0;
1436
1487
  try {
1437
1488
  while (!controller.signal.aborted) {
1438
1489
  const assistantMessage = {
@@ -1442,6 +1493,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1442
1493
  let committedMessages = activeMessages;
1443
1494
  let assistantCommitted = false;
1444
1495
  const commitAssistantMessage = () => {
1496
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1445
1497
  // v8 ignore start
1446
1498
  if (assistantCommitted) {
1447
1499
  if (committedMessages.at(-1)?.role === "assistant") {
@@ -1465,7 +1517,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1465
1517
  let nextMessages = null;
1466
1518
  for await (const chunk of streamChat(withSystemMessage(activeMessages), modelName, TOOLS, controller.signal)) {
1467
1519
  if (chunk.type === "content") {
1468
- assistantMessage.content += chunk.content;
1520
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1469
1521
  setStreamingMessage({ ...assistantMessage });
1470
1522
  continue;
1471
1523
  }
@@ -1486,7 +1538,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1486
1538
  // v8 ignore next
1487
1539
  const allowedTools = executionMode === "plan" ? READ_TOOLS : void 0;
1488
1540
  const result = await executeTool(normalized.name, normalized.arguments, { allowedTools });
1489
- toolResultMessages.push(buildToolResultMessage(normalized.name, result));
1541
+ toolResultMessages.push(buildToolResultMessage(normalized.name, result, normalized.arguments));
1490
1542
  } catch (error) {
1491
1543
  toolResultMessages.push(buildToolResultMessage(toolCall.function.name, {
1492
1544
  content: "",
@@ -1500,10 +1552,20 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1500
1552
  }
1501
1553
  if (!nextMessages) {
1502
1554
  await prewarmCodeBlocks(assistantMessage.content, theme);
1503
- commitAssistantMessage();
1555
+ const updatedMessages = commitAssistantMessage();
1556
+ if (hasUncalledToolIntent(assistantMessage.content) && toolIntentCorrections < MAX_TOOL_INTENT_CORRECTIONS) {
1557
+ toolIntentCorrections += 1;
1558
+ activeMessages = [...updatedMessages, {
1559
+ role: SYSTEM,
1560
+ content: TOOL_INTENT_CORRECTION
1561
+ }];
1562
+ setMessages(activeMessages);
1563
+ continue;
1564
+ }
1504
1565
  return;
1505
1566
  }
1506
1567
  toolTurns += 1;
1568
+ toolIntentCorrections = 0;
1507
1569
  /* v8 ignore start */
1508
1570
  if (toolTurns >= MAX_TOOL_TURNS) {
1509
1571
  setMessages([...nextMessages, {
@@ -1551,17 +1613,23 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1551
1613
  role: ASSISTANT,
1552
1614
  content: ""
1553
1615
  };
1616
+ const emptyAssistantMessage = {
1617
+ role: ASSISTANT,
1618
+ content: ""
1619
+ };
1554
1620
  let committedMessages = currentMessages;
1555
1621
  let assistantCommitted = false;
1556
1622
  const commitAssistantMessage = () => {
1623
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1624
+ /* v8 ignore start */
1557
1625
  if (assistantCommitted) {
1558
- // v8 ignore next
1559
1626
  if (committedMessages.at(-1)?.role === "assistant") {
1560
1627
  committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
1561
1628
  setMessages(committedMessages);
1562
1629
  }
1563
1630
  return committedMessages;
1564
1631
  }
1632
+ /* v8 ignore stop */
1565
1633
  assistantCommitted = true;
1566
1634
  setStreamingMessage(null);
1567
1635
  if (!assistantMessage.content) {
@@ -1572,17 +1640,32 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1572
1640
  setMessages(committedMessages);
1573
1641
  return committedMessages;
1574
1642
  };
1575
- setStreamingMessage(assistantMessage);
1643
+ setStreamingMessage(emptyAssistantMessage);
1576
1644
  try {
1577
1645
  const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
1578
- for await (const chunk of streamChat(withSystemMessage(currentMessages), modelName, readOnlyTools, controller.signal)) {
1646
+ const planResearchMessages = [...currentMessages, {
1647
+ role: SYSTEM,
1648
+ content: PLAN_INSTRUCTION
1649
+ }];
1650
+ for await (const chunk of streamChat(withSystemMessage(planResearchMessages), modelName, readOnlyTools, controller.signal)) {
1579
1651
  // v8 ignore next 3
1580
1652
  if (controller.signal.aborted) return;
1581
1653
  if (chunk.type === "content") {
1582
- assistantMessage.content += chunk.content;
1654
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1583
1655
  setStreamingMessage({ ...assistantMessage });
1584
1656
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
1585
- const updatedMessages = commitAssistantMessage();
1657
+ const toolName = toolCall.function.name;
1658
+ if (!READ_TOOLS.has(toolName)) {
1659
+ const correctionMessage = buildPlanModeCorrectionMessage(toolName);
1660
+ setStreamingMessage(null);
1661
+ const newMessages = [...committedMessages, correctionMessage];
1662
+ setMessages(newMessages);
1663
+ await processStreamReadOnly(newMessages);
1664
+ return;
1665
+ }
1666
+ setStreamingMessage(emptyAssistantMessage);
1667
+ assistantMessage.content = "";
1668
+ const updatedMessages = committedMessages;
1586
1669
  let normalized;
1587
1670
  try {
1588
1671
  normalized = normalizeToolCall(toolCall);
@@ -1597,15 +1680,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1597
1680
  await processStreamReadOnly(newMessages);
1598
1681
  return;
1599
1682
  }
1600
- if (!READ_TOOLS.has(normalized.name)) {
1601
- const correctionMessage = buildPlanModeCorrectionMessage(normalized.name);
1602
- const newMessages = [...updatedMessages, correctionMessage];
1603
- setMessages(newMessages);
1604
- await processStreamReadOnly(newMessages);
1605
- return;
1606
- }
1607
1683
  const result = await executeTool(normalized.name, normalized.arguments, { allowedTools: READ_TOOLS });
1608
- const toolResultMessage = buildToolResultMessage(normalized.name, result);
1684
+ const toolResultMessage = buildToolResultMessage(normalized.name, result, normalized.arguments);
1609
1685
  const newMessages = [...updatedMessages, toolResultMessage];
1610
1686
  setMessages(newMessages);
1611
1687
  await processStreamReadOnly(newMessages);
@@ -1614,6 +1690,14 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1614
1690
  }
1615
1691
  await prewarmCodeBlocks(assistantMessage.content, theme);
1616
1692
  const researchMessages = commitAssistantMessage();
1693
+ if (hasExecutablePlan(assistantMessage.content)) {
1694
+ setPendingPlan({
1695
+ planContent: assistantMessage.content,
1696
+ messages: researchMessages
1697
+ });
1698
+ setIsLoading(false);
1699
+ return;
1700
+ }
1617
1701
  const planInstruction = {
1618
1702
  role: SYSTEM,
1619
1703
  content: PLAN_GENERATION_INSTRUCTION
@@ -1623,13 +1707,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1623
1707
  role: ASSISTANT,
1624
1708
  content: ""
1625
1709
  };
1626
- setStreamingMessage(planAssistantMessage);
1710
+ setStreamingMessage(emptyAssistantMessage);
1627
1711
  try {
1628
1712
  for await (const chunk of streamChat(withSystemMessage(planMessages), modelName, [], controller.signal)) {
1629
1713
  // v8 ignore next 3
1630
1714
  if (controller.signal.aborted) return;
1631
1715
  if (chunk.type === "content") {
1632
- planAssistantMessage.content += chunk.content;
1716
+ planAssistantMessage.content = sanitizeAssistantContent(planAssistantMessage.content + chunk.content);
1633
1717
  setStreamingMessage({ ...planAssistantMessage });
1634
1718
  }
1635
1719
  }
@@ -1666,7 +1750,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1666
1750
  model,
1667
1751
  theme
1668
1752
  ]);
1669
- const handlePlanApproval = useCallback(async (mode) => {
1753
+ const handlePlanReview = useCallback(async (mode) => {
1670
1754
  // v8 ignore next
1671
1755
  if (!pendingPlan) return;
1672
1756
  const { messages: planMessages } = pendingPlan;
@@ -1702,10 +1786,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1702
1786
  switch (decision) {
1703
1787
  case APPROVE: {
1704
1788
  const result = await executeToolCall(toolCall);
1705
- const toolResultMessage = {
1706
- role: SYSTEM,
1707
- content: formatToolResultContent(toolCall.function.name, result)
1708
- };
1789
+ const toolResultMessage = buildToolResultMessage(toolCall.function.name, result, toolCall.function.arguments);
1709
1790
  const newMessages = [...approvedMessages, toolResultMessage];
1710
1791
  setMessages(newMessages);
1711
1792
  await processStream(newMessages, executionMode);
@@ -1717,7 +1798,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1717
1798
  content: formatToolResultContent(toolCall.function.name, {
1718
1799
  content: "",
1719
1800
  error: "Tool call rejected by user"
1720
- })
1801
+ }, toolCall.function.arguments)
1721
1802
  };
1722
1803
  setMessages([...approvedMessages, toolResultMessage]);
1723
1804
  setIsLoading(false);
@@ -1725,7 +1806,11 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1725
1806
  break;
1726
1807
  }
1727
1808
  }
1728
- }, [pendingToolCall, processStream]);
1809
+ }, [
1810
+ buildToolResultMessage,
1811
+ pendingToolCall,
1812
+ processStream
1813
+ ]);
1729
1814
  const handleSubmit = useCallback(async ({ content, images }) => {
1730
1815
  setInterruptReason(null);
1731
1816
  const userContent = content.trim();
@@ -1761,9 +1846,9 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1761
1846
  streamingMessage,
1762
1847
  theme
1763
1848
  }),
1764
- pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
1849
+ pendingPlan && /* @__PURE__ */ jsx(PlanReview, {
1765
1850
  planContent: pendingPlan.planContent,
1766
- onModeChange: handlePlanApproval,
1851
+ onModeChange: handlePlanReview,
1767
1852
  theme
1768
1853
  }),
1769
1854
  !pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
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.21.1";
53
+ var version = "0.23.0";
54
54
  //#endregion
55
55
  //#region src/constants/package.ts
56
56
  var NAME = name;
@@ -105,15 +105,54 @@ var BACK = {
105
105
  value: "back"
106
106
  };
107
107
  //#endregion
108
+ //#region src/constants/tool.ts
109
+ var tool_exports = /* @__PURE__ */ __exportAll({
110
+ EDIT_FILE: () => EDIT_FILE,
111
+ GREP_SEARCH: () => GREP_SEARCH,
112
+ LIST_DIR: () => LIST_DIR,
113
+ READ_FILE: () => READ_FILE,
114
+ READ_TOOL_NAMES: () => READ_TOOL_NAMES,
115
+ RUN_SHELL: () => RUN_SHELL,
116
+ VIEW_RANGE: () => VIEW_RANGE,
117
+ WEB_FETCH: () => WEB_FETCH,
118
+ WEB_SEARCH: () => WEB_SEARCH,
119
+ WRITE_FILE: () => WRITE_FILE,
120
+ WRITE_TOOL_NAMES: () => WRITE_TOOL_NAMES
121
+ });
122
+ var READ_FILE = "read_file";
123
+ var WRITE_FILE = "write_file";
124
+ var EDIT_FILE = "edit_file";
125
+ var RUN_SHELL = "run_shell";
126
+ var LIST_DIR = "list_dir";
127
+ var GREP_SEARCH = "grep_search";
128
+ var VIEW_RANGE = "view_range";
129
+ var WEB_SEARCH = "web_search";
130
+ var WEB_FETCH = "web_fetch";
131
+ var READ_TOOL_NAMES = [
132
+ READ_FILE,
133
+ LIST_DIR,
134
+ GREP_SEARCH,
135
+ VIEW_RANGE,
136
+ WEB_SEARCH,
137
+ WEB_FETCH
138
+ ];
139
+ var WRITE_TOOL_NAMES = [
140
+ WRITE_FILE,
141
+ EDIT_FILE,
142
+ RUN_SHELL
143
+ ];
144
+ //#endregion
108
145
  //#region src/constants/prompt.ts
146
+ var PLAN_READ_TOOLS = READ_TOOL_NAMES.join(", ");
147
+ var PLAN_WRITE_TOOLS = WRITE_TOOL_NAMES.join(", ");
109
148
  var BASE_SYSTEM_PROMPT = `You are a coding assistant that helps users write, edit, and understand code. You have access to tools for reading files, writing files, running shell commands, searching code, and searching the web
110
149
 
111
150
  Follow these rules:
112
- 1. Always use available tools rather than guessing file contents or code behavior
113
- 2. Read files before editing them to understand context
114
- 3. When writing files, provide complete, working code
115
- 4. Explain your reasoning when making non-trivial changes
116
- 5. Prefer minimal changes that achieve the goal
151
+ 1. Use available tools rather than guessing file contents, paths, or code behavior
152
+ 2. When a tool is needed, call it immediately instead of saying you will call it
153
+ 3. Read files before editing them to understand context
154
+ 4. Make the smallest exact change that satisfies the request
155
+ 5. Explain after tool results are available, unless the user asks for discussion or a plan
117
156
  6. Confirm with the user before destructive operations
118
157
 
119
158
  When tools return results, incorporate them into your response naturally`;
@@ -131,20 +170,61 @@ Always use tools when you need to:
131
170
  - Make file changes
132
171
  - Explore project structure
133
172
  - Search the codebase
134
- - Look up current or external information`;
135
- var PLAN_GENERATION_INSTRUCTION = `Based on the research above, decide whether the user request needs code or shell execution
173
+ - Look up current or external information
174
+
175
+ Path rules:
176
+ - Paths are relative to the project root unless absolute
177
+ - Preserve parent directories from listings; if list_dir("src") returns [d] utils, use src/utils
178
+ - If a path fails, inspect the parent directory or search before retrying`;
179
+ var PLAN_RESPONSE_TEMPLATE = `If important product, implementation, or safety details are missing, respond with this Markdown template:
180
+
181
+ ## Plan Needs Input
182
+
183
+ ### Questions
184
+ - ...
185
+
186
+ ### What I Found
187
+ - ...
188
+
189
+ ### Draft Plan
190
+ - ...
191
+
192
+ If the request is ready for execution, respond with this Markdown template:
193
+
194
+ ## Proposed Plan
195
+
196
+ ### Summary
197
+ ...
198
+
199
+ ### Changes
200
+ - ...
201
+
202
+ ### Test Plan
203
+ - ...
204
+
205
+ ### Execution Steps
206
+ - ...
207
+
208
+ Keep Execution Steps as human-readable bullets for mutating work that needs approval, not preliminary read-only research
209
+ Do not add extra wrapper text before or after the template
210
+ If no execution is needed, answer normally`;
211
+ var PLAN_GENERATION_INSTRUCTION = `Based on the research above, decide whether the user request is ready for execution
136
212
 
137
- If the request needs changes or commands, respond with a plan checklist only
138
213
  Do not execute any tools
139
214
  Do not claim any action was performed
140
- Display the plan as an unchecked Markdown checklist using only these forms:
215
+ Use the exact headings shown below
141
216
 
142
- - [ ] write_file("path/to/file", "content") - Brief description
143
- - [ ] edit_file("path/to/file", "oldText", "newText") - Brief description
144
- - [ ] run_shell("command") - Brief description
217
+ ${PLAN_RESPONSE_TEMPLATE}`;
218
+ var PLAN_INSTRUCTION = `Plan mode is active
145
219
 
146
- Only include write_file, edit_file, and run_shell tools in the checklist
147
- If no execution is needed, answer normally`;
220
+ Only use read-only tools: ${PLAN_READ_TOOLS}
221
+ Do not call ${PLAN_WRITE_TOOLS} during Plan mode
222
+ Use read-only tools to resolve discoverable facts before asking questions
223
+ If the user asks to search, inspect, find, read, or locate something, use read-only tools immediately
224
+ Only ask questions for user preferences or product decisions that cannot be discovered from available tools
225
+ When enough context is available, stop calling tools and produce either Plan Needs Input or Proposed Plan using the required template
226
+
227
+ ${PLAN_RESPONSE_TEMPLATE}`;
148
228
  //#endregion
149
229
  //#region src/constants/role.ts
150
230
  var USER = "user";
@@ -291,28 +371,6 @@ function getTheme(themeId = DEFAULT_THEME_ID) {
291
371
  return LIST.find(({ id }) => id === themeId) ?? LIST[0];
292
372
  }
293
373
  //#endregion
294
- //#region src/constants/tool.ts
295
- var tool_exports = /* @__PURE__ */ __exportAll({
296
- EDIT_FILE: () => EDIT_FILE,
297
- GREP_SEARCH: () => GREP_SEARCH,
298
- LIST_DIR: () => LIST_DIR,
299
- READ_FILE: () => READ_FILE,
300
- RUN_SHELL: () => RUN_SHELL,
301
- VIEW_RANGE: () => VIEW_RANGE,
302
- WEB_FETCH: () => WEB_FETCH,
303
- WEB_SEARCH: () => WEB_SEARCH,
304
- WRITE_FILE: () => WRITE_FILE
305
- });
306
- var READ_FILE = "read_file";
307
- var WRITE_FILE = "write_file";
308
- var EDIT_FILE = "edit_file";
309
- var RUN_SHELL = "run_shell";
310
- var LIST_DIR = "list_dir";
311
- var GREP_SEARCH = "grep_search";
312
- var VIEW_RANGE = "view_range";
313
- var WEB_SEARCH = "web_search";
314
- var WEB_FETCH = "web_fetch";
315
- //#endregion
316
374
  //#region src/constants/ui.ts
317
375
  var HEADER_PREFIX = "🦙 ";
318
376
  var WARNING = "⚠️";
@@ -490,6 +548,15 @@ function saveConfig(patch) {
490
548
  //#region src/utils/ollama.ts
491
549
  var { host } = loadConfig();
492
550
  var client = new Ollama({ host });
551
+ var TRAILING_CONTROL_TOKEN_REGEX = /(?:\s*<\|?channel\|?>)+\s*$/;
552
+ var TOOL_INTENT_REGEX = /\b(?:i\s+(?:will|am going to)|next,\s*i\s+will|now\s+i\s+will|first,\s*i\s+will)\b[\s\S]*\b(?:read|inspect|check|list|search|update|edit|write|modify|run)\b/i;
553
+ var TOOL_INTENT_CORRECTION = "You said you would use a tool but did not call one. Continue by calling the appropriate tool now. Do not describe the tool call.";
554
+ function sanitizeAssistantContent(content) {
555
+ return content.replace(TRAILING_CONTROL_TOKEN_REGEX, "");
556
+ }
557
+ function hasUncalledToolIntent(content) {
558
+ return TOOL_INTENT_REGEX.test(content);
559
+ }
493
560
  async function checkHealth() {
494
561
  try {
495
562
  return (await fetch(host)).ok;
@@ -498,9 +565,15 @@ async function checkHealth() {
498
565
  }
499
566
  }
500
567
  async function* streamChat(messages, model, tools, signal) {
568
+ const providerMessages = messages.map(({ role, content, images, tool_calls }) => ({
569
+ role,
570
+ content,
571
+ ...images ? { images } : {},
572
+ ...tool_calls ? { tool_calls } : {}
573
+ }));
501
574
  const response = await client.chat({
502
575
  model,
503
- messages,
576
+ messages: providerMessages,
504
577
  stream: true,
505
578
  tools,
506
579
  // v8 ignore next
@@ -782,10 +855,10 @@ var TOOLS = [
782
855
  type: "string",
783
856
  description: "The path to the directory to list"
784
857
  } }, ["path"]),
785
- defineTool(GREP_SEARCH, "Search for a pattern in files within a directory", {
858
+ defineTool(GREP_SEARCH, "Search files within a directory; multi-word queries also match common code identifier forms", {
786
859
  pattern: {
787
860
  type: "string",
788
- description: "The regex pattern to search for"
861
+ description: "The regex, phrase, or code concept to search for"
789
862
  },
790
863
  path: {
791
864
  type: "string",
@@ -819,19 +892,8 @@ var TOOLS = [
819
892
  description: "The full URL of the page to fetch"
820
893
  } }, ["url"])
821
894
  ];
822
- var READ_TOOLS = new Set([
823
- READ_FILE,
824
- LIST_DIR,
825
- GREP_SEARCH,
826
- VIEW_RANGE,
827
- WEB_SEARCH,
828
- WEB_FETCH
829
- ]);
830
- var WRITE_TOOLS = new Set([
831
- WRITE_FILE,
832
- EDIT_FILE,
833
- RUN_SHELL
834
- ]);
895
+ var READ_TOOLS = new Set(READ_TOOL_NAMES);
896
+ var WRITE_TOOLS = new Set(WRITE_TOOL_NAMES);
835
897
  //#endregion
836
898
  //#region src/utils/tools/shell.ts
837
899
  var execAsync = promisify(exec);
@@ -861,6 +923,76 @@ async function runShell(command) {
861
923
  }
862
924
  //#endregion
863
925
  //#region src/utils/tools/filesystem.ts
926
+ var DIFF_CONTEXT_LINES = 3;
927
+ var DIFF_MAX_LINES = 120;
928
+ var DIFF_MAX_CHARS = 12e3;
929
+ function splitLines(content) {
930
+ return content.split("\n");
931
+ }
932
+ function createUnifiedDiff(filePath, beforeContent, afterContent) {
933
+ const beforeLines = splitLines(beforeContent);
934
+ const afterLines = splitLines(afterContent);
935
+ let commonPrefix = 0;
936
+ while (commonPrefix < beforeLines.length && commonPrefix < afterLines.length && beforeLines[commonPrefix] === afterLines[commonPrefix]) commonPrefix += 1;
937
+ let commonSuffix = 0;
938
+ while (commonSuffix < beforeLines.length - commonPrefix && commonSuffix < afterLines.length - commonPrefix && beforeLines[beforeLines.length - 1 - commonSuffix] === afterLines[afterLines.length - 1 - commonSuffix]) commonSuffix += 1;
939
+ const beforeChangeEnd = beforeLines.length - commonSuffix;
940
+ const afterChangeEnd = afterLines.length - commonSuffix;
941
+ const hunkStart = Math.max(0, commonPrefix - DIFF_CONTEXT_LINES);
942
+ const beforeHunkEnd = Math.min(beforeLines.length, beforeChangeEnd + DIFF_CONTEXT_LINES);
943
+ const afterHunkEnd = Math.min(afterLines.length, afterChangeEnd + DIFF_CONTEXT_LINES);
944
+ const beforeHunkLines = beforeLines.slice(hunkStart, beforeHunkEnd);
945
+ const afterHunkLines = afterLines.slice(hunkStart, afterHunkEnd);
946
+ const beforeChangedLines = beforeLines.slice(commonPrefix, beforeChangeEnd);
947
+ const afterChangedLines = afterLines.slice(commonPrefix, afterChangeEnd);
948
+ const contextBefore = beforeLines.slice(hunkStart, commonPrefix);
949
+ const contextAfter = beforeLines.slice(beforeChangeEnd, beforeHunkEnd);
950
+ return [
951
+ `--- ${filePath}`,
952
+ `+++ ${filePath}`,
953
+ `@@ -${String(hunkStart + 1)},${String(beforeHunkLines.length)} +${String(hunkStart + 1)},${String(afterHunkLines.length)} @@`,
954
+ ...contextBefore.map((line) => ` ${line}`),
955
+ ...beforeChangedLines.map((line) => `-${line}`),
956
+ ...afterChangedLines.map((line) => `+${line}`),
957
+ ...contextAfter.map((line) => ` ${line}`)
958
+ ].join("\n");
959
+ }
960
+ function truncateDiff(diff) {
961
+ const lines = diff.split("\n");
962
+ let visibleLines = lines.slice(0, DIFF_MAX_LINES);
963
+ let truncated = lines.length > DIFF_MAX_LINES;
964
+ while (visibleLines.join("\n").length > DIFF_MAX_CHARS) {
965
+ visibleLines = visibleLines.slice(0, -1);
966
+ truncated = true;
967
+ }
968
+ if (truncated) visibleLines = [...visibleLines, `[diff truncated: showing ${String(visibleLines.length)} of ${String(lines.length)} lines]`];
969
+ return {
970
+ visible: visibleLines.join("\n"),
971
+ truncated,
972
+ totalLines: lines.length,
973
+ visibleLines: Math.min(visibleLines.length, lines.length)
974
+ };
975
+ }
976
+ function buildSearchPatterns(pattern) {
977
+ const words = pattern.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean);
978
+ if (words.length < 2) return [pattern];
979
+ const camelCase = words.map((word, index) => index === 0 ? word.toLowerCase() : capitalize(word.toLowerCase())).join("");
980
+ const pascalCase = words.map((word) => capitalize(word.toLowerCase())).join("");
981
+ const snakeCase = words.map((word) => word.toLowerCase()).join("_");
982
+ const upperSnakeCase = snakeCase.toUpperCase();
983
+ const flexibleWhitespace = words.join(String.raw`\s+`);
984
+ return Array.from(new Set([
985
+ pattern,
986
+ flexibleWhitespace,
987
+ snakeCase,
988
+ upperSnakeCase,
989
+ camelCase,
990
+ pascalCase
991
+ ]));
992
+ }
993
+ function capitalize(value) {
994
+ return value.charAt(0).toUpperCase() + value.slice(1);
995
+ }
864
996
  /**
865
997
  * Read file contents
866
998
  */
@@ -910,8 +1042,19 @@ function editFile(filePath, oldText, newText) {
910
1042
  content: "",
911
1043
  error: `Exact text matched multiple locations in file: ${filePath}`
912
1044
  };
913
- writeFileSync(filePath, content.replace(oldText, newText), "utf8");
914
- return { content: `File edited successfully: ${filePath}` };
1045
+ const updatedContent = content.replace(oldText, newText);
1046
+ const truncatedDiff = truncateDiff(createUnifiedDiff(filePath, content, updatedContent));
1047
+ writeFileSync(filePath, updatedContent, "utf8");
1048
+ return {
1049
+ content: `File edited successfully: ${filePath}`,
1050
+ diff: {
1051
+ path: filePath,
1052
+ visible: truncatedDiff.visible,
1053
+ truncated: truncatedDiff.truncated,
1054
+ totalLines: truncatedDiff.totalLines,
1055
+ visibleLines: truncatedDiff.visibleLines
1056
+ }
1057
+ };
915
1058
  } catch (error) {
916
1059
  return {
917
1060
  content: "",
@@ -966,17 +1109,17 @@ function listDir(dirPath) {
966
1109
  * Search for pattern in files using ripgrep if available, fallback to Node.js
967
1110
  */
968
1111
  async function grepSearch(pattern, dirPath) {
969
- try {
970
- const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
971
- // v8 ignore next
972
- return { content: stdout || "No matches found" };
1112
+ const patterns = buildSearchPatterns(pattern);
1113
+ for (const searchPattern of patterns) try {
1114
+ const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${searchPattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
1115
+ if (stdout) return { content: stdout };
973
1116
  } catch {}
974
1117
  try {
975
1118
  if (!existsSync(dirPath)) return {
976
1119
  content: "",
977
1120
  error: `Directory not found: ${dirPath}`
978
1121
  };
979
- const regex = new RegExp(pattern, "g");
1122
+ const regexes = patterns.map((searchPattern) => new RegExp(searchPattern));
980
1123
  const results = [];
981
1124
  function searchDirectory(currentPath) {
982
1125
  const entries = readdirSync(currentPath, { withFileTypes: true });
@@ -986,9 +1129,9 @@ async function grepSearch(pattern, dirPath) {
986
1129
  if (!entry.name.startsWith(".") && entry.name !== "node_modules") searchDirectory(fullPath);
987
1130
  } else if (entry.isFile()) try {
988
1131
  const lines = readFileSync(fullPath, "utf8").split("\n");
989
- for (let i = 0; i < lines.length; i++) {
990
- if (regex.test(lines[i])) results.push(`${fullPath}:${(i + 1).toString()}: ${lines[i].trim()}`);
991
- regex.lastIndex = 0;
1132
+ for (let i = 0; i < lines.length; i++) for (const regex of regexes) if (regex.test(lines[i])) {
1133
+ results.push(`${fullPath}:${(i + 1).toString()}: ${lines[i].trim()}`);
1134
+ break;
992
1135
  }
993
1136
  } catch {}
994
1137
  }
@@ -1193,7 +1336,7 @@ var REQUIRED_STRING_ARGS = {
1193
1336
  [WEB_SEARCH]: ["query"],
1194
1337
  [WEB_FETCH]: ["url"]
1195
1338
  };
1196
- var TOOL_NAMES = new Set(Object.values(tool_exports));
1339
+ var TOOL_NAMES = new Set(Object.values(tool_exports).filter((value) => typeof value === "string"));
1197
1340
  function isToolName(name) {
1198
1341
  return TOOL_NAMES.has(name);
1199
1342
  }
@@ -1241,17 +1384,25 @@ function normalizeToolCall(toolCall) {
1241
1384
  requiresApproval: WRITE_TOOLS.has(name)
1242
1385
  };
1243
1386
  }
1244
- function formatToolResultContent(toolName, result) {
1387
+ function formatToolResultContent(toolName, result, args) {
1388
+ const formattedArgs = args ? `(${formatToolArguments(args)})` : "";
1245
1389
  const status = result.error ? "The requested action was NOT performed" : "";
1246
1390
  const content = result.content ? `\n${result.content}` : "";
1247
1391
  const error = result.error ? `\nError: ${result.error}` : "";
1248
1392
  return [
1249
- `Tool ${toolName} result:`,
1393
+ `Tool ${toolName}${formattedArgs} result:`,
1250
1394
  status,
1251
1395
  content.trim(),
1252
1396
  error.trim()
1253
1397
  ].filter(Boolean).join("\n");
1254
1398
  }
1399
+ function formatToolArguments(args) {
1400
+ return JSON.stringify(args, (_, value) => {
1401
+ if (typeof value !== "string") return value;
1402
+ if (value.length <= 80 && !value.includes("\n")) return value;
1403
+ return `<${String(value.length)} chars>`;
1404
+ });
1405
+ }
1255
1406
  async function executeToolCall(toolCall, options) {
1256
1407
  try {
1257
1408
  const normalized = normalizeToolCall(toolCall);
@@ -1332,6 +1483,7 @@ async function checkForUpdate() {
1332
1483
  //#region src/cli.ts
1333
1484
  var cli = cac("code-ollama");
1334
1485
  var MAX_TOOL_TURNS = 25;
1486
+ var MAX_TOOL_INTENT_CORRECTIONS = 2;
1335
1487
  cli.version(VERSION);
1336
1488
  cli.help();
1337
1489
  cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model, prompt) => {
@@ -1366,6 +1518,7 @@ async function runPrompt(model, prompt) {
1366
1518
  async function processRunStream(messages, model) {
1367
1519
  let activeMessages = messages;
1368
1520
  let toolTurns = 0;
1521
+ let toolIntentCorrections = 0;
1369
1522
  while (toolTurns < MAX_TOOL_TURNS) {
1370
1523
  const assistantMessage = {
1371
1524
  role: ASSISTANT,
@@ -1374,10 +1527,11 @@ async function processRunStream(messages, model) {
1374
1527
  let nextMessages = null;
1375
1528
  for await (const chunk of streamChat(activeMessages, model, TOOLS)) {
1376
1529
  if (chunk.type === "content") {
1377
- assistantMessage.content += chunk.content;
1530
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1378
1531
  write(chunk.content);
1379
1532
  continue;
1380
1533
  }
1534
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1381
1535
  // v8 ignore next 3
1382
1536
  if (chunk.tool_calls.length === 0) continue;
1383
1537
  const committedMessages = [...activeMessages, assistantMessage];
@@ -1386,15 +1540,31 @@ async function processRunStream(messages, model) {
1386
1540
  const result = await executeToolCall(toolCall);
1387
1541
  toolResultMessages.push({
1388
1542
  role: SYSTEM,
1389
- content: formatToolResultContent(toolCall.function.name, result)
1543
+ content: formatToolResultContent(toolCall.function.name, result, toolCall.function.arguments)
1390
1544
  });
1391
1545
  }
1392
1546
  nextMessages = [...committedMessages, ...toolResultMessages];
1393
1547
  break;
1394
1548
  }
1395
- if (!nextMessages) return;
1549
+ if (!nextMessages) {
1550
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1551
+ if (hasUncalledToolIntent(assistantMessage.content) && toolIntentCorrections < MAX_TOOL_INTENT_CORRECTIONS) {
1552
+ toolIntentCorrections += 1;
1553
+ activeMessages = [
1554
+ ...activeMessages,
1555
+ assistantMessage,
1556
+ {
1557
+ role: SYSTEM,
1558
+ content: TOOL_INTENT_CORRECTION
1559
+ }
1560
+ ];
1561
+ continue;
1562
+ }
1563
+ return;
1564
+ }
1396
1565
  activeMessages = nextMessages;
1397
1566
  toolTurns += 1;
1567
+ toolIntentCorrections = 0;
1398
1568
  }
1399
1569
  // v8 ignore next 3
1400
1570
  writeError("Tool execution stopped because the maximum tool turn limit was reached\n");
@@ -1408,7 +1578,7 @@ async function main(args = process.argv.slice(2)) {
1408
1578
  else await launchTui();
1409
1579
  }
1410
1580
  async function launchTui(sessionId) {
1411
- const { renderApp } = await import("./assets/tui-BHkHnKUC.js");
1581
+ const { renderApp } = await import("./assets/tui-iewVFcZW.js");
1412
1582
  reset();
1413
1583
  renderApp(sessionId);
1414
1584
  }
@@ -1424,4 +1594,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
1424
1594
  if (isEntrypoint()) main();
1425
1595
  // v8 ignore stop
1426
1596
  //#endregion
1427
- export { saveClipboardImage as A, PLAN_GENERATION_INSTRUCTION as B, deleteModel as C, loadConfig as D, streamChat as E, LIST as F, PLAN as G, CATALOG as H, getTheme as I, REJECT as J, SAFE as K, ASSISTANT as L, withSystemMessage as M, HEADER_PREFIX as N, saveConfig as O, WARNING as P, SYSTEM as R, checkHealth as S, pullModel as T, AUTO as U, BACK as V, LABEL as W, VERSION as X, NAME as Y, LIST$1 as Z, loadSession as _, normalizeToolCall as a, reset as b, WRITE_TOOLS as c, write as d, appendMessage as f, listSessions as g, deleteSessionIfEmpty as h, formatToolResultContent as i, resetSystemMessage as j, removeClipboardImage as k, tick as l, deleteSession as m, main, executeTool as n, READ_TOOLS as o, createSession as p, APPROVE as q, executeToolCall as r, TOOLS as s, checkForUpdate as t, color as u, updateSessionModel as v, listModels as w, setClearHandler as x, clear as y, USER as z };
1597
+ export { NAME as $, loadConfig as A, ASSISTANT as B, checkHealth as C, pullModel as D, listModels as E, withSystemMessage as F, BACK as G, USER as H, HEADER_PREFIX as I, LABEL as J, CATALOG as K, WARNING as L, removeClipboardImage as M, saveClipboardImage as N, sanitizeAssistantContent as O, resetSystemMessage as P, REJECT as Q, LIST as R, TOOL_INTENT_CORRECTION as S, hasUncalledToolIntent as T, PLAN_GENERATION_INSTRUCTION as U, SYSTEM as V, PLAN_INSTRUCTION as W, SAFE as X, PLAN as Y, APPROVE as Z, loadSession as _, normalizeToolCall as a, reset as b, WRITE_TOOLS as c, write as d, VERSION as et, appendMessage as f, listSessions as g, deleteSessionIfEmpty as h, formatToolResultContent as i, saveConfig as j, streamChat as k, tick as l, deleteSession as m, main, executeTool as n, READ_TOOLS as o, createSession as p, AUTO as q, executeToolCall as r, TOOLS as s, checkForUpdate as t, LIST$1 as tt, color as u, updateSessionModel as v, deleteModel as w, setClearHandler as x, clear as y, getTheme as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.21.1",
3
+ "version": "0.23.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",