code-ollama 0.14.2 → 0.15.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.
@@ -169,7 +169,7 @@ var inlineMathExtension = {
169
169
  }
170
170
  };
171
171
  //#endregion
172
- //#region src/components/Markdown/Markdown.tsx
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,159 @@ var TURN_ABORTED_MESSAGE = [
217
219
  "</turn_aborted>"
218
220
  ].join("\n");
219
221
  //#endregion
220
- //#region src/components/Messages/utils.ts
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
288
+ var FENCE_LINE_REGEX = /^(?<indent>[ \t]*)(?<fence>`{3,})(?<language>\w+)?[ \t]*$/;
289
+ function flushTextSegment(segments, textLines) {
290
+ const textContent = textLines.join("\n").trim();
291
+ if (textContent) segments.push({
292
+ type: "text",
293
+ content: textContent
294
+ });
295
+ }
296
+ function flushCodeSegment(segments, codeLines, fenceState) {
297
+ if (fenceState.ambiguous) {
298
+ segments.push({
299
+ type: "raw",
300
+ content: fenceState.rawLines.join("\n")
301
+ });
302
+ return;
303
+ }
304
+ const codeContent = normalizeCodeBlockContent(codeLines.join("\n"), fenceState.indent);
305
+ if (codeContent) segments.push({
306
+ type: "code",
307
+ content: codeContent,
308
+ language: fenceState.language
309
+ });
310
+ }
311
+ function unwrapRawMarkdownFence(content) {
312
+ if (!content.startsWith("```markdown\n") || !content.endsWith("\n```")) return null;
313
+ return content.slice(12, -4);
314
+ }
315
+ function parseContent(content) {
316
+ const segments = [];
317
+ const lines = content.split("\n");
318
+ const textLines = [];
319
+ const codeLines = [];
320
+ let fenceState = null;
321
+ for (const line of lines) {
322
+ const fenceMatch = FENCE_LINE_REGEX.exec(line);
323
+ if (fenceMatch?.groups) {
324
+ const { indent, fence, language } = fenceMatch.groups;
325
+ if (!fenceState) {
326
+ flushTextSegment(segments, textLines);
327
+ textLines.length = 0;
328
+ fenceState = {
329
+ indent,
330
+ fence,
331
+ language,
332
+ rawLines: [line],
333
+ ambiguous: false,
334
+ rawFenceDepth: 1
335
+ };
336
+ continue;
337
+ }
338
+ if (indent === fenceState.indent && fence === fenceState.fence) {
339
+ fenceState.rawLines.push(line);
340
+ if (fenceState.ambiguous) {
341
+ if (language) {
342
+ fenceState.rawFenceDepth += 1;
343
+ continue;
344
+ }
345
+ fenceState.rawFenceDepth -= 1;
346
+ if (fenceState.rawFenceDepth === 0) {
347
+ flushCodeSegment(segments, codeLines, fenceState);
348
+ codeLines.length = 0;
349
+ fenceState = null;
350
+ }
351
+ continue;
352
+ }
353
+ if (!language) {
354
+ flushCodeSegment(segments, codeLines, fenceState);
355
+ codeLines.length = 0;
356
+ fenceState = null;
357
+ continue;
358
+ }
359
+ fenceState.ambiguous = true;
360
+ fenceState.rawFenceDepth += 1;
361
+ continue;
362
+ }
363
+ }
364
+ if (fenceState) {
365
+ fenceState.rawLines.push(line);
366
+ codeLines.push(line);
367
+ } else textLines.push(line);
368
+ }
369
+ if (fenceState) textLines.push(...fenceState.rawLines);
370
+ flushTextSegment(segments, textLines);
371
+ return segments;
372
+ }
373
+ //#endregion
374
+ //#region src/components/Messages/streaming.ts
221
375
  function isWordCharacter(char) {
222
376
  return char !== void 0 && /[A-Za-z0-9]/.test(char);
223
377
  }
@@ -312,7 +466,7 @@ function splitStreamingInlineContent(content) {
312
466
  return parts;
313
467
  }
314
468
  //#endregion
315
- //#region src/components/Messages/Messages.tsx
469
+ //#region src/components/Messages/styles.ts
316
470
  function getMessageColor(role) {
317
471
  switch (role) {
318
472
  case USER: return "black";
@@ -321,95 +475,18 @@ function getMessageColor(role) {
321
475
  default: return;
322
476
  }
323
477
  }
324
- var FENCE_LINE_REGEX = /^(?<indent>[ \t]*)(?<fence>`{3,})(?<language>\w+)?[ \t]*$/;
325
- function flushTextSegment(segments, textLines) {
326
- const textContent = textLines.join("\n").trim();
327
- if (textContent) segments.push({
328
- type: "text",
329
- content: textContent
330
- });
331
- }
332
- function flushCodeSegment(segments, codeLines, fenceState) {
333
- if (fenceState.ambiguous) {
334
- segments.push({
335
- type: "raw",
336
- content: fenceState.rawLines.join("\n")
337
- });
338
- return;
339
- }
340
- const codeContent = normalizeCodeBlockContent(codeLines.join("\n"), fenceState.indent);
341
- if (codeContent) segments.push({
342
- type: "code",
343
- content: codeContent,
344
- language: fenceState.language
345
- });
346
- }
347
- function unwrapRawMarkdownFence(content) {
348
- if (!content.startsWith("```markdown\n") || !content.endsWith("\n```")) return null;
349
- return content.slice(12, -4);
350
- }
351
- function parseContent(content) {
352
- const segments = [];
353
- const lines = content.split("\n");
354
- const textLines = [];
355
- const codeLines = [];
356
- let fenceState = null;
357
- for (const line of lines) {
358
- const fenceMatch = FENCE_LINE_REGEX.exec(line);
359
- if (fenceMatch?.groups) {
360
- const { indent, fence, language } = fenceMatch.groups;
361
- if (!fenceState) {
362
- flushTextSegment(segments, textLines);
363
- textLines.length = 0;
364
- fenceState = {
365
- indent,
366
- fence,
367
- language,
368
- rawLines: [line],
369
- ambiguous: false,
370
- rawFenceDepth: 1
371
- };
372
- continue;
373
- }
374
- if (indent === fenceState.indent && fence === fenceState.fence) {
375
- fenceState.rawLines.push(line);
376
- if (fenceState.ambiguous) {
377
- if (language) {
378
- fenceState.rawFenceDepth += 1;
379
- continue;
380
- }
381
- fenceState.rawFenceDepth -= 1;
382
- if (fenceState.rawFenceDepth === 0) {
383
- flushCodeSegment(segments, codeLines, fenceState);
384
- codeLines.length = 0;
385
- fenceState = null;
386
- }
387
- continue;
388
- }
389
- if (!language) {
390
- flushCodeSegment(segments, codeLines, fenceState);
391
- codeLines.length = 0;
392
- fenceState = null;
393
- continue;
394
- }
395
- fenceState.ambiguous = true;
396
- fenceState.rawFenceDepth += 1;
397
- continue;
398
- }
399
- }
400
- if (fenceState) {
401
- fenceState.rawLines.push(line);
402
- codeLines.push(line);
403
- } else textLines.push(line);
404
- }
405
- if (fenceState) textLines.push(...fenceState.rawLines);
406
- flushTextSegment(segments, textLines);
407
- return segments;
408
- }
409
- var Message = memo(function Message({ message, isStreaming = false }) {
478
+ //#endregion
479
+ //#region src/components/Messages/Messages.tsx
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 @@ var Message = memo(function Message({ message, isStreaming = false }) {
420
497
  children: message.content
421
498
  })
422
499
  });
