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.
- package/dist/assets/{tui-XD0Ekj5m.js → tui-JyjdXasW.js} +285 -73
- package/dist/cli.js +117 -5
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { A as
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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, {
|
|
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
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
}, [
|
|
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((
|
|
1101
|
-
if (
|
|
1102
|
-
if (hasFileSuggestionQuery(
|
|
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(
|
|
1274
|
+
submitAndReset(value);
|
|
1107
1275
|
}, [handleSelectFileSuggestion, submitAndReset]);
|
|
1108
|
-
const handleSubmitCommand = useCallback((
|
|
1109
|
-
if (LIST.find(({ name }) => name ===
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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 (
|
|
1676
|
+
const handleSubmit = useCallback(async ({ content, images }) => {
|
|
1484
1677
|
setInterruptReason(null);
|
|
1485
|
-
const userContent =
|
|
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 =
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
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
|
-
}
|
|
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:
|
|
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.
|
|
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-
|
|
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 {
|
|
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.
|
|
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
|
|
43
|
+
"@shikijs/cli": "4.1.0",
|
|
44
44
|
"cac": "7.0.0",
|
|
45
45
|
"ink": "7.0.3",
|
|
46
46
|
"marked": "15.0.12",
|