code-ollama 0.14.1 → 0.15.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.
@@ -7,18 +7,24 @@ import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
7
7
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { Select, Spinner } from "@inkjs/ui";
10
- import { marked } from "marked";
10
+ import { Marked } from "marked";
11
11
  import { markedTerminal } from "marked-terminal";
12
12
  //#region src/components/CodeBlock/CodeBlock.tsx
13
13
  var highlightCache = /* @__PURE__ */ new Map();
14
- var CODE_BLOCK_REGEX = /^(`{3,})(\w+)?[ \t]*\n([\s\S]*?)^\1[ \t]*$/gm;
14
+ var CODE_BLOCK_REGEX = /^(?<indent>[ \t]*)(`{3,})(\w+)?[ \t]*\n([\s\S]*?)^\k<indent>\2[ \t]*$/gm;
15
+ function normalizeCodeBlockContent(content, indent = "") {
16
+ if (!indent) return content.trim();
17
+ const indentPattern = new RegExp(`^${indent}`, "gm");
18
+ return content.replace(indentPattern, "").trim();
19
+ }
15
20
  async function prewarmCodeBlocks(content) {
16
21
  const promises = [];
17
22
  let match;
18
23
  CODE_BLOCK_REGEX.lastIndex = 0;
19
24
  while ((match = CODE_BLOCK_REGEX.exec(content)) !== null) {
20
- const language = match[2];
21
- const code = match[3].trim();
25
+ const indent = match[1];
26
+ const language = match[3];
27
+ const code = normalizeCodeBlockContent(match[4], indent);
22
28
  // v8 ignore next 2
23
29
  if (code) promises.push(prewarmHighlight(code, language));
24
30
  }
@@ -165,15 +171,29 @@ var inlineMathExtension = {
165
171
  //#endregion
166
172
  //#region src/components/Markdown/Markdown.tsx
167
173
  var HR_PLACEHOLDER = "__CODE_OLLAMA_HR_PLACEHOLDER__";
168
- marked.use(markedTerminal({ theme: "gitHub" }));
169
- marked.use({
170
- extensions: [inlineMathExtension],
171
- renderer: { hr: () => `${HR_PLACEHOLDER}\n` }
172
- });
173
174
  function renderMarkdown(content, hrWidth) {
174
175
  const hr = "─".repeat(Math.max(1, hrWidth));
176
+ const markdown = new Marked();
177
+ const rendererExtension = {
178
+ extensions: [inlineMathExtension],
179
+ useNewRenderer: true,
180
+ renderer: {
181
+ hr: () => `${HR_PLACEHOLDER}\n`,
182
+ text(token) {
183
+ const textToken = token;
184
+ if (typeof token === "object" && Array.isArray(textToken.tokens)) return this.parser.parseInline(textToken.tokens);
185
+ return String(textToken.text);
186
+ }
187
+ }
188
+ };
189
+ markdown.use(markedTerminal({
190
+ theme: "gitHub",
191
+ reflowText: true,
192
+ width: Math.max(1, hrWidth)
193
+ }));
194
+ markdown.use(rendererExtension);
175
195
  try {
176
- const result = marked.parse(content);
196
+ const result = markdown.parse(content);
177
197
  return (typeof result === "string" ? result.trim() : content).replaceAll(HR_PLACEHOLDER, hr);
178
198
  } catch {
179
199
  return content;
@@ -197,7 +217,92 @@ var TURN_ABORTED_MESSAGE = [
197
217
  "</turn_aborted>"
198
218
  ].join("\n");
199
219
  //#endregion
200
- //#region src/components/Messages/Messages.tsx
220
+ //#region src/components/Messages/utils.ts
221
+ var FENCE_LINE_REGEX = /^(?<indent>[ \t]*)(?<fence>`{3,})(?<language>\w+)?[ \t]*$/;
222
+ function flushTextSegment(segments, textLines) {
223
+ const textContent = textLines.join("\n").trim();
224
+ if (textContent) segments.push({
225
+ type: "text",
226
+ content: textContent
227
+ });
228
+ }
229
+ function flushCodeSegment(segments, codeLines, fenceState) {
230
+ if (fenceState.ambiguous) {
231
+ segments.push({
232
+ type: "raw",
233
+ content: fenceState.rawLines.join("\n")
234
+ });
235
+ return;
236
+ }
237
+ const codeContent = normalizeCodeBlockContent(codeLines.join("\n"), fenceState.indent);
238
+ if (codeContent) segments.push({
239
+ type: "code",
240
+ content: codeContent,
241
+ language: fenceState.language
242
+ });
243
+ }
244
+ function unwrapRawMarkdownFence(content) {
245
+ if (!content.startsWith("```markdown\n") || !content.endsWith("\n```")) return null;
246
+ return content.slice(12, -4);
247
+ }
248
+ function parseContent(content) {
249
+ const segments = [];
250
+ const lines = content.split("\n");
251
+ const textLines = [];
252
+ const codeLines = [];
253
+ let fenceState = null;
254
+ for (const line of lines) {
255
+ const fenceMatch = FENCE_LINE_REGEX.exec(line);
256
+ if (fenceMatch?.groups) {
257
+ const { indent, fence, language } = fenceMatch.groups;
258
+ if (!fenceState) {
259
+ flushTextSegment(segments, textLines);
260
+ textLines.length = 0;
261
+ fenceState = {
262
+ indent,
263
+ fence,
264
+ language,
265
+ rawLines: [line],
266
+ ambiguous: false,
267
+ rawFenceDepth: 1
268
+ };
269
+ continue;
270
+ }
271
+ if (indent === fenceState.indent && fence === fenceState.fence) {
272
+ fenceState.rawLines.push(line);
273
+ if (fenceState.ambiguous) {
274
+ if (language) {
275
+ fenceState.rawFenceDepth += 1;
276
+ continue;
277
+ }
278
+ fenceState.rawFenceDepth -= 1;
279
+ if (fenceState.rawFenceDepth === 0) {
280
+ flushCodeSegment(segments, codeLines, fenceState);
281
+ codeLines.length = 0;
282
+ fenceState = null;
283
+ }
284
+ continue;
285
+ }
286
+ if (!language) {
287
+ flushCodeSegment(segments, codeLines, fenceState);
288
+ codeLines.length = 0;
289
+ fenceState = null;
290
+ continue;
291
+ }
292
+ fenceState.ambiguous = true;
293
+ fenceState.rawFenceDepth += 1;
294
+ continue;
295
+ }
296
+ }
297
+ if (fenceState) {
298
+ fenceState.rawLines.push(line);
299
+ codeLines.push(line);
300
+ } else textLines.push(line);
301
+ }
302
+ if (fenceState) textLines.push(...fenceState.rawLines);
303
+ flushTextSegment(segments, textLines);
304
+ return segments;
305
+ }
201
306
  function getMessageColor(role) {
202
307
  switch (role) {
203
308
  case USER: return "black";
@@ -206,46 +311,102 @@ function getMessageColor(role) {
206
311
  default: return;
207
312
  }
208
313
  }
209
- function parseContent(content) {
210
- const segments = [];
211
- let lastIndex = 0;
212
- let match;
213
- CODE_BLOCK_REGEX.lastIndex = 0;
214
- while ((match = CODE_BLOCK_REGEX.exec(content)) !== null) {
215
- if (match.index > lastIndex) {
216
- const textContent = content.slice(lastIndex, match.index).trim();
217
- // v8 ignore next 2 - Defensive check for empty trimmed content
218
- if (textContent) segments.push({
219
- type: "text",
220
- content: textContent
314
+ function isWordCharacter(char) {
315
+ return char !== void 0 && /[A-Za-z0-9]/.test(char);
316
+ }
317
+ function isEscaped(content, index) {
318
+ let slashCount = 0;
319
+ for (let cursor = index - 1; cursor >= 0 && content[cursor] === "\\"; cursor--) slashCount += 1;
320
+ return slashCount % 2 === 1;
321
+ }
322
+ function canOpenEmphasis(content, index, length) {
323
+ const previous = content[index - 1];
324
+ const next = content[index + length];
325
+ if (!next || /\s/.test(next)) return false;
326
+ return !isWordCharacter(previous);
327
+ }
328
+ function canCloseEmphasis(content, index, length) {
329
+ const previous = content[index - 1];
330
+ const next = content[index + length];
331
+ if (!previous || /\s/.test(previous)) return false;
332
+ return !isWordCharacter(next);
333
+ }
334
+ function findUnmatchedInlineDelimiter(content) {
335
+ const stack = [];
336
+ for (let index = 0; index < content.length; index += 1) {
337
+ const current = content[index];
338
+ if (isEscaped(content, index)) continue;
339
+ const top = stack.at(-1);
340
+ if (top?.kind === "code") {
341
+ if (current === "`") stack.pop();
342
+ continue;
343
+ }
344
+ if (top?.kind === "latex") {
345
+ if (current === "$") stack.pop();
346
+ continue;
347
+ }
348
+ if (current === "`") {
349
+ stack.push({
350
+ index,
351
+ length: 1,
352
+ kind: "code",
353
+ marker: "`"
221
354
  });
355
+ continue;
356
+ }
357
+ if (current === "$") {
358
+ stack.push({
359
+ index,
360
+ length: 1,
361
+ kind: "latex",
362
+ marker: "$"
363
+ });
364
+ continue;
365
+ }
366
+ if (current !== "*") continue;
367
+ const marker = current;
368
+ const length = content[index + 1] === marker ? 2 : 1;
369
+ const token = marker.repeat(length);
370
+ const kind = length === 2 ? "bold" : "italic";
371
+ if (top?.marker === token && top.kind === kind && canCloseEmphasis(content, index, length)) {
372
+ stack.pop();
373
+ if (length === 2) index += 1;
374
+ continue;
375
+ }
376
+ if (canOpenEmphasis(content, index, length)) {
377
+ stack.push({
378
+ index,
379
+ length,
380
+ kind,
381
+ marker: token
382
+ });
383
+ if (length === 2) index += 1;
222
384
  }
223
- const language = match[2];
224
- const codeContent = match[3].trim();
225
- // v8 ignore next 2 - Defensive check for empty code block
226
- if (codeContent) segments.push({
227
- type: "code",
228
- content: codeContent,
229
- language
230
- });
231
- lastIndex = match.index + match[0].length;
232
- }
233
- if (lastIndex < content.length) {
234
- const textContent = content.slice(lastIndex).trim();
235
- // v8 ignore next 2 - Defensive check for empty trimmed content
236
- if (textContent) segments.push({
237
- type: "text",
238
- content: textContent
239
- });
240
385
  }
241
- // v8 ignore next 2 - Defensive fallback for edge case
242
- if (!segments.length && content.trim()) segments.push({
243
- type: "text",
244
- content: content.trim()
386
+ return stack[0] ?? null;
387
+ }
388
+ function splitStreamingInlineContent(content) {
389
+ const unmatched = findUnmatchedInlineDelimiter(content);
390
+ if (!unmatched) return [{
391
+ type: "markdown",
392
+ content
393
+ }];
394
+ const parts = [];
395
+ const prefix = content.slice(0, unmatched.index);
396
+ const plainSuffix = content.slice(unmatched.index + unmatched.length);
397
+ if (prefix) parts.push({
398
+ type: "markdown",
399
+ content: prefix
245
400
  });
246
- return segments;
401
+ if (plainSuffix) parts.push({
402
+ type: "plain",
403
+ content: plainSuffix
404
+ });
405
+ return parts;
247
406
  }
248
- var Message = memo(function Message({ message }) {
407
+ //#endregion
408
+ //#region src/components/Messages/Messages.tsx
409
+ function Message({ message, isStreaming = false }) {
249
410
  const messageColor = getMessageColor(message.role);
250
411
  const isSystem = message.role === SYSTEM;
251
412
  const isUser = message.role === USER;
@@ -275,20 +436,39 @@ var Message = memo(function Message({ message }) {
275
436
  role: message.role
276
437
  })
277
438
  }, index);
439
+ if (segment.type === "raw") {
440
+ const markdownSource = unwrapRawMarkdownFence(segment.content);
441
+ return /* @__PURE__ */ jsx(Box, {
442
+ marginX: 2,
443
+ children: /* @__PURE__ */ jsx(CodeBlock, {
444
+ code: markdownSource ?? segment.content,
445
+ language: markdownSource ? "markdown" : segment.language,
446
+ role: message.role
447
+ })
448
+ }, index);
449
+ }
450
+ const textParts = isStreaming && !isUser ? splitStreamingInlineContent(segment.content) : [{
451
+ type: "markdown",
452
+ content: segment.content
453
+ }];
278
454
  return isUser ? /* @__PURE__ */ jsx(Text, {
279
455
  color: messageColor,
280
456
  children: prefix + segment.content
281
457
  }, index) : /* @__PURE__ */ jsx(Box, {
458
+ flexDirection: "column",
282
459
  marginX: 2,
283
- children: /* @__PURE__ */ jsx(Markdown, {
284
- content: segment.content,
460
+ children: textParts.map((part, partIndex) => part.type === "plain" ? /* @__PURE__ */ jsx(Text, {
461
+ color: messageColor,
462
+ children: part.content
463
+ }, partIndex) : /* @__PURE__ */ jsx(Markdown, {
464
+ content: part.content,
285
465
  color: messageColor
286
- })
466
+ }, partIndex))
287
467
  }, index);
288
468
  })
289
469
  });
290
- });
291
- function Messages({ messages, isLoading, sessionId = 0, streamingMessage }) {
470
+ }
471
+ function Messages({ messages, isLoading, sessionId, streamingMessage }) {
292
472
  return /* @__PURE__ */ jsxs(Box, {
293
473
  flexDirection: "column",
294
474
  children: [
@@ -296,7 +476,10 @@ function Messages({ messages, isLoading, sessionId = 0, streamingMessage }) {
296
476
  items: messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE),
297
477
  children: (message, index) => /* @__PURE__ */ jsx(Message, { message }, index)
298
478
  }, sessionId),
299
- streamingMessage && /* @__PURE__ */ jsx(Message, { message: streamingMessage }),
479
+ streamingMessage && /* @__PURE__ */ jsx(Message, {
480
+ isStreaming: true,
481
+ message: streamingMessage
482
+ }),
300
483
  isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
301
484
  marginTop: -1,
302
485
  marginBottom: 1,
@@ -1373,47 +1556,35 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1373
1556
  }
1374
1557
  //#endregion
1375
1558
  //#region src/components/SearchSettings.tsx
1376
- var View = /* @__PURE__ */ function(View) {
1377
- View["Menu"] = "menu";
1378
- View["Edit"] = "edit";
1379
- return View;
1380
- }(View || {});
1381
- var Action = /* @__PURE__ */ function(Action) {
1382
- Action["Set"] = "set";
1383
- Action["Clear"] = "clear";
1384
- Action["Cancel"] = "cancel";
1385
- return Action;
1386
- }(Action || {});
1387
1559
  function SearchSettings({ currentUrl, onClose, onSave }) {
1388
- const [view, setView] = useState(View.Menu);
1560
+ const [view, setView] = useState("menu");
1389
1561
  const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
1390
1562
  const [error, setError] = useState(null);
1391
1563
  const options = useMemo(() => {
1392
1564
  const nextOptions = [{
1393
1565
  label: currentUrl ? "Update SearXNG URL" : "Set SearXNG URL",
1394
- value: Action.Set
1566
+ value: "set"
1395
1567
  }];
1396
1568
  if (currentUrl) nextOptions.push({
1397
1569
  label: "Clear SearXNG URL",
1398
- value: Action.Clear
1570
+ value: "clear"
1399
1571
  });
1400
1572
  nextOptions.push({
1401
1573
  label: "Cancel",
1402
- value: Action.Cancel
1574
+ value: "cancel"
1403
1575
  });
1404
1576
  return nextOptions;
1405
1577
  }, [currentUrl]);
1406
1578
  const handleChange = useCallback((value) => {
1407
1579
  setError(null);
1408
1580
  switch (value) {
1409
- case Action.Set:
1581
+ case "set":
1410
1582
  setDraftUrl(currentUrl ?? "");
1411
- setView(View.Edit);
1583
+ setView("edit");
1412
1584
  break;
1413
- case Action.Clear:
1585
+ case "clear":
1414
1586
  onSave({ searxngBaseUrl: void 0 });
1415
1587
  break;
1416
- case Action.Cancel:
1417
1588
  default: onClose();
1418
1589
  }
1419
1590
  }, [
@@ -1439,13 +1610,13 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1439
1610
  }
1440
1611
  }, [onSave]);
1441
1612
  useInput((input, key) => {
1442
- if (view === View.Edit && (key.escape || key.ctrl && input === "c")) {
1613
+ if (view === "edit" && (key.escape || key.ctrl && input === "c")) {
1443
1614
  setDraftUrl(currentUrl ?? "");
1444
1615
  setError(null);
1445
- setView(View.Menu);
1616
+ setView("menu");
1446
1617
  }
1447
1618
  });
1448
- if (view === View.Edit) return /* @__PURE__ */ jsxs(Box, {
1619
+ if (view === "edit") return /* @__PURE__ */ jsxs(Box, {
1449
1620
  flexDirection: "column",
1450
1621
  children: [
1451
1622
  /* @__PURE__ */ jsx(Text, { children: "Set the SearXNG base URL. DuckDuckGo remains the fallback." }),
@@ -1482,45 +1653,55 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1482
1653
  }
1483
1654
  //#endregion
1484
1655
  //#region src/components/SessionManager.tsx
1485
- var VIEW = /* @__PURE__ */ function(VIEW) {
1486
- VIEW["MAIN"] = "main";
1487
- VIEW["DELETE"] = "delete";
1488
- return VIEW;
1489
- }(VIEW || {});
1490
1656
  var ACTION = {
1491
1657
  BACK: "back",
1492
1658
  CLOSE: "close",
1493
1659
  DELETE_MENU: "delete-menu",
1494
1660
  DELETE_PREFIX: "delete:",
1495
1661
  NEW: "new",
1662
+ OPEN_MENU: "open-menu",
1496
1663
  OPEN_PREFIX: "open:"
1497
1664
  };
1498
- function formatSessionLabel(session) {
1499
- const timestamp = new Date(session.updatedAt).toLocaleString();
1500
- return `${session.title} (${timestamp})`;
1665
+ var SESSION_LABEL_PADDING = 4;
1666
+ function truncate(value, maxLength) {
1667
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
1668
+ }
1669
+ function formatSessionLabel(session, maxWidth, prefix = "") {
1670
+ const suffix = ` (${new Date(session.updatedAt).toLocaleString()})`;
1671
+ const availableTitleWidth = maxWidth - prefix.length - suffix.length;
1672
+ if (availableTitleWidth < 1) return truncate(`${prefix}${session.title}${suffix}`, maxWidth);
1673
+ return `${prefix}${truncate(session.title, availableTitleWidth)}${suffix}`;
1501
1674
  }
1502
1675
  function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
1503
- const [view, setView] = useState(VIEW.MAIN);
1676
+ const [view, setView] = useState("main");
1504
1677
  const [error, setError] = useState();
1505
1678
  const [, refreshSessionList] = useState(0);
1679
+ const { stdout } = useStdout();
1506
1680
  const sessions = listSessions();
1507
- const options = view === VIEW.DELETE ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1508
- label: `Delete ${formatSessionLabel(session)}`,
1681
+ const maxLabelWidth = Math.max(1, stdout.columns - SESSION_LABEL_PADDING);
1682
+ const options = view === "open" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1683
+ label: formatSessionLabel(session, maxLabelWidth),
1684
+ value: `${ACTION.OPEN_PREFIX}${session.id}`
1685
+ })), {
1686
+ label: "Back",
1687
+ value: ACTION.BACK
1688
+ }] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1689
+ label: formatSessionLabel(session, maxLabelWidth, "Delete "),
1509
1690
  value: `${ACTION.DELETE_PREFIX}${session.id}`
1510
1691
  })), {
1511
1692
  label: "Back",
1512
1693
  value: ACTION.BACK
1513
1694
  }] : [
1514
1695
  {
1515
- label: "Start new session",
1696
+ label: "New session",
1516
1697
  value: ACTION.NEW
1517
1698
  },
1518
- ...sessions.map((session) => ({
1519
- label: `${session.id === currentSessionId ? "Current: " : ""}${formatSessionLabel(session)}`,
1520
- value: `${ACTION.OPEN_PREFIX}${session.id}`
1521
- })),
1522
1699
  {
1523
- label: "Delete a session",
1700
+ label: "Open session",
1701
+ value: ACTION.OPEN_MENU
1702
+ },
1703
+ {
1704
+ label: "Delete session",
1524
1705
  value: ACTION.DELETE_MENU
1525
1706
  },
1526
1707
  {
@@ -1537,10 +1718,13 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1537
1718
  onNew();
1538
1719
  break;
1539
1720
  case value === ACTION.DELETE_MENU:
1540
- setView(VIEW.DELETE);
1721
+ setView("delete");
1722
+ break;
1723
+ case value === ACTION.OPEN_MENU:
1724
+ setView("open");
1541
1725
  break;
1542
1726
  case value === ACTION.BACK:
1543
- setView(VIEW.MAIN);
1727
+ setView("main");
1544
1728
  break;
1545
1729
  case value.startsWith(ACTION.DELETE_PREFIX):
1546
1730
  try {
@@ -1570,7 +1754,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1570
1754
  flexDirection: "column",
1571
1755
  children: [
1572
1756
  /* @__PURE__ */ jsx(Text, { children: "Sessions" }),
1573
- /* @__PURE__ */ jsx(SelectPromptHint, { message: view === VIEW.DELETE ? "Delete session" : "Select session" }),
1757
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: view === "delete" ? "Delete session" : view === "open" ? "Open session" : "Select session" }),
1574
1758
  error && /* @__PURE__ */ jsx(Box, {
1575
1759
  marginBottom: 1,
1576
1760
  children: /* @__PURE__ */ jsx(Text, {
@@ -1588,20 +1772,13 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
1588
1772
  }
1589
1773
  //#endregion
1590
1774
  //#region src/components/App.tsx
1591
- var SCREEN = /* @__PURE__ */ function(SCREEN) {
1592
- SCREEN["CHAT"] = "chat";
1593
- SCREEN["MODEL_PICKER"] = "model-picker";
1594
- SCREEN["SEARCH_SETTINGS"] = "search-settings";
1595
- SCREEN["SESSION_MANAGER"] = "session-manager";
1596
- return SCREEN;
1597
- }(SCREEN || {});
1598
1775
  function createSession(sessionId, model) {
1599
1776
  return sessionId ? loadSession(sessionId) : createSession$1(model);
1600
1777
  }
1601
1778
  function App({ sessionId }) {
1602
1779
  const { exit } = useApp();
1603
1780
  const [appConfig, setConfig] = useState(() => loadConfig());
1604
- const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1781
+ const [currentScreen, setScreen] = useState("chat");
1605
1782
  const [mode, setMode] = useState(SAFE);
1606
1783
  const [activeSession, setSession] = useState(() => createSession(sessionId, loadConfig().model));
1607
1784
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
@@ -1627,17 +1804,17 @@ function App({ sessionId }) {
1627
1804
  const handleCreateSession = useCallback(() => {
1628
1805
  const nextSession = createSession$1(appConfig.model);
1629
1806
  setActiveSession(nextSession);
1630
- setScreen(SCREEN.CHAT);
1807
+ setScreen("chat");
1631
1808
  clear(nextSession.metadata.id);
1632
1809
  return nextSession;
1633
1810
  }, [appConfig.model, setActiveSession]);
1634
1811
  const handleOpenSession = useCallback((sessionId) => {
1635
1812
  if (sessionRef.current.metadata.id === sessionId) {
1636
- setScreen(SCREEN.CHAT);
1813
+ setScreen("chat");
1637
1814
  return;
1638
1815
  }
1639
1816
  setActiveSession(loadSession(sessionId));
1640
- setScreen(SCREEN.CHAT);
1817
+ setScreen("chat");
1641
1818
  clear(sessionId);
1642
1819
  }, [setActiveSession]);
1643
1820
  const handleDeleteSession = useCallback((sessionId) => {
@@ -1646,7 +1823,7 @@ function App({ sessionId }) {
1646
1823
  if (current.metadata.id !== sessionId) return current;
1647
1824
  return createSession$1(appConfig.model);
1648
1825
  });
1649
- setScreen(SCREEN.SESSION_MANAGER);
1826
+ setScreen("session-manager");
1650
1827
  }, [appConfig.model]);
1651
1828
  const handleMessagesChange = useCallback((messages) => {
1652
1829
  setSession((current) => {
@@ -1663,17 +1840,17 @@ function App({ sessionId }) {
1663
1840
  const handleCommand = useCallback((command) => {
1664
1841
  switch (command) {
1665
1842
  case "/session":
1666
- setScreen(SCREEN.SESSION_MANAGER);
1843
+ setScreen("session-manager");
1667
1844
  break;
1668
1845
  case "/model":
1669
- setScreen(SCREEN.MODEL_PICKER);
1846
+ setScreen("model-picker");
1670
1847
  break;
1671
1848
  case "/search":
1672
- setScreen(SCREEN.SEARCH_SETTINGS);
1849
+ setScreen("search-settings");
1673
1850
  break;
1674
1851
  case "/clear": {
1675
1852
  resetSystemMessage();
1676
- setScreen(SCREEN.CHAT);
1853
+ setScreen("chat");
1677
1854
  const nextSession = createSession$1(appConfig.model);
1678
1855
  setActiveSession(nextSession);
1679
1856
  clear(nextSession.metadata.id);
@@ -1699,10 +1876,10 @@ function App({ sessionId }) {
1699
1876
  ...current,
1700
1877
  metadata: updateSessionModel(current.metadata.id, newModel)
1701
1878
  }));
1702
- setScreen(SCREEN.CHAT);
1879
+ setScreen("chat");
1703
1880
  }, []);
1704
1881
  const handleClose = useCallback(() => {
1705
- setScreen(SCREEN.CHAT);
1882
+ setScreen("chat");
1706
1883
  }, []);
1707
1884
  const handleToggleMode = useCallback(() => {
1708
1885
  setMode((mode) => {
@@ -1716,21 +1893,21 @@ function App({ sessionId }) {
1716
1893
  }, []);
1717
1894
  let screenContent;
1718
1895
  switch (currentScreen) {
1719
- case SCREEN.MODEL_PICKER:
1896
+ case "model-picker":
1720
1897
  screenContent = /* @__PURE__ */ jsx(ModelPicker, {
1721
1898
  currentModel: appConfig.model,
1722
1899
  onSelect: handleUpdateConfig,
1723
1900
  onClose: handleClose
1724
1901
  });
1725
1902
  break;
1726
- case SCREEN.SEARCH_SETTINGS:
1903
+ case "search-settings":
1727
1904
  screenContent = /* @__PURE__ */ jsx(SearchSettings, {
1728
1905
  currentUrl: appConfig.searxngBaseUrl,
1729
1906
  onSave: handleUpdateConfig,
1730
1907
  onClose: handleClose
1731
1908
  });
1732
1909
  break;
1733
- case SCREEN.SESSION_MANAGER:
1910
+ case "session-manager":
1734
1911
  screenContent = /* @__PURE__ */ jsx(SessionManager, {
1735
1912
  currentSessionId: activeSession.metadata.id,
1736
1913
  onClose: handleClose,
@@ -1739,7 +1916,7 @@ function App({ sessionId }) {
1739
1916
  onOpen: handleOpenSession
1740
1917
  });
1741
1918
  break;
1742
- case SCREEN.CHAT:
1919
+ case "chat":
1743
1920
  screenContent = /* @__PURE__ */ jsx(Chat, {
1744
1921
  initialMessages: activeSession.messages,
1745
1922
  model: appConfig.model,
package/dist/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { t as runShell } from "./assets/shell-CipXM_WI.js";
3
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
3
  import cac from "cac";
5
4
  import { homedir } from "node:os";
6
5
  import { join } from "node:path";
7
6
  import { Ollama } from "ollama";
8
7
  import { v7 } from "uuid";
8
+ import { exec } from "node:child_process";
9
+ import { promisify } from "node:util";
9
10
  //#region src/constants/command.ts
10
11
  var LIST = [
11
12
  {
@@ -32,7 +33,7 @@ var LIST = [
32
33
  //#endregion
33
34
  //#region package.json
34
35
  var name = "code-ollama";
35
- var version = "0.14.1";
36
+ var version = "0.15.0";
36
37
  //#endregion
37
38
  //#region src/constants/package.ts
38
39
  var NAME = name;
@@ -497,6 +498,33 @@ var WRITE_TOOLS = new Set([
497
498
  RUN_SHELL
498
499
  ]);
499
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
500
528
  //#region src/utils/tools/filesystem.ts
501
529
  /**
502
530
  * Read file contents
@@ -603,7 +631,6 @@ function listDir(dirPath) {
603
631
  * Search for pattern in files using ripgrep if available, fallback to Node.js
604
632
  */
605
633
  async function grepSearch(pattern, dirPath) {
606
- const { execShell } = await import("./assets/shell-CipXM_WI.js").then((n) => n.n);
607
634
  try {
608
635
  const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
609
636
  // v8 ignore next
@@ -904,7 +931,7 @@ async function main(args = process.argv.slice(2)) {
904
931
  else await launchTui();
905
932
  }
906
933
  async function launchTui(sessionId) {
907
- const { renderApp } = await import("./assets/tui-CoX71F7Y.js");
934
+ const { renderApp } = await import("./assets/tui-DPx5MGHZ.js");
908
935
  reset();
909
936
  renderApp(sessionId);
910
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.14.1",
3
+ "version": "0.15.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",
@@ -54,7 +54,7 @@
54
54
  "@commitlint/config-conventional": "21.0.1",
55
55
  "@eslint/config-helpers": "0.6.0",
56
56
  "@eslint/js": "10.0.1",
57
- "@types/node": "25.7.0",
57
+ "@types/node": "25.8.0",
58
58
  "@types/react": "19.2.14",
59
59
  "@vitest/coverage-v8": "4.1.6",
60
60
  "eslint": "10.3.0",
@@ -66,10 +66,10 @@
66
66
  "lint-staged": "17.0.4",
67
67
  "prettier": "3.8.3",
68
68
  "publint": "0.3.21",
69
- "tsx": "4.21.0",
69
+ "tsx": "4.22.0",
70
70
  "typescript": "6.0.3",
71
71
  "typescript-eslint": "8.59.3",
72
- "vite": "8.0.12",
72
+ "vite": "8.0.13",
73
73
  "vitest": "4.1.6"
74
74
  },
75
75
  "files": [
@@ -1,46 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { promisify } from "node:util";
3
- //#region \0rolldown/runtime.js
4
- var __defProp = Object.defineProperty;
5
- var __exportAll = (all, no_symbols) => {
6
- let target = {};
7
- for (var name in all) __defProp(target, name, {
8
- get: all[name],
9
- enumerable: true
10
- });
11
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
12
- return target;
13
- };
14
- //#endregion
15
- //#region src/utils/tools/shell.ts
16
- var shell_exports = /* @__PURE__ */ __exportAll({
17
- execShell: () => execShell,
18
- runShell: () => runShell
19
- });
20
- var execAsync = promisify(exec);
21
- var SHELL_EXEC_OPTIONS = {
22
- timeout: 3e4,
23
- maxBuffer: 1024 * 1024
24
- };
25
- /**
26
- * Execute shell command with shared options (throws on error)
27
- */
28
- function execShell(command) {
29
- return execAsync(command, SHELL_EXEC_OPTIONS);
30
- }
31
- /**
32
- * Execute shell command
33
- */
34
- async function runShell(command) {
35
- try {
36
- const { stdout, stderr } = await execShell(command);
37
- return { content: stdout || stderr };
38
- } catch (error) {
39
- return {
40
- content: "",
41
- error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
42
- };
43
- }
44
- }
45
- //#endregion
46
- export { shell_exports as n, runShell as t };