code-ollama 0.5.0 → 0.6.1

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,7 +1,10 @@
1
- import { a as setClearHandler, c as loadConfig, d as withSystemMessage, f as ROLE, i as executeTool, l as saveConfig, m as VERSION, n as READ_ONLY_TOOLS, o as listModels, p as PLAN_GENERATION_INSTRUCTION, r as TOOLS, s as streamChat, t as DANGEROUS_TOOLS, u as resetSystemMessage } from "../cli.js";
1
+ import { a as tick, c as streamChat, d as resetSystemMessage, f as withSystemMessage, h as VERSION, i as executeTool, l as loadConfig, m as PLAN_GENERATION_INSTRUCTION, n as TOOLS, o as setClearHandler, p as ROLE, r as WRITE_TOOLS, s as listModels, t as READ_TOOLS, u as saveConfig } from "../cli.js";
2
+ import { readdirSync } from "node:fs";
3
+ import { join, relative } from "node:path";
2
4
  import { homedir } from "node:os";
3
- import { Box, Text, render, useInput } from "ink";
4
- import { memo, useCallback, useEffect, useMemo, useState } from "react";
5
+ import { exec } from "node:child_process";
6
+ import { Box, Text, render, useApp, useInput } from "ink";
7
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
8
  import { Select, Spinner, TextInput } from "@inkjs/ui";
6
9
  import { jsx, jsxs } from "react/jsx-runtime";
7
10
  //#region src/constants/command.ts
@@ -76,6 +79,45 @@ function SelectPrompt({ children, onEscape, ...selectProps }) {
76
79
  children: [children, /* @__PURE__ */ jsx(Select, { ...selectProps })]
77
80
  });
78
81
  }
82
+ function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" }) {
83
+ return /* @__PURE__ */ jsxs(Box, {
84
+ flexDirection: "row",
85
+ children: [
86
+ /* @__PURE__ */ jsxs(Text, {
87
+ dimColor: true,
88
+ children: [message, " ("]
89
+ }),
90
+ /* @__PURE__ */ jsx(Text, {
91
+ bold: true,
92
+ children: "↑↓"
93
+ }),
94
+ /* @__PURE__ */ jsx(Text, {
95
+ dimColor: true,
96
+ children: " + "
97
+ }),
98
+ /* @__PURE__ */ jsx(Text, {
99
+ bold: true,
100
+ children: "Enter"
101
+ }),
102
+ /* @__PURE__ */ jsx(Text, {
103
+ dimColor: true,
104
+ children: " to confirm, "
105
+ }),
106
+ /* @__PURE__ */ jsx(Text, {
107
+ bold: true,
108
+ children: "Esc"
109
+ }),
110
+ /* @__PURE__ */ jsxs(Text, {
111
+ dimColor: true,
112
+ children: [
113
+ " to ",
114
+ escapeLabel,
115
+ ")"
116
+ ]
117
+ })
118
+ ]
119
+ });
120
+ }
79
121
  //#endregion
80
122
  //#region src/components/PlanApproval.tsx
81
123
  var options$1 = [
@@ -114,10 +156,7 @@ function PlanApproval({ planContent, onModeChange }) {
114
156
  marginY: 1,
115
157
  children: /* @__PURE__ */ jsx(Text, { children: planContent })
116
158
  }),
117
- /* @__PURE__ */ jsx(Text, {
118
- dimColor: true,
119
- children: "Select execution mode (↑↓ + Enter to confirm, Esc to cancel)"
120
- })
159
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select execution mode" })
121
160
  ]
122
161
  })
123
162
  });
@@ -168,9 +207,9 @@ function ToolApproval({ toolCall, onDecision }) {
168
207
  args
169
208
  ] })]
170
209
  }),
171
- /* @__PURE__ */ jsx(Text, {
172
- dimColor: true,
173
- children: "Select approval action (↑↓ + Enter to confirm, Esc to reject)"
210
+ /* @__PURE__ */ jsx(SelectPromptHint, {
211
+ message: "Select approval action",
212
+ escapeLabel: "reject"
174
213
  })
175
214
  ]
176
215
  });
@@ -200,36 +239,200 @@ function CommandMenu({ input, onSubmit }) {
200
239
  });
201
240
  }
202
241
  //#endregion
