code-ollama 0.18.2 → 0.19.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 { 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";
@@ -416,6 +416,26 @@ function Message({ message, isStreaming = false, theme }) {
416
416
  children: message.content
417
417
  })
418
418
  });
419
+ if (isUser) {
420
+ // v8 ignore start
421
+ const attachmentPrefix = (message.images ?? []).map((path) => `[${path.split(/[\\/]/).at(-1) ?? path}]`).join(" ");
422
+ // v8 ignore stop
423
+ return /* @__PURE__ */ jsx(Box, {
424
+ flexDirection: "column",
425
+ marginBottom: 1,
426
+ children: /* @__PURE__ */ jsxs(Text, {
427
+ color: messageColor,
428
+ children: [
429
+ "> ",
430
+ attachmentPrefix ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
431
+ color: theme.colors.accent,
432
+ children: attachmentPrefix
433
+ }), message.content ? " " : ""] }) : null,
434
+ message.content
435
+ ]
436
+ })
437
+ });
438
+ }
419
439
  const segments = parseContent(message.content);
420
440
  const availableWidth = getAssistantContentWidth(stdout.columns);
421
441
  if (stickyHeightRef.current.columns !== stdout.columns) stickyHeightRef.current = {
@@ -436,11 +456,7 @@ function Message({ message, isStreaming = false, theme }) {
436
456
  flexDirection: "column",
437
457
  marginBottom: 1,
438
458
  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, {
459
+ if (segment.type === "code") return /* @__PURE__ */ jsx(Box, {
444
460
  marginX: 2,
445
461
  children: /* @__PURE__ */ jsx(CodeBlock, {
446
462
  code: segment.content,
@@ -461,17 +477,13 @@ function Message({ message, isStreaming = false, theme }) {
461
477
  })
462
478
  }, index);
463
479
  }
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, {
480
+ return /* @__PURE__ */ jsx(Box, {
472
481
  flexDirection: "column",
473
482
  marginX: 2,
474
- children: textParts.map((part, partIndex) => /* @__PURE__ */ jsx(Markdown, {
483
+ children: [{
484
+ type: "markdown",
485
+ content: segment.content
486
+ }].map((part, partIndex) => /* @__PURE__ */ jsx(Markdown, {
475
487
  content: part.content,
476
488
  theme
477
489
  }, partIndex))
@@ -509,13 +521,27 @@ function Messages({ messages, isLoading, sessionId, streamingMessage, theme = ge
509
521
  //#endregion
510
522
  //#region src/components/SelectPrompt/SelectPrompt.tsx
511
523
  function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
524
+ const [isInteractive, setIsInteractive] = useState(false);
525
+ useEffect(() => {
526
+ let isMounted = true;
527
+ tick().then(() => {
528
+ // v8 ignore next
529
+ if (isMounted) setIsInteractive(true);
530
+ });
531
+ return () => {
532
+ isMounted = false;
533
+ };
534
+ }, []);
512
535
  useInput((input, key) => {
513
536
  if (key.escape || key.ctrl && input === "c") onCancel?.();
514
537
  });
515
538
  return /* @__PURE__ */ jsxs(Box, {
516
539
  borderStyle,
517
540
  flexDirection: "column",
518
- children: [children, /* @__PURE__ */ jsx(Select, { ...selectProps })]
541
+ children: [children, /* @__PURE__ */ jsx(Select, {
542
+ ...selectProps,
543
+ isDisabled: selectProps.isDisabled ?? !isInteractive
544
+ })]
519
545
  });
520
546
  }
521
547
  //#endregion
@@ -778,6 +804,7 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
778
804
  setCursorPosition(value.length);
779
805
  return;
780
806
  }
807
+ if (key.ctrl) return;
781
808
  // v8 ignore start
782
809
  if (input) {
783
810
  onChange(value.slice(0, cursorPosition) + input + value.slice(cursorPosition));
@@ -814,6 +841,74 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
814
841
  });
815
842
  }
816
843
  //#endregion
844
+ //#region src/components/Chat/attachments.ts
845
+ var IMAGE_EXTENSIONS = new Set([
846
+ ".avif",
847
+ ".bmp",
848
+ ".gif",
849
+ ".heic",
850
+ ".heif",
851
+ ".jpeg",
852
+ ".jpg",
853
+ ".png",
854
+ ".tif",
855
+ ".tiff",
856
+ ".webp"
857
+ ]);
858
+ 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;
859
+ function normalizeCandidatePath(value) {
860
+ return value.replaceAll(String.raw`\ `, " ");
861
+ }
862
+ function isPathLikeCandidate(candidate, matchedValue) {
863
+ return matchedValue.startsWith("\"") || matchedValue.startsWith("'") || candidate.includes("/") || candidate.includes("\\") || candidate.startsWith(".");
864
+ }
865
+ function getAttachmentLabel(path) {
866
+ return basename(path);
867
+ }
868
+ function isReadableImagePath(path) {
869
+ const normalizedPath = normalizeCandidatePath(path);
870
+ const extension = extname(normalizedPath).toLowerCase();
871
+ if (!IMAGE_EXTENSIONS.has(extension)) return false;
872
+ const resolvedPath = isAbsolute(normalizedPath) ? normalizedPath : resolve(normalizedPath);
873
+ if (!existsSync(resolvedPath)) return false;
874
+ try {
875
+ return statSync(resolvedPath).isFile();
876
+ } catch {
877
+ // v8 ignore next
878
+ return false;
879
+ }
880
+ }
881
+ function resolveAttachmentPath(path) {
882
+ const normalizedPath = normalizeCandidatePath(path);
883
+ return isAbsolute(normalizedPath) ? normalizedPath : resolve(normalizedPath);
884
+ }
885
+ function extractImageAttachments(input) {
886
+ const attachments = [];
887
+ const segments = [];
888
+ let lastIndex = 0;
889
+ for (const match of input.matchAll(PATH_CANDIDATE_PATTERN)) {
890
+ const matchedValue = match[0];
891
+ const candidate = match.slice(1).find((value) => Boolean(value));
892
+ // v8 ignore start
893
+ if (candidate === void 0) continue;
894
+ // v8 ignore stop
895
+ if (!isPathLikeCandidate(candidate, matchedValue)) continue;
896
+ if (!isReadableImagePath(candidate)) continue;
897
+ attachments.push(resolveAttachmentPath(candidate));
898
+ segments.push(input.slice(lastIndex, match.index));
899
+ lastIndex = match.index + matchedValue.length;
900
+ }
901
+ if (!attachments.length) return {
902
+ attachments,
903
+ remainingInput: input
904
+ };
905
+ segments.push(input.slice(lastIndex));
906
+ return {
907
+ attachments,
908
+ remainingInput: segments.join("").replaceAll(/\s{2,}/g, " ").trim()
909
+ };
910
+ }
911
+ //#endregion
817
912
  //#region src/components/Chat/CommandMenu.tsx
818
913
  function getMatchingCommands(input) {
819
914
  const normalizedInput = input.trim().toLowerCase();
@@ -1004,28 +1099,81 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
1004
1099
  function hasFileSuggestionQuery(input) {
1005
1100
  return /(^|.)@\S+/.test(input);
1006
1101
  }
1007
- function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, onSubmit }) {
1102
+ function toAttachment(path, index, isTemp = false) {
1103
+ return {
1104
+ id: `${path}-${String(index)}`,
1105
+ isTemp,
1106
+ label: getAttachmentLabel(path),
1107
+ path
1108
+ };
1109
+ }
1110
+ function cleanupAttachments(attachments) {
1111
+ for (const attachment of attachments) if (attachment.isTemp) removeClipboardImage(attachment.path);
1112
+ }
1113
+ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, onSubmit, theme = getTheme() }) {
1008
1114
  const { exit } = useApp();
1009
1115
  const [history, setHistory] = useState(sessionHistory);
1010
1116
  const [historyIndex, setHistoryIndex] = useState(null);
1011
1117
  const [input, setInput] = useState("");
1012
1118
  const [cursorPosition, setCursorPosition] = useState(void 0);
1119
+ const [attachments, setAttachments] = useState([]);
1120
+ const [error, setError] = useState(null);
1013
1121
  const fileSuggestionRef = useRef(null);
1122
+ const nextClipboardImageRef = useRef(1);
1123
+ const hasAttachments = attachments.length > 0;
1014
1124
  useEffect(() => {
1015
1125
  setHistory(sessionHistory);
1016
1126
  setHistoryIndex(null);
1017
1127
  setInput("");
1018
1128
  setCursorPosition(void 0);
1129
+ setError(null);
1019
1130
  fileSuggestionRef.current = null;
1131
+ nextClipboardImageRef.current = 1;
1132
+ setAttachments((previousAttachments) => {
1133
+ cleanupAttachments(previousAttachments);
1134
+ return [];
1135
+ });
1020
1136
  }, [sessionHistory]);
1021
- const resetInput = useCallback(() => {
1137
+ const resetInput = useCallback((deleteTempAttachments = false) => {
1022
1138
  setInput("");
1023
1139
  setCursorPosition(void 0);
1024
1140
  setHistoryIndex(null);
1141
+ setError(null);
1142
+ if (deleteTempAttachments) {
1143
+ setAttachments((previousAttachments) => {
1144
+ cleanupAttachments(previousAttachments);
1145
+ return [];
1146
+ });
1147
+ nextClipboardImageRef.current = 1;
1148
+ return;
1149
+ }
1150
+ setAttachments([]);
1151
+ }, []);
1152
+ const removeLastAttachment = useCallback(() => {
1153
+ setAttachments((previousAttachments) => {
1154
+ const removedAttachment = previousAttachments.at(-1);
1155
+ if (removedAttachment?.isTemp) removeClipboardImage(removedAttachment.path);
1156
+ return previousAttachments.slice(0, -1);
1157
+ });
1158
+ setError(null);
1159
+ }, []);
1160
+ const stageAttachments = useCallback((paths, isTemp = false) => {
1161
+ setAttachments((previousAttachments) => [...previousAttachments, ...paths.map((path, index) => toAttachment(path, previousAttachments.length + index, isTemp))]);
1162
+ setError(null);
1025
1163
  }, []);
1164
+ const attachClipboardImage = useCallback(() => {
1165
+ try {
1166
+ const path = saveClipboardImage(`image-${String(nextClipboardImageRef.current)}`);
1167
+ nextClipboardImageRef.current += 1;
1168
+ stageAttachments([path], true);
1169
+ } catch (error) {
1170
+ setError(error instanceof Error ? error.message : String(error));
1171
+ }
1172
+ }, [stageAttachments]);
1026
1173
  const handleSelectFileSuggestion = useCallback((nextInput) => {
1027
1174
  setInput(nextInput.value);
1028
1175
  setCursorPosition(nextInput.cursorPosition);
1176
+ setError(null);
1029
1177
  }, []);
1030
1178
  const handleFileSuggestionChange = useCallback((nextInput) => {
1031
1179
  if (nextInput) {
@@ -1046,21 +1194,40 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1046
1194
  } else fileSuggestionRef.current = null;
1047
1195
  }, [input]);
1048
1196
  const handleInputChange = useCallback((nextInput) => {
1197
+ if (nextInput.length - input.length > 1) {
1198
+ const { attachments: nextAttachments, remainingInput } = extractImageAttachments(nextInput);
1199
+ if (nextAttachments.length) {
1200
+ stageAttachments(nextAttachments);
1201
+ setInput(remainingInput);
1202
+ setCursorPosition(remainingInput.length);
1203
+ setHistoryIndex(null);
1204
+ return;
1205
+ }
1206
+ }
1049
1207
  setInput(nextInput);
1050
1208
  setHistoryIndex(null);
1051
- }, []);
1209
+ setError(null);
1210
+ }, [input, stageAttachments]);
1052
1211
  const submitAndReset = useCallback((input) => {
1053
1212
  const trimmedInput = input.trim();
1054
- if (!trimmedInput) return;
1055
- onSubmit(trimmedInput);
1056
- if (!trimmedInput.startsWith("/")) setHistory((previousHistory) => [...previousHistory, trimmedInput]);
1057
- resetInput();
1213
+ const imagePaths = attachments.map(({ path }) => path);
1214
+ if (!trimmedInput && !imagePaths.length) return;
1215
+ onSubmit({
1216
+ content: trimmedInput,
1217
+ ...imagePaths.length ? { images: imagePaths } : {}
1218
+ });
1219
+ if (trimmedInput && !trimmedInput.startsWith("/")) setHistory((previousHistory) => [...previousHistory, trimmedInput]);
1220
+ resetInput(trimmedInput.startsWith("/"));
1058
1221
  fileSuggestionRef.current = null;
1059
- }, [onSubmit, resetInput]);
1222
+ }, [
1223
+ attachments,
1224
+ onSubmit,
1225
+ resetInput
1226
+ ]);
1060
1227
  const showCommandMenu = input.startsWith("/");
1061
1228
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
1062
1229
  const handleHistoryNavigation = useCallback((direction) => {
1063
- if (!history.length || showFileSuggestions) return;
1230
+ if (!history.length || showFileSuggestions || hasAttachments) return;
1064
1231
  if (direction === "up") {
1065
1232
  if (historyIndex === null) {
1066
1233
  if (input) return;
@@ -1092,21 +1259,22 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1092
1259
  setInput(nextInput);
1093
1260
  setCursorPosition(nextInput.length);
1094
1261
  }, [
1262
+ hasAttachments,
1095
1263
  history,
1096
1264
  historyIndex,
1097
1265
  input,
1098
1266
  showFileSuggestions
1099
1267
  ]);
1100
- const handleSubmitText = useCallback((input) => {
1101
- if (input.startsWith("/")) return;
1102
- if (hasFileSuggestionQuery(input)) {
1268
+ const handleSubmitText = useCallback((value) => {
1269
+ if (value.startsWith("/")) return;
1270
+ if (hasFileSuggestionQuery(value)) {
1103
1271
  if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
1104
1272
  return;
1105
1273
  }
1106
- submitAndReset(input);
1274
+ submitAndReset(value);
1107
1275
  }, [handleSelectFileSuggestion, submitAndReset]);
1108
- const handleSubmitCommand = useCallback((input) => {
1109
- if (LIST.find(({ name }) => name === input)) submitAndReset(input);
1276
+ const handleSubmitCommand = useCallback((value) => {
1277
+ if (LIST.find(({ name }) => name === value)) submitAndReset(value);
1110
1278
  }, [submitAndReset]);
1111
1279
  useInput((inputKey, key) => {
1112
1280
  const isCtrlC = key.ctrl && inputKey === "c";
@@ -1114,6 +1282,14 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1114
1282
  if (key.escape || isCtrlC) onInterrupt?.();
1115
1283
  return;
1116
1284
  }
1285
+ if (key.ctrl && inputKey === "v") {
1286
+ attachClipboardImage();
1287
+ return;
1288
+ }
1289
+ if ((key.backspace || key.delete || inputKey === "") && !input) {
1290
+ if (hasAttachments) removeLastAttachment();
1291
+ return;
1292
+ }
1117
1293
  if (isCtrlC) {
1118
1294
  if (input) {
1119
1295
  resetInput();
@@ -1127,18 +1303,35 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1127
1303
  }
1128
1304
  if (key.downArrow) handleHistoryNavigation("down");
1129
1305
  });
1306
+ const attachmentPrefix = attachments.map(({ label }) => `[${label}]`).join(" ");
1307
+ const wrapIndent = 2 + (attachmentPrefix ? attachmentPrefix.length + 1 : 0);
1130
1308
  return /* @__PURE__ */ jsxs(Box, {
1131
1309
  flexDirection: "column",
1132
1310
  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
- })] }),
1311
+ error && /* @__PURE__ */ jsx(Box, {
1312
+ marginBottom: 1,
1313
+ marginX: 2,
1314
+ children: /* @__PURE__ */ jsx(Text, {
1315
+ color: theme.colors.error,
1316
+ children: error
1317
+ })
1318
+ }),
1319
+ /* @__PURE__ */ jsxs(Box, { children: [
1320
+ /* @__PURE__ */ jsx(Text, { children: "> " }),
1321
+ hasAttachments && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
1322
+ color: theme.colors.accent,
1323
+ children: attachmentPrefix
1324
+ }), /* @__PURE__ */ jsx(Text, { children: " " })] }),
1325
+ /* @__PURE__ */ jsx(TextInput, {
1326
+ value: input,
1327
+ isDisabled,
1328
+ cursorPosition,
1329
+ wrapIndent,
1330
+ onChange: handleInputChange,
1331
+ onSubmit: handleSubmitText,
1332
+ placeholder: hasAttachments ? void 0 : "Ask anything... (/ commands, @ files, Ctrl+V images)"
1333
+ })
1334
+ ] }),
1142
1335
  showCommandMenu && /* @__PURE__ */ jsx(CommandMenu, {
1143
1336
  input,
1144
1337
  onSubmit: handleSubmitCommand
@@ -1480,10 +1673,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1480
1673
  messages,
1481
1674
  processStream
1482
1675
  ]);
1483
- const handleSubmit = useCallback(async (value) => {
1676
+ const handleSubmit = useCallback(async ({ content, images }) => {
1484
1677
  setInterruptReason(null);
1485
- const userContent = value.trim();
1486
- if (!userContent) return;
1678
+ const userContent = content.trim();
1679
+ if (!userContent && !images?.length) return;
1487
1680
  if (userContent.startsWith("/")) {
1488
1681
  onCommand(userContent);
1489
1682
  return;
@@ -1491,7 +1684,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1491
1684
  setIsLoading(true);
1492
1685
  const userMessage = {
1493
1686
  role: USER,
1494
- content: userContent
1687
+ content: userContent,
1688
+ ...images?.length ? { images } : {}
1495
1689
  };
1496
1690
  const updatedMessages = [...messages, userMessage];
1497
1691
  setMessages(updatedMessages);
@@ -1537,7 +1731,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1537
1731
  history,
1538
1732
  isDisabled: isLoading,
1539
1733
  onInterrupt: handleInterrupt,
1540
- onSubmit: handleSubmit
1734
+ onSubmit: handleSubmit,
1735
+ theme
1541
1736
  })
1542
1737
  })
1543
1738
  ]
@@ -1874,6 +2069,7 @@ function ModelDeleteConfirmView({ deleteCandidate, isDeleting, notice, theme, on
1874
2069
  //#endregion
1875
2070
  //#region src/components/ModelManager/ModelDeleteView.tsx
1876
2071
  function ModelDeleteView({ currentModel, installedModels, isLoading, notice, theme, onCancel, onSelect }) {
2072
+ // v8 ignore next
1877
2073
  if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1878
2074
  return /* @__PURE__ */ jsxs(SelectPrompt, {
1879
2075
  options: [...buildInstalledModelOptions(installedModels.filter((model) => model !== currentModel), currentModel), BACK],
@@ -2372,6 +2568,24 @@ var ACTION = {
2372
2568
  OPEN_PREFIX: "open:"
2373
2569
  };
2374
2570
  var SESSION_LABEL_PADDING = 4;
2571
+ var MAIN_OPTIONS = [
2572
+ {
2573
+ label: "New session",
2574
+ value: ACTION.NEW
2575
+ },
2576
+ {
2577
+ label: "Open session",
2578
+ value: ACTION.OPEN_MENU
2579
+ },
2580
+ {
2581
+ label: "Delete session",
2582
+ value: ACTION.DELETE_MENU
2583
+ },
2584
+ {
2585
+ label: "Close",
2586
+ value: ACTION.CLOSE
2587
+ }
2588
+ ];
2375
2589
  function truncate(value, maxLength) {
2376
2590
  return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
2377
2591
  }
@@ -2384,34 +2598,29 @@ function formatSessionLabel(session, maxWidth, prefix = "") {
2384
2598
  function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, theme = getTheme() }) {
2385
2599
  const [view, setView] = useState("main");
2386
2600
  const [error, setError] = useState();
2387
- const [, refreshSessionList] = useState(0);
2601
+ const [sessionListVersion, refreshSessionList] = useState(0);
2388
2602
  const { stdout } = useStdout();
2389
2603
  const sessions = listSessions();
2390
2604
  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
2605
+ const options = useMemo(() => {
2606
+ switch (view) {
2607
+ case "open": return [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2608
+ label: formatSessionLabel(session, maxLabelWidth),
2609
+ value: `${ACTION.OPEN_PREFIX}${session.id}`
2610
+ })), BACK];
2611
+ case "delete": return [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2612
+ label: formatSessionLabel(session, maxLabelWidth, "Delete "),
2613
+ value: `${ACTION.DELETE_PREFIX}${session.id}`
2614
+ })), BACK];
2615
+ default: return MAIN_OPTIONS;
2413
2616
  }
2414
- ];
2617
+ }, [
2618
+ currentSessionId,
2619
+ maxLabelWidth,
2620
+ sessionListVersion,
2621
+ sessions,
2622
+ view
2623
+ ]);
2415
2624
  const handleChange = useCallback((value) => {
2416
2625
  switch (true) {
2417
2626
  case value === ACTION.CLOSE:
@@ -2469,7 +2678,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, th
2469
2678
  options,
2470
2679
  onCancel: onClose,
2471
2680
  onChange: handleChange
2472
- }, `${view}:${String(sessions.length)}`)
2681
+ })
2473
2682
  ]
2474
2683
  });
2475
2684
  }
@@ -2784,7 +2993,10 @@ function ReadinessCheck({ errorMessage, onCommand, setupState, theme = getTheme(
2784
2993
  }), getMessage(setupState, errorMessage)]
2785
2994
  }), /* @__PURE__ */ jsx(ChatInput, {
2786
2995
  history: [],
2787
- onSubmit: onCommand
2996
+ onSubmit: ({ content }) => {
2997
+ onCommand(content);
2998
+ },
2999
+ theme
2788
3000
  })]
2789
3001
  });
2790
3002
  }
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.0";
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
@@ -1130,7 +1242,7 @@ async function main(args = process.argv.slice(2)) {
1130
1242
  else await launchTui();
1131
1243
  }
1132
1244
  async function launchTui(sessionId) {
1133
- const { renderApp } = await import("./assets/tui-XD0Ekj5m.js");
1245
+ const { renderApp } = await import("./assets/tui-JyjdXasW.js");
1134
1246
  reset();
1135
1247
  renderApp(sessionId);
1136
1248
  }
@@ -1146,4 +1258,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
1146
1258
  if (isEntrypoint()) main();
1147
1259
  // v8 ignore stop
1148
1260
  //#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 };
1261
+ 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.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",
@@ -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",