code-ollama 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { _ as USER, a as tick, c as setClearHandler, d as loadConfig, f as saveConfig, g as SYSTEM, h as ASSISTANT, i as WRITE_TOOLS, l as listModels, m as withSystemMessage, n as READ_TOOLS, o as clear, p as resetSystemMessage, r as TOOLS, s as reset, t as executeTool, u as streamChat, v as PLAN_GENERATION_INSTRUCTION, y as VERSION } from "../cli.js";
1
+ import { A as LABEL, C as withSystemMessage, D as USER, E as SYSTEM, F as VERSION, I as LIST, M as SAFE, N as APPROVE, O as PLAN_GENERATION_INSTRUCTION, P as REJECT, S as resetSystemMessage, T as ASSISTANT, _ as setClearHandler, a as tick, b as loadConfig, c as appendMessage, d as deleteSessionIfEmpty, f as listSessions, g as reset, h as clear, i as WRITE_TOOLS, j as PLAN, k as AUTO, l as createSession$1, 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 listModels, w as HEADER_PREFIX, x as saveConfig, y as streamChat } from "../cli.js";
2
2
  import { readdirSync } from "node:fs";
3
- import { join, relative } from "node:path";
4
3
  import { homedir } from "node:os";
4
+ import { join, relative } 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";
@@ -9,43 +9,6 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { Select, Spinner } from "@inkjs/ui";
10
10
  import { marked } from "marked";
11
11
  import { markedTerminal } from "marked-terminal";
12
- //#region src/constants/command.ts
13
- var LIST = [
14
- {
15
- name: "/clear",
16
- description: "clear the current session"
17
- },
18
- {
19
- name: "/model",
20
- description: "switch the model"
21
- },
22
- {
23
- name: "/search",
24
- description: "configure web search"
25
- },
26
- {
27
- name: "/exit",
28
- description: "exit the application"
29
- }
30
- ];
31
- //#endregion
32
- //#region src/constants/decision.ts
33
- var APPROVE = "approve";
34
- var REJECT = "reject";
35
- //#endregion
36
- //#region src/constants/mode.ts
37
- var SAFE = "safe";
38
- var AUTO = "auto";
39
- var PLAN = "plan";
40
- var LABEL = {
41
- safe: "Safe",
42
- auto: "Auto",
43
- plan: "Plan"
44
- };
45
- //#endregion
46
- //#region src/constants/ui.ts
47
- var HEADER_PREFIX = "🦙 ";
48
- //#endregion
49
12
  //#region src/components/CodeBlock/CodeBlock.tsx
50
13
  var highlightCache = /* @__PURE__ */ new Map();