242
+ //#region src/components/Chat/FileSuggestions.tsx
243
+ var MAX_VISIBLE_OPTIONS = 5;
244
+ var MENTION_PATTERN = /(^|\s)@(\S+)$/;
245
+ var RIPGREP_MAX_BUFFER = 10 * 1024 * 1024;
246
+ function normalizePath(filePath) {
247
+ return filePath.replaceAll("\\", "/");
248
+ }
249
+ function getMentionMatch(input) {
250
+ const match = MENTION_PATTERN.exec(input);
251
+ if (!match) return null;
252
+ return {
253
+ prefix: input.slice(0, match.index + match[1].length),
254
+ query: match[2]
255
+ };
256
+ }
257
+ function buildNextInput(input, filePath) {
258
+ const mentionMatch = getMentionMatch(input);
259
+ // v8 ignore next 3
260
+ if (!mentionMatch) return input;
261
+ return `${mentionMatch.prefix}${filePath} `;
262
+ }
263
+ function listProjectFilesFallback(rootDir) {
264
+ const filePaths = [];
265
+ function walk(currentPath) {
266
+ const entries = readdirSync(currentPath, { withFileTypes: true });
267
+ for (const entry of entries) {
268
+ if (entry.name === ".git") continue;
269
+ const fullPath = join(currentPath, entry.name);
270
+ if (entry.isDirectory()) {
271
+ walk(fullPath);
272
+ continue;
273
+ }
274
+ if (entry.isFile()) filePaths.push(normalizePath(relative(rootDir, fullPath)));
275
+ }
276
+ }
277
+ walk(rootDir);
278
+ return filePaths.sort((left, right) => left.localeCompare(right));
279
+ }
280
+ function listProjectFilesWithRipgrep(rootDir) {
281
+ return new Promise((resolve, reject) => {
282
+ exec("rg --files --hidden -g \"!**/.git/**\"", {
283
+ cwd: rootDir,
284
+ maxBuffer: RIPGREP_MAX_BUFFER
285
+ }, (error, stdout) => {
286
+ if (error) {
287
+ reject(error);
288
+ return;
289
+ }
290
+ resolve(stdout.split("\n").map((line) => line.trim()).filter(Boolean).map(normalizePath).sort((left, right) => left.localeCompare(right)));
291
+ });
292
+ });
293
+ }
294
+ async function listProjectFiles(rootDir) {
295
+ try {
296
+ return await listProjectFilesWithRipgrep(rootDir);
297
+ } catch {
298
+ return listProjectFilesFallback(rootDir);
299
+ }
300
+ }
301
+ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
302
+ const [filePaths, setFilePaths] = useState([]);
303
+ const [focusedIndex, setFocusedIndex] = useState(0);
304
+ useEffect(() => {
305
+ async function loadProjectFiles() {
306
+ setFilePaths(await listProjectFiles(process.cwd()));
307
+ }
308
+ loadProjectFiles();
309
+ }, []);
310
+ const mentionMatch = getMentionMatch(input);
311
+ const options = useMemo(() => {
312
+ if (!mentionMatch) return [];
313
+ const normalizedQuery = mentionMatch.query.toLowerCase();
314
+ return filePaths.filter((filePath) => filePath.toLowerCase().includes(normalizedQuery));
315
+ }, [filePaths, mentionMatch]);
316
+ useEffect(() => {
317
+ setFocusedIndex(0);
318
+ }, [input]);
319
+ useEffect(() => {
320
+ if (!options.length) {
321
+ setFocusedIndex(0);
322
+ return;
323
+ }
324
+ setFocusedIndex((currentIndex) => Math.min(currentIndex, options.length - 1));
325
+ }, [options]);
326
+ useEffect(() => {
327
+ if (!onChange) return;
328
+ if (!mentionMatch || !options.length) {
329
+ onChange(null);
330
+ return;
331
+ }
332
+ onChange(buildNextInput(input, options[focusedIndex]));
333
+ }, [
334
+ focusedIndex,
335
+ input,
336
+ mentionMatch,
337
+ onChange,
338
+ options
339
+ ]);
340
+ useInput((_, key) => {
341
+ if (isDisabled || !options.length) return;
342
+ if (key.downArrow) {
343
+ setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
344
+ return;
345
+ }
346
+ if (key.upArrow) {
347
+ setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
348
+ return;
349
+ }
350
+ if (key.tab || key.return) onSelect(buildNextInput(input, options[focusedIndex]));
351
+ });
352
+ if (!mentionMatch || !options.length) return null;
353
+ const visibleStart = Math.min(Math.max(0, focusedIndex - MAX_VISIBLE_OPTIONS + 1), Math.max(0, options.length - MAX_VISIBLE_OPTIONS));
354
+ return /* @__PURE__ */ jsx(Box, {
355
+ flexDirection: "column",
356
+ children: options.slice(visibleStart, visibleStart + MAX_VISIBLE_OPTIONS).map((option, index) => {
357
+ return /* @__PURE__ */ jsx(Box, {
358
+ marginLeft: 2,
359
+ children: /* @__PURE__ */ jsx(Text, {
360
+ color: visibleStart + index === focusedIndex ? "cyan" : void 0,
361
+ children: option
362
+ })
363
+ }, option);
364
+ })
365
+ });
366
+ }
367
+ //#endregion
203
368
  //#region src/components/Chat/Input.tsx
