code-ollama 0.8.0 → 0.9.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.
@@ -5,8 +5,10 @@ import { homedir } from "node:os";
5
5
  import { exec } from "node:child_process";
6
6
  import { Box, Text, render, useApp, useInput } from "ink";
7
7
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
8
- import { Select, Spinner, TextInput } from "@inkjs/ui";
8
+ import { Select, Spinner } from "@inkjs/ui";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { marked } from "marked";
11
+ import TerminalRenderer from "marked-terminal";
10
12
  //#region src/constants/command.ts
11
13
  var LIST = [
12
14
  {
@@ -42,6 +44,76 @@ var LABEL = {
42
44
  //#region src/constants/ui.ts
43
45
  var HEADER_PREFIX = "🦙 ";
44
46
  //#endregion
47
+ //#region src/components/CodeBlock/CodeBlock.tsx
48
+ async function highlightCode(code, language = "text") {
49
+ const { codeToANSI } = await import("@shikijs/cli");
50
+ try {
51
+ return await codeToANSI(code, language, "github-light");
52
+ } catch {
53
+ // v8 ignore next - Defensive fallback for unsupported languages
54
+ return code;
55
+ }
56
+ }
57
+ var CodeBlock = memo(function CodeBlock({ code, language, role }) {
58
+ const [highlighted, setHighlighted] = useState(code);
59
+ useEffect(() => {
60
+ let canceled = false;
61
+ async function loadHighlight() {
62
+ try {
63
+ const result = await highlightCode(code, language);
64
+ if (!canceled) setHighlighted(result);
65
+ } catch {}
66
+ }
67
+ loadHighlight();
68
+ return () => {
69
+ canceled = true;
70
+ };
71
+ }, [code, language]);
72
+ const isSystem = role === ROLE.SYSTEM;
73
+ return /* @__PURE__ */ jsx(Box, {
74
+ flexDirection: "column",
75
+ borderStyle: "round",
76
+ borderColor: isSystem ? "gray" : "dim",
77
+ paddingX: 1,
78
+ marginY: 1,
79
+ children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, {
80
+ dimColor: isSystem,
81
+ children: highlighted
82
+ }) })
83
+ });
84
+ });
85
+ //#endregion
86
+ //#region src/components/Markdown/Markdown.tsx
87
+ marked.setOptions({ renderer: new TerminalRenderer({ theme: "gitHub" }) });
88
+ function renderMarkdown(content) {
89
+ const result = marked.parse(content);
90
+ // v8 ignore next - Defensive fallback for Promise return
91
+ return typeof result === "string" ? result.trim() : "";
92
+ }
93
+ var Markdown = memo(function Markdown({ content, color, dimColor }) {
94
+ const [rendered, setRendered] = useState(content);
95
+ useEffect(() => {
96
+ let canceled = false;
97
+ function loadMarkdown() {
98
+ try {
99
+ const result = renderMarkdown(content);
100
+ // v8 ignore start
101
+ if (!canceled) setRendered(result);
102
+ } catch {}
103
+ // v8 ignore stop
104
+ }
105
+ loadMarkdown();
106
+ return () => {
107
+ canceled = true;
108
+ };
109
+ }, [content]);
110
+ return /* @__PURE__ */ jsx(Text, {
111
+ color,
112
+ dimColor,
113
+ children: rendered
114
+ });
115
+ });
116
+ //#endregion
45
117
  //#region src/components/Messages/constants.ts
