code-ollama 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
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";
1
+ import { _ as NAME, a as setClearHandler, b as NAMES, c as loadConfig, d as withSystemMessage, f as HEADER_PREFIX, g as LABEL, h as VERSION, i as executeTool, l as saveConfig, m as PLAN_GENERATION_INSTRUCTION, n as READ_ONLY_TOOLS, o as listModels, p as ROLE, r as TOOLS, s as streamChat, t as DANGEROUS_TOOLS, u as resetSystemMessage, v as APPROVE, y as REJECT } from "../cli.js";
2
2
  import { homedir } from "node:os";
3
3
  import { Box, Text, render, useInput } from "ink";
4
- import { useCallback, useEffect, useState } from "react";
4
+ import { memo, useCallback, useEffect, useState } from "react";
5
5
  import { Select, Spinner, TextInput } from "@inkjs/ui";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/components/Messages.tsx
@@ -13,21 +13,28 @@ function getMessageColor(role) {
13
13
  default: return;
14
14
  }
15
15
  }
16
- function Messages({ messages, isLoading }) {
16
+ var MessageRow = memo(function MessageRow({ message }) {
17
+ return /* @__PURE__ */ jsx(Box, {
18
+ marginBottom: 1,
19
+ children: /* @__PURE__ */ jsxs(Text, {
20
+ color: getMessageColor(message.role),
21
+ dimColor: message.role === ROLE.SYSTEM,
22
+ children: [message.role === ROLE.USER ? "> " : "", message.content]
23
+ })
24
+ });
25
+ });
26
+ function Messages({ messages, isLoading, streamingMessage }) {
17
27
  return /* @__PURE__ */ jsxs(Box, {
18
28
  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]
29
+ children: [
30
+ messages.map((message, index) => /* @__PURE__ */ jsx(MessageRow, { message }, `${String(index)}-${message.role}-${message.content.slice(0, 16)}`)),
31
+ streamingMessage && /* @__PURE__ */ jsx(MessageRow, { message: streamingMessage }),
32
+ isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
33
+ marginTop: -1,
34
+ marginBottom: 1,
35
+ children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
25
36
  })
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
- })]
37
+ ]
31
38
  });
32
39
  }
33
40
  //#endregion
@@ -171,11 +178,19 @@ function hasExecutablePlan(content) {
171
178
  }
172
179
  //#endregion
173
180
  //#region src/components/Chat/Chat.tsx