423
- return /* @__PURE__ */ jsx(Box, {
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: parseContent(message.content).map((segment, index) => {
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,10 +555,10 @@ var Message = memo(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
- });
471
- function Messages({ messages, isLoading, sessionId = 0, streamingMessage }) {
560
+ }
561
+ function Messages({ messages, isLoading, sessionId, streamingMessage }) {
472
562
  return /* @__PURE__ */ jsxs(Box, {
473
563
  flexDirection: "column",
474
564
  children: [
@@ -1659,34 +1749,49 @@ var ACTION = {
1659
1749
  DELETE_MENU: "delete-menu",
1660
1750
  DELETE_PREFIX: "delete:",
1661
1751
  NEW: "new",
1752
+ OPEN_MENU: "open-menu",
1662
1753
  OPEN_PREFIX: "open:"
1663
1754
  };
1664
- function formatSessionLabel(session) {
1665
- const timestamp = new Date(session.updatedAt).toLocaleString();
1666
- return `${session.title} (${timestamp})`;
1755
+ var SESSION_LABEL_PADDING = 4;
1756
+ function truncate(value, maxLength) {
1757
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
1758
+ }
1759
+ function formatSessionLabel(session, maxWidth, prefix = "") {
1760
+ const suffix = ` (${new Date(session.updatedAt).toLocaleString()})`;
1761
+ const availableTitleWidth = maxWidth - prefix.length - suffix.length;
1762
+ if (availableTitleWidth < 1) return truncate(`${prefix}${session.title}${suffix}`, maxWidth);
1763
+ return `${prefix}${truncate(session.title, availableTitleWidth)}${suffix}`;
1667
1764
  }
1668
1765
  function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
1669
1766
  const [view, setView] = useState("main");
1670
1767
  const [error, setError] = useState();
1671
1768
  const [, refreshSessionList] = useState(0);
1769
+ const { stdout } = useStdout();
1672
1770
  const sessions = listSessions();
1673
- const options = view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1674
- label: `Delete ${formatSessionLabel(session)}`,
1771
+ const maxLabelWidth = Math.max(1, stdout.columns - SESSION_LABEL_PADDING);
1772
+ const options = view === "open" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1773
+ label: formatSessionLabel(session, maxLabelWidth),
1774
+ value: `${ACTION.OPEN_PREFIX}${session.id}`
1775
+ })), {
1776
+ label: "Back",
1777
+ value: ACTION.BACK
1778
+ }] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1779
+ label: formatSessionLabel(session, maxLabelWidth, "Delete "),
1675
1780
  value: `${ACTION.DELETE_PREFIX}${session.id}`
1676
1781
  })), {
1677
1782
  label: "Back",
1678
1783
  value: ACTION.BACK
1679
1784
  }] : [
1680
1785
  {
1681
- label: "Start new session",
1786
+ label: "New session",
1682
1787
  value: ACTION.NEW
1683
1788
  },
1684
- ...sessions.map((session) => ({
1685
- label: `${session.id === currentSessionId ? "Current: " : ""}${formatSessionLabel(session)}`,
1686
- value: `${ACTION.OPEN_PREFIX}${session.id}`
1687
- })),
1688
1789
  {
1689
- label: "Delete a session",
1790
+ label: "Open session",
1791
+ value: ACTION.OPEN_MENU
1792
+ },
1793
+ {
1794
+ label: "Delete session",
1690
1795
  value: ACTION.DELETE_MENU
1691
1796
  },
1692
1797
  {
@@ -1705,6 +1810,9 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1705
1810
  case value === ACTION.DELETE_MENU:
1706
1811
  setView("delete");
1707
1812
  break;
1813
+ case value === ACTION.OPEN_MENU:
1814
+ setView("open");
1815
+ break;
1708
1816
  case value === ACTION.BACK:
1709
1817
  setView("main");
1710
1818
  break;
@@ -1736,7 +1844,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1736
1844
  flexDirection: "column",
1737
1845
  children: [
1738
1846
  /* @__PURE__ */ jsx(Text, { children: "Sessions" }),
1739
- /* @__PURE__ */ jsx(SelectPromptHint, { message: view === "delete" ? "Delete session" : "Select session" }),
1847
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: view === "delete" ? "Delete session" : view === "open" ? "Open session" : "Select session" }),
1740
1848
  error && /* @__PURE__ */ jsx(Box, {
1741
1849
  marginBottom: 1,
1742
1850
  children: /* @__PURE__ */ jsx(Text, {
package/dist/cli.js CHANGED
@@ -7,18 +7,6 @@ import { Ollama } from "ollama";
7
7
  import { v7 } from "uuid";
8
8
  import { exec } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
- //#region \0rolldown/runtime.js
11
- var __defProp = Object.defineProperty;
12
- var __exportAll = (all, no_symbols) => {
13
- let target = {};
14
- for (var name in all) __defProp(target, name, {
15
- get: all[name],
16
- enumerable: true
17
- });
18
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
19
- return target;
20
- };
21
- //#endregion
22
10
  //#region src/constants/command.ts
23
11
  var LIST = [
24
12
  {
@@ -45,7 +33,7 @@ var LIST = [
45
33
  //#endregion
46
34
  //#region package.json
47
35
  var name = "code-ollama";
48
- var version = "0.14.2";
36
+ var version = "0.15.1";
49
37
  //#endregion
50
38
  //#region src/constants/package.ts
51
39
  var NAME = name;
@@ -510,6 +498,33 @@ var WRITE_TOOLS = new Set([
510
498
  RUN_SHELL
511
499
  ]);
512
500
  //#endregion
501
+ //#region src/utils/tools/shell.ts
502
+ var execAsync = promisify(exec);
503
+ var SHELL_EXEC_OPTIONS = {
504
+ timeout: 3e4,
505
+ maxBuffer: 1024 * 1024
506
+ };
507
+ /**
508
+ * Execute shell command with shared options (throws on error)
509
+ */
510
+ function execShell(command) {
511
+ return execAsync(command, SHELL_EXEC_OPTIONS);
512
+ }
513
+ /**
514
+ * Execute shell command
515
+ */
516
+ async function runShell(command) {
517
+ try {
518
+ const { stdout, stderr } = await execShell(command);
519
+ return { content: stdout || stderr };
520
+ } catch (error) {
521
+ return {
522
+ content: "",
523
+ error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
524
+ };
525
+ }
526
+ }
527
+ //#endregion
513
528
  //#region src/utils/tools/filesystem.ts
514
529
  /**
515
530
  * Read file contents
@@ -616,7 +631,6 @@ function listDir(dirPath) {
616
631
  * Search for pattern in files using ripgrep if available, fallback to Node.js
617
632
  */
618
633
  async function grepSearch(pattern, dirPath) {
619
- const { execShell } = await Promise.resolve().then(() => shell_exports);
620
634
  try {
621
635
  const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
622
636
  // v8 ignore next
@@ -655,37 +669,6 @@ async function grepSearch(pattern, dirPath) {
655
669
  }
656
670
  }
657
671
  //#endregion
658
- //#region src/utils/tools/shell.ts
659
- var shell_exports = /* @__PURE__ */ __exportAll({
660
- execShell: () => execShell,
661
- runShell: () => runShell
662
- });
663
- var execAsync = promisify(exec);
664
- var SHELL_EXEC_OPTIONS = {
665
- timeout: 3e4,
666
- maxBuffer: 1024 * 1024
667
- };
668
- /**
669
- * Execute shell command with shared options (throws on error)
670
- */
671
- function execShell(command) {
672
- return execAsync(command, SHELL_EXEC_OPTIONS);
673
- }
674
- /**
675
- * Execute shell command
676
- */
677
- async function runShell(command) {
678
- try {
679
- const { stdout, stderr } = await execShell(command);
680
- return { content: stdout || stderr };
681
- } catch (error) {
682
- return {
683
- content: "",
684
- error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
685
- };
686
- }
687
- }
688
- //#endregion
689
672
  //#region src/utils/tools/web/fetch.ts
690
673
  var FETCH_TIMEOUT_MS = 1e4;
691
674
  var BASE_HEADERS = { "user-agent": `${NAME}/${VERSION}` };
@@ -948,7 +931,7 @@ async function main(args = process.argv.slice(2)) {
948
931
  else await launchTui();
949
932
  }
950
933
  async function launchTui(sessionId) {
951
- const { renderApp } = await import("./assets/tui-Dse5XVJ_.js");
934
+ const { renderApp } = await import("./assets/tui-CSRbnCod.js");
952
935
  reset();
953
936
  renderApp(sessionId);
954
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.14.2",
3
+ "version": "0.15.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",