46
118
  var TURN_ABORTED_MESSAGE = [
47
119
  "<turn_aborted>",
@@ -53,18 +125,90 @@ var TURN_ABORTED_MESSAGE = [
53
125
  function getMessageColor(role) {
54
126
  switch (role) {
55
127
  case ROLE.USER: return "black";
56
- case ROLE.ASSISTANT: return "blue";
128
+ case ROLE.ASSISTANT: return "cyan";
57
129
  case ROLE.SYSTEM: return "gray";
58
130
  default: return;
59
131
  }
60
132
  }
133
+ function parseContent(content) {
134
+ const segments = [];
135
+ const codeBlockRegex = /```(\w+)?\n?([\s\S]*?)```/g;
136
+ let lastIndex = 0;
137
+ let match;
138
+ while ((match = codeBlockRegex.exec(content)) !== null) {
139
+ if (match.index > lastIndex) {
140
+ const textContent = content.slice(lastIndex, match.index).trim();
141
+ // v8 ignore next 2 - Defensive check for empty trimmed content
142
+ if (textContent) segments.push({
143
+ type: "text",
144
+ content: textContent
145
+ });
146
+ }
147
+ const language = match[1];
148
+ const codeContent = match[2].trim();
149
+ // v8 ignore next 2 - Defensive check for empty code block
150
+ if (codeContent) segments.push({
151
+ type: "code",
152
+ content: codeContent,
153
+ language
154
+ });
155
+ lastIndex = match.index + match[0].length;
156
+ }
157
+ if (lastIndex < content.length) {
158
+ const textContent = content.slice(lastIndex).trim();
159
+ // v8 ignore next 2 - Defensive check for empty trimmed content
160
+ if (textContent) segments.push({
161
+ type: "text",
162
+ content: textContent
163
+ });
164
+ }
165
+ // v8 ignore next 2 - Defensive fallback for edge case
166
+ if (segments.length === 0 && content.trim()) segments.push({
167
+ type: "text",
168
+ content: content.trim()
169
+ });
170
+ return segments;
171
+ }
61
172
  var Message = memo(function Message({ message }) {
173
+ const messageColor = getMessageColor(message.role);
174
+ const isSystem = message.role === ROLE.SYSTEM;
175
+ const isUser = message.role === ROLE.USER;
176
+ if (isSystem) return /* @__PURE__ */ jsx(Box, {
177
+ flexDirection: "column",
178
+ marginBottom: 1,
179
+ marginX: 2,
180
+ children: /* @__PURE__ */ jsx(Text, {
181
+ color: messageColor,
182
+ dimColor: true,
183
+ children: message.content
184
+ })
185
+ });
62
186
  return /* @__PURE__ */ jsx(Box, {
187
+ flexDirection: "column",
63
188
  marginBottom: 1,
64
- children: /* @__PURE__ */ jsxs(Text, {
65
- color: getMessageColor(message.role),
66
- dimColor: message.role === ROLE.SYSTEM,
67
- children: [message.role === ROLE.USER && "> ", message.content]
189
+ children: parseContent(message.content).map((segment, index) => {
190
+ const prefix = isUser && index === 0 ? "> " : "";
191
+ if (segment.type === "code") return isUser ? /* @__PURE__ */ jsx(Text, {
192
+ color: messageColor,
193
+ children: segment.content
194
+ }, index) : /* @__PURE__ */ jsx(Box, {
195
+ marginX: 2,
196
+ children: /* @__PURE__ */ jsx(CodeBlock, {
197
+ code: segment.content,
198
+ language: segment.language,
199
+ role: message.role
200
+ })
201
+ }, index);
202
+ return isUser ? /* @__PURE__ */ jsx(Text, {
203
+ color: messageColor,
204
+ children: prefix + segment.content
205
+ }, index) : /* @__PURE__ */ jsx(Box, {
206
+ marginX: 2,
207
+ children: /* @__PURE__ */ jsx(Markdown, {
208
+ content: segment.content,
209
+ color: messageColor
210
+ })
211
+ }, index);
68
212
  })
69
213
  });
70
214
  });
@@ -77,6 +221,7 @@ function Messages({ messages, isLoading, streamingMessage }) {
77
221
  isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
78
222
  marginTop: -1,
79
223
  marginBottom: 1,
224
+ marginX: 2,
80
225
  children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
81
226
  })
82
227
  ]
@@ -239,6 +384,71 @@ var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
239
384
  return INTERRUPT_REASON;
240
385
  }({});
241
386
  //#endregion
387
+ //#region src/components/TextInput/TextInput.tsx
388
+ function TextInput({ value, isDisabled = false, placeholder, onChange, onSubmit }) {
389
+ const [cursorPosition, setCursorPosition] = useState(value.length);
390
+ const prevValueRef = useRef(value);
391
+ useEffect(() => {
392
+ const prevValue = prevValueRef.current;
393
+ prevValueRef.current = value;
394
+ if (value === "") setCursorPosition(0);
395
+ else if (value.length > prevValue.length && cursorPosition <= prevValue.length) setCursorPosition(value.length);
396
+ else if (cursorPosition > value.length) setCursorPosition(value.length);
397
+ }, [value, cursorPosition]);
398
+ useInput((input, key) => {
399
+ // v8 ignore next
400
+ if (isDisabled) return;
401
+ if (key.return) {
402
+ onSubmit(value);
403
+ setCursorPosition(0);
404
+ return;
405
+ }
406
+ if (key.backspace) {
407
+ if (cursorPosition > 0) {
408
+ onChange(value.slice(0, cursorPosition - 1) + value.slice(cursorPosition));
409
+ setCursorPosition(cursorPosition - 1);
410
+ }
411
+ return;
412
+ }
413
+ // v8 ignore start
414
+ if (key.delete) {
415
+ if (cursorPosition < value.length) onChange(value.slice(0, cursorPosition) + value.slice(cursorPosition + 1));
416
+ return;
417
+ }
418
+ // v8 ignore stop
419
+ if (key.leftArrow) {
420
+ setCursorPosition(Math.max(0, cursorPosition - 1));
421
+ return;
422
+ }
423
+ if (key.rightArrow) {
424
+ setCursorPosition(Math.min(value.length, cursorPosition + 1));
425
+ return;
426
+ }
427
+ if (key.home) {
428
+ setCursorPosition(0);
429
+ return;
430
+ }
431
+ if (key.end) {
432
+ setCursorPosition(value.length);
433
+ return;
434
+ }
435
+ // v8 ignore start
436
+ if (input) {
437
+ onChange(value.slice(0, cursorPosition) + input + value.slice(cursorPosition));
438
+ setCursorPosition(cursorPosition + input.length);
439
+ }
440
+ // v8 ignore stop
441
+ }, { isActive: !isDisabled });
442
+ const displayValue = value || (placeholder ?? "");
443
+ const isPlaceholder = Boolean(!value && placeholder);
444
+ const char = displayValue[cursorPosition] || " ";
445
+ const before = displayValue.slice(0, cursorPosition);
446
+ const after = displayValue.slice(cursorPosition + 1);
447
+ const dimStyle = isPlaceholder ? "\x1B[2m" : "";
448
+ const resetDim = isPlaceholder ? "\x1B[22m" : "";
449
+ return /* @__PURE__ */ jsx(Text, { children: `${dimStyle}${before}${resetDim}\x1b[7m${char}\x1b[27m${dimStyle}${after}${resetDim}` });
450
+ }
451
+ //#endregion
242
452
  //#region src/components/Chat/CommandMenu.tsx
243
453
  function getMatchingCommands(input) {
244
454
  const normalizedInput = input.trim().toLowerCase();
@@ -391,15 +601,13 @@ function hasFileSuggestionQuery(input) {
391
601
  function Input({ isDisabled = false, onInterrupt, onSubmit }) {
392
602
  const { exit } = useApp();
393
603
  const [input, setInput] = useState("");
394
- const [inputKey, setInputKey] = useState(0);
395
604
  const fileSuggestionRef = useRef(null);
396
- const remountTextInput = useCallback(() => {
397
- setInputKey((key) => key + 1);
398
- }, [setInputKey]);
605
+ const resetInput = useCallback(() => {
606
+ setInput("");
607
+ }, []);
399
608
  const handleSelectFileSuggestion = useCallback((nextInput) => {
400
609
  setInput(nextInput);
401
- remountTextInput();
402
- }, [remountTextInput]);
610
+ }, []);
403
611
  const handleFileSuggestionChange = useCallback((nextInput) => {
404
612
  fileSuggestionRef.current = nextInput;
405
613
  }, []);
@@ -407,10 +615,9 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
407
615
  const trimmedInput = input.trim();
408
616
  if (!trimmedInput) return;
409
617
  onSubmit(trimmedInput);
410
- setInput("");
618
+ resetInput();
411
619
  fileSuggestionRef.current = null;
412
- remountTextInput();
413
- }, [onSubmit, remountTextInput]);
620
+ }, [onSubmit, resetInput]);
414
621
  const showCommandMenu = input.startsWith("/");
