code-ollama 0.21.1 → 0.22.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 VERSION, A as loadConfig, B as ASSISTANT, C as checkHealth, D as pullModel, E as listModels, F as withSystemMessage, G as CATALOG, H as USER, I as HEADER_PREFIX, J as PLAN, K as AUTO, L as WARNING, M as removeClipboardImage, N as saveClipboardImage, O as sanitizeAssistantContent, P as resetSystemMessage, Q as NAME, R as LIST$1, S as TOOL_INTENT_CORRECTION, T as hasUncalledToolIntent, U as PLAN_GENERATION_INSTRUCTION, V as SYSTEM, W as BACK, X as APPROVE, Y as SAFE, Z as REJECT, _ as loadSession, a as normalizeToolCall, b as reset, c as WRITE_TOOLS, d as write, et as LIST, 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 LABEL, r as executeToolCall, s as TOOLS, t as checkForUpdate, 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(" ");
@@ -1363,6 +1405,7 @@ function hasExecutablePlan(content) {
1363
1405
  //#endregion
1364
1406
  //#region src/components/Chat/Chat.tsx
1365
1407
  var MAX_TOOL_TURNS = 25;
1408
+ var MAX_TOOL_INTENT_CORRECTIONS = 2;
1366
1409
  function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1367
1410
  const sessionMessages = initialMessages ?? [];
1368
1411
  const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
@@ -1389,7 +1432,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1389
1432
  persistedSnapshotRef.current = snapshot;
1390
1433
  onMessagesChange?.(messages);
1391
1434
  }, [messages, onMessagesChange]);
