code-ollama 0.5.0 → 0.6.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,6 +1,9 @@
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";
5
+ import { exec } from "node:child_process";
6
+ import { Box, Text, render, useApp, useInput } from "ink";
4
7
  import { memo, useCallback, useEffect, useMemo, useState } from "react";
5
8
  import { Select, Spinner, TextInput } from "@inkjs/ui";
6
9
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -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,176 @@ 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, 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
+ useInput((_, key) => {
327
+ if (isDisabled || !options.length) return;
328
+ if (key.downArrow) {
329
+ setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
330
+ return;
331
+ }
332
+ if (key.upArrow) {
333
+ setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
334
+ return;
335
+ }
336
+ if (key.tab) onSelect(buildNextInput(input, options[focusedIndex]));
337
+ });
338
+ if (!mentionMatch || !options.length) return null;
339
+ const visibleStart = Math.min(Math.max(0, focusedIndex - MAX_VISIBLE_OPTIONS + 1), Math.max(0, options.length - MAX_VISIBLE_OPTIONS));
340
+ return /* @__PURE__ */ jsx(Box, {
341
+ flexDirection: "column",
342
+ children: options.slice(visibleStart, visibleStart + MAX_VISIBLE_OPTIONS).map((option, index) => {
343
+ return /* @__PURE__ */ jsx(Box, {
344
+ marginLeft: 2,
345
+ children: /* @__PURE__ */ jsx(Text, {
346
+ color: visibleStart + index === focusedIndex ? "cyan" : void 0,
347
+ children: option
348
+ })
349
+ }, option);
350
+ })
351
+ });
352
+ }
353
+ //#endregion
203
354
  //#region src/components/Chat/Input.tsx
355
+ function hasActiveMentionQuery(input) {
356
+ return /(^|\s)@\S+$/.test(input);
357
+ }
204
358
  function Input({ isDisabled = false, onSubmit }) {
359
+ const { exit } = useApp();
205
360
  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]);
361
+ const [inputKey, setInputKey] = useState(0);
362
+ const remountTextInput = useCallback(() => {
363
+ setInputKey((key) => key + 1);
364
+ }, [setInputKey]);
365
+ const handleSubmitText = useCallback(async (input) => {
366
+ await tick();
367
+ if (input.startsWith("/")) return;
368
+ const trimmedInput = input.trim();
369
+ if (!trimmedInput) return;
370
+ onSubmit(trimmedInput);
371
+ setInput("");
372
+ remountTextInput();
373
+ }, [onSubmit, remountTextInput]);
217
374
  const handleSubmitCommand = useCallback((input) => {
218
375
  if (!LIST.find(({ name }) => name === input)) return;
219
376
  onSubmit(input);
220
377
  setInput("");
221
- setResetKey((key) => key + 1);
222
- }, [onSubmit]);
378
+ remountTextInput();
379
+ }, [onSubmit, remountTextInput]);
380
+ const handleSelectFileSuggestion = useCallback((nextInput) => {
381
+ setInput(nextInput);
382
+ remountTextInput();
383
+ }, [remountTextInput]);
384
+ useInput((_input, key) => {
385
+ if (key.ctrl && _input === "c") if (input) {
386
+ setInput("");
387
+ remountTextInput();
388
+ } else exit();
389
+ });
390
+ const showCommandMenu = input.startsWith("/");
391
+ const showFileSuggestions = !showCommandMenu && hasActiveMentionQuery(input);
223
392
  return /* @__PURE__ */ jsxs(Box, {
224
393
  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
- })]
394
+ children: [
395
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
396
+ defaultValue: input,
397
+ isDisabled,
398
+ onChange: setInput,
399
+ onSubmit: handleSubmitText,
400
+ placeholder: "Ask anything... (/ commands, @ files)"
401
+ }, inputKey)] }),
402
+ showCommandMenu && /* @__PURE__ */ jsx(CommandMenu, {
403
+ input,
404
+ onSubmit: handleSubmitCommand
405
+ }),
406
+ showFileSuggestions && /* @__PURE__ */ jsx(FileSuggestions, {
407
+ input,
408
+ isDisabled,
409
+ onSelect: handleSelectFileSuggestion
410
+ })
411
+ ]
233
412
  });