51
14
  var CODE_BLOCK_REGEX = /^(`{3,})(\w+)?[ \t]*\n([\s\S]*?)^\1[ \t]*$/gm;
@@ -113,10 +76,100 @@ var CodeBlock = memo(function CodeBlock({ code, language, role }) {
113
76
  });
114
77
  });
115
78
  //#endregion
79
+ //#region src/components/Markdown/extensions.ts
80
+ var LATEX_COMMANDS = {
81
+ "\\rightarrow": "→",
82
+ "\\leftarrow": "←",
83
+ "\\Rightarrow": "⇒",
84
+ "\\Leftarrow": "⇐",
85
+ "\\leftrightarrow": "↔",
86
+ "\\Leftrightarrow": "⟺",
87
+ "\\uparrow": "↑",
88
+ "\\downarrow": "↓",
89
+ "\\to": "→",
90
+ "\\gets": "←",
91
+ "\\times": "×",
92
+ "\\div": "÷",
93
+ "\\pm": "±",
94
+ "\\leq": "≤",
95
+ "\\geq": "≥",
96
+ "\\neq": "≠",
97
+ "\\approx": "≈",
98
+ "\\equiv": "≡",
99
+ "\\infty": "∞",
100
+ "\\sum": "∑",
101
+ "\\prod": "∏",
102
+ "\\sqrt": "√",
103
+ "\\partial": "∂",
104
+ "\\nabla": "∇",
105
+ "\\in": "∈",
106
+ "\\notin": "∉",
107
+ "\\subset": "⊂",
108
+ "\\supset": "⊃",
109
+ "\\cup": "∪",
110
+ "\\cap": "∩",
111
+ "\\emptyset": "∅",
112
+ "\\alpha": "α",
113
+ "\\beta": "β",
114
+ "\\gamma": "γ",
115
+ "\\delta": "δ",
116
+ "\\epsilon": "ε",
117
+ "\\theta": "θ",
118
+ "\\lambda": "λ",
119
+ "\\mu": "μ",
120
+ "\\pi": "π",
121
+ "\\sigma": "σ",
122
+ "\\tau": "τ",
123
+ "\\phi": "φ",
124
+ "\\omega": "ω",
125
+ "\\$": "$",
126
+ "\\%": "%",
127
+ "\\&": "&",
128
+ "\\#": "#",
129
+ "\\{": "{",
130
+ "\\}": "}",
131
+ "\\^": "^",
132
+ "\\_": "_",
133
+ "\\cdot": "·",
134
+ "\\ldots": "…",
135
+ "\\cdots": "⋯"
136
+ };
137
+ function convertLatex(math) {
138
+ let result = math.trim();
139
+ for (const [cmd, unicode] of Object.entries(LATEX_COMMANDS)) result = result.replaceAll(cmd, unicode);
140
+ result = result.replace(/\\frac\{([^}]*)\}\{([^}]*)\}/g, "$1/$2");
141
+ result = result.replace(/\^\{([^}]*)\}/g, "^$1");
142
+ result = result.replace(/_\{([^}]*)\}/g, "_$1");
143
+ result = result.replace(/\\[,;!: ]/g, " ");
144
+ result = result.replace(/\\[a-zA-Z]+\{([^}]*)\}/g, "$1");
145
+ result = result.replace(/\\[a-zA-Z]+/g, "");
146
+ return result.trim();
147
+ }
148
+ var inlineMathExtension = {
149
+ name: "inlineMath",
150
+ level: "inline",
151
+ start: (src) => src.indexOf("$"),
152
+ tokenizer(src) {
153
+ const match = /^\$([^$\n]+?)\$/.exec(src);
154
+ if (match) return {
155
+ type: "inlineMath",
156
+ raw: match[0],
157
+ math: match[1]
158
+ };
159
+ },
160
+ renderer(token) {
161
+ // v8 ignore next
162
+ return convertLatex(token.math ?? "");
163
+ }
164
+ };
165
+ //#endregion
116
166
  //#region src/components/Markdown/Markdown.tsx
117
167
  var HR_PLACEHOLDER = "__CODE_OLLAMA_HR_PLACEHOLDER__";
118
168
  marked.use(markedTerminal({ theme: "gitHub" }));
119
- marked.use({ renderer: { hr: () => `${HR_PLACEHOLDER}\n` } });
169
+ marked.use({
170
+ extensions: [inlineMathExtension],
171
+ renderer: { hr: () => `${HR_PLACEHOLDER}\n` }
172
+ });
120
173
  function renderMarkdown(content, hrWidth) {
121
174
  const hr = "─".repeat(Math.max(1, hrWidth));
122
175
  try {
@@ -704,8 +757,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
704
757
  }, [onSubmit, resetInput]);
705
758
  const showCommandMenu = input.startsWith("/");
706
759
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
707
- const handleSubmitText = useCallback(async (input) => {
708
- await tick();
760
+ const handleSubmitText = useCallback((input) => {
709
761
  if (input.startsWith("/")) return;
710
762
  if (hasFileSuggestionQuery(input)) {
711
763
  if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
@@ -764,22 +816,31 @@ function hasExecutablePlan(content) {
764
816
  }
765
817
  //#endregion
766
818
  //#region src/components/Chat/Chat.tsx
767
- function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
768
- const [messages, setMessages] = useState([]);
819
+ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
820
+ const sessionMessages = initialMessages ?? [];
821
+ const [messages, setMessages] = useState(sessionMessages);
769
822
  const [streamingMessage, setStreamingMessage] = useState(null);
770
823
  const [isLoading, setIsLoading] = useState(false);
771
824
  const [pendingToolCall, setPendingToolCall] = useState(null);
772
825
  const [pendingPlan, setPendingPlan] = useState(null);
773
826
  const [interruptReason, setInterruptReason] = useState(null);
774
827
  const abortControllerRef = useRef(null);
828
+ const persistedSnapshotRef = useRef("");
775
829
  useEffect(() => {
776
- setMessages([]);
830
+ setMessages(sessionMessages);
777
831
  setStreamingMessage(null);
778
832
  setIsLoading(false);
779
833
  setPendingToolCall(null);
780
834
  setPendingPlan(null);
781
835
  setInterruptReason(null);
836
+ persistedSnapshotRef.current = JSON.stringify(sessionMessages);
782
837
  }, [sessionId]);
838
+ useEffect(() => {
839
+ const snapshot = JSON.stringify(messages);
840
+ if (snapshot === persistedSnapshotRef.current) return;
841
+ persistedSnapshotRef.current = snapshot;
842
+ onMessagesChange?.(messages);
843
+ }, [messages, onMessagesChange]);
783
844
  const buildToolResultMessage = useCallback((toolName, result) => {
784
845
  if (result.error?.startsWith("Tool not allowed:")) return {
785
846
  role: SYSTEM,
@@ -1362,48 +1423,224 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1362
1423
  });
1363
1424
  }
1364
1425
  //#endregion
1426
+ //#region src/components/SessionManager.tsx
1427
+ var VIEW = /* @__PURE__ */ function(VIEW) {
1428
+ VIEW["MAIN"] = "main";
1429
+ VIEW["DELETE"] = "delete";
1430
+ return VIEW;
1431
+ }(VIEW || {});
1432
+ var ACTION = {
1433
+ BACK: "back",
1434
+ CLOSE: "close",
1435
+ DELETE_MENU: "delete-menu",
1436
+ DELETE_PREFIX: "delete:",
1437
+ NEW: "new",
1438
+ OPEN_PREFIX: "open:"
1439
+ };
1440
+ function formatSessionLabel(session) {
1441
+ const timestamp = new Date(session.updatedAt).toLocaleString();
1442
+ return `${session.title} (${timestamp})`;
1443
+ }
1444
+ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
1445
+ const [view, setView] = useState(VIEW.MAIN);
1446
+ const [error, setError] = useState();
1447
+ const [, refreshSessionList] = useState(0);
1448
+ const sessions = listSessions();
1449
+ const options = view === VIEW.DELETE ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1450
+ label: `Delete ${formatSessionLabel(session)}`,
1451
+ value: `${ACTION.DELETE_PREFIX}${session.id}`
1452
+ })), {
1453
+ label: "Back",
1454
+ value: ACTION.BACK
1455
+ }] : [
1456
+ {
1457
+ label: "Start new session",
1458
+ value: ACTION.NEW
1459
+ },
1460
+ ...sessions.map((session) => ({
1461
+ label: `${session.id === currentSessionId ? "Current: " : ""}${formatSessionLabel(session)}`,
1462
+ value: `${ACTION.OPEN_PREFIX}${session.id}`
1463
+ })),
1464
+ {
1465
+ label: "Delete a session",
1466
+ value: ACTION.DELETE_MENU
1467
+ },
1468
+ {
1469
+ label: "Close",
1470
+ value: ACTION.CLOSE
1471
+ }
1472
+ ];
1473
+ const handleChange = useCallback((value) => {
1474
+ switch (true) {
1475
+ case value === ACTION.CLOSE:
1476
+ onClose();
1477
+ break;
1478
+ case value === ACTION.NEW:
1479
+ onNew();
1480
+ break;
1481
+ case value === ACTION.DELETE_MENU:
1482
+ setView(VIEW.DELETE);
1483
+ break;
1484
+ case value === ACTION.BACK:
1485
+ setView(VIEW.MAIN);
1486
+ break;
1487
+ case value.startsWith(ACTION.DELETE_PREFIX):
1488
+ try {
1489
+ onDelete(value.slice(ACTION.DELETE_PREFIX.length));
1490
+ setError(void 0);
1491
+ refreshSessionList((key) => key + 1);
1492
+ } catch (error) {
1493
+ setError(error instanceof Error ? error.message : "Failed to delete session");
1494
+ }
1495
+ break;
1496
+ case value.startsWith(ACTION.OPEN_PREFIX):
1497
+ try {
1498
+ onOpen(value.slice(ACTION.OPEN_PREFIX.length));
1499
+ setError(void 0);
1500
+ } catch (error) {
1501
+ setError(error instanceof Error ? error.message : "Failed to open session");
1502
+ }
1503
+ break;
1504
+ }
1505
+ }, [
1506
+ onClose,
1507
+ onDelete,
1508
+ onNew,
1509
+ onOpen
1510
+ ]);
1511
+ return /* @__PURE__ */ jsxs(Box, {
1512
+ flexDirection: "column",
1513
+ children: [
1514
+ /* @__PURE__ */ jsx(Text, { children: "Sessions" }),
1515
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: view === VIEW.DELETE ? "Delete session" : "Select session" }),
1516
+ error && /* @__PURE__ */ jsx(Box, {
1517
+ marginBottom: 1,
1518
+ children: /* @__PURE__ */ jsx(Text, {
1519
+ color: "red",
1520
+ children: error
1521
+ })
1522
+ }),
1523
+ /* @__PURE__ */ jsx(SelectPrompt, {
1524
+ options,
1525
+ onCancel: onClose,
1526
+ onChange: handleChange
1527
+ }, `${view}:${String(sessions.length)}`)
1528
+ ]
1529
+ });
1530
+ }
1531
+ //#endregion
1365
1532
  //#region src/components/App.tsx
1366
1533
  var SCREEN = /* @__PURE__ */ function(SCREEN) {
1367
1534
  SCREEN["CHAT"] = "chat";
1368
1535
  SCREEN["MODEL_PICKER"] = "model-picker";
1369
1536
  SCREEN["SEARCH_SETTINGS"] = "search-settings";
1537
+ SCREEN["SESSION_MANAGER"] = "session-manager";
1370
1538
  return SCREEN;
1371
1539
  }(SCREEN || {});
1372
- function App() {
1540
+ function createSession(sessionId, model) {
1541
+ return sessionId ? loadSession(sessionId) : createSession$1(model);
1542
+ }
1543
+ function App({ sessionId }) {
1373
1544
  const { exit } = useApp();
1374
1545
  const [appConfig, setConfig] = useState(() => loadConfig());
1375
1546
  const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1376
1547
  const [mode, setMode] = useState(SAFE);
1377
- const [sessionId, setSessionId] = useState(0);
1548
+ const [activeSession, setSession] = useState(() => createSession(sessionId, loadConfig().model));
1378
1549
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
1550
+ const sessionRef = useRef(activeSession);
1551
+ useEffect(() => {
1552
+ sessionRef.current = activeSession;
1553
+ }, [activeSession]);
1554
+ useEffect(() => {
1555
+ return () => {
1556
+ const currentSession = sessionRef.current;
1557
+ if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, "cyan")}\n`);
1558
+ };
1559
+ }, []);
1560
+ const setActiveSession = useCallback((nextSession) => {
1561
+ setSession((current) => {
1562
+ deleteSessionIfEmpty(current.metadata.id);
1563
+ return nextSession;
1564
+ });
1565
+ }, []);
1379
1566
  const handleHeaderLoad = useCallback(() => {
1380
1567
  setIsHeaderLoaded(true);
1381
1568
  }, []);