415
622
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
416
623
  const handleSubmitText = useCallback(async (input) => {
@@ -433,8 +640,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
433
640
  }
434
641
  if (isCtrlC) {
435
642
  if (input) {
436
- setInput("");
437
- remountTextInput();
643
+ resetInput();
438
644
  return;
439
645
  }
440
646
  exit();
@@ -444,12 +650,12 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
444
650
  flexDirection: "column",
445
651
  children: [
446
652
  /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
447
- defaultValue: input,
653
+ value: input,
448
654
  isDisabled,
449
655
  onChange: setInput,
450
656
  onSubmit: handleSubmitText,
451
657
  placeholder: "Ask anything... (/ commands, @ files)"
452
- }, inputKey)] }),
658
+ })] }),
453
659
  showCommandMenu && /* @__PURE__ */ jsx(CommandMenu, {
454
660
  input,
455
661
  onSubmit: handleSubmitCommand
@@ -1013,7 +1219,6 @@ function renderApp() {
1013
1219
  const tree = /* @__PURE__ */ jsx(App, {});
1014
1220
  const app = render(tree, {
1015
1221
  exitOnCtrlC: false,
1016
- incrementalRendering: true,
1017
1222
  maxFps: 60
1018
1223
  });
1019
1224
  setClearHandler(() => {
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.8.0";
11
+ var VERSION = "0.9.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
@@ -512,7 +512,7 @@ async function processRunStream(messages, model) {
512
512
  }
513
513
  async function main(args = process.argv.slice(2)) {
514
514
  if (!args.length) {
515
- const { renderApp } = await import("./assets/tui-BjeMfZFh.js");
515
+ const { renderApp } = await import("./assets/tui-VKBxlYAz.js");
516
516
  process.stdout.write("\x1Bc");
517
517
  renderApp();
518
518
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -40,8 +40,11 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "@inkjs/ui": "2.0.0",
43
+ "@shikijs/cli": "4.0.2",
43
44
  "cac": "7.0.0",
44
45
  "ink": "7.0.2",
46
+ "marked": "15.0.12",
47
+ "marked-terminal": "7.3.0",
45
48
  "ollama": "0.6.3",
46
49
  "react": "19.2.6"
47
50
  },