369
+ function hasFileSuggestionQuery(input) {
370
+ return /(^|\s)@\S+$/.test(input);
371
+ }
204
372
  function Input({ isDisabled = false, onSubmit }) {
373
+ const { exit } = useApp();
205
374
  const [input, setInput] = useState("");
206
- const [resetKey, setResetKey] = useState(0);
207
- const handleSubmitText = useCallback((input) => {
208
- setTimeout(() => {
209
- if (input.startsWith("/")) return;
210
- const trimmedInput = input.trim();
211
- if (!trimmedInput) return;
212
- onSubmit(trimmedInput);
213
- setInput("");
214
- setResetKey((key) => key + 1);
215
- });
216
- }, [onSubmit]);
217
- const handleSubmitCommand = useCallback((input) => {
218
- if (!LIST.find(({ name }) => name === input)) return;
219
- onSubmit(input);
375
+ const [inputKey, setInputKey] = useState(0);
376
+ const fileSuggestionRef = useRef(null);
377
+ const remountTextInput = useCallback(() => {
378
+ setInputKey((key) => key + 1);
379
+ }, [setInputKey]);
380
+ const handleSelectFileSuggestion = useCallback((nextInput) => {
381
+ setInput(nextInput);
382
+ remountTextInput();
383
+ }, [remountTextInput]);
384
+ const handleFileSuggestionChange = useCallback((nextInput) => {
385
+ fileSuggestionRef.current = nextInput;
386
+ }, []);
387
+ const submitAndReset = useCallback((input) => {
388
+ const trimmedInput = input.trim();
389
+ if (!trimmedInput) return;
390
+ onSubmit(trimmedInput);
220
391
  setInput("");
221
- setResetKey((key) => key + 1);
222
- }, [onSubmit]);
392
+ fileSuggestionRef.current = null;
393
+ remountTextInput();
394
+ }, [onSubmit, remountTextInput]);
395
+ const showCommandMenu = input.startsWith("/");
396
+ const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
397
+ const handleSubmitText = useCallback(async (input) => {
398
+ await tick();
399
+ if (input.startsWith("/")) return;
400
+ if (hasFileSuggestionQuery(input)) {
401
+ if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
402
+ return;
403
+ }
404
+ submitAndReset(input);
405
+ }, [handleSelectFileSuggestion, submitAndReset]);
406
+ const handleSubmitCommand = useCallback((input) => {
407
+ if (LIST.find(({ name }) => name === input)) submitAndReset(input);
408
+ }, [submitAndReset]);
409
+ useInput((_input, key) => {
410
+ if (key.ctrl && _input === "c") if (input) {
411
+ setInput("");
412
+ remountTextInput();
413
+ } else exit();
414
+ });
223
415
  return /* @__PURE__ */ jsxs(Box, {
224
416
  flexDirection: "column",
225
- children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
226
- isDisabled,
227
- onChange: setInput,
228
- onSubmit: handleSubmitText
229
- }, resetKey)] }), input.startsWith("/") && /* @__PURE__ */ jsx(CommandMenu, {
230
- input,
231
- onSubmit: handleSubmitCommand
232
- })]
417
+ children: [
418
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
419
+ defaultValue: input,
420
+ isDisabled,
421
+ onChange: setInput,
422
+ onSubmit: handleSubmitText,
423
+ placeholder: "Ask anything... (/ commands, @ files)"
424
+ }, inputKey)] }),
425
+ showCommandMenu && /* @__PURE__ */ jsx(CommandMenu, {
426
+ input,
427
+ onSubmit: handleSubmitCommand
428
+ }),
429
+ showFileSuggestions && /* @__PURE__ */ jsx(FileSuggestions, {
430
+ input,
431
+ isDisabled,
432
+ onChange: handleFileSuggestionChange,
433
+ onSelect: handleSelectFileSuggestion
434
+ })
435
+ ]
233
436
  });
