code-ollama 0.18.2 → 0.19.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.
@@ -1,7 +1,7 @@
1
- import { A as ASSISTANT, B as APPROVE, C as saveConfig, D as WARNING, E as HEADER_PREFIX, F as CATALOG, H as VERSION, I as AUTO, L as LABEL, M as USER, N as PLAN_GENERATION_INSTRUCTION, O as LIST$1, P as BACK, R as PLAN, S as loadConfig, T as withSystemMessage, U as LIST, V as REJECT, _ as checkHealth, a as color, b as pullModel, c as createSession, d as listSessions, f as loadSession, g as setClearHandler, h as reset, i as WRITE_TOOLS, j as SYSTEM, k as getTheme, l as deleteSession, m as clear, n as READ_TOOLS, o as write, p as updateSessionModel, r as TOOLS, s as appendMessage, t as executeTool, u as deleteSessionIfEmpty, v as deleteModel, w as resetSystemMessage, x as streamChat, y as listModels, z as SAFE } from "../cli.js";
2
- import { readdirSync } from "node:fs";
1
+ import { A as WARNING, B as LABEL, C as loadConfig, D as resetSystemMessage, E as saveClipboardImage, F as USER, G as VERSION, H as SAFE, I as PLAN_GENERATION_INSTRUCTION, K as LIST, L as BACK, M as getTheme, N as ASSISTANT, O as withSystemMessage, P as SYSTEM, R as CATALOG, S as streamChat, T as removeClipboardImage, U as APPROVE, V as PLAN, W as REJECT, _ as setClearHandler, a as tick, b as listModels, c as appendMessage, d as deleteSessionIfEmpty, f as listSessions, g as reset, h as clear, i as WRITE_TOOLS, j as LIST$1, k as HEADER_PREFIX, l as createSession, m as updateSessionModel, n as READ_TOOLS, o as color, p as loadSession, r as TOOLS, s as write, t as executeTool, u as deleteSession, v as checkHealth, w as saveConfig, x as pullModel, y as deleteModel, z as AUTO } from "../cli.js";
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { join, relative } from "node:path";
4
+ import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { exec } from "node:child_process";
6
6
  import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
7
7
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -227,8 +227,6 @@ var Markdown = memo(function Markdown({ content, color, dimColor, theme = getThe
227
227
  //#endregion
228
228
  //#region src/components/Messages/layout.ts
229
229
  var ANSI_REGEX = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
230
- var CODE_BLOCK_MARGIN_Y = 2;
231
- var CODE_BLOCK_BORDER_Y = 2;
232
230
  var CODE_BLOCK_CHROME_X = 4;
233
231
  function stripAnsi(value) {
234
232
  return value.replaceAll(ANSI_REGEX, "");
@@ -264,8 +262,7 @@ function countWrappedLines(content, width) {
264
262
  * @returns The total height in lines.
265
263
  */
266
264
  function getCodeBlockHeight(content, width) {
267
- const contentWidth = Math.max(1, width - CODE_BLOCK_CHROME_X);
268
- return CODE_BLOCK_MARGIN_Y + CODE_BLOCK_BORDER_Y + countWrappedLines(content, contentWidth);
265
+ return 4 + countWrappedLines(content, Math.max(1, width - CODE_BLOCK_CHROME_X));
269
266
  }
270
267
  /**
271
268
  * Calculates the total height of streaming text content based on wrapped lines.
@@ -416,6 +413,26 @@ function Message({ message, isStreaming = false, theme }) {
416
413
  children: message.content
417
414
  })
418
415
  });
416
+ if (isUser) {
417
+ // v8 ignore start
418
+ const attachmentPrefix = (message.images ?? []).map((path) => `[${path.split(/[\\/]/).at(-1) ?? path}]`).join(" ");
419
+ // v8 ignore stop
420
+ return /* @__PURE__ */ jsx(Box, {
421
+ flexDirection: "column",
422
+ marginBottom: 1,
423
+ children: /* @__PURE__ */ jsxs(Text, {
424
+ color: messageColor,
425
+ children: [
426
+ "> ",
427
+ attachmentPrefix ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
428
+ color: theme.colors.accent,
429
+ children: attachmentPrefix
430
+ }), message.content ? " " : ""] }) : null,
431
+ message.content
432
+ ]
433
+ })
434
+ });
435
+ }
419
436
  const segments = parseContent(message.content);
420
437
  const availableWidth = getAssistantContentWidth(stdout.columns);