1392
- const buildToolResultMessage = useCallback((toolName, result) => {
1435
+ const buildToolResultMessage = useCallback((toolName, result, args) => {
1393
1436
  if (result.error?.startsWith("Tool not allowed:")) return {
1394
1437
  role: SYSTEM,
1395
1438
  content: [
@@ -1401,7 +1444,11 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1401
1444
  };
1402
1445
  return {
1403
1446
  role: SYSTEM,
1404
- content: formatToolResultContent(toolName, result)
1447
+ content: formatToolResultContent(toolName, result, args),
1448
+ toolResult: {
1449
+ name: toolName,
1450
+ ...result.diff ? { diff: result.diff } : {}
1451
+ }
1405
1452
  };
1406
1453
  }, []);
1407
1454
  const buildPlanModeCorrectionMessage = useCallback((toolName) => ({
@@ -1433,6 +1480,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1433
1480
  abortControllerRef.current = controller;
1434
1481
  let activeMessages = currentMessages;
1435
1482
  let toolTurns = 0;
1483
+ let toolIntentCorrections = 0;
1436
1484
  try {
1437
1485
  while (!controller.signal.aborted) {
1438
1486
  const assistantMessage = {
@@ -1442,6 +1490,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1442
1490
  let committedMessages = activeMessages;
1443
1491
  let assistantCommitted = false;
1444
1492
  const commitAssistantMessage = () => {
1493
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1445
1494
  // v8 ignore start
1446
1495
  if (assistantCommitted) {
1447
1496
  if (committedMessages.at(-1)?.role === "assistant") {
@@ -1465,7 +1514,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1465
1514
  let nextMessages = null;
1466
1515
  for await (const chunk of streamChat(withSystemMessage(activeMessages), modelName, TOOLS, controller.signal)) {
1467
1516
  if (chunk.type === "content") {
1468
- assistantMessage.content += chunk.content;
1517
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1469
1518
  setStreamingMessage({ ...assistantMessage });
1470
1519
  continue;
1471
1520
  }
@@ -1486,7 +1535,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1486
1535
  // v8 ignore next
1487
1536
  const allowedTools = executionMode === "plan" ? READ_TOOLS : void 0;
1488
1537
  const result = await executeTool(normalized.name, normalized.arguments, { allowedTools });
1489
- toolResultMessages.push(buildToolResultMessage(normalized.name, result));
1538
+ toolResultMessages.push(buildToolResultMessage(normalized.name, result, normalized.arguments));
1490
1539
  } catch (error) {
1491
1540
  toolResultMessages.push(buildToolResultMessage(toolCall.function.name, {
1492
1541
  content: "",
@@ -1500,10 +1549,20 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1500
1549
  }
1501
1550
  if (!nextMessages) {
1502
1551
  await prewarmCodeBlocks(assistantMessage.content, theme);
1503
- commitAssistantMessage();
1552
+ const updatedMessages = commitAssistantMessage();
1553
+ if (hasUncalledToolIntent(assistantMessage.content) && toolIntentCorrections < MAX_TOOL_INTENT_CORRECTIONS) {
1554
+ toolIntentCorrections += 1;
1555
+ activeMessages = [...updatedMessages, {
1556
+ role: SYSTEM,
1557
+ content: TOOL_INTENT_CORRECTION
1558
+ }];
1559
+ setMessages(activeMessages);
1560
+ continue;
1561
+ }
1504
1562
  return;
1505
1563
  }
1506
1564
  toolTurns += 1;
1565
+ toolIntentCorrections = 0;
1507
1566
  /* v8 ignore start */
1508
1567
  if (toolTurns >= MAX_TOOL_TURNS) {
1509
1568
  setMessages([...nextMessages, {
@@ -1554,6 +1613,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1554
1613
  let committedMessages = currentMessages;
1555
1614
  let assistantCommitted = false;
1556
1615
  const commitAssistantMessage = () => {
1616
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1557
1617
  if (assistantCommitted) {
1558
1618
  // v8 ignore next
1559
1619
  if (committedMessages.at(-1)?.role === "assistant") {
@@ -1579,7 +1639,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1579
1639
  // v8 ignore next 3
1580
1640
  if (controller.signal.aborted) return;
1581
1641
  if (chunk.type === "content") {
1582
- assistantMessage.content += chunk.content;
1642
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1583
1643
  setStreamingMessage({ ...assistantMessage });
1584
1644
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
1585
1645
  const updatedMessages = commitAssistantMessage();
@@ -1605,7 +1665,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1605
1665
  return;
1606
1666
  }
1607
1667
  const result = await executeTool(normalized.name, normalized.arguments, { allowedTools: READ_TOOLS });
1608
- const toolResultMessage = buildToolResultMessage(normalized.name, result);
1668
+ const toolResultMessage = buildToolResultMessage(normalized.name, result, normalized.arguments);
1609
1669
  const newMessages = [...updatedMessages, toolResultMessage];
1610
1670
  setMessages(newMessages);
1611
1671
  await processStreamReadOnly(newMessages);
@@ -1629,7 +1689,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1629
1689
  // v8 ignore next 3
1630
1690
  if (controller.signal.aborted) return;
1631
1691
  if (chunk.type === "content") {
1632
- planAssistantMessage.content += chunk.content;
1692
+ planAssistantMessage.content = sanitizeAssistantContent(planAssistantMessage.content + chunk.content);
1633
1693
  setStreamingMessage({ ...planAssistantMessage });
1634
1694
  }
1635
1695
  }
@@ -1702,10 +1762,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1702
1762
  switch (decision) {
1703
1763
  case APPROVE: {
1704
1764
  const result = await executeToolCall(toolCall);
1705
- const toolResultMessage = {
1706
- role: SYSTEM,
1707
- content: formatToolResultContent(toolCall.function.name, result)
1708
- };
1765
+ const toolResultMessage = buildToolResultMessage(toolCall.function.name, result, toolCall.function.arguments);
1709
1766
  const newMessages = [...approvedMessages, toolResultMessage];
1710
1767
  setMessages(newMessages);
1711
1768
  await processStream(newMessages, executionMode);
@@ -1717,7 +1774,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1717
1774
  content: formatToolResultContent(toolCall.function.name, {
1718
1775
  content: "",
1719
1776
  error: "Tool call rejected by user"
1720
- })
1777
+ }, toolCall.function.arguments)
1721
1778
  };
1722
1779
  setMessages([...approvedMessages, toolResultMessage]);
1723
1780
  setIsLoading(false);
@@ -1725,7 +1782,11 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1725
1782
  break;
1726
1783
  }
1727
1784
  }
1728
- }, [pendingToolCall, processStream]);
1785
+ }, [
1786
+ buildToolResultMessage,
1787
+ pendingToolCall,
1788
+ processStream
1789
+ ]);
1729
1790
  const handleSubmit = useCallback(async ({ content, images }) => {
1730
1791
  setInterruptReason(null);
1731
1792
  const userContent = content.trim();
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.22.0";
54
54
  //#endregion
55
55
  //#region src/constants/package.ts
56
56
  var NAME = name;
@@ -109,11 +109,11 @@ var BACK = {
109
109
  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
110
 
111
111
  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
112
+ 1. Use available tools rather than guessing file contents, paths, or code behavior
113
+ 2. When a tool is needed, call it immediately instead of saying you will call it
114
+ 3. Read files before editing them to understand context
115
+ 4. Make the smallest exact change that satisfies the request
116
+ 5. Explain after tool results are available, unless the user asks for discussion or a plan
117
117
  6. Confirm with the user before destructive operations
118
118
 
119
119
  When tools return results, incorporate them into your response naturally`;
@@ -131,7 +131,12 @@ Always use tools when you need to:
131
131
  - Make file changes
132
132
  - Explore project structure
133
133
  - Search the codebase
134
- - Look up current or external information`;
134
+ - Look up current or external information
135
+
136
+ Path rules:
137
+ - Paths are relative to the project root unless absolute
138
+ - Preserve parent directories from listings; if list_dir("src") returns [d] utils, use src/utils
139
+ - If a path fails, inspect the parent directory or search before retrying`;
135
140
  var PLAN_GENERATION_INSTRUCTION = `Based on the research above, decide whether the user request needs code or shell execution
136
141
 
137
142
  If the request needs changes or commands, respond with a plan checklist only
@@ -490,6 +495,15 @@ function saveConfig(patch) {
490
495
  //#region src/utils/ollama.ts
491
496
  var { host } = loadConfig();
492
497
  var client = new Ollama({ host });
498
+ var TRAILING_CONTROL_TOKEN_REGEX = /(?:\s*<\|?channel\|?>)+\s*$/;
499
+ 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;
500
+ 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.";
501
+ function sanitizeAssistantContent(content) {
502
+ return content.replace(TRAILING_CONTROL_TOKEN_REGEX, "");
503
+ }
504
+ function hasUncalledToolIntent(content) {
505
+ return TOOL_INTENT_REGEX.test(content);
506
+ }
493
507
  async function checkHealth() {
494
508
  try {
495
509
  return (await fetch(host)).ok;
@@ -498,9 +512,15 @@ async function checkHealth() {
498
512
  }
499
513
  }
500
514
  async function* streamChat(messages, model, tools, signal) {
515
+ const providerMessages = messages.map(({ role, content, images, tool_calls }) => ({
516
+ role,
517
+ content,
518
+ ...images ? { images } : {},
519
+ ...tool_calls ? { tool_calls } : {}
520
+ }));
501
521
  const response = await client.chat({
502
522
  model,
503
- messages,
523
+ messages: providerMessages,
504
524
  stream: true,
505
525
  tools,
506
526
  // v8 ignore next
@@ -782,10 +802,10 @@ var TOOLS = [
782
802
  type: "string",
783
803
  description: "The path to the directory to list"
784
804
  } }, ["path"]),
785
- defineTool(GREP_SEARCH, "Search for a pattern in files within a directory", {
805
+ defineTool(GREP_SEARCH, "Search files within a directory; multi-word queries also match common code identifier forms", {
786
806
  pattern: {
787
807
  type: "string",
788
- description: "The regex pattern to search for"
808
+ description: "The regex, phrase, or code concept to search for"
789
809
  },
790
810
  path: {
791
811
  type: "string",
@@ -861,6 +881,76 @@ async function runShell(command) {
861
881
  }
862
882
  //#endregion
863
883
  //#region src/utils/tools/filesystem.ts
884
+ var DIFF_CONTEXT_LINES = 3;
885
+ var DIFF_MAX_LINES = 120;
886
+ var DIFF_MAX_CHARS = 12e3;
887
+ function splitLines(content) {
888
+ return content.split("\n");
889
+ }
890
+ function createUnifiedDiff(filePath, beforeContent, afterContent) {
891
+ const beforeLines = splitLines(beforeContent);
892
+ const afterLines = splitLines(afterContent);
893
+ let commonPrefix = 0;
894
+ while (commonPrefix < beforeLines.length && commonPrefix < afterLines.length && beforeLines[commonPrefix] === afterLines[commonPrefix]) commonPrefix += 1;
895
+ let commonSuffix = 0;
896
+ while (commonSuffix < beforeLines.length - commonPrefix && commonSuffix < afterLines.length - commonPrefix && beforeLines[beforeLines.length - 1 - commonSuffix] === afterLines[afterLines.length - 1 - commonSuffix]) commonSuffix += 1;
897
+ const beforeChangeEnd = beforeLines.length - commonSuffix;
898
+ const afterChangeEnd = afterLines.length - commonSuffix;
899
+ const hunkStart = Math.max(0, commonPrefix - DIFF_CONTEXT_LINES);
900
+ const beforeHunkEnd = Math.min(beforeLines.length, beforeChangeEnd + DIFF_CONTEXT_LINES);
901
+ const afterHunkEnd = Math.min(afterLines.length, afterChangeEnd + DIFF_CONTEXT_LINES);
902
+ const beforeHunkLines = beforeLines.slice(hunkStart, beforeHunkEnd);
903
+ const afterHunkLines = afterLines.slice(hunkStart, afterHunkEnd);
904
+ const beforeChangedLines = beforeLines.slice(commonPrefix, beforeChangeEnd);
905
+ const afterChangedLines = afterLines.slice(commonPrefix, afterChangeEnd);
906
+ const contextBefore = beforeLines.slice(hunkStart, commonPrefix);
907
+ const contextAfter = beforeLines.slice(beforeChangeEnd, beforeHunkEnd);
908
+ return [
909
+ `--- ${filePath}`,
910
+ `+++ ${filePath}`,
911
+ `@@ -${String(hunkStart + 1)},${String(beforeHunkLines.length)} +${String(hunkStart + 1)},${String(afterHunkLines.length)} @@`,
912
+ ...contextBefore.map((line) => ` ${line}`),
913
+ ...beforeChangedLines.map((line) => `-${line}`),
914
+ ...afterChangedLines.map((line) => `+${line}`),
915
+ ...contextAfter.map((line) => ` ${line}`)
916
+ ].join("\n");
917
+ }
918
+ function truncateDiff(diff) {
919
+ const lines = diff.split("\n");
920
+ let visibleLines = lines.slice(0, DIFF_MAX_LINES);
921
+ let truncated = lines.length > DIFF_MAX_LINES;
922
+ while (visibleLines.join("\n").length > DIFF_MAX_CHARS) {
923
+ visibleLines = visibleLines.slice(0, -1);
924
+ truncated = true;
925
+ }
926
+ if (truncated) visibleLines = [...visibleLines, `[diff truncated: showing ${String(visibleLines.length)} of ${String(lines.length)} lines]`];
927
+ return {
928
+ visible: visibleLines.join("\n"),
929
+ truncated,
930
+ totalLines: lines.length,
931
+ visibleLines: Math.min(visibleLines.length, lines.length)
932
+ };
933
+ }
934
+ function buildSearchPatterns(pattern) {
935
+ const words = pattern.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean);
936
+ if (words.length < 2) return [pattern];
937
+ const camelCase = words.map((word, index) => index === 0 ? word.toLowerCase() : capitalize(word.toLowerCase())).join("");
938
+ const pascalCase = words.map((word) => capitalize(word.toLowerCase())).join("");
939
+ const snakeCase = words.map((word) => word.toLowerCase()).join("_");
940
+ const upperSnakeCase = snakeCase.toUpperCase();
941
+ const flexibleWhitespace = words.join(String.raw`\s+`);
942
+ return Array.from(new Set([
943
+ pattern,
944
+ flexibleWhitespace,
945
+ snakeCase,
946
+ upperSnakeCase,
947
+ camelCase,
948
+ pascalCase
949
+ ]));
950
+ }
951
+ function capitalize(value) {
952
+ return value.charAt(0).toUpperCase() + value.slice(1);
953
+ }
864
954
  /**
865
955
  * Read file contents
866
956
  */
@@ -910,8 +1000,19 @@ function editFile(filePath, oldText, newText) {
910
1000
  content: "",
911
1001
  error: `Exact text matched multiple locations in file: ${filePath}`
912
1002
  };
913
- writeFileSync(filePath, content.replace(oldText, newText), "utf8");
914
- return { content: `File edited successfully: ${filePath}` };
1003
+ const updatedContent = content.replace(oldText, newText);
1004
+ const truncatedDiff = truncateDiff(createUnifiedDiff(filePath, content, updatedContent));
1005
+ writeFileSync(filePath, updatedContent, "utf8");
1006
+ return {
1007
+ content: `File edited successfully: ${filePath}`,
1008
+ diff: {
1009
+ path: filePath,
1010
+ visible: truncatedDiff.visible,
1011
+ truncated: truncatedDiff.truncated,
1012
+ totalLines: truncatedDiff.totalLines,
1013
+ visibleLines: truncatedDiff.visibleLines
1014
+ }
1015
+ };
915
1016
  } catch (error) {
916
1017
  return {
917
1018
  content: "",
@@ -966,17 +1067,17 @@ function listDir(dirPath) {
966
1067
  * Search for pattern in files using ripgrep if available, fallback to Node.js
967
1068
  */
968
1069
  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" };
1070
+ const patterns = buildSearchPatterns(pattern);
1071
+ for (const searchPattern of patterns) try {
1072
+ const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${searchPattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
1073
+ if (stdout) return { content: stdout };
973
1074
  } catch {}
974
1075
  try {
975
1076
  if (!existsSync(dirPath)) return {
976
1077
  content: "",
977
1078
  error: `Directory not found: ${dirPath}`
978
1079
  };
979
- const regex = new RegExp(pattern, "g");
1080
+ const regexes = patterns.map((searchPattern) => new RegExp(searchPattern));
980
1081
  const results = [];
981
1082
  function searchDirectory(currentPath) {
982
1083
  const entries = readdirSync(currentPath, { withFileTypes: true });
@@ -986,9 +1087,9 @@ async function grepSearch(pattern, dirPath) {
986
1087
  if (!entry.name.startsWith(".") && entry.name !== "node_modules") searchDirectory(fullPath);
987
1088
  } else if (entry.isFile()) try {
988
1089
  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;
1090
+ for (let i = 0; i < lines.length; i++) for (const regex of regexes) if (regex.test(lines[i])) {
1091
+ results.push(`${fullPath}:${(i + 1).toString()}: ${lines[i].trim()}`);
1092
+ break;
992
1093
  }
993
1094
  } catch {}
994
1095
  }
@@ -1241,17 +1342,25 @@ function normalizeToolCall(toolCall) {
1241
1342
  requiresApproval: WRITE_TOOLS.has(name)
1242
1343
  };
1243
1344
  }
1244
- function formatToolResultContent(toolName, result) {
1345
+ function formatToolResultContent(toolName, result, args) {
1346
+ const formattedArgs = args ? `(${formatToolArguments(args)})` : "";
1245
1347
  const status = result.error ? "The requested action was NOT performed" : "";
1246
1348
  const content = result.content ? `\n${result.content}` : "";
1247
1349
  const error = result.error ? `\nError: ${result.error}` : "";
1248
1350
  return [
1249
- `Tool ${toolName} result:`,
1351
+ `Tool ${toolName}${formattedArgs} result:`,
1250
1352
  status,
1251
1353
  content.trim(),
1252
1354
  error.trim()
1253
1355
  ].filter(Boolean).join("\n");
1254
1356
  }
1357
+ function formatToolArguments(args) {
1358
+ return JSON.stringify(args, (_, value) => {
1359
+ if (typeof value !== "string") return value;
1360
+ if (value.length <= 80 && !value.includes("\n")) return value;
1361
+ return `<${String(value.length)} chars>`;
1362
+ });
1363
+ }
1255
1364
  async function executeToolCall(toolCall, options) {
1256
1365
  try {
1257
1366
  const normalized = normalizeToolCall(toolCall);
@@ -1332,6 +1441,7 @@ async function checkForUpdate() {
1332
1441
  //#region src/cli.ts
1333
1442
  var cli = cac("code-ollama");
1334
1443
  var MAX_TOOL_TURNS = 25;
1444
+ var MAX_TOOL_INTENT_CORRECTIONS = 2;
1335
1445
  cli.version(VERSION);
1336
1446
  cli.help();
1337
1447
  cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model, prompt) => {
@@ -1366,6 +1476,7 @@ async function runPrompt(model, prompt) {
1366
1476
  async function processRunStream(messages, model) {
1367
1477
  let activeMessages = messages;
1368
1478
  let toolTurns = 0;
1479
+ let toolIntentCorrections = 0;
1369
1480
  while (toolTurns < MAX_TOOL_TURNS) {
1370
1481
  const assistantMessage = {
1371
1482
  role: ASSISTANT,
@@ -1374,10 +1485,11 @@ async function processRunStream(messages, model) {
1374
1485
  let nextMessages = null;
1375
1486
  for await (const chunk of streamChat(activeMessages, model, TOOLS)) {
1376
1487
  if (chunk.type === "content") {
1377
- assistantMessage.content += chunk.content;
1488
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1378
1489
  write(chunk.content);
1379
1490
  continue;
1380
1491
  }
1492
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1381
1493
  // v8 ignore next 3
1382
1494
  if (chunk.tool_calls.length === 0) continue;
1383
1495
  const committedMessages = [...activeMessages, assistantMessage];
@@ -1386,15 +1498,31 @@ async function processRunStream(messages, model) {
1386
1498
  const result = await executeToolCall(toolCall);
1387
1499
  toolResultMessages.push({
1388
1500
  role: SYSTEM,
1389
- content: formatToolResultContent(toolCall.function.name, result)
1501
+ content: formatToolResultContent(toolCall.function.name, result, toolCall.function.arguments)
1390
1502
  });
1391
1503
  }
1392
1504
  nextMessages = [...committedMessages, ...toolResultMessages];
1393
1505
  break;
1394
1506
  }
1395
- if (!nextMessages) return;
1507
+ if (!nextMessages) {
1508
+ assistantMessage.content = sanitizeAssistantContent(assistantMessage.content);
1509
+ if (hasUncalledToolIntent(assistantMessage.content) && toolIntentCorrections < MAX_TOOL_INTENT_CORRECTIONS) {
1510
+ toolIntentCorrections += 1;
1511
+ activeMessages = [
1512
+ ...activeMessages,
1513
+ assistantMessage,
1514
+ {
1515
+ role: SYSTEM,
1516
+ content: TOOL_INTENT_CORRECTION
1517
+ }
1518
+ ];
1519
+ continue;
1520
+ }
1521
+ return;
1522
+ }
1396
1523
  activeMessages = nextMessages;
1397
1524
  toolTurns += 1;
1525
+ toolIntentCorrections = 0;
1398
1526
  }
1399
1527
  // v8 ignore next 3
1400
1528
  writeError("Tool execution stopped because the maximum tool turn limit was reached\n");
@@ -1408,7 +1536,7 @@ async function main(args = process.argv.slice(2)) {
1408
1536
  else await launchTui();
1409
1537
  }
1410
1538
  async function launchTui(sessionId) {
1411
- const { renderApp } = await import("./assets/tui-BHkHnKUC.js");
1539
+ const { renderApp } = await import("./assets/tui-CzkVRFXf.js");
1412
1540
  reset();
1413
1541
  renderApp(sessionId);
1414
1542
  }
@@ -1424,4 +1552,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
1424
1552
  if (isEntrypoint()) main();
1425
1553
  // v8 ignore stop
1426
1554
  //#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 };
1555
+ export { VERSION as $, loadConfig as A, ASSISTANT as B, checkHealth as C, pullModel as D, listModels as E, withSystemMessage as F, CATALOG as G, USER as H, HEADER_PREFIX as I, PLAN as J, AUTO as K, WARNING as L, removeClipboardImage as M, saveClipboardImage as N, sanitizeAssistantContent as O, resetSystemMessage as P, NAME as Q, LIST as R, TOOL_INTENT_CORRECTION as S, hasUncalledToolIntent as T, PLAN_GENERATION_INSTRUCTION as U, SYSTEM as V, BACK as W, APPROVE as X, SAFE as Y, REJECT as Z, loadSession as _, normalizeToolCall as a, reset as b, WRITE_TOOLS as c, write as d, LIST$1 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, LABEL as q, executeToolCall as r, TOOLS as s, checkForUpdate as t, 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.22.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",