174
- function Chat({ model, onCommand, mode, onModeChange }) {
175
- const [messages, setMessages] = useState([createSystemMessage()]);
181
+ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
182
+ const [messages, setMessages] = useState([]);
183
+ const [streamingMessage, setStreamingMessage] = useState(null);
176
184
  const [isLoading, setIsLoading] = useState(false);
177
185
  const [pendingToolCall, setPendingToolCall] = useState(null);
178
186
  const [pendingPlan, setPendingPlan] = useState(null);
187
+ useEffect(() => {
188
+ setMessages([]);
189
+ setStreamingMessage(null);
190
+ setIsLoading(false);
191
+ setPendingToolCall(null);
192
+ setPendingPlan(null);
193
+ }, [sessionId]);
179
194
  const buildToolResultMessage = useCallback((toolName, result) => {
180
195
  if (result.error?.startsWith("Tool not allowed:")) return {
181
196
  role: ROLE.SYSTEM,
@@ -206,20 +221,38 @@ function Chat({ model, onCommand, mode, onModeChange }) {
206
221
  role: ROLE.ASSISTANT,
207
222
  content: ""
208
223
  };
209
- setMessages((previousMessages) => [...previousMessages, assistantMessage]);
224
+ let committedMessages = currentMessages;
225
+ let assistantCommitted = false;
226
+ const commitAssistantMessage = () => {
227
+ if (assistantCommitted) {
228
+ // v8 ignore next
229
+ if (committedMessages.at(-1)?.role === ROLE.ASSISTANT) {
230
+ committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
231
+ setMessages(committedMessages);
232
+ }
233
+ return committedMessages;
234
+ }
235
+ assistantCommitted = true;
236
+ setStreamingMessage(null);
237
+ if (!assistantMessage.content) {
238
+ setMessages(committedMessages);
239
+ return committedMessages;
240
+ }
241
+ committedMessages = [...committedMessages, { ...assistantMessage }];
242
+ setMessages(committedMessages);
243
+ return committedMessages;
244
+ };
245
+ setStreamingMessage(assistantMessage);
210
246
  try {
211
- for await (const chunk of streamChat(currentMessages, model, TOOLS)) if (chunk.type === "content") {
247
+ for await (const chunk of streamChat(withSystemMessage(currentMessages), model, TOOLS)) if (chunk.type === "content") {
212
248
  assistantMessage.content += chunk.content;
213
- setMessages((previousMessages) => {
214
- const newMessages = [...previousMessages];
215
- newMessages[newMessages.length - 1] = { ...assistantMessage };
216
- return newMessages;
217
- });
249
+ setStreamingMessage({ ...assistantMessage });
218
250
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
219
251
  const requiresApproval = DANGEROUS_TOOLS.has(toolCall.function.name);
220
252
  // v8 ignore start
221
253
  const allowedTools = executionMode === NAME.PLAN ? READ_ONLY_TOOLS : void 0;
222
254
  // v8 ignore stop
255
+ const updatedMessages = commitAssistantMessage();
223
256
  if (executionMode === NAME.SAFE && requiresApproval) {
224
257
  setPendingToolCall(toolCall);
225
258
  setIsLoading(false);
@@ -227,22 +260,15 @@ function Chat({ model, onCommand, mode, onModeChange }) {
227
260
  }
228
261
  const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools });
229
262
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
230
- const newMessages = [
231
- ...currentMessages,
232
- assistantMessage,
233
- toolResultMessage
234
- ];
235
- setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
263
+ const newMessages = [...updatedMessages, toolResultMessage];
264
+ setMessages(newMessages);
236
265
  await processStream(newMessages, executionMode);
237
266
  return;
238
267
  }
268
+ commitAssistantMessage();
239
269
  } catch (error) {
240
270
  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
- });
271
+ commitAssistantMessage();
246
272
  } finally {
247
273
  setIsLoading(false);
248
274
  }
@@ -256,73 +282,83 @@ function Chat({ model, onCommand, mode, onModeChange }) {
256
282
  role: ROLE.ASSISTANT,
257
283
  content: ""
258
284
  };
259
- setMessages((previousMessages) => [...previousMessages, assistantMessage]);
285
+ let committedMessages = currentMessages;
286
+ let assistantCommitted = false;
287
+ const commitAssistantMessage = () => {
288
+ if (assistantCommitted) {
289
+ // v8 ignore next
290
+ if (committedMessages.at(-1)?.role === ROLE.ASSISTANT) {
291
+ committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
292
+ setMessages(committedMessages);
293
+ }
294
+ return committedMessages;
295
+ }
296
+ assistantCommitted = true;
297
+ setStreamingMessage(null);
298
+ if (!assistantMessage.content) {
299
+ setMessages(committedMessages);
300
+ return committedMessages;
301
+ }
302
+ committedMessages = [...committedMessages, { ...assistantMessage }];
303
+ setMessages(committedMessages);
304
+ return committedMessages;
305
+ };
306
+ setStreamingMessage(assistantMessage);
260
307
  try {
261
308
  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") {
309
+ for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools)) if (chunk.type === "content") {
263
310
  assistantMessage.content += chunk.content;
264
- setMessages((previousMessages) => {
265
- const newMessages = [...previousMessages];
266
- newMessages[newMessages.length - 1] = { ...assistantMessage };
267
- return newMessages;
268
- });
311
+ setStreamingMessage({ ...assistantMessage });
269
312
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
313
+ const updatedMessages = commitAssistantMessage();
270
314
  if (!READ_ONLY_TOOLS.has(toolCall.function.name)) {
271
315
  const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
272
- const newMessages = [
273
- ...currentMessages,
274
- assistantMessage,
275
- correctionMessage
276
- ];
277
- setMessages((previousMessages) => [...previousMessages, correctionMessage]);
316
+ const newMessages = [...updatedMessages, correctionMessage];
317
+ setMessages(newMessages);
278
318
  await processStreamReadOnly(newMessages);
279
319
  return;
280
320
  }
281
321
  const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_ONLY_TOOLS });
282
322
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
283
- const newMessages = [
284
- ...currentMessages,
285
- assistantMessage,
286
- toolResultMessage
287
- ];
288
- setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
323
+ const newMessages = [...updatedMessages, toolResultMessage];
324
+ setMessages(newMessages);
289
325
  await processStreamReadOnly(newMessages);
290
326
  return;
291
327
  }
328
+ const researchMessages = commitAssistantMessage();
292
329
  const planInstruction = {
293
330
  role: ROLE.SYSTEM,
294
331
  content: PLAN_GENERATION_INSTRUCTION
295
332
  };