421
438
  if (stickyHeightRef.current.columns !== stdout.columns) stickyHeightRef.current = {
@@ -436,11 +453,7 @@ function Message({ message, isStreaming = false, theme }) {
436
453
  flexDirection: "column",
437
454
  marginBottom: 1,
438
455
  children: [segments.map((segment, index) => {
439
- const prefix = isUser && index === 0 ? "> " : "";
440
- if (segment.type === "code") return isUser ? /* @__PURE__ */ jsx(Text, {
441
- color: messageColor,
442
- children: segment.content
443
- }, index) : /* @__PURE__ */ jsx(Box, {
456
+ if (segment.type === "code") return /* @__PURE__ */ jsx(Box, {
444
457
  marginX: 2,
445
458
  children: /* @__PURE__ */ jsx(CodeBlock, {
446
459
  code: segment.content,
@@ -461,17 +474,13 @@ function Message({ message, isStreaming = false, theme }) {
461
474
  })
462
475
  }, index);
463
476
  }
464
- const textParts = [{
465
- type: "markdown",
466
- content: segment.content
467
- }];
468
- return isUser ? /* @__PURE__ */ jsx(Text, {
469
- color: messageColor,
470
- children: prefix + segment.content
471
- }, index) : /* @__PURE__ */ jsx(Box, {
477
+ return /* @__PURE__ */ jsx(Box, {
472
478
  flexDirection: "column",
473
479
  marginX: 2,
474
- children: textParts.map((part, partIndex) => /* @__PURE__ */ jsx(Markdown, {
480
+ children: [{
481
+ type: "markdown",
482
+ content: segment.content
483
+ }].map((part, partIndex) => /* @__PURE__ */ jsx(Markdown, {
475
484
  content: part.content,
476
485
  theme
477
486
  }, partIndex))
@@ -509,13 +518,27 @@ function Messages({ messages, isLoading, sessionId, streamingMessage, theme = ge
509
518
  //#endregion
510
519
  //#region src/components/SelectPrompt/SelectPrompt.tsx
511
520
  function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
521
+ const [isInteractive, setIsInteractive] = useState(false);
522
+ useEffect(() => {
523
+ let isMounted = true;
524
+ tick().then(() => {
525
+ // v8 ignore next
526
+ if (isMounted) setIsInteractive(true);
527
+ });
528
+ return () => {
529
+ isMounted = false;
530
+ };
531
+ }, []);
512
532
  useInput((input, key) => {
513
533
  if (key.escape || key.ctrl && input === "c") onCancel?.();
514
534
  });
515
535
  return /* @__PURE__ */ jsxs(Box, {
516
536
  borderStyle,
517
537
  flexDirection: "column",
518
- children: [children, /* @__PURE__ */ jsx(Select, { ...selectProps })]
538
+ children: [children, /* @__PURE__ */ jsx(Select, {
539
+ ...selectProps,
540
+ isDisabled: selectProps.isDisabled ?? !isInteractive
541
+ })]
519
542
  });
520
543
  }
521
544
  //#endregion
@@ -778,6 +801,7 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
778
801
  setCursorPosition(value.length);
779
802
  return;
780
803
  }
804
+ if (key.ctrl) return;
781
805
  // v8 ignore start
782
806
  if (input) {
783
807
  onChange(value.slice(0, cursorPosition) + input + value.slice(cursorPosition));
@@ -814,6 +838,74 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
814
838
  });
815
839
  }
816
840
  //#endregion
841
+ //#region src/components/Chat/attachments.ts
842
+ var IMAGE_EXTENSIONS = new Set([
843
+ ".avif",
844
+ ".bmp",
845
+ ".gif",
846
+ ".heic",
847
+ ".heif",
848
+ ".jpeg",
849
+ ".jpg",
850
+ ".png",
851
+ ".tif",
852
+ ".tiff",
853
+ ".webp"
854
+ ]);
855
+ var PATH_CANDIDATE_PATTERN = /"([^"\n\r]+\.(?:avif|bmp|gif|heic|heif|jpeg|jpg|png|tif|tiff|webp))"|'([^'\n\r]+\.(?:avif|bmp|gif|heic|heif|jpeg|jpg|png|tif|tiff|webp))'|([^\s"'`]+\.(?:avif|bmp|gif|heic|heif|jpeg|jpg|png|tif|tiff|webp))/gi;
856
+ function normalizeCandidatePath(value) {
857
+ return value.replaceAll(String.raw`\ `, " ");
858
+ }
859
+ function isPathLikeCandidate(candidate, matchedValue) {
860
+ return matchedValue.startsWith("\"") || matchedValue.startsWith("'") || candidate.includes("/") || candidate.includes("\\") || candidate.startsWith(".");
861
+ }
862
+ function getAttachmentLabel(path) {
863
+ return basename(path);
864
+ }
865
+ function isReadableImagePath(path) {
866
+ const normalizedPath = normalizeCandidatePath(path);
867
+ const extension = extname(normalizedPath).toLowerCase();
868
+ if (!IMAGE_EXTENSIONS.has(extension)) return false;
869
+ const resolvedPath = isAbsolute(normalizedPath) ? normalizedPath : resolve(normalizedPath);
870
+ if (!existsSync(resolvedPath)) return false;
871
+ try {
872
+ return statSync(resolvedPath).isFile();
873
+ } catch {
874
+ // v8 ignore next
875
+ return false;
876
+ }
877
+ }
878
+ function resolveAttachmentPath(path) {
879
+ const normalizedPath = normalizeCandidatePath(path);
880
+ return isAbsolute(normalizedPath) ? normalizedPath : resolve(normalizedPath);
881
+ }
882
+ function extractImageAttachments(input) {
883
+ const attachments = [];
884
+ const segments = [];
885
+ let lastIndex = 0;
886
+ for (const match of input.matchAll(PATH_CANDIDATE_PATTERN)) {
887
+ const matchedValue = match[0];
888
+ const candidate = match.slice(1).find((value) => Boolean(value));
889
+ // v8 ignore start
890
+ if (candidate === void 0) continue;
891
+ // v8 ignore stop
892
+ if (!isPathLikeCandidate(candidate, matchedValue)) continue;
893
+ if (!isReadableImagePath(candidate)) continue;
894
+ attachments.push(resolveAttachmentPath(candidate));
895
+ segments.push(input.slice(lastIndex, match.index));
896
+ lastIndex = match.index + matchedValue.length;
897
+ }
898
+ if (!attachments.length) return {
899
+ attachments,
900
+ remainingInput: input
901
+ };
902
+ segments.push(input.slice(lastIndex));
903
+ return {
904
+ attachments,
905
+ remainingInput: segments.join("").replaceAll(/\s{2,}/g, " ").trim()
906
+ };
907
+ }
908
+ //#endregion
817
909
  //#region src/components/Chat/CommandMenu.tsx
818
910
  function getMatchingCommands(input) {
819
911
  const normalizedInput = input.trim().toLowerCase();
@@ -1004,28 +1096,81 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
1004
1096
  function hasFileSuggestionQuery(input) {
1005
1097
  return /(^|.)@\S+/.test(input);
1006
1098
  }
1007
- function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, onSubmit }) {
1099
+ function toAttachment(path, index, isTemp = false) {
1100
+ return {
1101
+ id: `${path}-${String(index)}`,
1102
+ isTemp,
1103
+ label: getAttachmentLabel(path),
1104
+ path
1105
+ };
1106
+ }
1107
+ function cleanupAttachments(attachments) {
1108
+ for (const attachment of attachments) if (attachment.isTemp) removeClipboardImage(attachment.path);
1109
+ }
1110
+ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, onSubmit, theme = getTheme() }) {
1008
1111
  const { exit } = useApp();
1009
1112
  const [history, setHistory] = useState(sessionHistory);
1010
1113
  const [historyIndex, setHistoryIndex] = useState(null);
1011
1114
  const [input, setInput] = useState("");
1012
1115
  const [cursorPosition, setCursorPosition] = useState(void 0);
1116
+ const [attachments, setAttachments] = useState([]);
1117
+ const [error, setError] = useState(null);
1013
1118
  const fileSuggestionRef = useRef(null);
1119
+ const nextClipboardImageRef = useRef(1);
1120
+ const hasAttachments = attachments.length > 0;
1014
1121
  useEffect(() => {
1015
1122
  setHistory(sessionHistory);
1016
1123
  setHistoryIndex(null);
1017
1124
  setInput("");
1018
1125
  setCursorPosition(void 0);
1126
+ setError(null);
1019
1127
  fileSuggestionRef.current = null;
1128
+ nextClipboardImageRef.current = 1;
1129
+ setAttachments((previousAttachments) => {
1130
+ cleanupAttachments(previousAttachments);
1131
+ return [];
1132
+ });
1020
1133
  }, [sessionHistory]);
1021
- const resetInput = useCallback(() => {
1134
+ const resetInput = useCallback((deleteTempAttachments = false) => {
1022
1135
  setInput("");
1023
1136
  setCursorPosition(void 0);
1024
1137
  setHistoryIndex(null);
1138
+ setError(null);
1139
+ if (deleteTempAttachments) {
1140
+ setAttachments((previousAttachments) => {
1141
+ cleanupAttachments(previousAttachments);
1142
+ return [];
1143
+ });
1144
+ nextClipboardImageRef.current = 1;
1145
+ return;
1146
+ }
1147
+ setAttachments([]);
1148
+ }, []);
1149
+ const removeLastAttachment = useCallback(() => {
1150
+ setAttachments((previousAttachments) => {
1151
+ const removedAttachment = previousAttachments.at(-1);
1152
+ if (removedAttachment?.isTemp) removeClipboardImage(removedAttachment.path);
1153
+ return previousAttachments.slice(0, -1);
1154
+ });
1155
+ setError(null);
1156
+ }, []);
1157
+ const stageAttachments = useCallback((paths, isTemp = false) => {
1158
+ setAttachments((previousAttachments) => [...previousAttachments, ...paths.map((path, index) => toAttachment(path, previousAttachments.length + index, isTemp))]);
1159
+ setError(null);
1025
1160
  }, []);
1161
+ const attachClipboardImage = useCallback(() => {
1162
+ try {
1163
+ const path = saveClipboardImage(`image-${String(nextClipboardImageRef.current)}`);
1164
+ nextClipboardImageRef.current += 1;
1165
+ stageAttachments([path], true);
1166
+ } catch (error) {
1167
+ setError(error instanceof Error ? error.message : String(error));
1168
+ }
1169
+ }, [stageAttachments]);
1026
1170
  const handleSelectFileSuggestion = useCallback((nextInput) => {
1027
1171
  setInput(nextInput.value);
1028
1172
  setCursorPosition(nextInput.cursorPosition);
1173
+ setError(null);
1029
1174
  }, []);
1030
1175
  const handleFileSuggestionChange = useCallback((nextInput) => {
1031
1176
  if (nextInput) {
@@ -1046,21 +1191,40 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1046
1191
  } else fileSuggestionRef.current = null;
1047
1192
  }, [input]);
1048
1193
  const handleInputChange = useCallback((nextInput) => {
1194
+ if (nextInput.length - input.length > 1) {
1195
+ const { attachments: nextAttachments, remainingInput } = extractImageAttachments(nextInput);
1196
+ if (nextAttachments.length) {
1197
+ stageAttachments(nextAttachments);
1198
+ setInput(remainingInput);
1199
+ setCursorPosition(remainingInput.length);
1200
+ setHistoryIndex(null);
1201
+ return;
1202
+ }
1203
+ }
1049
1204
  setInput(nextInput);
1050
1205
  setHistoryIndex(null);
1051
- }, []);
1206
+ setError(null);
1207
+ }, [input, stageAttachments]);
1052
1208
  const submitAndReset = useCallback((input) => {
1053
1209
  const trimmedInput = input.trim();
1054
- if (!trimmedInput) return;
1055
- onSubmit(trimmedInput);
1056
- if (!trimmedInput.startsWith("/")) setHistory((previousHistory) => [...previousHistory, trimmedInput]);
1057
- resetInput();
1210
+ const imagePaths = attachments.map(({ path }) => path);
1211
+ if (!trimmedInput && !imagePaths.length) return;
1212
+ onSubmit({
1213
+ content: trimmedInput,
1214
+ ...imagePaths.length ? { images: imagePaths } : {}
1215
+ });
1216
+ if (trimmedInput && !trimmedInput.startsWith("/")) setHistory((previousHistory) => [...previousHistory, trimmedInput]);
1217
+ resetInput(trimmedInput.startsWith("/"));
1058
1218
  fileSuggestionRef.current = null;
1059
- }, [onSubmit, resetInput]);
1219
+ }, [
1220
+ attachments,
1221
+ onSubmit,
1222
+ resetInput
1223
+ ]);
1060
1224
  const showCommandMenu = input.startsWith("/");
1061
1225
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
1062
1226
  const handleHistoryNavigation = useCallback((direction) => {
1063
- if (!history.length || showFileSuggestions) return;
1227
+ if (!history.length || showFileSuggestions || hasAttachments) return;
1064
1228
  if (direction === "up") {
1065
1229
  if (historyIndex === null) {
1066
1230
  if (input) return;
@@ -1092,21 +1256,22 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1092
1256
  setInput(nextInput);
1093
1257
  setCursorPosition(nextInput.length);
1094
1258
  }, [
1259
+ hasAttachments,
1095
1260
  history,
1096
1261
  historyIndex,
1097
1262
  input,
1098
1263
  showFileSuggestions
1099
1264
  ]);
1100
- const handleSubmitText = useCallback((input) => {
1101
- if (input.startsWith("/")) return;
1102
- if (hasFileSuggestionQuery(input)) {
1265
+ const handleSubmitText = useCallback((value) => {
1266
+ if (value.startsWith("/")) return;
1267
+ if (hasFileSuggestionQuery(value)) {
1103
1268
  if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
1104
1269
  return;
1105
1270
  }
1106
- submitAndReset(input);
1271
+ submitAndReset(value);
1107
1272
  }, [handleSelectFileSuggestion, submitAndReset]);
1108
- const handleSubmitCommand = useCallback((input) => {
1109
- if (LIST.find(({ name }) => name === input)) submitAndReset(input);
1273
+ const handleSubmitCommand = useCallback((value) => {
1274
+ if (LIST.find(({ name }) => name === value)) submitAndReset(value);
1110
1275
  }, [submitAndReset]);
1111
1276
  useInput((inputKey, key) => {
1112
1277
  const isCtrlC = key.ctrl && inputKey === "c";
@@ -1114,6 +1279,14 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1114
1279
  if (key.escape || isCtrlC) onInterrupt?.();
1115
1280
  return;
1116
1281
  }
1282
+ if (key.ctrl && inputKey === "v") {
1283
+ attachClipboardImage();
1284
+ return;
1285
+ }
1286
+ if ((key.backspace || key.delete || inputKey === "") && !input) {
1287
+ if (hasAttachments) removeLastAttachment();
1288
+ return;
1289
+ }
1117
1290
  if (isCtrlC) {
1118
1291
  if (input) {
1119
1292
  resetInput();
@@ -1127,18 +1300,35 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1127
1300
  }
1128
1301
  if (key.downArrow) handleHistoryNavigation("down");
1129
1302
  });
1303
+ const attachmentPrefix = attachments.map(({ label }) => `[${label}]`).join(" ");
1304
+ const wrapIndent = 2 + (attachmentPrefix ? attachmentPrefix.length + 1 : 0);
1130
1305
  return /* @__PURE__ */ jsxs(Box, {
1131
1306
  flexDirection: "column",
1132
1307
  children: [
1133
- /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
1134
- value: input,
1135
- isDisabled,
1136
- cursorPosition,
1137
- wrapIndent: 2,
1138
- onChange: handleInputChange,
1139
- onSubmit: handleSubmitText,
1140
- placeholder: "Ask anything... (/ commands, @ files)"
1141
- })] }),
1308
+ error && /* @__PURE__ */ jsx(Box, {
1309
+ marginBottom: 1,
1310
+ marginX: 2,
1311
+ children: /* @__PURE__ */ jsx(Text, {
1312
+ color: theme.colors.error,
1313
+ children: error
1314
+ })
1315
+ }),
1316
+ /* @__PURE__ */ jsxs(Box, { children: [
1317
+ /* @__PURE__ */ jsx(Text, { children: "> " }),
1318
+ hasAttachments && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
1319
+ color: theme.colors.accent,
1320
+ children: attachmentPrefix
1321
+ }), /* @__PURE__ */ jsx(Text, { children: " " })] }),
1322
+ /* @__PURE__ */ jsx(TextInput, {
1323
+ value: input,
1324
+ isDisabled,
1325
+ cursorPosition,
1326
+ wrapIndent,
1327
+ onChange: handleInputChange,
1328
+ onSubmit: handleSubmitText,
1329
+ placeholder: hasAttachments ? void 0 : "Ask anything... (/ commands, @ files, Ctrl+V images)"
1330
+ })
1331
+ ] }),
1142
1332
  showCommandMenu && /* @__PURE__ */ jsx(CommandMenu, {
1143
1333
  input,
1144
1334
  onSubmit: handleSubmitCommand
@@ -1480,10 +1670,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1480
1670
  messages,
1481
1671
  processStream
1482
1672
  ]);
1483
- const handleSubmit = useCallback(async (value) => {
1673
+ const handleSubmit = useCallback(async ({ content, images }) => {
1484
1674
  setInterruptReason(null);
1485
- const userContent = value.trim();
1486
- if (!userContent) return;
1675
+ const userContent = content.trim();
1676
+ if (!userContent && !images?.length) return;
1487
1677
  if (userContent.startsWith("/")) {
1488
1678
  onCommand(userContent);
1489
1679
  return;
@@ -1491,7 +1681,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1491
1681
  setIsLoading(true);
1492
1682
  const userMessage = {
1493
1683
  role: USER,
1494
- content: userContent
1684
+ content: userContent,
1685
+ ...images?.length ? { images } : {}
1495
1686
  };
1496
1687
  const updatedMessages = [...messages, userMessage];
1497
1688
  setMessages(updatedMessages);
@@ -1537,7 +1728,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1537
1728
  history,
1538
1729
  isDisabled: isLoading,
1539
1730
  onInterrupt: handleInterrupt,
1540
- onSubmit: handleSubmit
1731
+ onSubmit: handleSubmit,
1732
+ theme
1541
1733
  })
1542
1734
  })
1543
1735
  ]
@@ -1874,6 +2066,7 @@ function ModelDeleteConfirmView({ deleteCandidate, isDeleting, notice, theme, on
1874
2066
  //#endregion
1875
2067
  //#region src/components/ModelManager/ModelDeleteView.tsx
1876
2068
  function ModelDeleteView({ currentModel, installedModels, isLoading, notice, theme, onCancel, onSelect }) {
2069
+ // v8 ignore next
1877
2070
  if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1878
2071
  return /* @__PURE__ */ jsxs(SelectPrompt, {
1879
2072
  options: [...buildInstalledModelOptions(installedModels.filter((model) => model !== currentModel), currentModel), BACK],
@@ -2372,6 +2565,24 @@ var ACTION = {
2372
2565
  OPEN_PREFIX: "open:"
2373
2566
  };
2374
2567
  var SESSION_LABEL_PADDING = 4;
2568
+ var MAIN_OPTIONS = [
2569
+ {
2570
+ label: "New session",
2571
+ value: ACTION.NEW
2572
+ },
2573
+ {
2574
+ label: "Open session",
2575
+ value: ACTION.OPEN_MENU
2576
+ },
2577
+ {
2578
+ label: "Delete session",
2579
+ value: ACTION.DELETE_MENU
2580
+ },
2581
+ {
2582
+ label: "Close",
2583
+ value: ACTION.CLOSE
2584
+ }
2585
+ ];
2375
2586
  function truncate(value, maxLength) {
2376
2587
  return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
2377
2588
  }
@@ -2384,34 +2595,29 @@ function formatSessionLabel(session, maxWidth, prefix = "") {
2384
2595
  function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, theme = getTheme() }) {
2385
2596
  const [view, setView] = useState("main");
2386
2597
  const [error, setError] = useState();
2387
- const [, refreshSessionList] = useState(0);
2598
+ const [sessionListVersion, refreshSessionList] = useState(0);
2388
2599
  const { stdout } = useStdout();
2389
2600
  const sessions = listSessions();
2390
2601
  const maxLabelWidth = Math.max(1, stdout.columns - SESSION_LABEL_PADDING);
2391
- const options = view === "open" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2392
- label: formatSessionLabel(session, maxLabelWidth),
2393
- value: `${ACTION.OPEN_PREFIX}${session.id}`
2394
- })), BACK] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2395
- label: formatSessionLabel(session, maxLabelWidth, "Delete "),
2396
- value: `${ACTION.DELETE_PREFIX}${session.id}`
2397
- })), BACK] : [
2398
- {
2399
- label: "New session",
2400
- value: ACTION.NEW
2401
- },
2402
- {
2403
- label: "Open session",
2404
- value: ACTION.OPEN_MENU
2405
- },
2406
- {
2407
- label: "Delete session",
2408
- value: ACTION.DELETE_MENU
2409
- },
2410
- {
2411
- label: "Close",
2412
- value: ACTION.CLOSE
2602
+ const options = useMemo(() => {
2603
+ switch (view) {
2604
+ case "open": return [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2605
+ label: formatSessionLabel(session, maxLabelWidth),
2606
+ value: `${ACTION.OPEN_PREFIX}${session.id}`
2607
+ })), BACK];
2608
+ case "delete": return [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2609
+ label: formatSessionLabel(session, maxLabelWidth, "Delete "),
2610
+ value: `${ACTION.DELETE_PREFIX}${session.id}`
2611
+ })), BACK];
2612
+ default: return MAIN_OPTIONS;
2413
2613
  }
2414
- ];
2614
+ }, [
2615
+ currentSessionId,
2616
+ maxLabelWidth,
2617
+ sessionListVersion,
2618
+ sessions,
2619
+ view
2620
+ ]);
2415
2621
  const handleChange = useCallback((value) => {
2416
2622
  switch (true) {
2417
2623
  case value === ACTION.CLOSE:
@@ -2469,7 +2675,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, th
2469
2675
  options,
2470
2676
  onCancel: onClose,
2471
2677
  onChange: handleChange
2472
- }, `${view}:${String(sessions.length)}`)
2678
+ })
2473
2679
  ]