1569
+ const handleCreateSession = useCallback(() => {
1570
+ const nextSession = createSession$1(appConfig.model);
1571
+ setActiveSession(nextSession);
1572
+ setScreen(SCREEN.CHAT);
1573
+ clear(nextSession.metadata.id);
1574
+ return nextSession;
1575
+ }, [appConfig.model, setActiveSession]);
1576
+ const handleOpenSession = useCallback((sessionId) => {
1577
+ if (sessionRef.current.metadata.id === sessionId) {
1578
+ setScreen(SCREEN.CHAT);
1579
+ return;
1580
+ }
1581
+ setActiveSession(loadSession(sessionId));
1582
+ setScreen(SCREEN.CHAT);
1583
+ clear(sessionId);
1584
+ }, [setActiveSession]);
1585
+ const handleDeleteSession = useCallback((sessionId) => {
1586
+ deleteSession(sessionId);
1587
+ setSession((current) => {
1588
+ if (current.metadata.id !== sessionId) return current;
1589
+ return createSession$1(appConfig.model);
1590
+ });
1591
+ setScreen(SCREEN.SESSION_MANAGER);
1592
+ }, [appConfig.model]);
1593
+ const handleMessagesChange = useCallback((messages) => {
1594
+ setSession((current) => {
1595
+ const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
1596
+ if (persistedMessages.length <= current.messages.length) return current;
1597
+ let metadata = current.metadata;
1598
+ for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, appConfig.model);
1599
+ return {
1600
+ metadata,
1601
+ messages: persistedMessages
1602
+ };
1603
+ });
1604
+ }, [appConfig.model]);
1382
1605
  const handleCommand = useCallback((command) => {
1383
1606
  switch (command) {
1607
+ case "/session":
1608
+ setScreen(SCREEN.SESSION_MANAGER);
1609
+ break;
1384
1610
  case "/model":
1385
1611
  setScreen(SCREEN.MODEL_PICKER);
1386
1612
  break;
1387
1613
  case "/search":
1388
1614
  setScreen(SCREEN.SEARCH_SETTINGS);
1389
1615
  break;
1390
- case "/clear":
1616
+ case "/clear": {
1391
1617
  resetSystemMessage();
1392
- clear();
1393
1618
  setScreen(SCREEN.CHAT);
1394
- setSessionId((sessionId) => sessionId + 1);
1619
+ const nextSession = createSession$1(appConfig.model);
1620
+ setActiveSession(nextSession);
1621
+ clear(nextSession.metadata.id);
1395
1622
  break;
1623
+ }
1396
1624
  case "/exit":
1397
1625
  exit();
1398
1626
  break;
1399
1627
  }
1400
- }, [exit]);
1628
+ }, [
1629
+ appConfig.model,
1630
+ exit,
1631
+ setActiveSession
1632
+ ]);
1401
1633
  const handleUpdateConfig = useCallback((update) => {
1402
1634
  setConfig((current) => ({
1403
1635
  ...current,
1404
1636
  ...update
1405
1637
  }));
1406
1638
  saveConfig(update);
1639
+ const newModel = update.model;
1640
+ if (newModel) setSession((current) => ({
1641
+ ...current,
1642
+ metadata: updateSessionModel(current.metadata.id, newModel)
1643
+ }));
1407
1644
  setScreen(SCREEN.CHAT);
1408
1645
  }, []);
