code-ollama 0.13.1 → 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;
@@ -794,8 +757,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
794
757
  }, [onSubmit, resetInput]);
795
758
  const showCommandMenu = input.startsWith("/");
796
759
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
797
- const handleSubmitText = useCallback(async (input) => {
798
- await tick();
760
+ const handleSubmitText = useCallback((input) => {
799
761
  if (input.startsWith("/")) return;
800
762
  if (hasFileSuggestionQuery(input)) {
801
763
  if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
@@ -854,22 +816,31 @@ function hasExecutablePlan(content) {
854
816
  }
855
817
  //#endregion
856
818
  //#region src/components/Chat/Chat.tsx
857
- function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
858
- const [messages, setMessages] = useState([]);
819
+ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
820
+ const sessionMessages = initialMessages ?? [];
821
+ const [messages, setMessages] = useState(sessionMessages);
859
822
  const [streamingMessage, setStreamingMessage] = useState(null);
860
823
  const [isLoading, setIsLoading] = useState(false);
861
824
  const [pendingToolCall, setPendingToolCall] = useState(null);
862
825
  const [pendingPlan, setPendingPlan] = useState(null);
863
826
  const [interruptReason, setInterruptReason] = useState(null);
864
827
  const abortControllerRef = useRef(null);
828
+ const persistedSnapshotRef = useRef("");
865
829
  useEffect(() => {
866
- setMessages([]);
830
+ setMessages(sessionMessages);
867
831
  setStreamingMessage(null);
868
832
  setIsLoading(false);
869
833
  setPendingToolCall(null);
870
834
  setPendingPlan(null);
871
835
  setInterruptReason(null);
836
+ persistedSnapshotRef.current = JSON.stringify(sessionMessages);
872
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]);
873
844
  const buildToolResultMessage = useCallback((toolName, result) => {
874
845
  if (result.error?.startsWith("Tool not allowed:")) return {
875
846
  role: SYSTEM,
@@ -1452,48 +1423,224 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1452
1423
  });
1453
1424
  }
1454
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
1455
1532
  //#region src/components/App.tsx
1456
1533
  var SCREEN = /* @__PURE__ */ function(SCREEN) {
1457
1534
  SCREEN["CHAT"] = "chat";
1458
1535
  SCREEN["MODEL_PICKER"] = "model-picker";
1459
1536
  SCREEN["SEARCH_SETTINGS"] = "search-settings";
1537
+ SCREEN["SESSION_MANAGER"] = "session-manager";
1460
1538
  return SCREEN;
1461
1539
  }(SCREEN || {});
1462
- function App() {
1540
+ function createSession(sessionId, model) {
1541
+ return sessionId ? loadSession(sessionId) : createSession$1(model);
1542
+ }
1543
+ function App({ sessionId }) {
1463
1544
  const { exit } = useApp();
1464
1545
  const [appConfig, setConfig] = useState(() => loadConfig());
1465
1546
  const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1466
1547
  const [mode, setMode] = useState(SAFE);
1467
- const [sessionId, setSessionId] = useState(0);
1548
+ const [activeSession, setSession] = useState(() => createSession(sessionId, loadConfig().model));
1468
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
+ }, []);
1469
1566
  const handleHeaderLoad = useCallback(() => {
1470
1567
  setIsHeaderLoaded(true);
1471
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]);
1472
1605
  const handleCommand = useCallback((command) => {
1473
1606
  switch (command) {
1607
+ case "/session":
1608
+ setScreen(SCREEN.SESSION_MANAGER);
1609
+ break;
1474
1610
  case "/model":
1475
1611
  setScreen(SCREEN.MODEL_PICKER);
1476
1612
  break;
1477
1613
  case "/search":
1478
1614
  setScreen(SCREEN.SEARCH_SETTINGS);
1479
1615
  break;
1480
- case "/clear":
1616
+ case "/clear": {
1481
1617
  resetSystemMessage();
1482
- clear();
1483
1618
  setScreen(SCREEN.CHAT);
1484
- setSessionId((sessionId) => sessionId + 1);
1619
+ const nextSession = createSession$1(appConfig.model);
1620
+ setActiveSession(nextSession);
1621
+ clear(nextSession.metadata.id);
1485
1622
  break;
1623
+ }
1486
1624
  case "/exit":
1487
1625
  exit();
1488
1626
  break;
1489
1627
  }
1490
- }, [exit]);
1628
+ }, [
1629
+ appConfig.model,
1630
+ exit,
1631
+ setActiveSession
1632
+ ]);
1491
1633
  const handleUpdateConfig = useCallback((update) => {
1492
1634
  setConfig((current) => ({
1493
1635
  ...current,
1494
1636
  ...update
1495
1637
  }));
1496
1638
  saveConfig(update);
1639
+ const newModel = update.model;
1640
+ if (newModel) setSession((current) => ({
1641
+ ...current,
1642
+ metadata: updateSessionModel(current.metadata.id, newModel)
1643
+ }));
1497
1644
  setScreen(SCREEN.CHAT);
1498
1645
  }, []);
1499
1646
  const handleClose = useCallback(() => {
@@ -1525,13 +1672,24 @@ function App() {
1525
1672
  onClose: handleClose
1526
1673
  });
1527
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;
1528
1684
  case SCREEN.CHAT:
1529
1685
  screenContent = /* @__PURE__ */ jsx(Chat, {
1686
+ initialMessages: activeSession.messages,
1530
1687
  model: appConfig.model,
1531
1688
  onCommand: handleCommand,
1689
+ onMessagesChange: handleMessagesChange,
1532
1690
  mode,
1533
1691
  onModeChange: setMode,
1534
- sessionId
1692
+ sessionId: activeSession.metadata.id
1535
1693
  });
1536
1694
  break;
1537
1695
  }
@@ -1553,15 +1711,15 @@ function App() {
1553
1711
  }
1554
1712
  //#endregion
1555
1713
  //#region src/tui.tsx
1556
- function renderApp() {
1714
+ function renderApp(sessionId) {
1557
1715
  let resetKey = 0;
1558
- const app = render(/* @__PURE__ */ jsx(App, {}, resetKey), {
1716
+ const app = render(/* @__PURE__ */ jsx(App, { sessionId }, resetKey), {
1559
1717
  exitOnCtrlC: false,
1560
1718
  maxFps: 60
1561
1719
  });
1562
- setClearHandler(() => {
1720
+ setClearHandler((nextSessionId) => {
1563
1721
  reset();
1564
- app.rerender(/* @__PURE__ */ jsx(App, {}, ++resetKey));
1722
+ app.rerender(/* @__PURE__ */ jsx(App, { sessionId: nextSessionId ?? sessionId }, ++resetKey));
1565
1723
  });
1566
1724
  }
1567
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.1";
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-CCa_vqh0.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.1",
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/"