234
437
  }
235
438
  //#endregion
@@ -312,9 +515,9 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
312
515
  assistantMessage.content += chunk.content;
313
516
  setStreamingMessage({ ...assistantMessage });
314
517
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
315
- const requiresApproval = DANGEROUS_TOOLS.has(toolCall.function.name);
518
+ const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
316
519
  // v8 ignore start
317
- const allowedTools = executionMode === NAME.PLAN ? READ_ONLY_TOOLS : void 0;
520
+ const allowedTools = executionMode === NAME.PLAN ? READ_TOOLS : void 0;
318
521
  // v8 ignore stop
319
522
  const updatedMessages = commitAssistantMessage();
320
523
  if (executionMode === NAME.SAFE && requiresApproval) {
@@ -331,6 +534,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
331
534
  }
332
535
  commitAssistantMessage();
333
536
  } catch (error) {
537
+ // v8 ignore next
334
538
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
335
539
  commitAssistantMessage();
336
540
  } finally {
@@ -369,20 +573,20 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
369
573
  };
370
574
  setStreamingMessage(assistantMessage);
371
575
  try {
372
- const readOnlyTools = TOOLS.filter((tool) => READ_ONLY_TOOLS.has(tool.function.name));
576
+ const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
373
577
  for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools)) if (chunk.type === "content") {
374
578
  assistantMessage.content += chunk.content;
375
579
  setStreamingMessage({ ...assistantMessage });
376
580
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
377
581
  const updatedMessages = commitAssistantMessage();
378
- if (!READ_ONLY_TOOLS.has(toolCall.function.name)) {
582
+ if (!READ_TOOLS.has(toolCall.function.name)) {
379
583
  const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
380
584
  const newMessages = [...updatedMessages, correctionMessage];
381
585
  setMessages(newMessages);
382
586
  await processStreamReadOnly(newMessages);
383
587
  return;
384
588
  }
385
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_ONLY_TOOLS });
589
+ const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_TOOLS });
386
590
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
387
591
  const newMessages = [...updatedMessages, toolResultMessage];
388
592
  setMessages(newMessages);
@@ -406,6 +610,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
406
610
  setStreamingMessage({ ...planAssistantMessage });
407
611
  }
408
612
  } catch (error) {
613
+ // v8 ignore next
409
614
  planAssistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
410
615
  setMessages([...planMessages, { ...planAssistantMessage }]);
411
616
  setStreamingMessage(null);
@@ -421,6 +626,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
421
626
  });
422
627
  setIsLoading(false);
423
628
  } catch (error) {
629
+ // v8 ignore next
424
630
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
425
631
  commitAssistantMessage();
426
632
  } finally {
@@ -655,10 +861,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
655
861
  defaultValue: currentModel,
656
862
  onChange: onSelect,
657
863
  onEscape: onClose,
658
- children: /* @__PURE__ */ jsx(Text, {
659
- dimColor: true,
660
- children: "Select a model (↑↓ + Enter to confirm, Esc to cancel)"
661
- })
864
+ children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select a model" })
662
865
  });
663
866
  }
664
867
  //#endregion