1409
1646
  const handleClose = useCallback(() => {
@@ -1435,13 +1672,24 @@ function App() {
1435
1672
  onClose: handleClose
1436
1673
  });
1437
1674
  break;
1675
+ case SCREEN.SESSION_MANAGER:
1676
+ screenContent = /* @__PURE__ */ jsx(SessionManager, {
1677
+ currentSessionId: activeSession.metadata.id,
1678
+ onClose: handleClose,
1679
+ onDelete: handleDeleteSession,
1680
+ onNew: handleCreateSession,
1681
+ onOpen: handleOpenSession
1682
+ });
1683
+ break;
1438
1684
  case SCREEN.CHAT:
1439
1685
  screenContent = /* @__PURE__ */ jsx(Chat, {
1686
+ initialMessages: activeSession.messages,
1440
1687
  model: appConfig.model,
1441
1688
  onCommand: handleCommand,
1689
+ onMessagesChange: handleMessagesChange,
1442
1690
  mode,
1443
1691
  onModeChange: setMode,
1444
- sessionId
1692
+ sessionId: activeSession.metadata.id
1445
1693
  });
1446
1694
  break;
1447
1695
  }
@@ -1463,15 +1711,15 @@ function App() {
1463
1711
  }
1464
1712
  //#endregion
1465
1713
  //#region src/tui.tsx