296
- const planMessages = [
297
- ...currentMessages,
298
- assistantMessage,
299
- planInstruction
300
- ];
333
+ const planMessages = [...researchMessages, planInstruction];
301
334
  const planAssistantMessage = {
302
335
  role: ROLE.ASSISTANT,
303
336
  content: ""
304
337
  };
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
- });
338
+ setStreamingMessage(planAssistantMessage);
339
+ try {
340
+ for await (const chunk of streamChat(withSystemMessage(planMessages), model, [])) if (chunk.type === "content") {
341
+ planAssistantMessage.content += chunk.content;
342
+ setStreamingMessage({ ...planAssistantMessage });
343
+ }
344
+ } catch (error) {
345
+ planAssistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
346
+ setMessages([...planMessages, { ...planAssistantMessage }]);
347
+ setStreamingMessage(null);
348
+ setIsLoading(false);
349
+ return;
313
350
  }
351
+ const finalPlanMessages = [...planMessages, { ...planAssistantMessage }];
352
+ setMessages(finalPlanMessages);
353
+ setStreamingMessage(null);
314
354
  if (hasExecutablePlan(planAssistantMessage.content)) setPendingPlan({
315
355
  planContent: planAssistantMessage.content,
316
- messages: [...planMessages, planAssistantMessage]
356
+ messages: finalPlanMessages
317
357
  });
318
358
  setIsLoading(false);
319
359
  } catch (error) {
320
360
  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
- });
361
+ commitAssistantMessage();
326
362
  } finally {
327
363
  setIsLoading(false);
328
364
  }
@@ -404,8 +440,8 @@ function Chat({ model, onCommand, mode, onModeChange }) {
404
440
  role: ROLE.USER,
405
441
  content: userContent
406
442
  };
407
- setMessages((previousMessages) => [...previousMessages, userMessage]);
408
443
  const updatedMessages = [...messages, userMessage];
444
+ setMessages(updatedMessages);
409
445
  if (mode === NAME.PLAN) await processStreamReadOnly(updatedMessages);
410
446
  else await processStream(updatedMessages);
