code-ollama 0.6.1 → 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.
@@ -8,13 +8,20 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
8
8
  import { Select, Spinner, TextInput } from "@inkjs/ui";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/constants/command.ts
11
- var LIST = [{
12
- name: "/clear",
13
- description: "clear the current session"
14
- }, {
15
- name: "/model",
16
- description: "switch the model"
17
- }];
11
+ var LIST = [
12
+ {
13
+ name: "/clear",
14
+ description: "clear the current session"
15
+ },
16
+ {
17
+ name: "/model",
18
+ description: "switch the model"
19
+ },
20
+ {
21
+ name: "/exit",
22
+ description: "exit the application"
23
+ }
24
+ ];
18
25
  //#endregion
19
26
  //#region src/constants/decision.ts
20
27
  var APPROVE = "approve";
@@ -33,9 +40,16 @@ var LABEL = {
33
40
  };
34
41
  //#endregion
35
42
  //#region src/constants/ui.ts
36
- var HEADER_PREFIX = "🦙";
43
+ var HEADER_PREFIX = "🦙 ";
44
+ //#endregion
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");
37
51
  //#endregion
38
- //#region src/components/Messages.tsx
52
+ //#region src/components/Messages/Messages.tsx
39
53
  function getMessageColor(role) {
40
54
  switch (role) {
41
55
  case ROLE.USER: return "black";
@@ -44,13 +58,13 @@ function getMessageColor(role) {
44
58
  default: return;
45
59
  }
46
60
  }
47
- var MessageRow = memo(function MessageRow({ message }) {
61
+ var Message = memo(function Message({ message }) {
48
62
  return /* @__PURE__ */ jsx(Box, {
49
63
  marginBottom: 1,
50
64
  children: /* @__PURE__ */ jsxs(Text, {
51
65
  color: getMessageColor(message.role),
52
66
  dimColor: message.role === ROLE.SYSTEM,
53
- children: [message.role === ROLE.USER ? "> " : "", message.content]
67
+ children: [message.role === ROLE.USER && "> ", message.content]
54
68
  })
55
69
  });
56
70
  });
@@ -58,8 +72,8 @@ function Messages({ messages, isLoading, streamingMessage }) {
58
72
  return /* @__PURE__ */ jsxs(Box, {
59
73
  flexDirection: "column",
60
74
  children: [
61
- messages.map((message, index) => /* @__PURE__ */ jsx(MessageRow, { message }, `${String(index)}-${message.role}-${message.content.slice(0, 16)}`)),
62
- 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 }),
63
77
  isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
64
78
  marginTop: -1,
65
79
  marginBottom: 1,
@@ -219,6 +233,11 @@ function ToolApproval({ toolCall, onDecision }) {
219
233
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
220
234
  var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
221
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
+ }({});
222
241
  //#endregion
223
242
  //#region src/components/Chat/CommandMenu.tsx
224
243
  function getMatchingCommands(input) {
@@ -369,7 +388,7 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
369
388
  function hasFileSuggestionQuery(input) {
370
389
  return /(^|\s)@\S+$/.test(input);
371
390
  }
372
- function Input({ isDisabled = false, onSubmit }) {
391
+ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
373
392
  const { exit } = useApp();
374
393
  const [input, setInput] = useState("");
375
394
  const [inputKey, setInputKey] = useState(0);
@@ -407,10 +426,19 @@ function Input({ isDisabled = false, onSubmit }) {
407
426
  if (LIST.find(({ name }) => name === input)) submitAndReset(input);
408
427
  }, [submitAndReset]);
409
428
  useInput((_input, key) => {
410
- if (key.ctrl && _input === "c") if (input) {
411
- setInput("");
412
- remountTextInput();
413
- } 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
+ }
414
442
  });
415
443
  return /* @__PURE__ */ jsxs(Box, {
416
444
  flexDirection: "column",
@@ -451,12 +479,15 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
451
479
  const [isLoading, setIsLoading] = useState(false);
452
480
  const [pendingToolCall, setPendingToolCall] = useState(null);
453
481
  const [pendingPlan, setPendingPlan] = useState(null);
482
+ const [interruptReason, setInterruptReason] = useState(null);
483
+ const abortControllerRef = useRef(null);
454
484
  useEffect(() => {
455
485
  setMessages([]);
456
486
  setStreamingMessage(null);
457
487
  setIsLoading(false);
458
488
  setPendingToolCall(null);
459
489
  setPendingPlan(null);
490
+ setInterruptReason(null);
460
491
  }, [sessionId]);
461
492
  const buildToolResultMessage = useCallback((toolName, result) => {
462
493
  if (result.error?.startsWith("Tool not allowed:")) return {
@@ -483,7 +514,20 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
483
514
  PLAN_EXECUTION_REMINDER
484
515
  ].join("\n")
485
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
+ }, []);
486
528
  const processStream = useCallback(async (currentMessages, executionMode = mode) => {
529
+ const controller = new AbortController();
530
+ abortControllerRef.current = controller;
487
531
  const assistantMessage = {
488
532
  role: ROLE.ASSISTANT,
489
533
  content: ""
@@ -511,33 +555,40 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
511
555
  };
512
556
  setStreamingMessage(assistantMessage);
513
557
  try {
514
- for await (const chunk of streamChat(withSystemMessage(currentMessages), model, TOOLS)) if (chunk.type === "content") {
515
- assistantMessage.content += chunk.content;
516
- setStreamingMessage({ ...assistantMessage });
517
- } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
518
- const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
519
- // v8 ignore start
520
- const allowedTools = executionMode === NAME.PLAN ? READ_TOOLS : void 0;
521
- // v8 ignore stop
522
- const updatedMessages = commitAssistantMessage();
523
- if (executionMode === NAME.SAFE && requiresApproval) {
524
- setPendingToolCall(toolCall);
525
- 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);
526
580
  return;
527
581
  }
528
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools });
529
- const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
530
- const newMessages = [...updatedMessages, toolResultMessage];
531
- setMessages(newMessages);
532
- await processStream(newMessages, executionMode);
533
- return;
534
582
  }
535
583
  commitAssistantMessage();
536
584
  } catch (error) {
537
585
  // v8 ignore next
538
- assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
539
- commitAssistantMessage();
586
+ if (!controller.signal.aborted) {
587
+ assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
588
+ commitAssistantMessage();
589
+ }
540
590
  } finally {
591
+ if (abortControllerRef.current === controller) abortControllerRef.current = null;
541
592
  setIsLoading(false);
542
593
  }
543
594
  }, [
@@ -546,6 +597,8 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
546
597
  mode
547
598
  ]);
548
599
  const processStreamReadOnly = useCallback(async (currentMessages) => {
600
+ const controller = new AbortController();
601
+ abortControllerRef.current = controller;
549
602
  const assistantMessage = {
550
603
  role: ROLE.ASSISTANT,
551
604
  content: ""
@@ -574,24 +627,28 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
574
627
  setStreamingMessage(assistantMessage);
575
628
  try {
576
629
  const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
577
- for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools)) if (chunk.type === "content") {
578
- assistantMessage.content += chunk.content;
579
- setStreamingMessage({ ...assistantMessage });
580
- } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
581
- const updatedMessages = commitAssistantMessage();
582
- if (!READ_TOOLS.has(toolCall.function.name)) {
583
- const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
584
- 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];
585
648
  setMessages(newMessages);
586
649
  await processStreamReadOnly(newMessages);
587
650
  return;
588
651
  }
589
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_TOOLS });
590
- const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
591
- const newMessages = [...updatedMessages, toolResultMessage];
592
- setMessages(newMessages);
593
- await processStreamReadOnly(newMessages);
594
- return;
595
652
  }
596
653
  const researchMessages = commitAssistantMessage();
597
654
  const planInstruction = {
@@ -605,9 +662,13 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
605
662
  };
606
663
  setStreamingMessage(planAssistantMessage);
607
664
  try {
608
- for await (const chunk of streamChat(withSystemMessage(planMessages), model, [])) if (chunk.type === "content") {
609
- planAssistantMessage.content += chunk.content;
610
- 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
+ }
611
672
  }
612
673
  } catch (error) {
613
674
  // v8 ignore next
@@ -627,9 +688,12 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
627
688
  setIsLoading(false);
628
689
  } catch (error) {
629
690
  // v8 ignore next
630
- assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
631
- commitAssistantMessage();
691
+ if (!controller.signal.aborted) {
692
+ assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
693
+ commitAssistantMessage();
694
+ }
632
695
  } finally {
696
+ if (abortControllerRef.current === controller) abortControllerRef.current = null;
633
697
  setIsLoading(false);
634
698
  }
