code-ollama 0.1.0 → 0.2.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.
@@ -0,0 +1,438 @@
1
+ import { a as streamChat, c as createSystemMessage, i as listModels, l as ROLE, n as TOOLS_REQUIRING_APPROVAL, o as loadConfig, r as executeTool, s as saveConfig, t as TOOLS, u as VERSION } 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/constants/commands.ts
8
+ var COMMANDS = [{
9
+ name: "/model",
10
+ description: "switch the model"
11
+ }];
12
+ //#endregion
13
+ //#region src/constants/ui.ts
14
+ var HEADER_PREFIX = "🦙";
15
+ //#endregion
16
+ //#region src/components/Autocomplete.tsx
17
+ function getMatches(input) {
18
+ if (!input.startsWith("/")) return [];
19
+ return COMMANDS.filter((command) => command.name.startsWith(input));
20
+ }
21
+ function Autocomplete({ isDisabled = false, onSubmit }) {
22
+ const [value, setValue] = useState("");
23
+ const [selectedIndex, setSelectedIndex] = useState(0);
24
+ const [inputKey, setInputKey] = useState(0);
25
+ const matches = getMatches(value);
26
+ const isCommandMode = value.startsWith("/");
27
+ useInput((_char, key) => {
28
+ // v8 ignore next
29
+ if (!isCommandMode) return;
30
+ if (key.upArrow) {
31
+ setSelectedIndex((i) => Math.max(0, i - 1));
32
+ return;
33
+ }
34
+ if (key.downArrow) {
35
+ setSelectedIndex((i) => Math.min(matches.length - 1, i + 1));
36
+ return;
37
+ }
38
+ if (key.tab && matches.length > 0) {
39
+ setValue((matches[selectedIndex] ?? matches[0]).name);
40
+ setSelectedIndex(0);
41
+ setInputKey((key) => key + 1);
42
+ return;
43
+ }
44
+ }, { isActive: !isDisabled && isCommandMode });
45
+ const handleSubmit = useCallback((input) => {
46
+ const trimmed = (isCommandMode && matches.length > 0 && matches[selectedIndex] ? matches[selectedIndex].name : input).trim();
47
+ if (trimmed) {
48
+ onSubmit(trimmed);
49
+ setValue("");
50
+ setSelectedIndex(0);
51
+ setInputKey((key) => key + 1);
52
+ }
53
+ }, [
54
+ isCommandMode,
55
+ matches,
56
+ onSubmit,
57
+ selectedIndex
58
+ ]);
59
+ return /* @__PURE__ */ jsxs(Box, {
60
+ flexDirection: "column",
61
+ children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
62
+ isDisabled,
63
+ defaultValue: value,
64
+ onChange: setValue,
65
+ onSubmit: handleSubmit
66
+ }, inputKey)] }), isCommandMode && matches.length > 0 && /* @__PURE__ */ jsx(Box, {
67
+ flexDirection: "column",
68
+ marginLeft: 2,
69
+ children: matches.map((command, index) => {
70
+ const isHighlighted = index === selectedIndex;
71
+ return /* @__PURE__ */ jsxs(Box, {
72
+ gap: 3,
73
+ children: [/* @__PURE__ */ jsx(Text, {
74
+ color: isHighlighted ? "cyan" : void 0,
75
+ bold: isHighlighted,
76
+ children: command.name
77
+ }), /* @__PURE__ */ jsx(Text, {
78
+ dimColor: true,
79
+ children: command.description
80
+ })]
81
+ }, command.name);
82
+ })
83
+ })]
84
+ });
85
+ }
86
+ //#endregion
87
+ //#region src/components/Messages.tsx
88
+ function getMessageColor(role) {
89
+ switch (role) {
90
+ case ROLE.USER: return "black";
91
+ case ROLE.ASSISTANT: return "blue";
92
+ case ROLE.SYSTEM: return "gray";
93
+ default: return;
94
+ }
95
+ }
96
+ function Messages({ messages, isLoading }) {
97
+ return /* @__PURE__ */ jsxs(Box, {
98
+ flexDirection: "column",
99
+ children: [messages.map((message, index) => /* @__PURE__ */ jsx(Box, {
100
+ marginBottom: 1,
101
+ children: /* @__PURE__ */ jsxs(Text, {
102
+ color: getMessageColor(message.role),
103
+ dimColor: message.role === ROLE.SYSTEM,
104
+ children: [message.role === ROLE.USER ? "> " : "", message.content]
105
+ })
106
+ }, index)), isLoading && messages[messages.length - 1]?.content === "" && /* @__PURE__ */ jsx(Box, {
107
+ marginTop: -1,
108
+ marginBottom: 1,
109
+ children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
110
+ })]
111
+ });
112
+ }
113
+ //#endregion
114
+ //#region src/components/ToolApproval.tsx
115
+ function ToolApproval({ toolCall, onApprove, onReject }) {
116
+ const [selected, setSelected] = useState("yes");
117
+ useInput((_, key) => {
118
+ if (key.return) if (selected === "yes") onApprove();
119
+ else onReject();
120
+ else if (key.leftArrow || key.rightArrow) setSelected((prev) => prev === "yes" ? "no" : "yes");
121
+ // v8 ignore stop
122
+ });
123
+ const args = JSON.stringify(toolCall.function.arguments, null, 2);
124
+ return /* @__PURE__ */ jsxs(Box, {
125
+ flexDirection: "column",
126
+ marginY: 1,
127
+ children: [
128
+ /* @__PURE__ */ jsx(Text, {
129
+ color: "yellow",
130
+ bold: true,
131
+ children: "⚠️ Tool requires approval:"
132
+ }),
133
+ /* @__PURE__ */ jsxs(Box, {
134
+ marginX: 2,
135
+ flexDirection: "column",
136
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
137
+ /* @__PURE__ */ jsx(Text, {
138
+ bold: true,
139
+ children: "Tool:"
140
+ }),
141
+ " ",
142
+ toolCall.function.name
143
+ ] }), /* @__PURE__ */ jsxs(Text, { children: [
144
+ /* @__PURE__ */ jsx(Text, {
145
+ bold: true,
146
+ children: "Arguments:"
147
+ }),
148
+ " ",
149
+ args
150
+ ] })]
151
+ }),
152
+ /* @__PURE__ */ jsxs(Box, {
153
+ marginTop: 1,
154
+ gap: 2,
155
+ children: [/* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, {
156
+ color: selected === "yes" ? "green" : void 0,
157
+ children: [selected === "yes" ? "▶ " : " ", "✓ Yes (Enter)"]
158
+ }) }), /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, {
159
+ color: selected === "no" ? "red" : void 0,
160
+ children: [selected === "no" ? "▶ " : " ", "✗ No (Esc)"]
161
+ }) })]
162
+ })
163
+ ]
164
+ });
165
+ }
166
+ //#endregion
167
+ //#region src/components/Chat.tsx
168
+ function Chat({ model, onCommand, autoExecute }) {
169
+ const [messages, setMessages] = useState([createSystemMessage()]);
170
+ const [submitKey, setSubmitKey] = useState(0);
171
+ const [isLoading, setIsLoading] = useState(false);
172
+ const [pendingToolCall, setPendingToolCall] = useState(null);
173
+ const processStream = useCallback(async (currentMessages) => {
174
+ const assistantMessage = {
175
+ role: ROLE.ASSISTANT,
176
+ content: ""
177
+ };
178
+ setMessages((previousMessages) => [...previousMessages, assistantMessage]);
179
+ try {
180
+ for await (const chunk of streamChat(currentMessages, model, TOOLS)) if (chunk.type === "content") {
181
+ assistantMessage.content += chunk.content;
182
+ setMessages((previousMessages) => {
183
+ const newMessages = [...previousMessages];
184
+ newMessages[newMessages.length - 1] = { ...assistantMessage };
185
+ return newMessages;
186
+ });
187
+ } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
188
+ const requiresApproval = TOOLS_REQUIRING_APPROVAL.has(toolCall.function.name);
189
+ if (!autoExecute && requiresApproval) {
190
+ setPendingToolCall(toolCall);
191
+ setIsLoading(false);
192
+ return;
193
+ }
194
+ const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
195
+ const toolResultMessage = {
196
+ role: ROLE.SYSTEM,
197
+ content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
198
+ };
199
+ const newMessages = [
200
+ ...currentMessages,
201
+ assistantMessage,
202
+ toolResultMessage
203
+ ];
204
+ setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
205
+ await processStream(newMessages);
206
+ return;
207
+ }
208
+ } catch (error) {
209
+ assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
210
+ setMessages((previousMessages) => {
211
+ const newMessages = [...previousMessages];
212
+ newMessages[newMessages.length - 1] = { ...assistantMessage };
213
+ return newMessages;
214
+ });
215
+ } finally {
216
+ setIsLoading(false);
217
+ }
218
+ }, [model, autoExecute]);
219
+ const handleToolApproval = useCallback(async (approved) => {
220
+ // v8 ignore next
221
+ if (!pendingToolCall) return;
222
+ const toolCall = pendingToolCall;
223
+ setPendingToolCall(null);
224
+ setIsLoading(true);
225
+ if (approved) {
226
+ const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
227
+ const toolResultMessage = {
228
+ role: ROLE.SYSTEM,
229
+ content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
230
+ };
231
+ const newMessages = [...messages, toolResultMessage];
232
+ setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
233
+ await processStream(newMessages);
234
+ } else {
235
+ const rejectionMessage = {
236
+ role: ROLE.SYSTEM,
237
+ content: `User declined to execute tool ${toolCall.function.name}`
238
+ };
239
+ const newMessages = [...messages, rejectionMessage];
240
+ setMessages((previousMessages) => [...previousMessages, rejectionMessage]);
241
+ await processStream(newMessages);
242
+ }
243
+ }, [
244
+ pendingToolCall,
245
+ messages,
246
+ processStream
247
+ ]);
248
+ const handleSubmit = useCallback(async (value) => {
249
+ const userContent = value.trim();
250
+ if (!userContent) return;
251
+ setSubmitKey((key) => key + 1);
252
+ if (userContent.startsWith("/")) {
253
+ onCommand(userContent);
254
+ return;
255
+ }
256
+ setIsLoading(true);
257
+ const userMessage = {
258
+ role: ROLE.USER,
259
+ content: userContent
260
+ };
261
+ setMessages((previousMessages) => [...previousMessages, userMessage]);
262
+ await processStream([...messages, userMessage]);
263
+ }, [
264
+ messages,
265
+ onCommand,
266
+ processStream
267
+ ]);
268
+ return /* @__PURE__ */ jsxs(Box, {
269
+ flexDirection: "column",
270
+ children: [
271
+ /* @__PURE__ */ jsx(Messages, {
272
+ messages: messages.slice(1),
273
+ isLoading
274
+ }),
275
+ pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
276
+ toolCall: pendingToolCall,
277
+ onApprove: () => void handleToolApproval(true),
278
+ onReject: () => void handleToolApproval(false)
279
+ }),
280
+ !pendingToolCall && /* @__PURE__ */ jsx(Autocomplete, {
281
+ isDisabled: isLoading,
282
+ onSubmit: (val) => {
283
+ handleSubmit(val);
284
+ }
285
+ }, submitKey)
286
+ ]
287
+ });
288
+ }
289
+ //#endregion
290
+ //#region src/components/Footer.tsx
291
+ function Footer({ autoExecute, onToggleMode }) {
292
+ useInput((_, key) => {
293
+ if (key.tab && key.shift) onToggleMode();
294
+ });
295
+ return /* @__PURE__ */ jsx(Box, {
296
+ justifyContent: "space-between",
297
+ marginTop: 1,
298
+ children: /* @__PURE__ */ jsxs(Text, {
299
+ dimColor: true,
300
+ children: [
301
+ "Mode: ",
302
+ autoExecute ? "Auto" : "Safe",
303
+ " (Shift+Tab to toggle)"
304
+ ]
305
+ })
306
+ });
307
+ }
308
+ //#endregion
309
+ //#region src/components/Header.tsx
310
+ function abbreviatePath(dir) {
311
+ const home = homedir();
312
+ return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
313
+ }
314
+ function Header({ model }) {
315
+ const directory = abbreviatePath(process.cwd());
316
+ return /* @__PURE__ */ jsxs(Box, {
317
+ borderStyle: "round",
318
+ flexDirection: "column",
319
+ paddingX: 1,
320
+ children: [
321
+ /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
322
+ bold: true,
323
+ children: [HEADER_PREFIX, "Code Ollama"]
324
+ }), /* @__PURE__ */ jsxs(Text, {
325
+ dimColor: true,
326
+ children: [
327
+ " (v",
328
+ VERSION,
329
+ ")"
330
+ ]
331
+ })] }),
332
+ /* @__PURE__ */ jsx(Text, { children: " " }),
333
+ /* @__PURE__ */ jsxs(Box, { children: [
334
+ /* @__PURE__ */ jsx(Text, {
335
+ dimColor: true,
336
+ children: "model:".padEnd(11)
337
+ }),
338
+ /* @__PURE__ */ jsxs(Text, { children: [model, " "] }),
339
+ /* @__PURE__ */ jsx(Text, {
340
+ color: "cyan",
341
+ children: "/model"
342
+ }),
343
+ /* @__PURE__ */ jsx(Text, {
344
+ dimColor: true,
345
+ children: " to switch"
346
+ })
347
+ ] }),
348
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
349
+ dimColor: true,
350
+ children: "directory:".padEnd(11)
351
+ }), /* @__PURE__ */ jsx(Text, { children: directory })] })
352
+ ]
353
+ });
354
+ }
355
+ //#endregion
356
+ //#region src/components/ModelPicker.tsx
357
+ function ModelPicker({ currentModel, onSelect, onCancel }) {
358
+ const [options, setOptions] = useState([]);
359
+ const [error, setError] = useState(null);
360
+ useEffect(() => {
361
+ async function load() {
362
+ try {
363
+ setOptions((await listModels()).map((name) => ({
364
+ label: name,
365
+ value: name
366
+ })));
367
+ } catch (err) {
368
+ setError(err instanceof Error ? err.message : String(err));
369
+ }
370
+ }
371
+ load();
372
+ }, []);
373
+ useInput((_, key) => {
374
+ if (key.escape) onCancel();
375
+ });
376
+ if (error) return /* @__PURE__ */ jsxs(Text, {
377
+ color: "red",
378
+ children: ["Error loading models: ", error]
379
+ });
380
+ if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
381
+ return /* @__PURE__ */ jsxs(Box, {
382
+ flexDirection: "column",
383
+ children: [/* @__PURE__ */ jsx(Text, {
384
+ dimColor: true,
385
+ children: "Select a model (↑↓ + Enter to confirm, Esc to cancel)"
386
+ }), /* @__PURE__ */ jsx(Select, {
387
+ options,
388
+ defaultValue: currentModel,
389
+ onChange: onSelect
390
+ })]
391
+ });
392
+ }
393
+ //#endregion
394
+ //#region src/components/App.tsx
395
+ function App() {
396
+ const [model, setModel] = useState(() => loadConfig().model);
397
+ const [picking, setPicking] = useState(false);
398
+ const [autoExecute, setAutoExecute] = useState(false);
399
+ const handleCommand = useCallback((command) => {
400
+ if (command === "/model") setPicking(true);
401
+ }, []);
402
+ const handleSelect = useCallback((selected) => {
403
+ setModel(selected);
404
+ saveConfig({ model: selected });
405
+ setPicking(false);
406
+ }, []);
407
+ const handleCancel = useCallback(() => {
408
+ setPicking(false);
409
+ }, []);
410
+ return /* @__PURE__ */ jsxs(Box, {
411
+ flexDirection: "column",
412
+ children: [
413
+ /* @__PURE__ */ jsx(Header, { model }),
414
+ picking ? /* @__PURE__ */ jsx(ModelPicker, {
415
+ currentModel: model,
416
+ onSelect: handleSelect,
417
+ onCancel: handleCancel
418
+ }) : /* @__PURE__ */ jsx(Chat, {
419
+ model,
420
+ onCommand: handleCommand,
421
+ autoExecute
422
+ }),
423
+ /* @__PURE__ */ jsx(Footer, {
424
+ autoExecute,
425
+ onToggleMode: () => {
426
+ setAutoExecute((isAutoExecute) => !isAutoExecute);
427
+ }
428
+ })
429
+ ]
430
+ });
431
+ }
432
+ //#endregion
433
+ //#region src/tui.tsx
434
+ function renderApp() {
435
+ render(/* @__PURE__ */ jsx(App, {}));
436
+ }
437
+ //#endregion
438
+ export { renderApp };