411
447
  }, [
@@ -419,8 +455,9 @@ function Chat({ model, onCommand, mode, onModeChange }) {
419
455
  flexDirection: "column",
420
456
  children: [
421
457
  /* @__PURE__ */ jsx(Messages, {
422
- messages: messages.slice(1),
423
- isLoading
458
+ messages,
459
+ isLoading,
460
+ streamingMessage
424
461
  }),
425
462
  pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
426
463
  planContent: pendingPlan.planContent,
@@ -566,8 +603,18 @@ function App() {
566
603
  const [model, setModel] = useState(() => loadConfig().model);
567
604
  const [picking, setPicking] = useState(false);
568
605
  const [mode, setMode] = useState(NAME.SAFE);
606
+ const [sessionId, setSessionId] = useState(0);
569
607
  const handleCommand = useCallback((command) => {
570
- if (command === "/model") setPicking(true);
608
+ switch (command) {
609
+ case "/model":
610
+ setPicking(true);
611
+ break;
612
+ case "/clear":
613
+ resetSystemMessage();
614
+ setPicking(false);
615
+ setSessionId((sessionId) => sessionId + 1);
616
+ break;
617
+ }
571
618
  }, []);
572
619
  const handleSelect = useCallback((selected) => {
573
620
  setModel(selected);
@@ -577,32 +624,43 @@ function App() {
577
624
  const handleClose = useCallback(() => {
578
625
  setPicking(false);
579
626
  }, []);
580
- return /* @__PURE__ */ jsxs(Box, {
581
- flexDirection: "column",
582
- children: [
583
- /* @__PURE__ */ jsx(Header, { model }),
584
- picking ? /* @__PURE__ */ jsx(ModelPicker, {
627
+ const handleToggleMode = useCallback(() => {
628
+ setMode((mode) => {
629
+ switch (mode) {
630
+ case NAME.SAFE: return NAME.AUTO;
631
+ case NAME.AUTO: return NAME.PLAN;
632
+ case NAME.PLAN:
633
+ default: return NAME.SAFE;
634
+ }
635
+ });
636
+ }, []);
637
+ let body;
638
+ switch (true) {
639
+ case picking:
640
+ body = /* @__PURE__ */ jsx(ModelPicker, {
585
641
  currentModel: model,
586
642
  onSelect: handleSelect,
587
643
  onClose: handleClose
588
- }) : /* @__PURE__ */ jsx(Chat, {
644
+ });
645
+ break;
646
+ default:
647
+ body = /* @__PURE__ */ jsx(Chat, {
589
648
  model,
590
649
  onCommand: handleCommand,
591
650
  mode,
592
- onModeChange: setMode
593
- }),
651
+ onModeChange: setMode,
652
+ sessionId
653
+ });
654
+ break;
655
+ }
656
+ return /* @__PURE__ */ jsxs(Box, {
657
+ flexDirection: "column",
658
+ children: [
659
+ /* @__PURE__ */ jsx(Header, { model }),
660
+ body,
594
661
  /* @__PURE__ */ jsx(Footer, {
595
662
  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
- }
663
+ onToggleMode: handleToggleMode
606
664
  })
607
665
  ]
608
666
  });
@@ -610,7 +668,15 @@ function App() {
610
668
  //#endregion
611
669
  //#region src/tui.tsx
612
670
  function renderApp() {
613
- render(/* @__PURE__ */ jsx(App, {}));
671
+ const tree = /* @__PURE__ */ jsx(App, {});
672
+ const app = render(tree, {
673
+ incrementalRendering: true,
674
+ maxFps: 60
675
+ });
676
+ setClearHandler(() => {
677
+ app.clear();
678
+ app.rerender(tree);
679
+ });
614
680
  }
615
681
  //#endregion
616
682
  export { renderApp };
package/dist/cli.js CHANGED
@@ -7,6 +7,9 @@ import { Ollama } from "ollama";
7
7
  import { exec } from "node:child_process";
8
8
  import { promisify } from "node:util";
9
9
  var NAMES = [{
10
+ name: "/clear",
11
+ description: "clear the current session"
12
+ }, {
10
13
  name: "/model",
11
14
  description: "switch the model"
12
15
  }].map(({ name }) => name);
@@ -28,7 +31,7 @@ var LABEL = {
28
31
  };
29
32
  //#endregion
30
33
  //#region src/constants/package.ts
31
- var VERSION = "0.3.1";
34
+ var VERSION = "0.4.0";
32
35
  //#endregion
33
36
  //#region src/constants/prompt.ts
34
37
  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
@@ -108,12 +111,20 @@ function buildSystemPrompt() {
108
111
  parts.push("\n\n", TOOL_INSTRUCTIONS);
109
112
  return parts.join("");
110
113
  }
114
+ var systemMessage = null;
111
115
  function createSystemMessage() {
112
116
  return {
113
117
  role: ROLE.SYSTEM,
114
118
  content: buildSystemPrompt()
115
119
  };
116
120
  }
121
+ function resetSystemMessage() {
122
+ systemMessage = null;
123
+ }
124
+ function withSystemMessage(messages) {
125
+ systemMessage ??= createSystemMessage();
126
+ return [systemMessage, ...messages];
127
+ }
117
128
  //#endregion
118
129
  //#region src/utils/config.ts
119
130
  var CONFIG_DIR = join(homedir(), ".code-ollama");
@@ -171,12 +182,7 @@ async function listModels() {
171
182
  const { models } = await client.list();
172
183
  return models.map(({ name }) => name);
173
184
  }
174
- //#endregion
175
- //#region src/utils/screen.ts
176
- var CLEAR = "\x1Bc";
177
- function clear() {
178
- process.stdout.write(CLEAR);
179
- }
185
+ function setClearHandler(handler) {}
180
186
  //#endregion
181
187
  //#region src/utils/tools.ts
182
188
  var execAsync = promisify(exec);
@@ -519,8 +525,8 @@ async function processRunStream(messages, model) {
519
525
  }
520
526
  async function main(args = process.argv.slice(2)) {
521
527
  if (!args.length) {
522
- const { renderApp } = await import("./assets/tui-CVsodXv3.js");
523
- clear();
528
+ const { renderApp } = await import("./assets/tui-BLJeczya.js");
529
+ process.stdout.write("\x1Bc");
524
530
  renderApp();
525
531
  return;
526
532
  }
@@ -530,7 +536,7 @@ async function main(args = process.argv.slice(2)) {
530
536
  ...args
531
537
  ]);
532
538
  }
533
- /* v8 ignore start */
539
+ // v8 ignore start
534
540
  function isEntrypoint(argv1 = process.argv[1]) {
535
541
  if (!argv1) return false;
536
542
  try {
@@ -540,6 +546,6 @@ function isEntrypoint(argv1 = process.argv[1]) {
540
546
  }
541
547
  }
542
548
  if (isEntrypoint()) main();
543
- /* v8 ignore stop */
549
+ // v8 ignore stop
544
550
  //#endregion
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 };
551
+ export { NAME$1 as _, setClearHandler as a, NAMES as b, loadConfig as c, withSystemMessage as d, HEADER_PREFIX as f, LABEL as g, VERSION as h, executeTool as i, saveConfig as l, PLAN_GENERATION_INSTRUCTION as m, main, READ_ONLY_TOOLS as n, listModels as o, ROLE as p, TOOLS as r, streamChat as s, DANGEROUS_TOOLS as t, resetSystemMessage as u, APPROVE as v, REJECT as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",