code-ollama 0.15.0 → 0.16.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.
|
@@ -169,7 +169,7 @@ var inlineMathExtension = {
|
|
|
169
169
|
}
|
|
170
170
|
};
|
|
171
171
|
//#endregion
|
|
172
|
-
//#region src/components/Markdown/
|
|
172
|
+
//#region src/components/Markdown/render.ts
|
|
173
173
|
var HR_PLACEHOLDER = "__CODE_OLLAMA_HR_PLACEHOLDER__";
|
|
174
174
|
function renderMarkdown(content, hrWidth) {
|
|
175
175
|
const hr = "─".repeat(Math.max(1, hrWidth));
|
|
@@ -196,10 +196,12 @@ function renderMarkdown(content, hrWidth) {
|
|
|
196
196
|
const result = markdown.parse(content);
|
|
197
197
|
return (typeof result === "string" ? result.trim() : content).replaceAll(HR_PLACEHOLDER, hr);
|
|
198
198
|
} catch {
|
|
199
|
+
// v8 ignore next
|
|
199
200
|
return content;
|
|
200
201
|
}
|
|
201
|
-
// v8 ignore stop
|
|
202
202
|
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/components/Markdown/Markdown.tsx
|
|
203
205
|
var Markdown = memo(function Markdown({ content, color, dimColor }) {
|
|
204
206
|
const { stdout } = useStdout();
|
|
205
207
|
const availableWidth = stdout.columns - 4;
|
|
@@ -217,7 +219,72 @@ var TURN_ABORTED_MESSAGE = [
|
|
|
217
219
|
"</turn_aborted>"
|
|
218
220
|
].join("\n");
|
|
219
221
|
//#endregion
|
|
220
|
-
//#region src/components/Messages/
|
|
222
|
+
//#region src/components/Messages/layout.ts
|
|
223
|
+
var ANSI_REGEX = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
|
|
224
|
+
var CODE_BLOCK_MARGIN_Y = 2;
|
|
225
|
+
var CODE_BLOCK_BORDER_Y = 2;
|
|
226
|
+
var CODE_BLOCK_CHROME_X = 4;
|
|
227
|
+
function stripAnsi(value) {
|
|
228
|
+
return value.replaceAll(ANSI_REGEX, "");
|
|
229
|
+
}
|
|
230
|
+
function countLineWidth(value) {
|
|
231
|
+
return Array.from(stripAnsi(value)).length;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Counts the number of wrapped lines for a given content and width.
|
|
235
|
+
*
|
|
236
|
+
* This function splits the content by newlines and calculates how many lines
|
|
237
|
+
* each segment would wrap to based on the available width.
|
|
238
|
+
*
|
|
239
|
+
* @param content The text content to wrap.
|
|
240
|
+
* @param width The available width for wrapping.
|
|
241
|
+
* @returns The number of wrapped lines.
|
|
242
|
+
*/
|
|
243
|
+
function countWrappedLines(content, width) {
|
|
244
|
+
const safeWidth = Math.max(1, width);
|
|
245
|
+
return content.split("\n").reduce((lineCount, line) => {
|
|
246
|
+
const visibleWidth = countLineWidth(line);
|
|
247
|
+
return lineCount + Math.max(1, Math.ceil(visibleWidth / safeWidth));
|
|
248
|
+
}, 0);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Calculates the height of a code block based on its content and width.
|
|
252
|
+
*
|
|
253
|
+
* This function accounts for margins, borders, and wrapped lines to determine
|
|
254
|
+
* the total height required for displaying a code block.
|
|
255
|
+
*
|
|
256
|
+
* @param content The code block content to render.
|
|
257
|
+
* @param width The available width for the code block.
|
|
258
|
+
* @returns The total height in lines.
|
|
259
|
+
*/
|
|
260
|
+
function getCodeBlockHeight(content, width) {
|
|
261
|
+
const contentWidth = Math.max(1, width - CODE_BLOCK_CHROME_X);
|
|
262
|
+
return CODE_BLOCK_MARGIN_Y + CODE_BLOCK_BORDER_Y + countWrappedLines(content, contentWidth);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Calculates the total height of streaming text content based on wrapped lines.
|
|
266
|
+
*
|
|
267
|
+
* @param textParts Array of text parts with their content and type.
|
|
268
|
+
* @param width The available width for wrapping text.
|
|
269
|
+
* @returns The total height in lines.
|
|
270
|
+
*/
|
|
271
|
+
function getStreamingTextHeight(textParts, width) {
|
|
272
|
+
return textParts.reduce((height, part) => {
|
|
273
|
+
const renderMarkdown$1 = renderMarkdown;
|
|
274
|
+
return height + countWrappedLines(part.type === "markdown" ? renderMarkdown$1(part.content, width) : part.content, width);
|
|
275
|
+
}, 0);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Calculates the available width for assistant content after accounting for margins.
|
|
279
|
+
*
|
|
280
|
+
* @param columns The total number of columns in the terminal.
|
|
281
|
+
* @returns The available width for content (always at least 1).
|
|
282
|
+
*/
|
|
283
|
+
function getAssistantContentWidth(columns) {
|
|
284
|
+
return Math.max(1, columns - 4);
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/components/Messages/parsing.ts
|
|
221
288
|
var FENCE_LINE_REGEX = /^(?<indent>[ \t]*)(?<fence>`{3,})(?<language>\w+)?[ \t]*$/;
|
|
222
289
|
function flushTextSegment(segments, textLines) {
|
|
223
290
|
const textContent = textLines.join("\n").trim();
|
|
@@ -303,14 +370,8 @@ function parseContent(content) {
|
|
|
303
370
|
flushTextSegment(segments, textLines);
|
|
304
371
|
return segments;
|
|
305
372
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
case USER: return "black";
|
|
309
|
-
case ASSISTANT: return "cyan";
|
|
310
|
-
case SYSTEM: return "gray";
|
|
311
|
-
default: return;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/components/Messages/streaming.ts
|
|
314
375
|
function isWordCharacter(char) {
|
|
315
376
|
return char !== void 0 && /[A-Za-z0-9]/.test(char);
|
|
316
377
|
}
|
|
@@ -405,11 +466,27 @@ function splitStreamingInlineContent(content) {
|
|
|
405
466
|
return parts;
|
|
406
467
|
}
|
|
407
468
|
//#endregion
|
|
469
|
+
//#region src/components/Messages/styles.ts
|
|
470
|
+
function getMessageColor(role) {
|
|
471
|
+
switch (role) {
|
|
472
|
+
case USER: return "black";
|
|
473
|
+
case ASSISTANT: return "cyan";
|
|
474
|
+
case SYSTEM: return "gray";
|
|
475
|
+
default: return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
408
479
|
//#region src/components/Messages/Messages.tsx
|
|
409
480
|
function Message({ message, isStreaming = false }) {
|
|
481
|
+
const { stdout } = useStdout();
|
|
410
482
|
const messageColor = getMessageColor(message.role);
|
|
411
483
|
const isSystem = message.role === SYSTEM;
|
|
412
484
|
const isUser = message.role === USER;
|
|
485
|
+
const isStreamingAssistant = isStreaming && !isUser && !isSystem;
|
|
486
|
+
const stickyHeightRef = useRef({
|
|
487
|
+
columns: stdout.columns,
|
|
488
|
+
maxHeight: 0
|
|
489
|
+
});
|
|
413
490
|
if (isSystem) return /* @__PURE__ */ jsx(Box, {
|
|
414
491
|
flexDirection: "column",
|
|
415
492
|
marginBottom: 1,
|
|
@@ -420,10 +497,23 @@ function Message({ message, isStreaming = false }) {
|
|
|
420
497
|
children: message.content
|
|
421
498
|
})
|
|
422
499
|
});
|
|
423
|
-
|
|
500
|
+
const segments = parseContent(message.content);
|
|
501
|
+
const availableWidth = getAssistantContentWidth(stdout.columns);
|
|
502
|
+
if (stickyHeightRef.current.columns !== stdout.columns) stickyHeightRef.current = {
|
|
503
|
+
columns: stdout.columns,
|
|
504
|
+
maxHeight: 0
|
|
505
|
+
};
|
|
506
|
+
const streamingHeight = isStreamingAssistant ? segments.reduce((height, segment) => {
|
|
507
|
+
if (segment.type === "code") return height + getCodeBlockHeight(segment.content, availableWidth);
|
|
508
|
+
if (segment.type === "raw") return height + getCodeBlockHeight(unwrapRawMarkdownFence(segment.content) ?? segment.content, availableWidth);
|
|
509
|
+
return height + getStreamingTextHeight(splitStreamingInlineContent(segment.content), availableWidth);
|
|
510
|
+
}, 0) : 0;
|
|
511
|
+
if (isStreamingAssistant) stickyHeightRef.current.maxHeight = Math.max(stickyHeightRef.current.maxHeight, streamingHeight);
|
|
512
|
+
const stickyPaddingLines = isStreamingAssistant ? stickyHeightRef.current.maxHeight - streamingHeight : 0;
|
|
513
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
424
514
|
flexDirection: "column",
|
|
425
515
|
marginBottom: 1,
|
|
426
|
-
children:
|
|
516
|
+
children: [segments.map((segment, index) => {
|
|
427
517
|
const prefix = isUser && index === 0 ? "> " : "";
|
|
428
518
|
if (segment.type === "code") return isUser ? /* @__PURE__ */ jsx(Text, {
|
|
429
519
|
color: messageColor,
|
|
@@ -465,7 +555,7 @@ function Message({ message, isStreaming = false }) {
|
|
|
465
555
|
color: messageColor
|
|
466
556
|
}, partIndex))
|
|
467
557
|
}, index);
|
|
468
|
-
})
|
|
558
|
+
}), Array.from({ length: stickyPaddingLines }, (_, index) => /* @__PURE__ */ jsx(Text, { children: " " }, "padding-" + String(index)))]
|
|
469
559
|
});
|
|
470
560
|
}
|
|
471
561
|
function Messages({ messages, isLoading, sessionId, streamingMessage }) {
|
|
@@ -644,16 +734,6 @@ function ToolApproval({ toolCall, onDecision }) {
|
|
|
644
734
|
});
|
|
645
735
|
}
|
|
646
736
|
//#endregion
|
|
647
|
-
//#region src/components/Chat/constants.ts
|
|
648
|
-
var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
|
|
649
|
-
var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
|
|
650
|
-
var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
|
|
651
|
-
var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
|
|
652
|
-
INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
|
|
653
|
-
INTERRUPT_REASON["REJECTED"] = "rejected";
|
|
654
|
-
return INTERRUPT_REASON;
|
|
655
|
-
}({});
|
|
656
|
-
//#endregion
|
|
657
737
|
//#region src/components/TextInput/TextInput.tsx
|
|
658
738
|
function buildLineSegments(displayValue, cursorPosition, width) {
|
|
659
739
|
const safeWidth = Math.max(1, width);
|
|
@@ -953,17 +1033,28 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
|
|
|
953
1033
|
});
|
|
954
1034
|
}
|
|
955
1035
|
//#endregion
|
|
956
|
-
//#region src/components/Chat/
|
|
1036
|
+
//#region src/components/Chat/ChatInput.tsx
|
|
957
1037
|
function hasFileSuggestionQuery(input) {
|
|
958
1038
|
return /(^|.)@\S+/.test(input);
|
|
959
1039
|
}
|
|
960
|
-
function
|
|
1040
|
+
function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, onSubmit }) {
|
|
961
1041
|
const { exit } = useApp();
|
|
1042
|
+
const [history, setHistory] = useState(sessionHistory);
|
|
1043
|
+
const [historyIndex, setHistoryIndex] = useState(null);
|
|
962
1044
|
const [input, setInput] = useState("");
|
|
963
1045
|
const [cursorPosition, setCursorPosition] = useState(void 0);
|
|
964
1046
|
const fileSuggestionRef = useRef(null);
|
|
1047
|
+
useEffect(() => {
|
|
1048
|
+
setHistory(sessionHistory);
|
|
1049
|
+
setHistoryIndex(null);
|
|
1050
|
+
setInput("");
|
|
1051
|
+
setCursorPosition(void 0);
|
|
1052
|
+
fileSuggestionRef.current = null;
|
|
1053
|
+
}, [sessionHistory]);
|
|
965
1054
|
const resetInput = useCallback(() => {
|
|
966
1055
|
setInput("");
|
|
1056
|
+
setCursorPosition(void 0);
|
|
1057
|
+
setHistoryIndex(null);
|
|
967
1058
|
}, []);
|
|
968
1059
|
const handleSelectFileSuggestion = useCallback((nextInput) => {
|
|
969
1060
|
setInput(nextInput.value);
|
|
@@ -987,15 +1078,58 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
|
|
|
987
1078
|
};
|
|
988
1079
|
} else fileSuggestionRef.current = null;
|
|
989
1080
|
}, [input]);
|
|
1081
|
+
const handleInputChange = useCallback((nextInput) => {
|
|
1082
|
+
setInput(nextInput);
|
|
1083
|
+
setHistoryIndex(null);
|
|
1084
|
+
}, []);
|
|
990
1085
|
const submitAndReset = useCallback((input) => {
|
|
991
1086
|
const trimmedInput = input.trim();
|
|
992
1087
|
if (!trimmedInput) return;
|
|
993
1088
|
onSubmit(trimmedInput);
|
|
1089
|
+
if (!trimmedInput.startsWith("/")) setHistory((previousHistory) => [...previousHistory, trimmedInput]);
|
|
994
1090
|
resetInput();
|
|
995
1091
|
fileSuggestionRef.current = null;
|
|
996
1092
|
}, [onSubmit, resetInput]);
|
|
997
1093
|
const showCommandMenu = input.startsWith("/");
|
|
998
1094
|
const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
|
|
1095
|
+
const handleHistoryNavigation = useCallback((direction) => {
|
|
1096
|
+
if (!history.length || showFileSuggestions) return;
|
|
1097
|
+
if (direction === "up") {
|
|
1098
|
+
if (historyIndex === null) {
|
|
1099
|
+
if (input) return;
|
|
1100
|
+
const nextIndex = history.length - 1;
|
|
1101
|
+
const nextInput = history[nextIndex];
|
|
1102
|
+
setHistoryIndex(nextIndex);
|
|
1103
|
+
setInput(nextInput);
|
|
1104
|
+
setCursorPosition(nextInput.length);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (historyIndex === 0) return;
|
|
1108
|
+
const nextIndex = historyIndex - 1;
|
|
1109
|
+
const nextInput = history[nextIndex];
|
|
1110
|
+
setHistoryIndex(nextIndex);
|
|
1111
|
+
setInput(nextInput);
|
|
1112
|
+
setCursorPosition(nextInput.length);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (historyIndex === null) return;
|
|
1116
|
+
if (historyIndex === history.length - 1) {
|
|
1117
|
+
setHistoryIndex(null);
|
|
1118
|
+
setInput("");
|
|
1119
|
+
setCursorPosition(0);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const nextIndex = historyIndex + 1;
|
|
1123
|
+
const nextInput = history[nextIndex];
|
|
1124
|
+
setHistoryIndex(nextIndex);
|
|
1125
|
+
setInput(nextInput);
|
|
1126
|
+
setCursorPosition(nextInput.length);
|
|
1127
|
+
}, [
|
|
1128
|
+
history,
|
|
1129
|
+
historyIndex,
|
|
1130
|
+
input,
|
|
1131
|
+
showFileSuggestions
|
|
1132
|
+
]);
|
|
999
1133
|
const handleSubmitText = useCallback((input) => {
|
|
1000
1134
|
if (input.startsWith("/")) return;
|
|
1001
1135
|
if (hasFileSuggestionQuery(input)) {
|
|
@@ -1007,8 +1141,8 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
|
|
|
1007
1141
|
const handleSubmitCommand = useCallback((input) => {
|
|
1008
1142
|
if (LIST.find(({ name }) => name === input)) submitAndReset(input);
|
|
1009
1143
|
}, [submitAndReset]);
|
|
1010
|
-
useInput((
|
|
1011
|
-
const isCtrlC = key.ctrl &&
|
|
1144
|
+
useInput((inputKey, key) => {
|
|
1145
|
+
const isCtrlC = key.ctrl && inputKey === "c";
|
|
1012
1146
|
if (isDisabled) {
|
|
1013
1147
|
if (key.escape || isCtrlC) onInterrupt?.();
|
|
1014
1148
|
return;
|
|
@@ -1020,6 +1154,11 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
|
|
|
1020
1154
|
}
|
|
1021
1155
|
exit();
|
|
1022
1156
|
}
|
|
1157
|
+
if (key.upArrow) {
|
|
1158
|
+
handleHistoryNavigation("up");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (key.downArrow) handleHistoryNavigation("down");
|
|
1023
1162
|
});
|
|
1024
1163
|
return /* @__PURE__ */ jsxs(Box, {
|
|
1025
1164
|
flexDirection: "column",
|
|
@@ -1029,7 +1168,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
|
|
|
1029
1168
|
isDisabled,
|
|
1030
1169
|
cursorPosition,
|
|
1031
1170
|
wrapIndent: 2,
|
|
1032
|
-
onChange:
|
|
1171
|
+
onChange: handleInputChange,
|
|
1033
1172
|
onSubmit: handleSubmitText,
|
|
1034
1173
|
placeholder: "Ask anything... (/ commands, @ files)"
|
|
1035
1174
|
})] }),
|
|
@@ -1047,6 +1186,16 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
|
|
|
1047
1186
|
});
|
|
1048
1187
|
}
|
|
1049
1188
|
//#endregion
|
|
1189
|
+
//#region src/components/Chat/constants.ts
|
|
1190
|
+
var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
|
|
1191
|
+
var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
|
|
1192
|
+
var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
|
|
1193
|
+
var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
|
|
1194
|
+
INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
|
|
1195
|
+
INTERRUPT_REASON["REJECTED"] = "rejected";
|
|
1196
|
+
return INTERRUPT_REASON;
|
|
1197
|
+
}({});
|
|
1198
|
+
//#endregion
|
|
1050
1199
|
//#region src/components/Chat/plan.ts
|
|
1051
1200
|
function hasExecutablePlan(content) {
|
|
1052
1201
|
return content.split("\n").some((line) => {
|
|
@@ -1058,6 +1207,7 @@ function hasExecutablePlan(content) {
|
|
|
1058
1207
|
//#region src/components/Chat/Chat.tsx
|
|
1059
1208
|
function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
|
|
1060
1209
|
const sessionMessages = initialMessages ?? [];
|
|
1210
|
+
const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
|
|
1061
1211
|
const [messages, setMessages] = useState(sessionMessages);
|
|
1062
1212
|
const [streamingMessage, setStreamingMessage] = useState(null);
|
|
1063
1213
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -1406,7 +1556,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1406
1556
|
}),
|
|
1407
1557
|
!pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Box, {
|
|
1408
1558
|
marginTop: 1,
|
|
1409
|
-
children: /* @__PURE__ */ jsx(
|
|
1559
|
+
children: /* @__PURE__ */ jsx(ChatInput, {
|
|
1560
|
+
history,
|
|
1410
1561
|
isDisabled: isLoading,
|
|
1411
1562
|
onInterrupt: handleInterrupt,
|
|
1412
1563
|
onSubmit: handleSubmit
|
package/dist/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ var LIST = [
|
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region package.json
|
|
35
35
|
var name = "code-ollama";
|
|
36
|
-
var version = "0.
|
|
36
|
+
var version = "0.16.0";
|
|
37
37
|
//#endregion
|
|
38
38
|
//#region src/constants/package.ts
|
|
39
39
|
var NAME = name;
|
|
@@ -931,7 +931,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
931
931
|
else await launchTui();
|
|
932
932
|
}
|
|
933
933
|
async function launchTui(sessionId) {
|
|
934
|
-
const { renderApp } = await import("./assets/tui-
|
|
934
|
+
const { renderApp } = await import("./assets/tui-85A3pZD2.js");
|
|
935
935
|
reset();
|
|
936
936
|
renderApp(sessionId);
|
|
937
937
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-ollama",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@types/node": "25.8.0",
|
|
58
58
|
"@types/react": "19.2.14",
|
|
59
59
|
"@vitest/coverage-v8": "4.1.6",
|
|
60
|
-
"eslint": "10.
|
|
60
|
+
"eslint": "10.4.0",
|
|
61
61
|
"eslint-plugin-prettier": "5.5.5",
|
|
62
62
|
"eslint-plugin-simple-import-sort": "13.0.0",
|
|
63
63
|
"globals": "17.6.0",
|