code-ollama 0.7.0 → 0.8.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.
@@ -40,9 +40,16 @@ var LABEL = {
40
40
  };
41
41
  //#endregion
42
42
  //#region src/constants/ui.ts
43
- var HEADER_PREFIX = "🦙";
43
+ var HEADER_PREFIX = "🦙 ";
44
44
  //#endregion
45
- //#region src/components/Messages.tsx
45
+ //#region src/components/Messages/constants.ts
46
+ var TURN_ABORTED_MESSAGE = [
47
+ "<turn_aborted>",
48
+ "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.",
49
+ "</turn_aborted>"
50
+ ].join("\n");
51
+ //#endregion
52
+ //#region src/components/Messages/Messages.tsx
46
53
  function getMessageColor(role) {
47
54
  switch (role) {
48
55
  case ROLE.USER: return "black";
@@ -51,13 +58,13 @@ function getMessageColor(role) {
51
58
  default: return;
52
59
  }
53
60
  }
54
- var MessageRow = memo(function MessageRow({ message }) {
61
+ var Message = memo(function Message({ message }) {
55
62
  return /* @__PURE__ */ jsx(Box, {
56
63
  marginBottom: 1,
57
64
  children: /* @__PURE__ */ jsxs(Text, {
58
65
  color: getMessageColor(message.role),
59
66
  dimColor: message.role === ROLE.SYSTEM,
60
- children: [message.role === ROLE.USER ? "> " : "", message.content]
67
+ children: [message.role === ROLE.USER && "> ", message.content]
61
68
  })
62
69
  });
63
70
  });
@@ -65,8 +72,8 @@ function Messages({ messages, isLoading, streamingMessage }) {
65
72
  return /* @__PURE__ */ jsxs(Box, {
66
73
  flexDirection: "column",
67
74
  children: [
68
- messages.map((message, index) => /* @__PURE__ */ jsx(MessageRow, { message }, `${String(index)}-${message.role}-${message.content.slice(0, 16)}`)),
69
- streamingMessage && /* @__PURE__ */ jsx(MessageRow, { message: streamingMessage }),
75
+ messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE).map((message, index) => /* @__PURE__ */ jsx(Message, { message }, index)),
76
+ streamingMessage && /* @__PURE__ */ jsx(Message, { message: streamingMessage }),
70
77
  isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
71
78
  marginTop: -1,
72
79
  marginBottom: 1,
@@ -226,6 +233,11 @@ function ToolApproval({ toolCall, onDecision }) {
226
233
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
227
234
  var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
228
235
  var PLAN_EXECUTION_REMINDER = "Do not claim success and do not call write_file or run_shell until the user approves execution";
236
+ var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
237
+ INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
238
+ INTERRUPT_REASON["REJECTED"] = "rejected";
239
+ return INTERRUPT_REASON;
240
+ }({});
229
241
  //#endregion
230
242
  //#region src/components/Chat/CommandMenu.tsx
231
243
  function getMatchingCommands(input) {
@@ -376,7 +388,7 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
376
388
  function hasFileSuggestionQuery(input) {
377
389
  return /(^|\s)@\S+$/.test(input);
378
390
  }
379
- function Input({ isDisabled = false, onSubmit }) {
391
+ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
380
392
  const { exit } = useApp();
381
393
  const [input, setInput] = useState("");
382
394
  const [inputKey, setInputKey] = useState(0);
@@ -414,10 +426,19 @@ function Input({ isDisabled = false, onSubmit }) {
414
426
  if (LIST.find(({ name }) => name === input)) submitAndReset(input);
415
427
  }, [submitAndReset]);
416
428
  useInput((_input, key) => {
417
- if (key.ctrl && _input === "c") if (input) {
418
- setInput("");
419
- remountTextInput();
420
- } else exit();
429
+ const isCtrlC = key.ctrl && _input === "c";
430
+ if (isDisabled) {
431
+ if (key.escape || isCtrlC) onInterrupt?.();
432
+ return;
433
+ }
434
+ if (isCtrlC) {
435
+ if (input) {
436
+ setInput("");
437
+ remountTextInput();
438
+ return;
439
+ }
440
+ exit();
441
+ }
421
442
  });
422
443
  return /* @__PURE__ */ jsxs(Box, {
423
444
  flexDirection: "column",
@@ -458,12 +479,15 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
458
479
  const [isLoading, setIsLoading] = useState(false);
459
480
  const [pendingToolCall, setPendingToolCall] = useState(null);
460
481
  const [pendingPlan, setPendingPlan] = useState(null);
482
+ const [interruptReason, setInterruptReason] = useState(null);
483
+ const abortControllerRef = useRef(null);
461
484
  useEffect(() => {
462
485
  setMessages([]);
463
486
  setStreamingMessage(null);
464
487
  setIsLoading(false);
465
488
  setPendingToolCall(null);
466
489
  setPendingPlan(null);
490
+ setInterruptReason(null);
467
491
  }, [sessionId]);
468
492
  const buildToolResultMessage = useCallback((toolName, result) => {
469
493
  if (result.error?.startsWith("Tool not allowed:")) return {
@@ -490,7 +514,20 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
490
514
  PLAN_EXECUTION_REMINDER
491
515
  ].join("\n")
492
516
  }), []);
517
+ const handleInterrupt = useCallback(() => {
518
+ abortControllerRef.current?.abort();
519
+ abortControllerRef.current = null;
520
+ setIsLoading(false);
521
+ setStreamingMessage(null);
522
+ setInterruptReason(INTERRUPT_REASON.INTERRUPTED);
523
+ setMessages((prev) => [...prev, {
524
+ role: ROLE.USER,
525
+ content: TURN_ABORTED_MESSAGE
526
+ }]);
527
+ }, []);
493
528
  const processStream = useCallback(async (currentMessages, executionMode = mode) => {
529
+ const controller = new AbortController();
530
+ abortControllerRef.current = controller;
494
531
  const assistantMessage = {
495
532
  role: ROLE.ASSISTANT,
496
533
  content: ""
@@ -518,33 +555,40 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
518
555
  };
519
556
  setStreamingMessage(assistantMessage);
520
557
  try {
521
- for await (const chunk of streamChat(withSystemMessage(currentMessages), model, TOOLS)) if (chunk.type === "content") {
522
- assistantMessage.content += chunk.content;
523
- setStreamingMessage({ ...assistantMessage });
524
- } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
525
- const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
526
- // v8 ignore start
527
- const allowedTools = executionMode === NAME.PLAN ? READ_TOOLS : void 0;
528
- // v8 ignore stop
529
- const updatedMessages = commitAssistantMessage();
530
- if (executionMode === NAME.SAFE && requiresApproval) {
531
- setPendingToolCall(toolCall);
532
- setIsLoading(false);
558
+ for await (const chunk of streamChat(withSystemMessage(currentMessages), model, TOOLS, controller.signal)) {
559
+ // v8 ignore next 3
560
+ if (controller.signal.aborted) return;
561
+ if (chunk.type === "content") {
562
+ assistantMessage.content += chunk.content;
563
+ setStreamingMessage({ ...assistantMessage });
564
+ } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
565
+ const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
566
+ // v8 ignore start
567
+ const allowedTools = executionMode === NAME.PLAN ? READ_TOOLS : void 0;
568
+ // v8 ignore stop
569
+ const updatedMessages = commitAssistantMessage();
570
+ if (executionMode === NAME.SAFE && requiresApproval) {
571
+ setPendingToolCall(toolCall);
572
+ setIsLoading(false);
573
+ return;
574
+ }
575
+ const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools });
576
+ const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
577
+ const newMessages = [...updatedMessages, toolResultMessage];
578
+ setMessages(newMessages);
579
+ await processStream(newMessages, executionMode);
533
580
  return;
534
581
  }
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
582
  }
542
583
  commitAssistantMessage();
543
584
  } catch (error) {
544
585
  // v8 ignore next
545
- assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
546
- commitAssistantMessage();
586
+ if (!controller.signal.aborted) {
587
+ assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
588
+ commitAssistantMessage();
589
+ }
547
590
  } finally {
591
+ if (abortControllerRef.current === controller) abortControllerRef.current = null;
548
592
  setIsLoading(false);
549
593
  }
550
594
  }, [
@@ -553,6 +597,8 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
553
597
  mode
554
598
  ]);
555
599
  const processStreamReadOnly = useCallback(async (currentMessages) => {
600
+ const controller = new AbortController();
601
+ abortControllerRef.current = controller;
556
602
  const assistantMessage = {
557
603
  role: ROLE.ASSISTANT,
558
604
  content: ""
@@ -581,24 +627,28 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
581
627
  setStreamingMessage(assistantMessage);
582
628
  try {
583
629
  const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
584
- for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools)) if (chunk.type === "content") {
585
- assistantMessage.content += chunk.content;
586
- setStreamingMessage({ ...assistantMessage });
587
- } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
588
- const updatedMessages = commitAssistantMessage();
589
- if (!READ_TOOLS.has(toolCall.function.name)) {
590
- const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
591
- const newMessages = [...updatedMessages, correctionMessage];
630
+ for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools, controller.signal)) {
631
+ // v8 ignore next 3
632
+ if (controller.signal.aborted) return;
633
+ if (chunk.type === "content") {
634
+ assistantMessage.content += chunk.content;
635
+ setStreamingMessage({ ...assistantMessage });
636
+ } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
637
+ const updatedMessages = commitAssistantMessage();
638
+ if (!READ_TOOLS.has(toolCall.function.name)) {
639
+ const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
640
+ const newMessages = [...updatedMessages, correctionMessage];
641
+ setMessages(newMessages);
642
+ await processStreamReadOnly(newMessages);
643
+ return;
644
+ }
645
+ const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_TOOLS });
646
+ const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
647
+ const newMessages = [...updatedMessages, toolResultMessage];
592
648
  setMessages(newMessages);
593
649
  await processStreamReadOnly(newMessages);
594
650
  return;
595
651
  }
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
652
  }
603
653
  const researchMessages = commitAssistantMessage();
604
654
  const planInstruction = {
@@ -612,9 +662,13 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
612
662
  };
613
663
  setStreamingMessage(planAssistantMessage);
614
664
  try {
615
- for await (const chunk of streamChat(withSystemMessage(planMessages), model, [])) if (chunk.type === "content") {
616
- planAssistantMessage.content += chunk.content;
617
- setStreamingMessage({ ...planAssistantMessage });
665
+ for await (const chunk of streamChat(withSystemMessage(planMessages), model, [], controller.signal)) {
666
+ // v8 ignore next 3
667
+ if (controller.signal.aborted) return;
668
+ if (chunk.type === "content") {
669
+ planAssistantMessage.content += chunk.content;
670
+ setStreamingMessage({ ...planAssistantMessage });
671
+ }
618
672
  }
619
673
  } catch (error) {
620
674
  // v8 ignore next
@@ -634,9 +688,12 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
634
688
  setIsLoading(false);
635
689
  } catch (error) {
636
690
  // v8 ignore next
637
- assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
638
- commitAssistantMessage();
691
+ if (!controller.signal.aborted) {
692
+ assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
693
+ commitAssistantMessage();
694
+ }
639
695
  } finally {
696
+ if (abortControllerRef.current === controller) abortControllerRef.current = null;
640
697
  setIsLoading(false);
641
698
  }
642
699
  }, [
@@ -689,16 +746,14 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
689
746
  await processStream(newMessages);
690
747
  break;
691
748
  }
692
- case REJECT: {
693
- const rejectionMessage = {
694
- role: ROLE.SYSTEM,
695
- content: `User declined to execute tool ${toolCall.function.name}`
696
- };
697
- const newMessages = [...messages, rejectionMessage];
698
- setMessages((previousMessages) => [...previousMessages, rejectionMessage]);
699
- await processStream(newMessages);
749
+ case REJECT:
750
+ setMessages((previousMessages) => [...previousMessages, {
751
+ role: ROLE.USER,
752
+ content: TURN_ABORTED_MESSAGE
753
+ }]);
754
+ setIsLoading(false);
755
+ setInterruptReason(INTERRUPT_REASON.REJECTED);
700
756
  break;
701
- }
702
757
  }
703
758
  }, [
704
759
  pendingToolCall,
@@ -706,6 +761,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
706
761
  processStream
707
762
  ]);
708
763
  const handleSubmit = useCallback(async (value) => {
764
+ setInterruptReason(null);
709
765
  const userContent = value.trim();
710
766
  if (!userContent) return;
711
767
  if (userContent.startsWith("/")) {
@@ -744,8 +800,16 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
744
800
  toolCall: pendingToolCall,
745
801
  onDecision: handleToolApproval
746
802
  }),
803
+ interruptReason && !isLoading && /* @__PURE__ */ jsx(Box, {
804
+ marginBottom: 1,
805
+ children: /* @__PURE__ */ jsx(Text, {
806
+ color: "red",
807
+ children: interruptReason === INTERRUPT_REASON.REJECTED ? "❗ Tool call rejected." : "❗ Execution interrupted."
808
+ })
809
+ }),
747
810
  !pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Input, {
748
811
  isDisabled: isLoading,
812
+ onInterrupt: handleInterrupt,
749
813
  onSubmit: handleSubmit
750
814
  })
751
815
  ]
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.7.0";
11
+ var VERSION = "0.8.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
- for await (const chunk of response) {
145
- if (chunk.message.content) yield {
146
- type: "content",
147
- content: chunk.message.content
148
- };
149
- if (chunk.message.tool_calls) yield {
150
- type: "tool_calls",
151
- tool_calls: chunk.message.tool_calls
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-CDaKDOEJ.js");
515
+ const { renderApp } = await import("./assets/tui-BjeMfZFh.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.7.0",
3
+ "version": "0.8.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",