1466
- function renderApp() {
1714
+ function renderApp(sessionId) {
1467
1715
  let resetKey = 0;
1468
- const app = render(/* @__PURE__ */ jsx(App, {}, resetKey), {
1716
+ const app = render(/* @__PURE__ */ jsx(App, { sessionId }, resetKey), {
1469
1717
  exitOnCtrlC: false,
1470
1718
  maxFps: 60
1471
1719
  });
1472
- setClearHandler(() => {
1720
+ setClearHandler((nextSessionId) => {
1473
1721
  reset();
1474
- app.rerender(/* @__PURE__ */ jsx(App, {}, ++resetKey));
1722
+ app.rerender(/* @__PURE__ */ jsx(App, { sessionId: nextSessionId ?? sessionId }, ++resetKey));
1475
1723
  });
1476
1724
  }
1477
1725
  //#endregion
package/dist/cli.js CHANGED
@@ -1,18 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as runShell } from "./assets/shell-CipXM_WI.js";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import cac from "cac";
5
- import { join } from "node:path";
6
5
  import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
7
  import { Ollama } from "ollama";
8
+ import { v7 } from "uuid";
9
+ //#region src/constants/command.ts
10
+ var LIST = [
11
+ {
12
+ name: "/clear",
13
+ description: "clear the current session"
14
+ },
15
+ {
16
+ name: "/session",
17
+ description: "manage sessions"
18
+ },
19
+ {
20
+ name: "/model",
21
+ description: "switch the model"
22
+ },
23
+ {
24
+ name: "/search",
25
+ description: "configure web search"
26
+ },
27
+ {
28
+ name: "/exit",
29
+ description: "exit the application"
30
+ }
31
+ ];
32
+ //#endregion
8
33
  //#region package.json
9
34
  var name = "code-ollama";
10
- var version = "0.13.0";
35
+ var version = "0.14.0";
11
36
  //#endregion
12
37
  //#region src/constants/package.ts
13
38
  var NAME = name;
14
39
  var VERSION = version;
15
40
  //#endregion
41
+ //#region src/constants/config.ts
42
+ var DIRECTORY = join(homedir(), `.${NAME}`);
43
+ //#endregion
44
+ //#region src/constants/decision.ts
45
+ var APPROVE = "approve";
46
+ var REJECT = "reject";
47
+ //#endregion
48
+ //#region src/constants/mode.ts
49
+ var SAFE = "safe";
50
+ var AUTO = "auto";
51
+ var PLAN = "plan";
52
+ var LABEL = {
53
+ safe: "Safe",
54
+ auto: "Auto",
55
+ plan: "Plan"
56
+ };
57
+ //#endregion
16
58
  //#region src/constants/prompt.ts
17
59
  var BASE_SYSTEM_PROMPT = `You are a coding assistant that helps users write, edit, and understand code. You have access to tools for reading files, writing files, running shell commands, searching code, and searching the web
18
60
 
@@ -70,6 +112,9 @@ var VIEW_RANGE = "view_range";
70
112
  var WEB_SEARCH = "web_search";
71
113
  var WEB_FETCH = "web_fetch";
72
114
  //#endregion
115
+ //#region src/constants/ui.ts
116
+ var HEADER_PREFIX = "🦙 ";
117
+ //#endregion
73
118
  //#region src/utils/agents.ts
74
119
  var AGENTS_FILE = "AGENTS.md";
75
120
  function loadAgentsContent() {
@@ -104,8 +149,7 @@ function withSystemMessage(messages) {
104
149
  }
105
150
  //#endregion
106
151
  //#region src/utils/config.ts
107
- var CONFIG_DIRECTORY = join(homedir(), `.${NAME}`);
108
- var CONFIG_PATH = join(CONFIG_DIRECTORY, "config.json");
152
+ var CONFIG_PATH = join(DIRECTORY, "config.json");
109
153
  var DEFAULT_HOST = "http://localhost:11434";
110
154
  var DEFAULT_MODEL$1 = "gemma4";
111
155
  function readFile$1() {
@@ -129,7 +173,7 @@ function saveConfig(patch) {
129
173
  ...readFile$1(),
130
174
  ...patch
131
175
  };
132
- mkdirSync(CONFIG_DIRECTORY, { recursive: true });
176
+ mkdirSync(DIRECTORY, { recursive: true });
133
177
  writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
134
178
  }
135
179
  //#endregion
@@ -177,8 +221,8 @@ function setClearHandler(handler) {
177
221
  /**
178
222
  * Clear the screen with Ink.
179
223
  */
180
- function clear() {
181
- clearHandler?.();
224
+ function clear(sessionId) {
225
+ clearHandler?.(sessionId);
182
226
  }
183
227
  /**
184
228
  * Reset the screen with ANSI escape sequence.
@@ -187,6 +231,156 @@ function reset() {
187
231
  process.stdout.write("\x1Bc\x1B[?25l");
188
232
  }
189
233
  //#endregion
234
+ //#region src/utils/session.ts
235
+ var SESSIONS_DIRECTORY = join(DIRECTORY, "sessions");
236
+ var METADATA_FILE_NAME = "metadata.json";
237
+ var MESSAGES_FILE_NAME = "messages.jsonl";
238
+ var DEFAULT_TITLE = "New session";
239
+ var TITLE_MAX_LENGTH = 80;
240
+ function getSessionDirectory(id) {
241
+ return join(SESSIONS_DIRECTORY, id);
242
+ }
243
+ function getMetadataPath(id) {
244
+ return join(getSessionDirectory(id), METADATA_FILE_NAME);
245
+ }
246
+ function getMessagesPath(id) {
247
+ return join(getSessionDirectory(id), MESSAGES_FILE_NAME);
248
+ }
249
+ function ensureSessionsDirectory() {
250
+ mkdirSync(SESSIONS_DIRECTORY, { recursive: true });
251
+ }
252
+ function ensureSessionDirectory(id) {
253
+ const directory = getSessionDirectory(id);
254
+ mkdirSync(directory, { recursive: true });
255
+ return directory;
256
+ }
257
+ function readMetadata(id) {
258
+ const path = getMetadataPath(id);
259
+ if (!existsSync(path)) throw new Error(`Session not found: ${id}`);
260
+ try {
261
+ return JSON.parse(readFileSync(path, "utf8"));
262
+ } catch {
263
+ throw new Error(`Invalid session metadata: ${id}`);
264
+ }
265
+ }
266
+ function writeMetadata(metadata) {
267
+ ensureSessionDirectory(metadata.id);
268
+ writeFileSync(getMetadataPath(metadata.id), JSON.stringify(metadata, null, 2) + "\n", "utf8");
269
+ }
270
+ function readMessages(id) {
271
+ const path = getMessagesPath(id);
272
+ if (!existsSync(path)) return [];
273
+ const content = readFileSync(path, "utf8").trim();
274
+ if (!content) return [];
275
+ try {
276
+ return content.split("\n").filter(Boolean).map((line) => JSON.parse(line));
277
+ } catch {
278
+ throw new Error(`Invalid session messages: ${id}`);
279
+ }
280
+ }
281
+ function deriveTitle(message) {
282
+ // v8 ignore next - title derivation is only reached for user messages
283
+ if (message.role !== "user") return DEFAULT_TITLE;
284
+ const normalized = message.content.replace(/\s+/g, " ").trim();
285
+ if (!normalized) return DEFAULT_TITLE;
286
+ return normalized.length > TITLE_MAX_LENGTH ? normalized.slice(0, TITLE_MAX_LENGTH - 1).trimEnd() + "…" : normalized;
287
+ }
288
+ function updateTitle(metadata, message) {
289
+ if (metadata.title !== DEFAULT_TITLE || message.role !== "user") return metadata;
290
+ return {
291
+ ...metadata,
292
+ title: deriveTitle(message)
293
+ };
294
+ }
295
+ function createSession(model) {
296
+ ensureSessionsDirectory();
297
+ const id = v7();
298
+ const now = (/* @__PURE__ */ new Date()).toISOString();
299
+ const metadata = {
300
+ id,
301
+ createdAt: now,
302
+ updatedAt: now,
303
+ title: DEFAULT_TITLE,
304
+ model
305
+ };
306
+ ensureSessionDirectory(id);
307
+ writeMetadata(metadata);
308
+ writeFileSync(getMessagesPath(id), "", "utf8");
309
+ return {
310
+ metadata,
311
+ messages: []
312
+ };
313
+ }
314
+ function listSessions() {
315
+ if (!existsSync(SESSIONS_DIRECTORY)) return [];
316
+ return readdirSync(SESSIONS_DIRECTORY, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
317
+ try {
318
+ return [readMetadata(entry.name)];
319
+ } catch {
320
+ return [];
321
+ }
322
+ }).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
323
+ }
324
+ function loadSession(id) {
325
+ return {
326
+ metadata: readMetadata(id),
327
+ messages: readMessages(id)
328
+ };
329
+ }
330
+ function appendMessage(id, message, model) {
331
+ ensureSessionDirectory(id);
332
+ let metadata = readMetadata(id);
333
+ metadata = updateTitle(metadata, message);
334
+ metadata = {
335
+ ...metadata,
336
+ model,
337
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
338
+ };
339
+ appendFileSync(getMessagesPath(id), JSON.stringify(message) + "\n", "utf8");
340
+ writeMetadata(metadata);
341
+ return metadata;
342
+ }
343
+ function updateSessionModel(id, model) {
344
+ const metadata = {
345
+ ...readMetadata(id),
346
+ model
347
+ };
348
+ writeMetadata(metadata);
349
+ return metadata;
350
+ }
351
+ function deleteSessionIfEmpty(id) {
352
+ const directory = getSessionDirectory(id);
353
+ if (!existsSync(directory)) return false;
354
+ const messagesPath = getMessagesPath(id);
355
+ if (existsSync(messagesPath) && readFileSync(messagesPath, "utf8").trim() !== "") return false;
356
+ rmSync(directory, {
357
+ recursive: true,
358
+ force: false
359
+ });
360
+ return true;
361
+ }
362
+ function deleteSession(id) {
363
+ const directory = getSessionDirectory(id);
364
+ if (!existsSync(directory)) throw new Error(`Session not found: ${id}`);
365
+ rmSync(directory, {
366
+ recursive: true,
367
+ force: false
368
+ });
369
+ }
370
+ //#endregion
371
+ //#region src/utils/terminal.ts
372
+ var ANSI_COLOR = { cyan: ["\x1B[36m", "\x1B[39m"] };
373
+ function color(text, name) {
374
+ const [open, close] = ANSI_COLOR[name];
375
+ return `${open}${text}${close}`;
376
+ }
377
+ function write(text) {
378
+ process.stdout.write(text);
379
+ }
380
+ function writeError(text) {
381
+ process.stderr.write(text);
382
+ }
383
+ //#endregion
190
384
  //#region src/utils/time.ts
191
385
  var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
192
386
  //#endregion
@@ -655,9 +849,16 @@ cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model,
655
849
  try {
656
850
  await runPrompt(model, prompt);
657
851
  } catch (error) {
658
- // v8 ignore next
659
- const message = error instanceof Error ? error.message : "Unknown error";
660
- process.stderr.write(`Error: ${message}\n`);
852
+ writeError(`Error: ${error instanceof Error ? error.message : "Unknown error"}\n`);
853
+ process.exitCode = 1;
854
+ }
855
+ });
856
+ cli.command("resume <sessionId>", "Resume a saved session").action(async (sessionId) => {
857
+ try {
858
+ loadSession(sessionId);
859
+ await launchTui(sessionId);
860
+ } catch (error) {
861
+ writeError(`Error: ${error instanceof Error ? error.message : "Unknown error"}\n`);
661
862
  process.exitCode = 1;
662
863
  }
663
864
  });
@@ -666,7 +867,7 @@ async function runPrompt(model, prompt) {
666
867
  role: USER,
667
868
  content: prompt
668
869
  }], model);
669
- process.stdout.write("\n");
870
+ write("\n");
670
871
  }
671
872
  async function processRunStream(messages, model) {
672
873
  const assistantMessage = {
@@ -676,7 +877,7 @@ async function processRunStream(messages, model) {
676
877
  for await (const chunk of streamChat(messages, model, TOOLS)) {
677
878
  if (chunk.type === "content") {
678
879
  assistantMessage.content += chunk.content;
679
- process.stdout.write(chunk.content);
880
+ write(chunk.content);
680
881
  continue;
681
882
  }
682
883
  for (const toolCall of chunk.tool_calls) {
@@ -695,17 +896,17 @@ async function processRunStream(messages, model) {
695
896
  }
696
897
  }
697
898
  async function main(args = process.argv.slice(2)) {
698
- if (!args.length) {
699
- const { renderApp } = await import("./assets/tui-ByqNs9kx.js");
700
- reset();
701
- renderApp();
702
- return;
703
- }
704
- cli.parse([
899
+ if (args.length) cli.parse([
705
900
  "node",
706
901
  "code-ollama",
707
902
  ...args
708
903
  ]);
904
+ else await launchTui();
905
+ }
906
+ async function launchTui(sessionId) {
907
+ const { renderApp } = await import("./assets/tui-TX7C0xYg.js");
908
+ reset();
909
+ renderApp(sessionId);
709
910
  }
710
911
  // v8 ignore start
711
912
  function isEntrypoint(argv1 = process.argv[1]) {
@@ -719,4 +920,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
719
920
  if (isEntrypoint()) main();
720
921
  // v8 ignore stop
721
922
  //#endregion
722
- export { USER as _, tick as a, setClearHandler as c, loadConfig as d, saveConfig as f, SYSTEM as g, ASSISTANT as h, WRITE_TOOLS as i, listModels as l, withSystemMessage as m, main, READ_TOOLS as n, clear as o, resetSystemMessage as p, TOOLS as r, reset as s, executeTool as t, streamChat as u, PLAN_GENERATION_INSTRUCTION as v, VERSION as y };
923
+ export { LABEL as A, withSystemMessage as C, USER as D, SYSTEM as E, VERSION as F, LIST as I, SAFE as M, APPROVE as N, PLAN_GENERATION_INSTRUCTION as O, REJECT as P, resetSystemMessage as S, ASSISTANT as T, setClearHandler as _, tick as a, loadConfig as b, appendMessage as c, deleteSessionIfEmpty as d, listSessions as f, reset as g, clear as h, WRITE_TOOLS as i, PLAN as j, AUTO 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, listModels as v, HEADER_PREFIX as w, saveConfig as x, streamChat as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",
@@ -46,16 +46,17 @@
46
46
  "marked": "15.0.12",
47
47
  "marked-terminal": "7.3.0",
48
48
  "ollama": "0.6.3",
49
- "react": "19.2.6"
49
+ "react": "19.2.6",
50
+ "uuid": "14.0.0"
50
51
  },
51
52
  "devDependencies": {
52
- "@commitlint/cli": "21.0.0",
53
- "@commitlint/config-conventional": "21.0.0",
53
+ "@commitlint/cli": "21.0.1",
54
+ "@commitlint/config-conventional": "21.0.1",
54
55
  "@eslint/config-helpers": "0.6.0",
55
56
  "@eslint/js": "10.0.1",
56
- "@types/node": "25.6.2",
57
+ "@types/node": "25.7.0",
57
58
  "@types/react": "19.2.14",
58
- "@vitest/coverage-v8": "4.1.5",
59
+ "@vitest/coverage-v8": "4.1.6",
59
60
  "eslint": "10.3.0",
60
61
  "eslint-plugin-prettier": "5.5.5",
61
62
  "eslint-plugin-simple-import-sort": "13.0.0",
@@ -64,12 +65,12 @@
64
65
  "ink-testing-library": "4.0.0",
65
66
  "lint-staged": "17.0.4",
66
67
  "prettier": "3.8.3",
67
- "publint": "0.3.20",
68
+ "publint": "0.3.21",
68
69
  "tsx": "4.21.0",
69
70
  "typescript": "6.0.3",
70
- "typescript-eslint": "8.59.2",
71
- "vite": "8.0.11",
72
- "vitest": "4.1.5"
71
+ "typescript-eslint": "8.59.3",
72
+ "vite": "8.0.12",
73
+ "vitest": "4.1.6"
73
74
  },
74
75
  "files": [
75
76
  "dist/"