2474
2680
  });
2475
2681
  }
@@ -2784,7 +2990,10 @@ function ReadinessCheck({ errorMessage, onCommand, setupState, theme = getTheme(
2784
2990
  }), getMessage(setupState, errorMessage)]
2785
2991
  }), /* @__PURE__ */ jsx(ChatInput, {
2786
2992
  history: [],
2787
- onSubmit: onCommand
2993
+ onSubmit: ({ content }) => {
2994
+ onCommand(content);
2995
+ },
2996
+ theme
2788
2997
  })]
2789
2998
  });
2790
2999
  }
package/dist/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
3
  import cac from "cac";
4
- import { homedir } from "node:os";
4
+ import { homedir, tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
+ import { exec, execFileSync, spawnSync } from "node:child_process";
7
+ import { randomUUID } from "node:crypto";
6
8
  import { Ollama } from "ollama";
7
9
  import { v7 } from "uuid";
8
- import { exec } from "node:child_process";
9
10
  import { promisify } from "node:util";
10
11
  //#region src/constants/command.ts
11
12
  var LIST$1 = [
@@ -37,7 +38,7 @@ var LIST$1 = [
37
38
  //#endregion
38
39
  //#region package.json
39
40
  var name = "code-ollama";
40
- var version = "0.18.2";
41
+ var version = "0.19.1";
41
42
  //#endregion
42
43
  //#region src/constants/package.ts
43
44
  var NAME = name;
@@ -326,6 +327,114 @@ function withSystemMessage(messages) {
326
327
  return [systemMessage, ...messages];
327
328
  }
328
329
  //#endregion
330
+ //#region src/utils/clipboard.ts
331
+ var TEMP_IMAGES_DIRECTORY = join(tmpdir(), "code-ollama", "images");
332
+ var WINDOWS_CLIPBOARD_EXIT_CODE = 11;
333
+ function ensureTempDirectory(directory) {
334
+ mkdirSync(directory, { recursive: true });
335
+ return directory;
336
+ }
337
+ function buildTargetPath(directory, extension) {
338
+ const uniqueName = `${randomUUID()}.${extension}`;
339
+ return join(ensureTempDirectory(directory), uniqueName);
340
+ }
341
+ function readMacClipboardImage(path) {
342
+ execFileSync("osascript", ["-e", `
343
+ set outputPath to POSIX file ${JSON.stringify(path)}
344
+ try
345
+ set clipboardData to the clipboard as «class PNGf»
346
+ on error
347
+ error "Clipboard does not contain an image"
348
+ end try
349
+ set fileHandle to open for access outputPath with write permission
350
+ try
351
+ set eof fileHandle to 0
352
+ write clipboardData to fileHandle
353
+ on error errorMessage
354
+ close access fileHandle
355
+ error errorMessage
356
+ end try
357
+ close access fileHandle
358
+ `], { stdio: "ignore" });
359
+ }
360
+ function getClipboardErrorMessage(error) {
361
+ if (error.message.includes("Clipboard does not contain an image")) return "Clipboard does not contain an image.";
362
+ return "Clipboard image paste failed. Paste an image path instead.";
363
+ }
364
+ function readWindowsClipboardImage(path) {
365
+ execFileSync("powershell", [
366
+ "-NoProfile",
367
+ "-Command",
368
+ `
369
+ Add-Type -AssemblyName System.Windows.Forms
370
+ Add-Type -AssemblyName System.Drawing
371
+ $image = [Windows.Forms.Clipboard]::GetImage()
372
+ if ($null -eq $image) { exit ${String(WINDOWS_CLIPBOARD_EXIT_CODE)} }
373
+ $image.Save($args[0], [System.Drawing.Imaging.ImageFormat]::Png)
374
+ `,
375
+ path
376
+ ], { stdio: "ignore" });
377
+ }
378
+ function readLinuxClipboardImage(directory) {
379
+ const wlPng = spawnSync("wl-paste", [
380
+ "--no-newline",
381
+ "--type",
382
+ "image/png"
383
+ ], { encoding: "buffer" });
384
+ if (wlPng.status === 0 && wlPng.stdout.length > 0) {
385
+ const path = buildTargetPath(directory, "png");
386
+ writeClipboardImageFile(path, wlPng.stdout);
387
+ return path;
388
+ }
389
+ const xclipPng = spawnSync("xclip", [
390
+ "-selection",
391
+ "clipboard",
392
+ "-t",
393
+ "image/png",
394
+ "-o"
395
+ ], { encoding: "buffer" });
396
+ if (xclipPng.status === 0 && xclipPng.stdout.length > 0) {
397
+ const path = buildTargetPath(directory, "png");
398
+ writeClipboardImageFile(path, xclipPng.stdout);
399
+ return path;
400
+ }
401
+ throw new Error("Clipboard image paste is unavailable. Paste an image path instead.");
402
+ }
403
+ function writeClipboardImageFile(path, data) {
404
+ writeFileSync(path, data, {
405
+ flag: "wx",
406
+ mode: 384
407
+ });
408
+ }
409
+ function saveClipboardImage(baseName, directory = TEMP_IMAGES_DIRECTORY) {
410
+ try {
411
+ switch (process.platform) {
412
+ case "darwin": {
413
+ const path = buildTargetPath(directory, "png");
414
+ readMacClipboardImage(path);
415
+ return path;
416
+ }
417
+ case "win32": {
418
+ const path = buildTargetPath(directory, "png");
419
+ readWindowsClipboardImage(path);
420
+ return path;
421
+ }
422
+ case "linux": return readLinuxClipboardImage(directory);
423
+ default: throw new Error("Clipboard image paste is not supported on this platform. Paste an image path instead.");
424
+ }
425
+ } catch (error) {
426
+ if (error instanceof Error && "status" in error && error.status === WINDOWS_CLIPBOARD_EXIT_CODE) throw new Error("Clipboard does not contain an image.", { cause: error });
427
+ const path = join(directory, `${baseName}.png`);
428
+ if (existsSync(path)) rmSync(path, { force: true });
429
+ if (error instanceof Error) throw new Error(getClipboardErrorMessage(error), { cause: error });
430
+ // v8 ignore next
431
+ throw error;
432
+ }
433
+ }
434
+ function removeClipboardImage(path) {
435
+ if (existsSync(path)) rmSync(path, { force: true });
436
+ }
437
+ //#endregion
329
438
  //#region src/utils/config.ts
330
439
  var CONFIG_PATH = join(DIRECTORY, "config.json");
331
440
  var DEFAULT_HOST = "http://localhost:11434";
@@ -584,6 +693,9 @@ function writeError(text) {
584
693
  process.stderr.write(text);
585
694
  }
586
695
  //#endregion
696
+ //#region src/utils/time.ts
697
+ var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
698
+ //#endregion
587
699
  //#region src/utils/tools/definitions.ts
588
700
  /**
589
701
  * Helper to define tool parameters
@@ -1042,6 +1154,29 @@ function formatSearchResults(source, results, note) {
1042
1154
  }
1043
1155
  //#endregion
1044
1156
  //#region src/utils/tools/dispatcher.ts
1157
+ var REQUIRED_STRING_ARGS = {
1158
+ [READ_FILE]: ["path"],
1159
+ [WRITE_FILE]: ["path", "content"],
1160
+ [EDIT_FILE]: [
1161
+ "path",
1162
+ "oldText",
1163
+ "newText"
1164
+ ],
1165
+ [RUN_SHELL]: ["command"],
1166
+ [LIST_DIR]: ["path"],
1167
+ [GREP_SEARCH]: ["pattern", "path"],
1168
+ [VIEW_RANGE]: ["path"],
1169
+ [WEB_SEARCH]: ["query"],
1170
+ [WEB_FETCH]: ["url"]
1171
+ };
1172
+ function validateArgs(name, args) {
1173
+ const required = REQUIRED_STRING_ARGS[name] ?? [];
1174
+ const received = Object.keys(args).join(", ") || "none";
1175
+ for (const key of required) if (typeof args[key] !== "string") return {
1176
+ content: "",
1177
+ error: `Missing required argument: ${key} (received keys: ${received})`
1178
+ };
1179
+ }
1045
1180
  /**
1046
1181
  * Execute a tool by name with arguments
1047
1182
  */
@@ -1050,16 +1185,19 @@ async function executeTool(name, args, options) {
1050
1185
  content: "",
1051
1186
  error: `Tool not allowed: ${name}`
1052
1187
  };
1188
+ const invalid = validateArgs(name, args);
1189
+ if (invalid) return invalid;
1190
+ const stringArgs = args;
1053
1191
  switch (name) {
1054
- case READ_FILE: return readFile(args.path);
1055
- case WRITE_FILE: return writeFile(args.path, args.content);
1056
- case EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
1057
- case RUN_SHELL: return runShell(args.command);
1058
- case LIST_DIR: return listDir(args.path);
1059
- case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
1060
- case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
1061
- case WEB_SEARCH: return await webSearch(args.query);
1062
- case WEB_FETCH: return await webFetch(args.url);
1192
+ case READ_FILE: return readFile(stringArgs.path);
1193
+ case WRITE_FILE: return writeFile(stringArgs.path, stringArgs.content);
1194
+ case EDIT_FILE: return editFile(stringArgs.path, stringArgs.oldText, stringArgs.newText);
1195
+ case RUN_SHELL: return runShell(stringArgs.command);
1196
+ case LIST_DIR: return listDir(stringArgs.path);
1197
+ case GREP_SEARCH: return await grepSearch(stringArgs.pattern, stringArgs.path);
1198
+ case VIEW_RANGE: return viewRange(stringArgs.path, args.start, args.end);
1199
+ case WEB_SEARCH: return await webSearch(stringArgs.query);
1200
+ case WEB_FETCH: return await webFetch(stringArgs.url);
1063
1201
  default: return {
1064
1202
  content: "",
1065
1203
  error: `Unknown tool: ${name}`
@@ -1130,7 +1268,7 @@ async function main(args = process.argv.slice(2)) {
1130
1268
  else await launchTui();
1131
1269
  }
1132
1270
  async function launchTui(sessionId) {
1133
- const { renderApp } = await import("./assets/tui-XD0Ekj5m.js");
1271
+ const { renderApp } = await import("./assets/tui-DDbMjcMS.js");
1134
1272
  reset();
1135
1273
  renderApp(sessionId);
1136
1274
  }
@@ -1146,4 +1284,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
1146
1284
  if (isEntrypoint()) main();
1147
1285
  // v8 ignore stop
1148
1286
  //#endregion
1149
- export { ASSISTANT as A, APPROVE as B, saveConfig as C, WARNING as D, HEADER_PREFIX as E, CATALOG as F, VERSION as H, AUTO as I, LABEL as L, USER as M, PLAN_GENERATION_INSTRUCTION as N, LIST as O, BACK as P, PLAN as R, loadConfig as S, withSystemMessage as T, LIST$1 as U, REJECT as V, checkHealth as _, color as a, pullModel as b, createSession as c, listSessions as d, loadSession as f, setClearHandler as g, reset as h, WRITE_TOOLS as i, SYSTEM as j, getTheme as k, deleteSession as l, clear as m, main, READ_TOOLS as n, write as o, updateSessionModel as p, TOOLS as r, appendMessage as s, executeTool as t, deleteSessionIfEmpty as u, deleteModel as v, resetSystemMessage as w, streamChat as x, listModels as y, SAFE as z };
1287
+ export { WARNING as A, LABEL as B, loadConfig as C, resetSystemMessage as D, saveClipboardImage as E, USER as F, VERSION as G, SAFE as H, PLAN_GENERATION_INSTRUCTION as I, LIST$1 as K, BACK as L, getTheme as M, ASSISTANT as N, withSystemMessage as O, SYSTEM as P, CATALOG as R, streamChat as S, removeClipboardImage as T, APPROVE as U, PLAN as V, REJECT as W, setClearHandler as _, tick as a, listModels as b, appendMessage as c, deleteSessionIfEmpty as d, listSessions as f, reset as g, clear as h, WRITE_TOOLS as i, LIST as j, HEADER_PREFIX as k, createSession as l, updateSessionModel as m, main, READ_TOOLS as n, color as o, loadSession as p, TOOLS as r, write as s, executeTool as t, deleteSession as u, checkHealth as v, saveConfig as w, pullModel as x, deleteModel as y, AUTO as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.18.2",
3
+ "version": "0.19.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",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "build": "vite build",
13
- "start": "tsx --tsconfig tsconfig.test.json src/cli.ts",
13
+ "start": "tsx src/cli.ts",
14
14
  "clean": "rm -rf coverage dist docs",
15
15
  "lint": "eslint .",
16
16
  "lint:fix": "npm run lint -- --fix",
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "@inkjs/ui": "2.0.0",
43
- "@shikijs/cli": "4.0.2",
43
+ "@shikijs/cli": "4.1.0",
44
44
  "cac": "7.0.0",
45
45
  "ink": "7.0.3",
46
46
  "marked": "15.0.12",
@@ -53,9 +53,9 @@
53
53
  "@commitlint/cli": "21.0.1",
54
54
  "@commitlint/config-conventional": "21.0.1",
55
55
  "@eslint/js": "10.0.1",
56
- "@types/node": "25.9.0",
57
- "@types/react": "19.2.14",
58
- "@vitest/coverage-v8": "4.1.6",
56
+ "@types/node": "25.9.1",
57
+ "@types/react": "19.2.15",
58
+ "@vitest/coverage-v8": "4.1.7",
59
59
  "eslint": "10.4.0",
60
60
  "eslint-plugin-prettier": "5.5.5",
61
61
  "eslint-plugin-simple-import-sort": "13.0.0",
@@ -65,11 +65,11 @@
65
65
  "lint-staged": "17.0.5",
66
66
  "prettier": "3.8.3",
67
67
  "publint": "0.3.21",
68
- "tsx": "4.22.2",
68
+ "tsx": "4.22.3",
69
69
  "typescript": "6.0.3",
70
70
  "typescript-eslint": "8.59.4",
71
- "vite": "8.0.13",
72
- "vitest": "4.1.6"
71
+ "vite": "8.0.14",
72
+ "vitest": "4.1.7"
73
73
  },
74
74
  "files": [
75
75
  "dist/"