code-ollama 0.2.1 → 0.3.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.
- package/README.md +4 -1
- package/dist/assets/tui-CVsodXv3.js +616 -0
- package/dist/cli.js +44 -5
- package/package.json +3 -3
- package/dist/assets/tui-eBtT-5hi.js +0 -370
package/README.md
CHANGED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { _ as REJECT, a as listModels, c as saveConfig, d as ROLE, f as PLAN_GENERATION_INSTRUCTION, g as APPROVE, h as NAME, i as executeTool, l as createSystemMessage, m as LABEL, n as READ_ONLY_TOOLS, o as streamChat, p as VERSION, r as TOOLS, s as loadConfig, t as DANGEROUS_TOOLS, u as HEADER_PREFIX, v as NAMES } from "../cli.js";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { Box, Text, render, useInput } from "ink";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { Select, Spinner, TextInput } from "@inkjs/ui";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/components/Messages.tsx
|
|
8
|
+
function getMessageColor(role) {
|
|
9
|
+
switch (role) {
|
|
10
|
+
case ROLE.USER: return "black";
|
|
11
|
+
case ROLE.ASSISTANT: return "blue";
|
|
12
|
+
case ROLE.SYSTEM: return "gray";
|
|
13
|
+
default: return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function Messages({ messages, isLoading }) {
|
|
17
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
18
|
+
flexDirection: "column",
|
|
19
|
+
children: [messages.map((message, index) => /* @__PURE__ */ jsx(Box, {
|
|
20
|
+
marginBottom: 1,
|
|
21
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
22
|
+
color: getMessageColor(message.role),
|
|
23
|
+
dimColor: message.role === ROLE.SYSTEM,
|
|
24
|
+
children: [message.role === ROLE.USER ? "> " : "", message.content]
|
|
25
|
+
})
|
|
26
|
+
}, index)), isLoading && messages[messages.length - 1]?.content === "" && /* @__PURE__ */ jsx(Box, {
|
|
27
|
+
marginTop: -1,
|
|
28
|
+
marginBottom: 1,
|
|
29
|
+
children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
|
|
30
|
+
})]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/components/SelectPrompt.tsx
|
|
35
|
+
function SelectPrompt({ children, onEscape, ...selectProps }) {
|
|
36
|
+
useInput((_, key) => {
|
|
37
|
+
if (key.escape) onEscape?.();
|
|
38
|
+
});
|
|
39
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
40
|
+
flexDirection: "column",
|
|
41
|
+
children: [children, /* @__PURE__ */ jsx(Select, { ...selectProps })]
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/components/PlanApproval.tsx
|
|
46
|
+
var options$1 = [
|
|
47
|
+
{
|
|
48
|
+
label: "Auto - Execute tools automatically",
|
|
49
|
+
value: NAME.AUTO
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: "Safe - Approve each tool",
|
|
53
|
+
value: NAME.SAFE
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "Cancel - Continue planning",
|
|
57
|
+
value: NAME.PLAN
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
function PlanApproval({ planContent, onModeChange }) {
|
|
61
|
+
return /* @__PURE__ */ jsx(SelectPrompt, {
|
|
62
|
+
options: options$1,
|
|
63
|
+
onChange: useCallback((value) => {
|
|
64
|
+
onModeChange(value);
|
|
65
|
+
}, [onModeChange]),
|
|
66
|
+
onEscape: useCallback(() => {
|
|
67
|
+
onModeChange(NAME.PLAN);
|
|
68
|
+
}, [onModeChange]),
|
|
69
|
+
children: /* @__PURE__ */ jsxs(Box, {
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
marginTop: 1,
|
|
72
|
+
children: [
|
|
73
|
+
/* @__PURE__ */ jsx(Text, {
|
|
74
|
+
bold: true,
|
|
75
|
+
color: "magenta",
|
|
76
|
+
children: "Plan Generated - Choose execution mode:"
|
|
77
|
+
}),
|
|
78
|
+
/* @__PURE__ */ jsx(Box, {
|
|
79
|
+
marginY: 1,
|
|
80
|
+
children: /* @__PURE__ */ jsx(Text, { children: planContent })
|
|
81
|
+
}),
|
|
82
|
+
/* @__PURE__ */ jsx(Text, {
|
|
83
|
+
dimColor: true,
|
|
84
|
+
children: "Select execution mode (↑↓ + Enter to confirm, Esc to cancel)"
|
|
85
|
+
})
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/components/ToolApproval.tsx
|
|
92
|
+
var options = [{
|
|
93
|
+
label: "Approve tool call",
|
|
94
|
+
value: APPROVE
|
|
95
|
+
}, {
|
|
96
|
+
label: "Reject tool call",
|
|
97
|
+
value: REJECT
|
|
98
|
+
}];
|
|
99
|
+
function ToolApproval({ toolCall, onDecision }) {
|
|
100
|
+
const handleChange = useCallback((value) => {
|
|
101
|
+
onDecision(value);
|
|
102
|
+
}, [onDecision]);
|
|
103
|
+
const handleEscape = useCallback(() => {
|
|
104
|
+
onDecision(REJECT);
|
|
105
|
+
}, [onDecision]);
|
|
106
|
+
const args = JSON.stringify(toolCall.function.arguments, null, 2);
|
|
107
|
+
return /* @__PURE__ */ jsxs(SelectPrompt, {
|
|
108
|
+
options,
|
|
109
|
+
onChange: handleChange,
|
|
110
|
+
onEscape: handleEscape,
|
|
111
|
+
children: [
|
|
112
|
+
/* @__PURE__ */ jsx(Text, {
|
|
113
|
+
color: "yellow",
|
|
114
|
+
children: "⚠️ Tool requires approval:"
|
|
115
|
+
}),
|
|
116
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
117
|
+
marginX: 3,
|
|
118
|
+
marginBottom: 1,
|
|
119
|
+
flexDirection: "column",
|
|
120
|
+
children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
121
|
+
/* @__PURE__ */ jsx(Text, {
|
|
122
|
+
italic: true,
|
|
123
|
+
children: "Tool:"
|
|
124
|
+
}),
|
|
125
|
+
" ",
|
|
126
|
+
toolCall.function.name
|
|
127
|
+
] }), /* @__PURE__ */ jsxs(Text, { children: [
|
|
128
|
+
/* @__PURE__ */ jsx(Text, {
|
|
129
|
+
italic: true,
|
|
130
|
+
children: "Arguments:"
|
|
131
|
+
}),
|
|
132
|
+
" ",
|
|
133
|
+
args
|
|
134
|
+
] })]
|
|
135
|
+
}),
|
|
136
|
+
/* @__PURE__ */ jsx(Text, {
|
|
137
|
+
dimColor: true,
|
|
138
|
+
children: "Select approval action (↑↓ + Enter to confirm, Esc to reject)"
|
|
139
|
+
})
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/components/Chat/constants.ts
|
|
145
|
+
var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
|
|
146
|
+
var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
|
|
147
|
+
var PLAN_EXECUTION_REMINDER = "Do not claim success and do not call write_file or run_shell until the user approves execution";
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/components/Chat/Input.tsx
|
|
150
|
+
function Input({ isDisabled = false, onSubmit }) {
|
|
151
|
+
const [resetKey, setResetKey] = useState(0);
|
|
152
|
+
const handleSubmit = useCallback((input) => {
|
|
153
|
+
const trimmed = input.trim();
|
|
154
|
+
if (!trimmed) return;
|
|
155
|
+
onSubmit(trimmed);
|
|
156
|
+
setResetKey((key) => key + 1);
|
|
157
|
+
}, [onSubmit]);
|
|
158
|
+
return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
|
|
159
|
+
isDisabled,
|
|
160
|
+
suggestions: NAMES,
|
|
161
|
+
onSubmit: handleSubmit
|
|
162
|
+
}, resetKey)] });
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/components/Chat/plan.ts
|
|
166
|
+
function hasExecutablePlan(content) {
|
|
167
|
+
return content.split("\n").some((line) => {
|
|
168
|
+
const trimmedLine = line.trim();
|
|
169
|
+
return /^- \[ \] (write_file|edit_file|run_shell)\(/.test(trimmedLine);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/components/Chat/Chat.tsx
|
|
174
|
+
function Chat({ model, onCommand, mode, onModeChange }) {
|
|
175
|
+
const [messages, setMessages] = useState([createSystemMessage()]);
|
|
176
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
177
|
+
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
178
|
+
const [pendingPlan, setPendingPlan] = useState(null);
|
|
179
|
+
const buildToolResultMessage = useCallback((toolName, result) => {
|
|
180
|
+
if (result.error?.startsWith("Tool not allowed:")) return {
|
|
181
|
+
role: ROLE.SYSTEM,
|
|
182
|
+
content: [
|
|
183
|
+
`Tool ${toolName} was blocked by execution policy`,
|
|
184
|
+
ACTION_NOT_PERFORMED,
|
|
185
|
+
`Blocked because ${result.error}`,
|
|
186
|
+
"Do not claim success. Either continue with allowed read-only tools or explain that approval/execution mode must change"
|
|
187
|
+
].join("\n")
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
role: ROLE.SYSTEM,
|
|
191
|
+
content: `Tool ${toolName} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
|
|
192
|
+
};
|
|
193
|
+
}, []);
|
|
194
|
+
const buildPlanModeCorrectionMessage = useCallback((toolName) => ({
|
|
195
|
+
role: ROLE.SYSTEM,
|
|
196
|
+
content: [
|
|
197
|
+
`Plan mode policy: ${toolName} cannot be executed during planning`,
|
|
198
|
+
ACTION_NOT_PERFORMED,
|
|
199
|
+
"Continue by using only read-only tools for research if needed",
|
|
200
|
+
PLAN_CHECKLIST_REMINDER,
|
|
201
|
+
PLAN_EXECUTION_REMINDER
|
|
202
|
+
].join("\n")
|
|
203
|
+
}), []);
|
|
204
|
+
const processStream = useCallback(async (currentMessages, executionMode = mode) => {
|
|
205
|
+
const assistantMessage = {
|
|
206
|
+
role: ROLE.ASSISTANT,
|
|
207
|
+
content: ""
|
|
208
|
+
};
|
|
209
|
+
setMessages((previousMessages) => [...previousMessages, assistantMessage]);
|
|
210
|
+
try {
|
|
211
|
+
for await (const chunk of streamChat(currentMessages, model, TOOLS)) if (chunk.type === "content") {
|
|
212
|
+
assistantMessage.content += chunk.content;
|
|
213
|
+
setMessages((previousMessages) => {
|
|
214
|
+
const newMessages = [...previousMessages];
|
|
215
|
+
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
216
|
+
return newMessages;
|
|
217
|
+
});
|
|
218
|
+
} else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
|
|
219
|
+
const requiresApproval = DANGEROUS_TOOLS.has(toolCall.function.name);
|
|
220
|
+
// v8 ignore start
|
|
221
|
+
const allowedTools = executionMode === NAME.PLAN ? READ_ONLY_TOOLS : void 0;
|
|
222
|
+
// v8 ignore stop
|
|
223
|
+
if (executionMode === NAME.SAFE && requiresApproval) {
|
|
224
|
+
setPendingToolCall(toolCall);
|
|
225
|
+
setIsLoading(false);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools });
|
|
229
|
+
const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
|
|
230
|
+
const newMessages = [
|
|
231
|
+
...currentMessages,
|
|
232
|
+
assistantMessage,
|
|
233
|
+
toolResultMessage
|
|
234
|
+
];
|
|
235
|
+
setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
|
|
236
|
+
await processStream(newMessages, executionMode);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
241
|
+
setMessages((previousMessages) => {
|
|
242
|
+
const newMessages = [...previousMessages];
|
|
243
|
+
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
244
|
+
return newMessages;
|
|
245
|
+
});
|
|
246
|
+
} finally {
|
|
247
|
+
setIsLoading(false);
|
|
248
|
+
}
|
|
249
|
+
}, [
|
|
250
|
+
buildToolResultMessage,
|
|
251
|
+
model,
|
|
252
|
+
mode
|
|
253
|
+
]);
|
|
254
|
+
const processStreamReadOnly = useCallback(async (currentMessages) => {
|
|
255
|
+
const assistantMessage = {
|
|
256
|
+
role: ROLE.ASSISTANT,
|
|
257
|
+
content: ""
|
|
258
|
+
};
|
|
259
|
+
setMessages((previousMessages) => [...previousMessages, assistantMessage]);
|
|
260
|
+
try {
|
|
261
|
+
const readOnlyTools = TOOLS.filter((tool) => READ_ONLY_TOOLS.has(tool.function.name));
|
|
262
|
+
for await (const chunk of streamChat(currentMessages, model, readOnlyTools)) if (chunk.type === "content") {
|
|
263
|
+
assistantMessage.content += chunk.content;
|
|
264
|
+
setMessages((previousMessages) => {
|
|
265
|
+
const newMessages = [...previousMessages];
|
|
266
|
+
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
267
|
+
return newMessages;
|
|
268
|
+
});
|
|
269
|
+
} else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
|
|
270
|
+
if (!READ_ONLY_TOOLS.has(toolCall.function.name)) {
|
|
271
|
+
const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
|
|
272
|
+
const newMessages = [
|
|
273
|
+
...currentMessages,
|
|
274
|
+
assistantMessage,
|
|
275
|
+
correctionMessage
|
|
276
|
+
];
|
|
277
|
+
setMessages((previousMessages) => [...previousMessages, correctionMessage]);
|
|
278
|
+
await processStreamReadOnly(newMessages);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_ONLY_TOOLS });
|
|
282
|
+
const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
|
|
283
|
+
const newMessages = [
|
|
284
|
+
...currentMessages,
|
|
285
|
+
assistantMessage,
|
|
286
|
+
toolResultMessage
|
|
287
|
+
];
|
|
288
|
+
setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
|
|
289
|
+
await processStreamReadOnly(newMessages);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const planInstruction = {
|
|
293
|
+
role: ROLE.SYSTEM,
|
|
294
|
+
content: PLAN_GENERATION_INSTRUCTION
|
|
295
|
+
};
|
|
296
|
+
const planMessages = [
|
|
297
|
+
...currentMessages,
|
|
298
|
+
assistantMessage,
|
|
299
|
+
planInstruction
|
|
300
|
+
];
|
|
301
|
+
const planAssistantMessage = {
|
|
302
|
+
role: ROLE.ASSISTANT,
|
|
303
|
+
content: ""
|
|
304
|
+
};
|
|
305
|
+
setMessages((previousMessages) => [...previousMessages, planAssistantMessage]);
|
|
306
|
+
for await (const chunk of streamChat(planMessages, model, [])) if (chunk.type === "content") {
|
|
307
|
+
planAssistantMessage.content += chunk.content;
|
|
308
|
+
setMessages((previousMessages) => {
|
|
309
|
+
const newMessages = [...previousMessages];
|
|
310
|
+
newMessages[newMessages.length - 1] = { ...planAssistantMessage };
|
|
311
|
+
return newMessages;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (hasExecutablePlan(planAssistantMessage.content)) setPendingPlan({
|
|
315
|
+
planContent: planAssistantMessage.content,
|
|
316
|
+
messages: [...planMessages, planAssistantMessage]
|
|
317
|
+
});
|
|
318
|
+
setIsLoading(false);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
321
|
+
setMessages((previousMessages) => {
|
|
322
|
+
const newMessages = [...previousMessages];
|
|
323
|
+
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
324
|
+
return newMessages;
|
|
325
|
+
});
|
|
326
|
+
} finally {
|
|
327
|
+
setIsLoading(false);
|
|
328
|
+
}
|
|
329
|
+
}, [
|
|
330
|
+
buildPlanModeCorrectionMessage,
|
|
331
|
+
buildToolResultMessage,
|
|
332
|
+
model
|
|
333
|
+
]);
|
|
334
|
+
const handlePlanApproval = useCallback(async (choice) => {
|
|
335
|
+
// v8 ignore next
|
|
336
|
+
if (!pendingPlan) return;
|
|
337
|
+
const { messages: planMessages } = pendingPlan;
|
|
338
|
+
setPendingPlan(null);
|
|
339
|
+
if (choice === NAME.PLAN) {
|
|
340
|
+
onModeChange(NAME.PLAN);
|
|
341
|
+
const cancelMessage = {
|
|
342
|
+
role: ROLE.SYSTEM,
|
|
343
|
+
content: "Continuing in Plan mode. No tools were executed."
|
|
344
|
+
};
|
|
345
|
+
setMessages((previousMessages) => [...previousMessages, cancelMessage]);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const selectedMode = choice === NAME.AUTO ? NAME.AUTO : NAME.SAFE;
|
|
349
|
+
onModeChange(selectedMode);
|
|
350
|
+
setIsLoading(true);
|
|
351
|
+
const executeInstruction = {
|
|
352
|
+
role: ROLE.SYSTEM,
|
|
353
|
+
content: choice === NAME.AUTO ? "Execute the plan above. Use tools as needed without asking for further confirmation." : "Execute the plan above one step at a time. Wait for user approval before each tool call that modifies files or runs commands."
|
|
354
|
+
};
|
|
355
|
+
await processStream([...planMessages, executeInstruction], selectedMode);
|
|
356
|
+
}, [
|
|
357
|
+
onModeChange,
|
|
358
|
+
pendingPlan,
|
|
359
|
+
processStream
|
|
360
|
+
]);
|
|
361
|
+
const handleToolApproval = useCallback(async (decision) => {
|
|
362
|
+
// v8 ignore next
|
|
363
|
+
if (!pendingToolCall) return;
|
|
364
|
+
const toolCall = pendingToolCall;
|
|
365
|
+
setPendingToolCall(null);
|
|
366
|
+
setIsLoading(true);
|
|
367
|
+
switch (decision) {
|
|
368
|
+
case APPROVE: {
|
|
369
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
|
|
370
|
+
const toolResultMessage = {
|
|
371
|
+
role: ROLE.SYSTEM,
|
|
372
|
+
content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
|
|
373
|
+
};
|
|
374
|
+
const newMessages = [...messages, toolResultMessage];
|
|
375
|
+
setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
|
|
376
|
+
await processStream(newMessages);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case REJECT: {
|
|
380
|
+
const rejectionMessage = {
|
|
381
|
+
role: ROLE.SYSTEM,
|
|
382
|
+
content: `User declined to execute tool ${toolCall.function.name}`
|
|
383
|
+
};
|
|
384
|
+
const newMessages = [...messages, rejectionMessage];
|
|
385
|
+
setMessages((previousMessages) => [...previousMessages, rejectionMessage]);
|
|
386
|
+
await processStream(newMessages);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}, [
|
|
391
|
+
pendingToolCall,
|
|
392
|
+
messages,
|
|
393
|
+
processStream
|
|
394
|
+
]);
|
|
395
|
+
const handleSubmit = useCallback(async (value) => {
|
|
396
|
+
const userContent = value.trim();
|
|
397
|
+
if (!userContent) return;
|
|
398
|
+
if (userContent.startsWith("/")) {
|
|
399
|
+
onCommand(userContent);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
setIsLoading(true);
|
|
403
|
+
const userMessage = {
|
|
404
|
+
role: ROLE.USER,
|
|
405
|
+
content: userContent
|
|
406
|
+
};
|
|
407
|
+
setMessages((previousMessages) => [...previousMessages, userMessage]);
|
|
408
|
+
const updatedMessages = [...messages, userMessage];
|
|
409
|
+
if (mode === NAME.PLAN) await processStreamReadOnly(updatedMessages);
|
|
410
|
+
else await processStream(updatedMessages);
|
|
411
|
+
}, [
|
|
412
|
+
messages,
|
|
413
|
+
onCommand,
|
|
414
|
+
processStream,
|
|
415
|
+
processStreamReadOnly,
|
|
416
|
+
mode
|
|
417
|
+
]);
|
|
418
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
419
|
+
flexDirection: "column",
|
|
420
|
+
children: [
|
|
421
|
+
/* @__PURE__ */ jsx(Messages, {
|
|
422
|
+
messages: messages.slice(1),
|
|
423
|
+
isLoading
|
|
424
|
+
}),
|
|
425
|
+
pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
|
|
426
|
+
planContent: pendingPlan.planContent,
|
|
427
|
+
onModeChange: (selectedMode) => void handlePlanApproval(selectedMode)
|
|
428
|
+
}),
|
|
429
|
+
!pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
|
|
430
|
+
toolCall: pendingToolCall,
|
|
431
|
+
onDecision: handleToolApproval
|
|
432
|
+
}),
|
|
433
|
+
!pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Input, {
|
|
434
|
+
isDisabled: isLoading,
|
|
435
|
+
onSubmit: handleSubmit
|
|
436
|
+
})
|
|
437
|
+
]
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/components/Footer.tsx
|
|
442
|
+
function getModeColor(mode) {
|
|
443
|
+
switch (mode) {
|
|
444
|
+
case NAME.PLAN: return "blue";
|
|
445
|
+
case NAME.AUTO: return "red";
|
|
446
|
+
case NAME.SAFE:
|
|
447
|
+
default: return "green";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function Footer({ mode, onToggleMode }) {
|
|
451
|
+
useInput((_, key) => {
|
|
452
|
+
if (key.tab && key.shift) onToggleMode();
|
|
453
|
+
});
|
|
454
|
+
const modeLabel = LABEL[mode];
|
|
455
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
456
|
+
justifyContent: "space-between",
|
|
457
|
+
marginTop: 1,
|
|
458
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
459
|
+
dimColor: true,
|
|
460
|
+
children: [
|
|
461
|
+
"Mode: ",
|
|
462
|
+
/* @__PURE__ */ jsx(Text, {
|
|
463
|
+
color: getModeColor(mode),
|
|
464
|
+
children: modeLabel
|
|
465
|
+
}),
|
|
466
|
+
/* @__PURE__ */ jsx(Text, {
|
|
467
|
+
dimColor: true,
|
|
468
|
+
children: " (Shift+Tab to toggle)"
|
|
469
|
+
})
|
|
470
|
+
]
|
|
471
|
+
})
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/components/Header.tsx
|
|
476
|
+
function abbreviatePath(dir) {
|
|
477
|
+
const home = homedir();
|
|
478
|
+
return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
|
|
479
|
+
}
|
|
480
|
+
function Header({ model }) {
|
|
481
|
+
const directory = abbreviatePath(process.cwd());
|
|
482
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
483
|
+
borderStyle: "round",
|
|
484
|
+
flexDirection: "column",
|
|
485
|
+
paddingX: 1,
|
|
486
|
+
children: [
|
|
487
|
+
/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
488
|
+
bold: true,
|
|
489
|
+
children: [HEADER_PREFIX, "Code Ollama"]
|
|
490
|
+
}), /* @__PURE__ */ jsxs(Text, {
|
|
491
|
+
dimColor: true,
|
|
492
|
+
children: [
|
|
493
|
+
" (v",
|
|
494
|
+
VERSION,
|
|
495
|
+
")"
|
|
496
|
+
]
|
|
497
|
+
})] }),
|
|
498
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
499
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
500
|
+
/* @__PURE__ */ jsx(Text, {
|
|
501
|
+
dimColor: true,
|
|
502
|
+
children: "model:".padEnd(11)
|
|
503
|
+
}),
|
|
504
|
+
/* @__PURE__ */ jsxs(Text, { children: [model, " "] }),
|
|
505
|
+
/* @__PURE__ */ jsx(Text, {
|
|
506
|
+
color: "cyan",
|
|
507
|
+
children: "/model"
|
|
508
|
+
}),
|
|
509
|
+
/* @__PURE__ */ jsx(Text, {
|
|
510
|
+
dimColor: true,
|
|
511
|
+
children: " to switch"
|
|
512
|
+
})
|
|
513
|
+
] }),
|
|
514
|
+
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
515
|
+
dimColor: true,
|
|
516
|
+
children: "directory:".padEnd(11)
|
|
517
|
+
}), /* @__PURE__ */ jsx(Text, { children: directory })] })
|
|
518
|
+
]
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/components/ModelPicker.tsx
|
|
523
|
+
function ModelPicker({ currentModel, onSelect, onClose }) {
|
|
524
|
+
const [options, setOptions] = useState([]);
|
|
525
|
+
const [error, setError] = useState(null);
|
|
526
|
+
useInput((_, key) => {
|
|
527
|
+
if (options.length && key.return) setTimeout(onClose);
|
|
528
|
+
});
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
async function load() {
|
|
531
|
+
try {
|
|
532
|
+
const models = await listModels();
|
|
533
|
+
if (models.includes(currentModel)) {
|
|
534
|
+
models.splice(models.indexOf(currentModel), 1);
|
|
535
|
+
models.unshift(currentModel);
|
|
536
|
+
}
|
|
537
|
+
setOptions(models.map((model) => ({
|
|
538
|
+
label: model,
|
|
539
|
+
value: model
|
|
540
|
+
})));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
load();
|
|
546
|
+
}, [currentModel]);
|
|
547
|
+
if (error) return /* @__PURE__ */ jsxs(Text, {
|
|
548
|
+
color: "red",
|
|
549
|
+
children: ["Error loading models: ", error]
|
|
550
|
+
});
|
|
551
|
+
if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
|
|
552
|
+
return /* @__PURE__ */ jsx(SelectPrompt, {
|
|
553
|
+
options,
|
|
554
|
+
defaultValue: currentModel,
|
|
555
|
+
onChange: onSelect,
|
|
556
|
+
onEscape: onClose,
|
|
557
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
558
|
+
dimColor: true,
|
|
559
|
+
children: "Select a model (↑↓ + Enter to confirm, Esc to cancel)"
|
|
560
|
+
})
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/components/App.tsx
|
|
565
|
+
function App() {
|
|
566
|
+
const [model, setModel] = useState(() => loadConfig().model);
|
|
567
|
+
const [picking, setPicking] = useState(false);
|
|
568
|
+
const [mode, setMode] = useState(NAME.SAFE);
|
|
569
|
+
const handleCommand = useCallback((command) => {
|
|
570
|
+
if (command === "/model") setPicking(true);
|
|
571
|
+
}, []);
|
|
572
|
+
const handleSelect = useCallback((selected) => {
|
|
573
|
+
setModel(selected);
|
|
574
|
+
saveConfig({ model: selected });
|
|
575
|
+
setPicking(false);
|
|
576
|
+
}, []);
|
|
577
|
+
const handleClose = useCallback(() => {
|
|
578
|
+
setPicking(false);
|
|
579
|
+
}, []);
|
|
580
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
581
|
+
flexDirection: "column",
|
|
582
|
+
children: [
|
|
583
|
+
/* @__PURE__ */ jsx(Header, { model }),
|
|
584
|
+
picking ? /* @__PURE__ */ jsx(ModelPicker, {
|
|
585
|
+
currentModel: model,
|
|
586
|
+
onSelect: handleSelect,
|
|
587
|
+
onClose: handleClose
|
|
588
|
+
}) : /* @__PURE__ */ jsx(Chat, {
|
|
589
|
+
model,
|
|
590
|
+
onCommand: handleCommand,
|
|
591
|
+
mode,
|
|
592
|
+
onModeChange: setMode
|
|
593
|
+
}),
|
|
594
|
+
/* @__PURE__ */ jsx(Footer, {
|
|
595
|
+
mode,
|
|
596
|
+
onToggleMode: () => {
|
|
597
|
+
setMode((mode) => {
|
|
598
|
+
switch (mode) {
|
|
599
|
+
case NAME.SAFE: return NAME.AUTO;
|
|
600
|
+
case NAME.AUTO: return NAME.PLAN;
|
|
601
|
+
case NAME.PLAN:
|
|
602
|
+
default: return NAME.SAFE;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
]
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/tui.tsx
|
|
612
|
+
function renderApp() {
|
|
613
|
+
render(/* @__PURE__ */ jsx(App, {}));
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
export { renderApp };
|
package/dist/cli.js
CHANGED
|
@@ -11,8 +11,24 @@ var NAMES = [{
|
|
|
11
11
|
description: "switch the model"
|
|
12
12
|
}].map(({ name }) => name);
|
|
13
13
|
//#endregion
|
|
14
|
+
//#region src/constants/decision.ts
|
|
15
|
+
var APPROVE = "approve";
|
|
16
|
+
var REJECT = "reject";
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/constants/mode.ts
|
|
19
|
+
var NAME$1 = {
|
|
20
|
+
SAFE: "safe",
|
|
21
|
+
AUTO: "auto",
|
|
22
|
+
PLAN: "plan"
|
|
23
|
+
};
|
|
24
|
+
var LABEL = {
|
|
25
|
+
safe: "Safe",
|
|
26
|
+
auto: "Auto",
|
|
27
|
+
plan: "Plan"
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
14
30
|
//#region src/constants/package.ts
|
|
15
|
-
var VERSION = "0.
|
|
31
|
+
var VERSION = "0.3.1";
|
|
16
32
|
//#endregion
|
|
17
33
|
//#region src/constants/prompt.ts
|
|
18
34
|
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
|
|
@@ -39,6 +55,19 @@ Always use tools when you need to:
|
|
|
39
55
|
- Make file changes
|
|
40
56
|
- Explore project structure
|
|
41
57
|
- Search the codebase`;
|
|
58
|
+
var PLAN_GENERATION_INSTRUCTION = `Based on the research above, decide whether the user request needs code or shell execution
|
|
59
|
+
|
|
60
|
+
If the request needs changes or commands, respond with a plan checklist only
|
|
61
|
+
Do not execute any tools
|
|
62
|
+
Do not claim any action was performed
|
|
63
|
+
Display the plan as an unchecked Markdown checklist using only these forms:
|
|
64
|
+
|
|
65
|
+
- [ ] write_file("path/to/file", "content") - Brief description
|
|
66
|
+
- [ ] edit_file("path/to/file", "oldText", "newText") - Brief description
|
|
67
|
+
- [ ] run_shell("command") - Brief description
|
|
68
|
+
|
|
69
|
+
Only include write_file, edit_file, and run_shell tools in the checklist
|
|
70
|
+
If no execution is needed, answer normally`;
|
|
42
71
|
//#endregion
|
|
43
72
|
//#region src/constants/role.ts
|
|
44
73
|
var ROLE = {
|
|
@@ -241,7 +270,13 @@ var TOOLS = [
|
|
|
241
270
|
"end"
|
|
242
271
|
])
|
|
243
272
|
];
|
|
244
|
-
var
|
|
273
|
+
var READ_ONLY_TOOLS = new Set([
|
|
274
|
+
NAME.READ_FILE,
|
|
275
|
+
NAME.LIST_DIR,
|
|
276
|
+
NAME.GREP_SEARCH,
|
|
277
|
+
NAME.VIEW_RANGE
|
|
278
|
+
]);
|
|
279
|
+
var DANGEROUS_TOOLS = new Set([
|
|
245
280
|
NAME.WRITE_FILE,
|
|
246
281
|
NAME.EDIT_FILE,
|
|
247
282
|
NAME.RUN_SHELL
|
|
@@ -249,7 +284,11 @@ var TOOLS_REQUIRING_APPROVAL = new Set([
|
|
|
249
284
|
/**
|
|
250
285
|
* Execute a tool by name with arguments
|
|
251
286
|
*/
|
|
252
|
-
async function executeTool(name, args) {
|
|
287
|
+
async function executeTool(name, args, options) {
|
|
288
|
+
if (options?.allowedTools && !options.allowedTools.has(name)) return {
|
|
289
|
+
content: "",
|
|
290
|
+
error: `Tool not allowed: ${name}`
|
|
291
|
+
};
|
|
253
292
|
switch (name) {
|
|
254
293
|
case NAME.READ_FILE: return readFile(args.path);
|
|
255
294
|
case NAME.WRITE_FILE: return writeFile(args.path, args.content);
|
|
@@ -480,7 +519,7 @@ async function processRunStream(messages, model) {
|
|
|
480
519
|
}
|
|
481
520
|
async function main(args = process.argv.slice(2)) {
|
|
482
521
|
if (!args.length) {
|
|
483
|
-
const { renderApp } = await import("./assets/tui-
|
|
522
|
+
const { renderApp } = await import("./assets/tui-CVsodXv3.js");
|
|
484
523
|
clear();
|
|
485
524
|
renderApp();
|
|
486
525
|
return;
|
|
@@ -503,4 +542,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
|
|
|
503
542
|
if (isEntrypoint()) main();
|
|
504
543
|
/* v8 ignore stop */
|
|
505
544
|
//#endregion
|
|
506
|
-
export {
|
|
545
|
+
export { REJECT as _, listModels as a, saveConfig as c, ROLE as d, PLAN_GENERATION_INSTRUCTION as f, APPROVE as g, NAME$1 as h, executeTool as i, createSystemMessage as l, LABEL as m, main, READ_ONLY_TOOLS as n, streamChat as o, VERSION as p, TOOLS as r, loadConfig as s, DANGEROUS_TOOLS as t, HEADER_PREFIX as u, NAMES as v };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-ollama",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"cac": "7.0.0",
|
|
44
44
|
"ink": "7.0.2",
|
|
45
45
|
"ollama": "0.6.3",
|
|
46
|
-
"react": "19.2.
|
|
46
|
+
"react": "19.2.6"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@commitlint/cli": "20.5.3",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"globals": "17.6.0",
|
|
60
60
|
"husky": "9.1.7",
|
|
61
61
|
"ink-testing-library": "4.0.0",
|
|
62
|
-
"lint-staged": "
|
|
62
|
+
"lint-staged": "17.0.2",
|
|
63
63
|
"prettier": "3.8.3",
|
|
64
64
|
"publint": "0.3.19",
|
|
65
65
|
"tsx": "4.21.0",
|
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
import { a as streamChat, c as createSystemMessage, d as VERSION, f as NAMES, i as listModels, l as HEADER_PREFIX, n as TOOLS_REQUIRING_APPROVAL, o as loadConfig, r as executeTool, s as saveConfig, t as TOOLS, u as ROLE } from "../cli.js";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { Box, Text, render, useInput } from "ink";
|
|
4
|
-
import { useCallback, useEffect, useState } from "react";
|
|
5
|
-
import { Select, Spinner, TextInput } from "@inkjs/ui";
|
|
6
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
//#region src/components/ChatInput.tsx
|
|
8
|
-
function ChatInput({ isDisabled = false, onSubmit }) {
|
|
9
|
-
const [resetKey, setResetKey] = useState(0);
|
|
10
|
-
const handleSubmit = useCallback((input) => {
|
|
11
|
-
const trimmed = input.trim();
|
|
12
|
-
if (!trimmed) return;
|
|
13
|
-
onSubmit(trimmed);
|
|
14
|
-
setResetKey((key) => key + 1);
|
|
15
|
-
}, [onSubmit]);
|
|
16
|
-
return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
|
|
17
|
-
isDisabled,
|
|
18
|
-
suggestions: NAMES,
|
|
19
|
-
onSubmit: handleSubmit
|
|
20
|
-
}, resetKey)] });
|
|
21
|
-
}
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/components/Messages.tsx
|
|
24
|
-
function getMessageColor(role) {
|
|
25
|
-
switch (role) {
|
|
26
|
-
case ROLE.USER: return "black";
|
|
27
|
-
case ROLE.ASSISTANT: return "blue";
|
|
28
|
-
case ROLE.SYSTEM: return "gray";
|
|
29
|
-
default: return;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function Messages({ messages, isLoading }) {
|
|
33
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
34
|
-
flexDirection: "column",
|
|
35
|
-
children: [messages.map((message, index) => /* @__PURE__ */ jsx(Box, {
|
|
36
|
-
marginBottom: 1,
|
|
37
|
-
children: /* @__PURE__ */ jsxs(Text, {
|
|
38
|
-
color: getMessageColor(message.role),
|
|
39
|
-
dimColor: message.role === ROLE.SYSTEM,
|
|
40
|
-
children: [message.role === ROLE.USER ? "> " : "", message.content]
|
|
41
|
-
})
|
|
42
|
-
}, index)), isLoading && messages[messages.length - 1]?.content === "" && /* @__PURE__ */ jsx(Box, {
|
|
43
|
-
marginTop: -1,
|
|
44
|
-
marginBottom: 1,
|
|
45
|
-
children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
|
|
46
|
-
})]
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region src/components/ToolApproval.tsx
|
|
51
|
-
function ToolApproval({ toolCall, onApprove, onReject }) {
|
|
52
|
-
const [selected, setSelected] = useState("yes");
|
|
53
|
-
useInput((_, key) => {
|
|
54
|
-
if (key.return) if (selected === "yes") onApprove();
|
|
55
|
-
else onReject();
|
|
56
|
-
else if (key.leftArrow || key.rightArrow) setSelected((prev) => prev === "yes" ? "no" : "yes");
|
|
57
|
-
// v8 ignore stop
|
|
58
|
-
});
|
|
59
|
-
const args = JSON.stringify(toolCall.function.arguments, null, 2);
|
|
60
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
61
|
-
flexDirection: "column",
|
|
62
|
-
marginY: 1,
|
|
63
|
-
children: [
|
|
64
|
-
/* @__PURE__ */ jsx(Text, {
|
|
65
|
-
color: "yellow",
|
|
66
|
-
bold: true,
|
|
67
|
-
children: "⚠️ Tool requires approval:"
|
|
68
|
-
}),
|
|
69
|
-
/* @__PURE__ */ jsxs(Box, {
|
|
70
|
-
marginX: 2,
|
|
71
|
-
flexDirection: "column",
|
|
72
|
-
children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
73
|
-
/* @__PURE__ */ jsx(Text, {
|
|
74
|
-
bold: true,
|
|
75
|
-
children: "Tool:"
|
|
76
|
-
}),
|
|
77
|
-
" ",
|
|
78
|
-
toolCall.function.name
|
|
79
|
-
] }), /* @__PURE__ */ jsxs(Text, { children: [
|
|
80
|
-
/* @__PURE__ */ jsx(Text, {
|
|
81
|
-
bold: true,
|
|
82
|
-
children: "Arguments:"
|
|
83
|
-
}),
|
|
84
|
-
" ",
|
|
85
|
-
args
|
|
86
|
-
] })]
|
|
87
|
-
}),
|
|
88
|
-
/* @__PURE__ */ jsxs(Box, {
|
|
89
|
-
marginTop: 1,
|
|
90
|
-
gap: 2,
|
|
91
|
-
children: [/* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, {
|
|
92
|
-
color: selected === "yes" ? "green" : void 0,
|
|
93
|
-
children: [selected === "yes" ? "▶ " : " ", "✓ Yes (Enter)"]
|
|
94
|
-
}) }), /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, {
|
|
95
|
-
color: selected === "no" ? "red" : void 0,
|
|
96
|
-
children: [selected === "no" ? "▶ " : " ", "✗ No (Esc)"]
|
|
97
|
-
}) })]
|
|
98
|
-
})
|
|
99
|
-
]
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
//#endregion
|
|
103
|
-
//#region src/components/Chat.tsx
|
|
104
|
-
function Chat({ model, onCommand, autoExecute }) {
|
|
105
|
-
const [messages, setMessages] = useState([createSystemMessage()]);
|
|
106
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
107
|
-
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
108
|
-
const processStream = useCallback(async (currentMessages) => {
|
|
109
|
-
const assistantMessage = {
|
|
110
|
-
role: ROLE.ASSISTANT,
|
|
111
|
-
content: ""
|
|
112
|
-
};
|
|
113
|
-
setMessages((previousMessages) => [...previousMessages, assistantMessage]);
|
|
114
|
-
try {
|
|
115
|
-
for await (const chunk of streamChat(currentMessages, model, TOOLS)) if (chunk.type === "content") {
|
|
116
|
-
assistantMessage.content += chunk.content;
|
|
117
|
-
setMessages((previousMessages) => {
|
|
118
|
-
const newMessages = [...previousMessages];
|
|
119
|
-
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
120
|
-
return newMessages;
|
|
121
|
-
});
|
|
122
|
-
} else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
|
|
123
|
-
const requiresApproval = TOOLS_REQUIRING_APPROVAL.has(toolCall.function.name);
|
|
124
|
-
if (!autoExecute && requiresApproval) {
|
|
125
|
-
setPendingToolCall(toolCall);
|
|
126
|
-
setIsLoading(false);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
|
|
130
|
-
const toolResultMessage = {
|
|
131
|
-
role: ROLE.SYSTEM,
|
|
132
|
-
content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
|
|
133
|
-
};
|
|
134
|
-
const newMessages = [
|
|
135
|
-
...currentMessages,
|
|
136
|
-
assistantMessage,
|
|
137
|
-
toolResultMessage
|
|
138
|
-
];
|
|
139
|
-
setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
|
|
140
|
-
await processStream(newMessages);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
145
|
-
setMessages((previousMessages) => {
|
|
146
|
-
const newMessages = [...previousMessages];
|
|
147
|
-
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
148
|
-
return newMessages;
|
|
149
|
-
});
|
|
150
|
-
} finally {
|
|
151
|
-
setIsLoading(false);
|
|
152
|
-
}
|
|
153
|
-
}, [model, autoExecute]);
|
|
154
|
-
const handleToolApproval = useCallback(async (approved) => {
|
|
155
|
-
// v8 ignore next
|
|
156
|
-
if (!pendingToolCall) return;
|
|
157
|
-
const toolCall = pendingToolCall;
|
|
158
|
-
setPendingToolCall(null);
|
|
159
|
-
setIsLoading(true);
|
|
160
|
-
if (approved) {
|
|
161
|
-
const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
|
|
162
|
-
const toolResultMessage = {
|
|
163
|
-
role: ROLE.SYSTEM,
|
|
164
|
-
content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
|
|
165
|
-
};
|
|
166
|
-
const newMessages = [...messages, toolResultMessage];
|
|
167
|
-
setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
|
|
168
|
-
await processStream(newMessages);
|
|
169
|
-
} else {
|
|
170
|
-
const rejectionMessage = {
|
|
171
|
-
role: ROLE.SYSTEM,
|
|
172
|
-
content: `User declined to execute tool ${toolCall.function.name}`
|
|
173
|
-
};
|
|
174
|
-
const newMessages = [...messages, rejectionMessage];
|
|
175
|
-
setMessages((previousMessages) => [...previousMessages, rejectionMessage]);
|
|
176
|
-
await processStream(newMessages);
|
|
177
|
-
}
|
|
178
|
-
}, [
|
|
179
|
-
pendingToolCall,
|
|
180
|
-
messages,
|
|
181
|
-
processStream
|
|
182
|
-
]);
|
|
183
|
-
const handleSubmit = useCallback(async (value) => {
|
|
184
|
-
const userContent = value.trim();
|
|
185
|
-
if (!userContent) return;
|
|
186
|
-
if (userContent.startsWith("/")) {
|
|
187
|
-
onCommand(userContent);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
setIsLoading(true);
|
|
191
|
-
const userMessage = {
|
|
192
|
-
role: ROLE.USER,
|
|
193
|
-
content: userContent
|
|
194
|
-
};
|
|
195
|
-
setMessages((previousMessages) => [...previousMessages, userMessage]);
|
|
196
|
-
await processStream([...messages, userMessage]);
|
|
197
|
-
}, [
|
|
198
|
-
messages,
|
|
199
|
-
onCommand,
|
|
200
|
-
processStream
|
|
201
|
-
]);
|
|
202
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
203
|
-
flexDirection: "column",
|
|
204
|
-
children: [
|
|
205
|
-
/* @__PURE__ */ jsx(Messages, {
|
|
206
|
-
messages: messages.slice(1),
|
|
207
|
-
isLoading
|
|
208
|
-
}),
|
|
209
|
-
pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
|
|
210
|
-
toolCall: pendingToolCall,
|
|
211
|
-
onApprove: () => void handleToolApproval(true),
|
|
212
|
-
onReject: () => void handleToolApproval(false)
|
|
213
|
-
}),
|
|
214
|
-
!pendingToolCall && /* @__PURE__ */ jsx(ChatInput, {
|
|
215
|
-
isDisabled: isLoading,
|
|
216
|
-
onSubmit: handleSubmit
|
|
217
|
-
})
|
|
218
|
-
]
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
//#endregion
|
|
222
|
-
//#region src/components/Footer.tsx
|
|
223
|
-
function Footer({ autoExecute, onToggleMode }) {
|
|
224
|
-
useInput((_, key) => {
|
|
225
|
-
if (key.tab && key.shift) onToggleMode();
|
|
226
|
-
});
|
|
227
|
-
return /* @__PURE__ */ jsx(Box, {
|
|
228
|
-
justifyContent: "space-between",
|
|
229
|
-
marginTop: 1,
|
|
230
|
-
children: /* @__PURE__ */ jsxs(Text, {
|
|
231
|
-
dimColor: true,
|
|
232
|
-
children: [
|
|
233
|
-
"Mode: ",
|
|
234
|
-
autoExecute ? "Auto" : "Safe",
|
|
235
|
-
" (Shift+Tab to toggle)"
|
|
236
|
-
]
|
|
237
|
-
})
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
//#endregion
|
|
241
|
-
//#region src/components/Header.tsx
|
|
242
|
-
function abbreviatePath(dir) {
|
|
243
|
-
const home = homedir();
|
|
244
|
-
return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
|
|
245
|
-
}
|
|
246
|
-
function Header({ model }) {
|
|
247
|
-
const directory = abbreviatePath(process.cwd());
|
|
248
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
249
|
-
borderStyle: "round",
|
|
250
|
-
flexDirection: "column",
|
|
251
|
-
paddingX: 1,
|
|
252
|
-
children: [
|
|
253
|
-
/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
254
|
-
bold: true,
|
|
255
|
-
children: [HEADER_PREFIX, "Code Ollama"]
|
|
256
|
-
}), /* @__PURE__ */ jsxs(Text, {
|
|
257
|
-
dimColor: true,
|
|
258
|
-
children: [
|
|
259
|
-
" (v",
|
|
260
|
-
VERSION,
|
|
261
|
-
")"
|
|
262
|
-
]
|
|
263
|
-
})] }),
|
|
264
|
-
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
265
|
-
/* @__PURE__ */ jsxs(Box, { children: [
|
|
266
|
-
/* @__PURE__ */ jsx(Text, {
|
|
267
|
-
dimColor: true,
|
|
268
|
-
children: "model:".padEnd(11)
|
|
269
|
-
}),
|
|
270
|
-
/* @__PURE__ */ jsxs(Text, { children: [model, " "] }),
|
|
271
|
-
/* @__PURE__ */ jsx(Text, {
|
|
272
|
-
color: "cyan",
|
|
273
|
-
children: "/model"
|
|
274
|
-
}),
|
|
275
|
-
/* @__PURE__ */ jsx(Text, {
|
|
276
|
-
dimColor: true,
|
|
277
|
-
children: " to switch"
|
|
278
|
-
})
|
|
279
|
-
] }),
|
|
280
|
-
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
281
|
-
dimColor: true,
|
|
282
|
-
children: "directory:".padEnd(11)
|
|
283
|
-
}), /* @__PURE__ */ jsx(Text, { children: directory })] })
|
|
284
|
-
]
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
//#endregion
|
|
288
|
-
//#region src/components/ModelPicker.tsx
|
|
289
|
-
function ModelPicker({ currentModel, onSelect, onCancel }) {
|
|
290
|
-
const [options, setOptions] = useState([]);
|
|
291
|
-
const [error, setError] = useState(null);
|
|
292
|
-
useEffect(() => {
|
|
293
|
-
async function load() {
|
|
294
|
-
try {
|
|
295
|
-
setOptions((await listModels()).map((name) => ({
|
|
296
|
-
label: name,
|
|
297
|
-
value: name
|
|
298
|
-
})));
|
|
299
|
-
} catch (err) {
|
|
300
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
load();
|
|
304
|
-
}, []);
|
|
305
|
-
useInput((_, key) => {
|
|
306
|
-
if (key.escape) onCancel();
|
|
307
|
-
});
|
|
308
|
-
if (error) return /* @__PURE__ */ jsxs(Text, {
|
|
309
|
-
color: "red",
|
|
310
|
-
children: ["Error loading models: ", error]
|
|
311
|
-
});
|
|
312
|
-
if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
|
|
313
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
314
|
-
flexDirection: "column",
|
|
315
|
-
children: [/* @__PURE__ */ jsx(Text, {
|
|
316
|
-
dimColor: true,
|
|
317
|
-
children: "Select a model (↑↓ + Enter to confirm, Esc to cancel)"
|
|
318
|
-
}), /* @__PURE__ */ jsx(Select, {
|
|
319
|
-
options,
|
|
320
|
-
defaultValue: currentModel,
|
|
321
|
-
onChange: onSelect
|
|
322
|
-
})]
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
//#endregion
|
|
326
|
-
//#region src/components/App.tsx
|
|
327
|
-
function App() {
|
|
328
|
-
const [model, setModel] = useState(() => loadConfig().model);
|
|
329
|
-
const [picking, setPicking] = useState(false);
|
|
330
|
-
const [autoExecute, setAutoExecute] = useState(false);
|
|
331
|
-
const handleCommand = useCallback((command) => {
|
|
332
|
-
if (command === "/model") setPicking(true);
|
|
333
|
-
}, []);
|
|
334
|
-
const handleSelect = useCallback((selected) => {
|
|
335
|
-
setModel(selected);
|
|
336
|
-
saveConfig({ model: selected });
|
|
337
|
-
setPicking(false);
|
|
338
|
-
}, []);
|
|
339
|
-
const handleCancel = useCallback(() => {
|
|
340
|
-
setPicking(false);
|
|
341
|
-
}, []);
|
|
342
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
343
|
-
flexDirection: "column",
|
|
344
|
-
children: [
|
|
345
|
-
/* @__PURE__ */ jsx(Header, { model }),
|
|
346
|
-
picking ? /* @__PURE__ */ jsx(ModelPicker, {
|
|
347
|
-
currentModel: model,
|
|
348
|
-
onSelect: handleSelect,
|
|
349
|
-
onCancel: handleCancel
|
|
350
|
-
}) : /* @__PURE__ */ jsx(Chat, {
|
|
351
|
-
model,
|
|
352
|
-
onCommand: handleCommand,
|
|
353
|
-
autoExecute
|
|
354
|
-
}),
|
|
355
|
-
/* @__PURE__ */ jsx(Footer, {
|
|
356
|
-
autoExecute,
|
|
357
|
-
onToggleMode: () => {
|
|
358
|
-
setAutoExecute((isAutoExecute) => !isAutoExecute);
|
|
359
|
-
}
|
|
360
|
-
})
|
|
361
|
-
]
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
//#endregion
|
|
365
|
-
//#region src/tui.tsx
|
|
366
|
-
function renderApp() {
|
|
367
|
-
render(/* @__PURE__ */ jsx(App, {}));
|
|
368
|
-
}
|
|
369
|
-
//#endregion
|
|
370
|
-
export { renderApp };
|