635
699
  }, [
@@ -682,16 +746,14 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
682
746
  await processStream(newMessages);
683
747
  break;
684
748
  }
685
- case REJECT: {
686
- const rejectionMessage = {
687
- role: ROLE.SYSTEM,
688
- content: `User declined to execute tool ${toolCall.function.name}`
689
- };
690
- const newMessages = [...messages, rejectionMessage];
691
- setMessages((previousMessages) => [...previousMessages, rejectionMessage]);
692
- 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);
693
756
  break;
694
- }
695
757
  }
696
758
  }, [
697
759
  pendingToolCall,
@@ -699,6 +761,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
699
761
  processStream
700
762
  ]);
701
763
  const handleSubmit = useCallback(async (value) => {
764
+ setInterruptReason(null);
702
765
  const userContent = value.trim();
703
766
  if (!userContent) return;
704
767
  if (userContent.startsWith("/")) {
@@ -737,8 +800,16 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
737
800
  toolCall: pendingToolCall,
738
801
  onDecision: handleToolApproval
739
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
+ }),
740
810
  !pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Input, {
741
811
  isDisabled: isLoading,
812
+ onInterrupt: handleInterrupt,
742
813
  onSubmit: handleSubmit
743
814
  })
744
815
  ]
@@ -867,6 +938,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
867
938
  //#endregion
868
939
  //#region src/components/App.tsx
869
940
  function App() {
941
+ const { exit } = useApp();
870
942
  const [model, setModel] = useState(() => loadConfig().model);
871
943
  const [picking, setPicking] = useState(false);
872
944
  const [mode, setMode] = useState(NAME.SAFE);
@@ -881,8 +953,11 @@ function App() {
881
953
  setPicking(false);
882
954
  setSessionId((sessionId) => sessionId + 1);
883
955
  break;
956
+ case "/exit":
957
+ exit();
958
+ break;
884
959
  }
885
- }, []);
960
+ }, [exit]);
886
961
  const handleSelect = useCallback((selected) => {
887
962
  setModel(selected);
888
963
  saveConfig({ model: selected });
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.6.1";
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-B1jg-OeC.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.6.1",
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",