@@ -734,6 +937,7 @@ function App() {
734
937
  function renderApp() {
735
938
  const tree = /* @__PURE__ */ jsx(App, {});
736
939
  const app = render(tree, {
940
+ exitOnCtrlC: false,
737
941
  incrementalRendering: true,
738
942
  maxFps: 60
739
943
  });
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { exec } from "node:child_process";
8
8
  import { promisify } from "node:util";
9
9
  //#endregion
10
10
  //#region src/constants/package.ts
11
- var VERSION = "0.5.0";
11
+ var VERSION = "0.6.1";
12
12
  //#endregion
13
13
  //#region src/constants/prompt.ts
14
14
  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, and searching code
@@ -158,6 +158,9 @@ async function listModels() {
158
158
  }
159
159
  function setClearHandler(handler) {}
160
160
  //#endregion
161
+ //#region src/utils/time.ts
162
+ var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
163
+ //#endregion
161
164
  //#region src/utils/tools.ts
162
165
  var execAsync = promisify(exec);
163
166
  /**
@@ -250,13 +253,13 @@ var TOOLS = [
250
253
  "end"
251
254
  ])
252
255
  ];
253
- var READ_ONLY_TOOLS = new Set([
256
+ var READ_TOOLS = new Set([
254
257
  NAME.READ_FILE,
255
258
  NAME.LIST_DIR,
256
259
  NAME.GREP_SEARCH,
257
260
  NAME.VIEW_RANGE
258
261
  ]);
259
- var DANGEROUS_TOOLS = new Set([
262
+ var WRITE_TOOLS = new Set([
260
263
  NAME.WRITE_FILE,
261
264
  NAME.EDIT_FILE,
262
265
  NAME.RUN_SHELL
@@ -416,7 +419,7 @@ async function grepSearch(pattern, dirPath) {
416
419
  }
417
420
  }
418
421
  searchDirectory(dirPath);
419
- if (results.length === 0) return { content: "No matches found" };
422
+ if (!results.length) return { content: "No matches found" };
420
423
  return { content: results.join("\n") };
421
424
  } catch (error) {
422
425
  return {
@@ -499,7 +502,7 @@ async function processRunStream(messages, model) {
499
502
  }
500
503
  async function main(args = process.argv.slice(2)) {
501
504
  if (!args.length) {
502
- const { renderApp } = await import("./assets/tui-Da6uWrqo.js");
505
+ const { renderApp } = await import("./assets/tui-B1jg-OeC.js");
503
506
  process.stdout.write("\x1Bc");
504
507
  renderApp();
505
508
  return;
@@ -522,4 +525,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
522
525
  if (isEntrypoint()) main();
523
526
  // v8 ignore stop
524
527
  //#endregion
525
- export { setClearHandler as a, loadConfig as c, withSystemMessage as d, ROLE as f, executeTool as i, saveConfig as l, VERSION as m, main, READ_ONLY_TOOLS as n, listModels as o, PLAN_GENERATION_INSTRUCTION as p, TOOLS as r, streamChat as s, DANGEROUS_TOOLS as t, resetSystemMessage as u };
528
+ export { tick as a, streamChat as c, resetSystemMessage as d, withSystemMessage as f, VERSION as h, executeTool as i, loadConfig as l, PLAN_GENERATION_INSTRUCTION as m, main, TOOLS as n, setClearHandler as o, ROLE as p, WRITE_TOOLS as r, listModels as s, READ_TOOLS as t, saveConfig as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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",
@@ -46,11 +46,11 @@
46
46
  "react": "19.2.6"
47
47
  },
48
48
  "devDependencies": {
49
- "@commitlint/cli": "20.5.3",
50
- "@commitlint/config-conventional": "20.5.3",
51
- "@eslint/compat": "2.0.5",
49
+ "@commitlint/cli": "21.0.0",
50
+ "@commitlint/config-conventional": "21.0.0",
51
+ "@eslint/config-helpers": "0.6.0",
52
52
  "@eslint/js": "10.0.1",
53
- "@types/node": "25.6.1",
53
+ "@types/node": "25.6.2",
54
54
  "@types/react": "19.2.14",
55
55
  "@vitest/coverage-v8": "4.1.5",
56
56
  "eslint": "10.3.0",
@@ -59,9 +59,9 @@
59
59
  "globals": "17.6.0",
60
60
  "husky": "9.1.7",
61
61
  "ink-testing-library": "4.0.0",
62
- "lint-staged": "17.0.2",
62
+ "lint-staged": "17.0.3",
63
63
  "prettier": "3.8.3",
64
- "publint": "0.3.19",
64
+ "publint": "0.3.20",
65
65
  "tsx": "4.21.0",
66
66
  "typescript": "6.0.3",
67
67
  "typescript-eslint": "8.59.2",