234
413
  }
235
414
  //#endregion
@@ -312,9 +491,9 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
312
491
  assistantMessage.content += chunk.content;
313
492
  setStreamingMessage({ ...assistantMessage });
314
493
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
315
- const requiresApproval = DANGEROUS_TOOLS.has(toolCall.function.name);
494
+ const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
316
495
  // v8 ignore start
317
- const allowedTools = executionMode === NAME.PLAN ? READ_ONLY_TOOLS : void 0;
496
+ const allowedTools = executionMode === NAME.PLAN ? READ_TOOLS : void 0;
318
497
  // v8 ignore stop
319
498
  const updatedMessages = commitAssistantMessage();
320
499
  if (executionMode === NAME.SAFE && requiresApproval) {
@@ -331,6 +510,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
331
510
  }
332
511
  commitAssistantMessage();
333
512
  } catch (error) {
513
+ // v8 ignore next
334
514
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
335
515
  commitAssistantMessage();
336
516
  } finally {
@@ -369,20 +549,20 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
369
549
  };
370
550
  setStreamingMessage(assistantMessage);
371
551
  try {
372
- const readOnlyTools = TOOLS.filter((tool) => READ_ONLY_TOOLS.has(tool.function.name));
552
+ const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
373
553
  for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools)) if (chunk.type === "content") {
374
554
  assistantMessage.content += chunk.content;
375
555
  setStreamingMessage({ ...assistantMessage });
376
556
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
377
557
  const updatedMessages = commitAssistantMessage();
378
- if (!READ_ONLY_TOOLS.has(toolCall.function.name)) {
558
+ if (!READ_TOOLS.has(toolCall.function.name)) {
379
559
  const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
380
560
  const newMessages = [...updatedMessages, correctionMessage];
381
561
  setMessages(newMessages);
382
562
  await processStreamReadOnly(newMessages);
383
563
  return;
384
564
  }
385
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_ONLY_TOOLS });
565
+ const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_TOOLS });
386
566
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
387
567
  const newMessages = [...updatedMessages, toolResultMessage];
388
568
  setMessages(newMessages);
@@ -406,6 +586,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
406
586
  setStreamingMessage({ ...planAssistantMessage });
407
587
  }
408
588
  } catch (error) {
589
+ // v8 ignore next
409
590
  planAssistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
410
591
  setMessages([...planMessages, { ...planAssistantMessage }]);
411
592
  setStreamingMessage(null);
@@ -421,6 +602,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
421
602
  });
422
603
  setIsLoading(false);
423
604
  } catch (error) {
605
+ // v8 ignore next
424
606
  assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
425
607
  commitAssistantMessage();
426
608
  } finally {
@@ -655,10 +837,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
655
837
  defaultValue: currentModel,
656
838
  onChange: onSelect,
657
839
  onEscape: onClose,
658
- children: /* @__PURE__ */ jsx(Text, {
659
- dimColor: true,
660
- children: "Select a model (↑↓ + Enter to confirm, Esc to cancel)"
661
- })
840
+ children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select a model" })
662
841
  });
663
842
  }
664
843
  //#endregion
@@ -734,6 +913,7 @@ function App() {
734
913
  function renderApp() {
735
914
  const tree = /* @__PURE__ */ jsx(App, {});
736
915
  const app = render(tree, {
916
+ exitOnCtrlC: false,
737
917
  incrementalRendering: true,
738
918
  maxFps: 60
739
919
  });
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.0";
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-UfyDivSA.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.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",
@@ -50,7 +50,7 @@
50
50
  "@commitlint/config-conventional": "20.5.3",
51
51
  "@eslint/compat": "2.0.5",
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",