code-ollama 0.7.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.
- package/dist/assets/{tui-CDaKDOEJ.js → tui-VKBxlYAz.js} +345 -76
- package/dist/cli.js +23 -13
- package/package.json +4 -1
|
@@ -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
|
|
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
|
{
|
|
@@ -40,24 +42,173 @@ var LABEL = {
|
|
|
40
42
|
};
|
|
41
43
|
//#endregion
|
|
42
44
|
//#region src/constants/ui.ts
|
|
43
|
-
var HEADER_PREFIX = "🦙";
|
|
45
|
+
var HEADER_PREFIX = "🦙 ";
|
|
44
46
|
//#endregion
|
|
45
|
-
//#region src/components/
|
|
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
|
|
117
|
+
//#region src/components/Messages/constants.ts
|
|
118
|
+
var TURN_ABORTED_MESSAGE = [
|
|
119
|
+
"<turn_aborted>",
|
|
120
|
+
"The user interrupted the previous turn on purpose. Any running commands may still be running in the background. If any tools were aborted, they may have partially executed.",
|
|
121
|
+
"</turn_aborted>"
|
|
122
|
+
].join("\n");
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/components/Messages/Messages.tsx
|
|
46
125
|
function getMessageColor(role) {
|
|
47
126
|
switch (role) {
|
|
48
127
|
case ROLE.USER: return "black";
|
|
49
|
-
case ROLE.ASSISTANT: return "
|
|
128
|
+
case ROLE.ASSISTANT: return "cyan";
|
|
50
129
|
case ROLE.SYSTEM: return "gray";
|
|
51
130
|
default: return;
|
|
52
131
|
}
|
|
53
132
|
}
|
|
54
|
-
|
|
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
|
+
}
|
|
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
|
+
});
|
|
55
186
|
return /* @__PURE__ */ jsx(Box, {
|
|
187
|
+
flexDirection: "column",
|
|
56
188
|
marginBottom: 1,
|
|
57
|
-
children:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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);
|
|
61
212
|
})
|
|
62
213
|
});
|
|
63
214
|
});
|
|
@@ -65,11 +216,12 @@ function Messages({ messages, isLoading, streamingMessage }) {
|
|
|
65
216
|
return /* @__PURE__ */ jsxs(Box, {
|
|
66
217
|
flexDirection: "column",
|
|
67
218
|
children: [
|
|
68
|
-
messages.map((message, index) => /* @__PURE__ */ jsx(
|
|
69
|
-
streamingMessage && /* @__PURE__ */ jsx(
|
|
219
|
+
messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE).map((message, index) => /* @__PURE__ */ jsx(Message, { message }, index)),
|
|
220
|
+
streamingMessage && /* @__PURE__ */ jsx(Message, { message: streamingMessage }),
|
|
70
221
|
isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
|
|
71
222
|
marginTop: -1,
|
|
72
223
|
marginBottom: 1,
|
|
224
|
+
marginX: 2,
|
|
73
225
|
children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
|
|
74
226
|
})
|
|
75
227
|
]
|
|
@@ -226,6 +378,76 @@ function ToolApproval({ toolCall, onDecision }) {
|
|
|
226
378
|
var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
|
|
227
379
|
var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
|
|
228
380
|
var PLAN_EXECUTION_REMINDER = "Do not claim success and do not call write_file or run_shell until the user approves execution";
|
|
381
|
+
var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
|
|
382
|
+
INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
|
|
383
|
+
INTERRUPT_REASON["REJECTED"] = "rejected";
|
|
384
|
+
return INTERRUPT_REASON;
|
|
385
|
+
}({});
|
|
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
|
+
}
|
|
229
451
|
//#endregion
|
|
230
452
|
//#region src/components/Chat/CommandMenu.tsx
|
|
231
453
|
function getMatchingCommands(input) {
|
|
@@ -376,18 +598,16 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
|
|
|
376
598
|
function hasFileSuggestionQuery(input) {
|
|
377
599
|
return /(^|\s)@\S+$/.test(input);
|
|
378
600
|
}
|
|
379
|
-
function Input({ isDisabled = false, onSubmit }) {
|
|
601
|
+
function Input({ isDisabled = false, onInterrupt, onSubmit }) {
|
|
380
602
|
const { exit } = useApp();
|
|
381
603
|
const [input, setInput] = useState("");
|
|
382
|
-
const [inputKey, setInputKey] = useState(0);
|
|
383
604
|
const fileSuggestionRef = useRef(null);
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
}, [
|
|
605
|
+
const resetInput = useCallback(() => {
|
|
606
|
+
setInput("");
|
|
607
|
+
}, []);
|
|
387
608
|
const handleSelectFileSuggestion = useCallback((nextInput) => {
|
|
388
609
|
setInput(nextInput);
|
|
389
|
-
|
|
390
|
-
}, [remountTextInput]);
|
|
610
|
+
}, []);
|
|
391
611
|
const handleFileSuggestionChange = useCallback((nextInput) => {
|
|
392
612
|
fileSuggestionRef.current = nextInput;
|
|
393
613
|
}, []);
|
|
@@ -395,10 +615,9 @@ function Input({ isDisabled = false, onSubmit }) {
|
|
|
395
615
|
const trimmedInput = input.trim();
|
|
396
616
|
if (!trimmedInput) return;
|
|
397
617
|
onSubmit(trimmedInput);
|
|
398
|
-
|
|
618
|
+
resetInput();
|
|
399
619
|
fileSuggestionRef.current = null;
|
|
400
|
-
|
|
401
|
-
}, [onSubmit, remountTextInput]);
|
|
620
|
+
}, [onSubmit, resetInput]);
|
|
402
621
|
const showCommandMenu = input.startsWith("/");
|
|
403
622
|
const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
|
|
404
623
|
const handleSubmitText = useCallback(async (input) => {
|
|
@@ -414,21 +633,29 @@ function Input({ isDisabled = false, onSubmit }) {
|
|
|
414
633
|
if (LIST.find(({ name }) => name === input)) submitAndReset(input);
|
|
415
634
|
}, [submitAndReset]);
|
|
416
635
|
useInput((_input, key) => {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
636
|
+
const isCtrlC = key.ctrl && _input === "c";
|
|
637
|
+
if (isDisabled) {
|
|
638
|
+
if (key.escape || isCtrlC) onInterrupt?.();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (isCtrlC) {
|
|
642
|
+
if (input) {
|
|
643
|
+
resetInput();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
exit();
|
|
647
|
+
}
|
|
421
648
|
});
|
|
422
649
|
return /* @__PURE__ */ jsxs(Box, {
|
|
423
650
|
flexDirection: "column",
|
|
424
651
|
children: [
|
|
425
652
|
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
|
|
426
|
-
|
|
653
|
+
value: input,
|
|
427
654
|
isDisabled,
|
|
428
655
|
onChange: setInput,
|
|
429
656
|
onSubmit: handleSubmitText,
|
|
430
657
|
placeholder: "Ask anything... (/ commands, @ files)"
|
|
431
|
-
}
|
|
658
|
+
})] }),
|
|
432
659
|
showCommandMenu && /* @__PURE__ */ jsx(CommandMenu, {
|
|
433
660
|
input,
|
|
434
661
|
onSubmit: handleSubmitCommand
|
|
@@ -458,12 +685,15 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
458
685
|
const [isLoading, setIsLoading] = useState(false);
|
|
459
686
|
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
460
687
|
const [pendingPlan, setPendingPlan] = useState(null);
|
|
688
|
+
const [interruptReason, setInterruptReason] = useState(null);
|
|
689
|
+
const abortControllerRef = useRef(null);
|
|
461
690
|
useEffect(() => {
|
|
462
691
|
setMessages([]);
|
|
463
692
|
setStreamingMessage(null);
|
|
464
693
|
setIsLoading(false);
|
|
465
694
|
setPendingToolCall(null);
|
|
466
695
|
setPendingPlan(null);
|
|
696
|
+
setInterruptReason(null);
|
|
467
697
|
}, [sessionId]);
|
|
468
698
|
const buildToolResultMessage = useCallback((toolName, result) => {
|
|
469
699
|
if (result.error?.startsWith("Tool not allowed:")) return {
|
|
@@ -490,7 +720,20 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
490
720
|
PLAN_EXECUTION_REMINDER
|
|
491
721
|
].join("\n")
|
|
492
722
|
}), []);
|
|
723
|
+
const handleInterrupt = useCallback(() => {
|
|
724
|
+
abortControllerRef.current?.abort();
|
|
725
|
+
abortControllerRef.current = null;
|
|
726
|
+
setIsLoading(false);
|
|
727
|
+
setStreamingMessage(null);
|
|
728
|
+
setInterruptReason(INTERRUPT_REASON.INTERRUPTED);
|
|
729
|
+
setMessages((prev) => [...prev, {
|
|
730
|
+
role: ROLE.USER,
|
|
731
|
+
content: TURN_ABORTED_MESSAGE
|
|
732
|
+
}]);
|
|
733
|
+
}, []);
|
|
493
734
|
const processStream = useCallback(async (currentMessages, executionMode = mode) => {
|
|
735
|
+
const controller = new AbortController();
|
|
736
|
+
abortControllerRef.current = controller;
|
|
494
737
|
const assistantMessage = {
|
|
495
738
|
role: ROLE.ASSISTANT,
|
|
496
739
|
content: ""
|
|
@@ -518,33 +761,40 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
518
761
|
};
|
|
519
762
|
setStreamingMessage(assistantMessage);
|
|
520
763
|
try {
|
|
521
|
-
for await (const chunk of streamChat(withSystemMessage(currentMessages), model, TOOLS
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
764
|
+
for await (const chunk of streamChat(withSystemMessage(currentMessages), model, TOOLS, controller.signal)) {
|
|
765
|
+
// v8 ignore next 3
|
|
766
|
+
if (controller.signal.aborted) return;
|
|
767
|
+
if (chunk.type === "content") {
|
|
768
|
+
assistantMessage.content += chunk.content;
|
|
769
|
+
setStreamingMessage({ ...assistantMessage });
|
|
770
|
+
} else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
|
|
771
|
+
const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
|
|
772
|
+
// v8 ignore start
|
|
773
|
+
const allowedTools = executionMode === NAME.PLAN ? READ_TOOLS : void 0;
|
|
774
|
+
// v8 ignore stop
|
|
775
|
+
const updatedMessages = commitAssistantMessage();
|
|
776
|
+
if (executionMode === NAME.SAFE && requiresApproval) {
|
|
777
|
+
setPendingToolCall(toolCall);
|
|
778
|
+
setIsLoading(false);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools });
|
|
782
|
+
const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
|
|
783
|
+
const newMessages = [...updatedMessages, toolResultMessage];
|
|
784
|
+
setMessages(newMessages);
|
|
785
|
+
await processStream(newMessages, executionMode);
|
|
533
786
|
return;
|
|
534
787
|
}
|
|
535
|
-
const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools });
|
|
536
|
-
const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
|
|
537
|
-
const newMessages = [...updatedMessages, toolResultMessage];
|
|
538
|
-
setMessages(newMessages);
|
|
539
|
-
await processStream(newMessages, executionMode);
|
|
540
|
-
return;
|
|
541
788
|
}
|
|
542
789
|
commitAssistantMessage();
|
|
543
790
|
} catch (error) {
|
|
544
791
|
// v8 ignore next
|
|
545
|
-
|
|
546
|
-
|
|
792
|
+
if (!controller.signal.aborted) {
|
|
793
|
+
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
794
|
+
commitAssistantMessage();
|
|
795
|
+
}
|
|
547
796
|
} finally {
|
|
797
|
+
if (abortControllerRef.current === controller) abortControllerRef.current = null;
|
|
548
798
|
setIsLoading(false);
|
|
549
799
|
}
|
|
550
800
|
}, [
|
|
@@ -553,6 +803,8 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
553
803
|
mode
|
|
554
804
|
]);
|
|
555
805
|
const processStreamReadOnly = useCallback(async (currentMessages) => {
|
|
806
|
+
const controller = new AbortController();
|
|
807
|
+
abortControllerRef.current = controller;
|
|
556
808
|
const assistantMessage = {
|
|
557
809
|
role: ROLE.ASSISTANT,
|
|
558
810
|
content: ""
|
|
@@ -581,24 +833,28 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
581
833
|
setStreamingMessage(assistantMessage);
|
|
582
834
|
try {
|
|
583
835
|
const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
|
|
584
|
-
for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
836
|
+
for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools, controller.signal)) {
|
|
837
|
+
// v8 ignore next 3
|
|
838
|
+
if (controller.signal.aborted) return;
|
|
839
|
+
if (chunk.type === "content") {
|
|
840
|
+
assistantMessage.content += chunk.content;
|
|
841
|
+
setStreamingMessage({ ...assistantMessage });
|
|
842
|
+
} else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
|
|
843
|
+
const updatedMessages = commitAssistantMessage();
|
|
844
|
+
if (!READ_TOOLS.has(toolCall.function.name)) {
|
|
845
|
+
const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
|
|
846
|
+
const newMessages = [...updatedMessages, correctionMessage];
|
|
847
|
+
setMessages(newMessages);
|
|
848
|
+
await processStreamReadOnly(newMessages);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_TOOLS });
|
|
852
|
+
const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
|
|
853
|
+
const newMessages = [...updatedMessages, toolResultMessage];
|
|
592
854
|
setMessages(newMessages);
|
|
593
855
|
await processStreamReadOnly(newMessages);
|
|
594
856
|
return;
|
|
595
857
|
}
|
|
596
|
-
const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_TOOLS });
|
|
597
|
-
const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
|
|
598
|
-
const newMessages = [...updatedMessages, toolResultMessage];
|
|
599
|
-
setMessages(newMessages);
|
|
600
|
-
await processStreamReadOnly(newMessages);
|
|
601
|
-
return;
|
|
602
858
|
}
|
|
603
859
|
const researchMessages = commitAssistantMessage();
|
|
604
860
|
const planInstruction = {
|
|
@@ -612,9 +868,13 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
612
868
|
};
|
|
613
869
|
setStreamingMessage(planAssistantMessage);
|
|
614
870
|
try {
|
|
615
|
-
for await (const chunk of streamChat(withSystemMessage(planMessages), model, []
|
|
616
|
-
|
|
617
|
-
|
|
871
|
+
for await (const chunk of streamChat(withSystemMessage(planMessages), model, [], controller.signal)) {
|
|
872
|
+
// v8 ignore next 3
|
|
873
|
+
if (controller.signal.aborted) return;
|
|
874
|
+
if (chunk.type === "content") {
|
|
875
|
+
planAssistantMessage.content += chunk.content;
|
|
876
|
+
setStreamingMessage({ ...planAssistantMessage });
|
|
877
|
+
}
|
|
618
878
|
}
|
|
619
879
|
} catch (error) {
|
|
620
880
|
// v8 ignore next
|
|
@@ -634,9 +894,12 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
634
894
|
setIsLoading(false);
|
|
635
895
|
} catch (error) {
|
|
636
896
|
// v8 ignore next
|
|
637
|
-
|
|
638
|
-
|
|
897
|
+
if (!controller.signal.aborted) {
|
|
898
|
+
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
899
|
+
commitAssistantMessage();
|
|
900
|
+
}
|
|
639
901
|
} finally {
|
|
902
|
+
if (abortControllerRef.current === controller) abortControllerRef.current = null;
|
|
640
903
|
setIsLoading(false);
|
|
641
904
|
}
|
|
642
905
|
}, [
|
|
@@ -689,16 +952,14 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
689
952
|
await processStream(newMessages);
|
|
690
953
|
break;
|
|
691
954
|
}
|
|
692
|
-
case REJECT:
|
|
693
|
-
|
|
694
|
-
role: ROLE.
|
|
695
|
-
content:
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
await processStream(newMessages);
|
|
955
|
+
case REJECT:
|
|
956
|
+
setMessages((previousMessages) => [...previousMessages, {
|
|
957
|
+
role: ROLE.USER,
|
|
958
|
+
content: TURN_ABORTED_MESSAGE
|
|
959
|
+
}]);
|
|
960
|
+
setIsLoading(false);
|
|
961
|
+
setInterruptReason(INTERRUPT_REASON.REJECTED);
|
|
700
962
|
break;
|
|
701
|
-
}
|
|
702
963
|
}
|
|
703
964
|
}, [
|
|
704
965
|
pendingToolCall,
|
|
@@ -706,6 +967,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
706
967
|
processStream
|
|
707
968
|
]);
|
|
708
969
|
const handleSubmit = useCallback(async (value) => {
|
|
970
|
+
setInterruptReason(null);
|
|
709
971
|
const userContent = value.trim();
|
|
710
972
|
if (!userContent) return;
|
|
711
973
|
if (userContent.startsWith("/")) {
|
|
@@ -744,8 +1006,16 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
|
744
1006
|
toolCall: pendingToolCall,
|
|
745
1007
|
onDecision: handleToolApproval
|
|
746
1008
|
}),
|
|
1009
|
+
interruptReason && !isLoading && /* @__PURE__ */ jsx(Box, {
|
|
1010
|
+
marginBottom: 1,
|
|
1011
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
1012
|
+
color: "red",
|
|
1013
|
+
children: interruptReason === INTERRUPT_REASON.REJECTED ? "❗ Tool call rejected." : "❗ Execution interrupted."
|
|
1014
|
+
})
|
|
1015
|
+
}),
|
|
747
1016
|
!pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Input, {
|
|
748
1017
|
isDisabled: isLoading,
|
|
1018
|
+
onInterrupt: handleInterrupt,
|
|
749
1019
|
onSubmit: handleSubmit
|
|
750
1020
|
})
|
|
751
1021
|
]
|
|
@@ -949,7 +1219,6 @@ function renderApp() {
|
|
|
949
1219
|
const tree = /* @__PURE__ */ jsx(App, {});
|
|
950
1220
|
const app = render(tree, {
|
|
951
1221
|
exitOnCtrlC: false,
|
|
952
|
-
incrementalRendering: true,
|
|
953
1222
|
maxFps: 60
|
|
954
1223
|
});
|
|
955
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.
|
|
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
|
|
@@ -134,22 +134,32 @@ function saveConfig(patch) {
|
|
|
134
134
|
//#region src/utils/ollama.ts
|
|
135
135
|
var { host, model: DEFAULT_MODEL } = loadConfig();
|
|
136
136
|
var client = new Ollama({ host });
|
|
137
|
-
async function* streamChat(messages, model = DEFAULT_MODEL, tools) {
|
|
137
|
+
async function* streamChat(messages, model = DEFAULT_MODEL, tools, signal) {
|
|
138
138
|
const response = await client.chat({
|
|
139
139
|
model,
|
|
140
140
|
messages,
|
|
141
141
|
stream: true,
|
|
142
|
-
tools
|
|
142
|
+
tools,
|
|
143
|
+
// v8 ignore next
|
|
144
|
+
...signal ? { signal } : {}
|
|
143
145
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
try {
|
|
147
|
+
for await (const chunk of response) {
|
|
148
|
+
// v8 ignore next 3
|
|
149
|
+
if (signal?.aborted) return;
|
|
150
|
+
if (chunk.message.content) yield {
|
|
151
|
+
type: "content",
|
|
152
|
+
content: chunk.message.content
|
|
153
|
+
};
|
|
154
|
+
if (chunk.message.tool_calls) yield {
|
|
155
|
+
type: "tool_calls",
|
|
156
|
+
tool_calls: chunk.message.tool_calls
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// v8 ignore start
|
|
161
|
+
if (error instanceof Error && (error.name === "AbortError" || signal?.aborted)) return;
|
|
162
|
+
throw error;
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
async function listModels() {
|
|
@@ -502,7 +512,7 @@ async function processRunStream(messages, model) {
|
|
|
502
512
|
}
|
|
503
513
|
async function main(args = process.argv.slice(2)) {
|
|
504
514
|
if (!args.length) {
|
|
505
|
-
const { renderApp } = await import("./assets/tui-
|
|
515
|
+
const { renderApp } = await import("./assets/tui-VKBxlYAz.js");
|
|
506
516
|
process.stdout.write("\x1Bc");
|
|
507
517
|
renderApp();
|
|
508
518
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-ollama",
|
|
3
|
-
"version": "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
|
},
|