code-ollama 0.13.1 → 0.14.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.
@@ -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;
@@ -102,7 +65,7 @@ var CodeBlock = memo(function CodeBlock({ code, language, role }) {
102
65
  const isSystem = role === SYSTEM;
103
66
  return /* @__PURE__ */ jsx(Box, {
104
67
  flexDirection: "column",
105
- borderStyle: "round",
68
+ borderStyle: "bold",
106
69
  borderColor: isSystem ? "gray" : "dim",
107
70
  paddingX: 1,
108
71
  marginY: 1,
@@ -345,11 +308,12 @@ function Messages({ messages, isLoading, sessionId = 0, streamingMessage }) {
345
308
  }
346
309
  //#endregion
347
310
  //#region src/components/SelectPrompt.tsx
348
- function SelectPrompt({ children, onCancel, ...selectProps }) {
311
+ function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
349
312
  useInput((input, key) => {
350
313
  if (key.escape || key.ctrl && input === "c") onCancel?.();
351
314
  });
352
315
  return /* @__PURE__ */ jsxs(Box, {
316
+ borderStyle,
353
317
  flexDirection: "column",
354
318
  children: [children, /* @__PURE__ */ jsx(Select, { ...selectProps })]
355
319
  });
@@ -410,29 +374,32 @@ var options$1 = [
410
374
  }
411
375
  ];
412
376
  function PlanApproval({ planContent, onModeChange }) {
413
- return /* @__PURE__ */ jsx(SelectPrompt, {
414
- options: options$1,
415
- onChange: useCallback((value) => {
416
- onModeChange(value);
417
- }, [onModeChange]),
418
- onCancel: useCallback(() => {
419
- onModeChange(PLAN);
420
- }, [onModeChange]),
421
- children: /* @__PURE__ */ jsxs(Box, {
422
- flexDirection: "column",
423
- marginTop: 1,
424
- children: [
425
- /* @__PURE__ */ jsx(Text, {
426
- bold: true,
427
- color: "magenta",
428
- children: "Plan Generated - Choose execution mode:"
429
- }),
430
- /* @__PURE__ */ jsx(Box, {
431
- marginY: 1,
432
- children: /* @__PURE__ */ jsx(Text, { children: planContent })
433
- }),
434
- /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select execution mode" })
435
- ]
377
+ return /* @__PURE__ */ jsx(Box, {
378
+ marginX: 2,
379
+ children: /* @__PURE__ */ jsx(SelectPrompt, {
380
+ borderStyle: "bold",
381
+ options: options$1,
382
+ onChange: useCallback((value) => {
383
+ onModeChange(value);
384
+ }, [onModeChange]),
385
+ onCancel: useCallback(() => {
386
+ onModeChange(PLAN);
387
+ }, [onModeChange]),
388
+ children: /* @__PURE__ */ jsxs(Box, {
389
+ flexDirection: "column",
390
+ children: [
391
+ /* @__PURE__ */ jsx(Text, {
392
+ bold: true,
393
+ color: "magenta",
394
+ children: "Plan Generated - Choose execution mode:"
395
+ }),
396
+ /* @__PURE__ */ jsx(Box, {
397
+ marginY: 1,
398
+ children: /* @__PURE__ */ jsx(Text, { children: planContent })
399
+ }),
400
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select execution mode" })
401
+ ]
402
+ })
436
403
  })
437
404
  });
438
405
  }
@@ -453,47 +420,51 @@ function ToolApproval({ toolCall, onDecision }) {
453
420
  onDecision(REJECT);
454
421
  }, [onDecision]);
455
422
  const args = JSON.stringify(toolCall.function.arguments, null, 2);
456
- return /* @__PURE__ */ jsxs(SelectPrompt, {
457
- options,
458
- onChange: handleChange,
459
- onCancel: handleEscape,
460
- children: [
461
- /* @__PURE__ */ jsx(Text, {
462
- color: "yellow",
463
- children: "⚠️ Tool requires approval:"
464
- }),
465
- /* @__PURE__ */ jsxs(Box, {
466
- marginX: 3,
467
- marginBottom: 1,
468
- flexDirection: "column",
469
- children: [/* @__PURE__ */ jsxs(Text, { children: [
470
- /* @__PURE__ */ jsx(Text, {
471
- italic: true,
472
- children: "Tool:"
473
- }),
474
- " ",
475
- toolCall.function.name
476
- ] }), /* @__PURE__ */ jsxs(Text, { children: [
477
- /* @__PURE__ */ jsx(Text, {
478
- italic: true,
479
- children: "Arguments:"
480
- }),
481
- " ",
482
- args
483
- ] })]
484
- }),
485
- /* @__PURE__ */ jsx(SelectPromptHint, {
486
- message: "Select approval action",
487
- escapeLabel: "reject"
488
- })
489
- ]
423
+ return /* @__PURE__ */ jsx(Box, {
424
+ marginX: 2,
425
+ children: /* @__PURE__ */ jsxs(SelectPrompt, {
426
+ borderStyle: "bold",
427
+ options,
428
+ onChange: handleChange,
429
+ onCancel: handleEscape,
430
+ children: [
431
+ /* @__PURE__ */ jsx(Text, {
432
+ color: "yellow",
433
+ children: "Tool requires approval ⚠️ "
434
+ }),
435
+ /* @__PURE__ */ jsxs(Box, {
436
+ flexDirection: "column",
437
+ marginBottom: 1,
438
+ marginX: 2,
439
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
440
+ /* @__PURE__ */ jsx(Text, {
441
+ dimColor: true,
442
+ children: "Tool:"
443
+ }),
444
+ " ",
445
+ toolCall.function.name
446
+ ] }), /* @__PURE__ */ jsxs(Text, { children: [
447
+ /* @__PURE__ */ jsx(Text, {
448
+ dimColor: true,
449
+ children: "Arguments:"
450
+ }),
451
+ " ",
452
+ args
453
+ ] })]
454
+ }),
455
+ /* @__PURE__ */ jsx(SelectPromptHint, {
456
+ message: "Select approval action",
457
+ escapeLabel: "reject"
458
+ })
459
+ ]
460
+ })
490
461
  });
491
462
  }
492
463
  //#endregion
493
464
  //#region src/components/Chat/constants.ts
494
465
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
495
466
  var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
496
- var PLAN_EXECUTION_REMINDER = "Do not claim success and do not call write_file or run_shell until the user approves execution";
467
+ var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
497
468
  var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
498
469
  INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
499
470
  INTERRUPT_REASON["REJECTED"] = "rejected";
@@ -501,7 +472,39 @@ var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
501
472
  }({});
502
473
  //#endregion
503
474
  //#region src/components/TextInput/TextInput.tsx
504
- function TextInput({ value, isDisabled = false, placeholder, cursorPosition: externalCursorPosition, onChange, onSubmit }) {
475
+ function buildLineSegments(displayValue, cursorPosition, width) {
476
+ const safeWidth = Math.max(1, width);
477
+ const cursorChar = displayValue[cursorPosition] || " ";
478
+ const renderValue = displayValue.slice(0, cursorPosition) + cursorChar + displayValue.slice(cursorPosition + 1);
479
+ const totalLength = Math.max(1, renderValue.length);
480
+ const lines = [];
481
+ for (let start = 0; start < totalLength; start += safeWidth) {
482
+ const end = start + safeWidth;
483
+ const text = renderValue.slice(start, end);
484
+ const hasCursor = cursorPosition >= start && cursorPosition < end;
485
+ if (!hasCursor) {
486
+ lines.push({
487
+ text,
488
+ hasCursor,
489
+ beforeCursor: "",
490
+ cursorChar: " ",
491
+ afterCursor: ""
492
+ });
493
+ continue;
494
+ }
495
+ const offset = cursorPosition - start;
496
+ lines.push({
497
+ text,
498
+ hasCursor,
499
+ beforeCursor: text.slice(0, offset),
500
+ cursorChar: text[offset] || " ",
501
+ afterCursor: text.slice(offset + 1)
502
+ });
503
+ }
504
+ return lines;
505
+ }
506
+ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: externalCursorPosition, wrapIndent = 0, onChange, onSubmit }) {
507
+ const { stdout } = useStdout();
505
508
  const [cursorPosition, setCursorPosition] = useState(value.length);
506
509
  const prevValueRef = useRef(value);
507
510
  const prevExternalCursorRef = useRef(externalCursorPosition);
@@ -559,6 +562,14 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
559
562
  setCursorPosition(value.length);
560
563
  return;
561
564
  }
565
+ if (key.ctrl && input === "a") {
566
+ setCursorPosition(0);
567
+ return;
568
+ }
569
+ if (key.ctrl && input === "e") {
570
+ setCursorPosition(value.length);
571
+ return;
572
+ }
562
573
  // v8 ignore start
563
574
  if (input) {
564
575
  onChange(value.slice(0, cursorPosition) + input + value.slice(cursorPosition));
@@ -568,23 +579,31 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
568
579
  }, { isActive: !isDisabled });
569
580
  const displayValue = value || (placeholder ?? "");
570
581
  const isPlaceholder = Boolean(!value && placeholder);
571
- const cursorChar = displayValue[cursorPosition] || " ";
572
- const before = displayValue.slice(0, cursorPosition);
573
- const after = displayValue.slice(cursorPosition + 1);
574
- return /* @__PURE__ */ jsxs(Fragment, { children: [
575
- /* @__PURE__ */ jsx(Text, {
576
- dimColor: isPlaceholder,
577
- children: before
578
- }),
579
- /* @__PURE__ */ jsx(Text, {
580
- inverse: true,
581
- children: cursorChar
582
- }),
583
- /* @__PURE__ */ jsx(Text, {
582
+ const availableWidth = Math.max(1, stdout.columns - wrapIndent);
583
+ return /* @__PURE__ */ jsx(Box, {
584
+ flexDirection: "column",
585
+ children: useMemo(() => buildLineSegments(displayValue, cursorPosition, availableWidth), [
586
+ availableWidth,
587
+ cursorPosition,
588
+ displayValue
589
+ ]).map((line, index) => /* @__PURE__ */ jsx(Text, { children: line.hasCursor ? /* @__PURE__ */ jsxs(Fragment, { children: [
590
+ /* @__PURE__ */ jsx(Text, {
591
+ dimColor: isPlaceholder,
592
+ children: line.beforeCursor
593
+ }),
594
+ /* @__PURE__ */ jsx(Text, {
595
+ inverse: true,
596
+ children: line.cursorChar
597
+ }),
598
+ /* @__PURE__ */ jsx(Text, {
599
+ dimColor: isPlaceholder,
600
+ children: line.afterCursor
601
+ })
602
+ ] }) : /* @__PURE__ */ jsx(Text, {
584
603
  dimColor: isPlaceholder,
585
- children: after
586
- })
587
- ] });
604
+ children: line.text
605
+ }) }, `${String(index)}-${line.text}`))
606
+ });
588
607
  }
589
608
  //#endregion
590
609
  //#region src/components/Chat/CommandMenu.tsx
@@ -794,8 +813,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
794
813
  }, [onSubmit, resetInput]);
795
814
  const showCommandMenu = input.startsWith("/");
796
815
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
797
- const handleSubmitText = useCallback(async (input) => {
798
- await tick();
816
+ const handleSubmitText = useCallback((input) => {
799
817
  if (input.startsWith("/")) return;
800
818
  if (hasFileSuggestionQuery(input)) {
801
819
  if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
@@ -827,6 +845,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
827
845
  value: input,
828
846
  isDisabled,
829
847
  cursorPosition,
848
+ wrapIndent: 2,
830
849
  onChange: setInput,
831
850
  onSubmit: handleSubmitText,
832
851
  placeholder: "Ask anything... (/ commands, @ files)"
@@ -854,22 +873,31 @@ function hasExecutablePlan(content) {
854
873
  }
855
874
  //#endregion
856
875
  //#region src/components/Chat/Chat.tsx
857
- function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
858
- const [messages, setMessages] = useState([]);
876
+ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
877
+ const sessionMessages = initialMessages ?? [];
878
+ const [messages, setMessages] = useState(sessionMessages);
859
879
  const [streamingMessage, setStreamingMessage] = useState(null);
860
880
  const [isLoading, setIsLoading] = useState(false);
861
881
  const [pendingToolCall, setPendingToolCall] = useState(null);
862
882
  const [pendingPlan, setPendingPlan] = useState(null);
863
883
  const [interruptReason, setInterruptReason] = useState(null);
864
884
  const abortControllerRef = useRef(null);
885
+ const persistedSnapshotRef = useRef("");
865
886
  useEffect(() => {
866
- setMessages([]);
887
+ setMessages(sessionMessages);
867
888
  setStreamingMessage(null);
868
889
  setIsLoading(false);
869
890
  setPendingToolCall(null);
870
891
  setPendingPlan(null);
871
892
  setInterruptReason(null);
893
+ persistedSnapshotRef.current = JSON.stringify(sessionMessages);
872
894
  }, [sessionId]);
895
+ useEffect(() => {
896
+ const snapshot = JSON.stringify(messages);
897
+ if (snapshot === persistedSnapshotRef.current) return;
898
+ persistedSnapshotRef.current = snapshot;
899
+ onMessagesChange?.(messages);
900
+ }, [messages, onMessagesChange]);
873
901
  const buildToolResultMessage = useCallback((toolName, result) => {
874
902
  if (result.error?.startsWith("Tool not allowed:")) return {
875
903
  role: SYSTEM,
@@ -1257,7 +1285,7 @@ function Header({ model, onLoad }) {
1257
1285
  return /* @__PURE__ */ jsx(Static, {
1258
1286
  items: [0],
1259
1287
  children: (key) => /* @__PURE__ */ jsxs(Box, {
1260
- borderStyle: "round",
1288
+ borderStyle: "bold",
1261
1289
  flexDirection: "column",
1262
1290
  paddingX: 1,
1263
1291
  children: [
@@ -1423,6 +1451,7 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1423
1451
  /* @__PURE__ */ jsx(Text, { children: "Set the SearXNG base URL. DuckDuckGo remains the fallback." }),
1424
1452
  /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
1425
1453
  value: draftUrl,
1454
+ wrapIndent: 2,
1426
1455
  onChange: setDraftUrl,
1427
1456
  onSubmit: handleSubmit,
1428
1457
  placeholder: "http://localhost:8080"
@@ -1452,48 +1481,224 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1452
1481
  });
1453
1482
  }
1454
1483
  //#endregion
1484
+ //#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
+ var ACTION = {
1491
+ BACK: "back",
1492
+ CLOSE: "close",
1493
+ DELETE_MENU: "delete-menu",
1494
+ DELETE_PREFIX: "delete:",
1495
+ NEW: "new",
1496
+ OPEN_PREFIX: "open:"
1497
+ };
1498
+ function formatSessionLabel(session) {
1499
+ const timestamp = new Date(session.updatedAt).toLocaleString();
1500
+ return `${session.title} (${timestamp})`;
1501
+ }
1502
+ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
1503
+ const [view, setView] = useState(VIEW.MAIN);
1504
+ const [error, setError] = useState();
1505
+ const [, refreshSessionList] = useState(0);
1506
+ const sessions = listSessions();
1507
+ const options = view === VIEW.DELETE ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1508
+ label: `Delete ${formatSessionLabel(session)}`,
1509
+ value: `${ACTION.DELETE_PREFIX}${session.id}`
1510
+ })), {
1511
+ label: "Back",
1512
+ value: ACTION.BACK
1513
+ }] : [
1514
+ {
1515
+ label: "Start new session",
1516
+ value: ACTION.NEW
1517
+ },
1518
+ ...sessions.map((session) => ({
1519
+ label: `${session.id === currentSessionId ? "Current: " : ""}${formatSessionLabel(session)}`,
1520
+ value: `${ACTION.OPEN_PREFIX}${session.id}`
1521
+ })),
1522
+ {
1523
+ label: "Delete a session",
1524
+ value: ACTION.DELETE_MENU
1525
+ },
1526
+ {
1527
+ label: "Close",
1528
+ value: ACTION.CLOSE
1529
+ }
1530
+ ];
1531
+ const handleChange = useCallback((value) => {
1532
+ switch (true) {
1533
+ case value === ACTION.CLOSE:
1534
+ onClose();
1535
+ break;
1536
+ case value === ACTION.NEW:
1537
+ onNew();
1538
+ break;
1539
+ case value === ACTION.DELETE_MENU:
1540
+ setView(VIEW.DELETE);
1541
+ break;
1542
+ case value === ACTION.BACK:
1543
+ setView(VIEW.MAIN);
1544
+ break;
1545
+ case value.startsWith(ACTION.DELETE_PREFIX):
1546
+ try {
1547
+ onDelete(value.slice(ACTION.DELETE_PREFIX.length));
1548
+ setError(void 0);
1549
+ refreshSessionList((key) => key + 1);
1550
+ } catch (error) {
1551
+ setError(error instanceof Error ? error.message : "Failed to delete session");
1552
+ }
1553
+ break;
1554
+ case value.startsWith(ACTION.OPEN_PREFIX):
1555
+ try {
1556
+ onOpen(value.slice(ACTION.OPEN_PREFIX.length));
1557
+ setError(void 0);
1558
+ } catch (error) {
1559
+ setError(error instanceof Error ? error.message : "Failed to open session");
1560
+ }
1561
+ break;
1562
+ }
1563
+ }, [
1564
+ onClose,
1565
+ onDelete,
1566
+ onNew,
1567
+ onOpen
1568
+ ]);
1569
+ return /* @__PURE__ */ jsxs(Box, {
1570
+ flexDirection: "column",
1571
+ children: [
1572
+ /* @__PURE__ */ jsx(Text, { children: "Sessions" }),
1573
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: view === VIEW.DELETE ? "Delete session" : "Select session" }),
1574
+ error && /* @__PURE__ */ jsx(Box, {
1575
+ marginBottom: 1,
1576
+ children: /* @__PURE__ */ jsx(Text, {
1577
+ color: "red",
1578
+ children: error
1579
+ })
1580
+ }),
1581
+ /* @__PURE__ */ jsx(SelectPrompt, {
1582
+ options,
1583
+ onCancel: onClose,
1584
+ onChange: handleChange
1585
+ }, `${view}:${String(sessions.length)}`)
1586
+ ]
1587
+ });
1588
+ }
1589
+ //#endregion
1455
1590
  //#region src/components/App.tsx
1456
1591
  var SCREEN = /* @__PURE__ */ function(SCREEN) {
1457
1592
  SCREEN["CHAT"] = "chat";
1458
1593
  SCREEN["MODEL_PICKER"] = "model-picker";
1459
1594
  SCREEN["SEARCH_SETTINGS"] = "search-settings";
1595
+ SCREEN["SESSION_MANAGER"] = "session-manager";
1460
1596
  return SCREEN;
1461
1597
  }(SCREEN || {});
1462
- function App() {
1598
+ function createSession(sessionId, model) {
1599
+ return sessionId ? loadSession(sessionId) : createSession$1(model);
1600
+ }
1601
+ function App({ sessionId }) {
1463
1602
  const { exit } = useApp();
1464
1603
  const [appConfig, setConfig] = useState(() => loadConfig());
1465
1604
  const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1466
1605
  const [mode, setMode] = useState(SAFE);
1467
- const [sessionId, setSessionId] = useState(0);
1606
+ const [activeSession, setSession] = useState(() => createSession(sessionId, loadConfig().model));
1468
1607
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
1608
+ const sessionRef = useRef(activeSession);
1609
+ useEffect(() => {
1610
+ sessionRef.current = activeSession;
1611
+ }, [activeSession]);
1612
+ useEffect(() => {
1613
+ return () => {
1614
+ const currentSession = sessionRef.current;
1615
+ if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, "cyan")}\n`);
1616
+ };
1617
+ }, []);
1618
+ const setActiveSession = useCallback((nextSession) => {
1619
+ setSession((current) => {
1620
+ deleteSessionIfEmpty(current.metadata.id);
1621
+ return nextSession;
1622
+ });
1623
+ }, []);
1469
1624
  const handleHeaderLoad = useCallback(() => {
1470
1625
  setIsHeaderLoaded(true);
1471
1626
  }, []);
1627
+ const handleCreateSession = useCallback(() => {
1628
+ const nextSession = createSession$1(appConfig.model);
1629
+ setActiveSession(nextSession);
1630
+ setScreen(SCREEN.CHAT);
1631
+ clear(nextSession.metadata.id);
1632
+ return nextSession;
1633
+ }, [appConfig.model, setActiveSession]);
1634
+ const handleOpenSession = useCallback((sessionId) => {
1635
+ if (sessionRef.current.metadata.id === sessionId) {
1636
+ setScreen(SCREEN.CHAT);
1637
+ return;
1638
+ }
1639
+ setActiveSession(loadSession(sessionId));
1640
+ setScreen(SCREEN.CHAT);
1641
+ clear(sessionId);
1642
+ }, [setActiveSession]);
1643
+ const handleDeleteSession = useCallback((sessionId) => {
1644
+ deleteSession(sessionId);
1645
+ setSession((current) => {
1646
+ if (current.metadata.id !== sessionId) return current;
1647
+ return createSession$1(appConfig.model);
1648
+ });
1649
+ setScreen(SCREEN.SESSION_MANAGER);
1650
+ }, [appConfig.model]);
1651
+ const handleMessagesChange = useCallback((messages) => {
1652
+ setSession((current) => {
1653
+ const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
1654
+ if (persistedMessages.length <= current.messages.length) return current;
1655
+ let metadata = current.metadata;
1656
+ for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, appConfig.model);
1657
+ return {
1658
+ metadata,
1659
+ messages: persistedMessages
1660
+ };
1661
+ });
1662
+ }, [appConfig.model]);
1472
1663
  const handleCommand = useCallback((command) => {
1473
1664
  switch (command) {
1665
+ case "/session":
1666
+ setScreen(SCREEN.SESSION_MANAGER);
1667
+ break;
1474
1668
  case "/model":
1475
1669
  setScreen(SCREEN.MODEL_PICKER);
1476
1670
  break;
1477
1671
  case "/search":
1478
1672
  setScreen(SCREEN.SEARCH_SETTINGS);
1479
1673
  break;
1480
- case "/clear":
1674
+ case "/clear": {
1481
1675
  resetSystemMessage();
1482
- clear();
1483
1676
  setScreen(SCREEN.CHAT);
1484
- setSessionId((sessionId) => sessionId + 1);
1677
+ const nextSession = createSession$1(appConfig.model);
1678
+ setActiveSession(nextSession);
1679
+ clear(nextSession.metadata.id);
1485
1680
  break;
1681
+ }
1486
1682
  case "/exit":
1487
1683
  exit();
1488
1684
  break;
1489
1685
  }
1490
- }, [exit]);
1686
+ }, [
1687
+ appConfig.model,
1688
+ exit,
1689
+ setActiveSession
1690
+ ]);
1491
1691
  const handleUpdateConfig = useCallback((update) => {
1492
1692
  setConfig((current) => ({
1493
1693
  ...current,
1494
1694
  ...update
1495
1695
  }));
1496
1696
  saveConfig(update);
1697
+ const newModel = update.model;
1698
+ if (newModel) setSession((current) => ({
1699
+ ...current,
1700
+ metadata: updateSessionModel(current.metadata.id, newModel)
1701
+ }));
1497
1702
  setScreen(SCREEN.CHAT);
1498
1703
  }, []);
1499
1704
  const handleClose = useCallback(() => {
@@ -1525,13 +1730,24 @@ function App() {
1525
1730
  onClose: handleClose
1526
1731
  });
1527
1732
  break;
1733
+ case SCREEN.SESSION_MANAGER:
1734
+ screenContent = /* @__PURE__ */ jsx(SessionManager, {
1735
+ currentSessionId: activeSession.metadata.id,
1736
+ onClose: handleClose,
1737
+ onDelete: handleDeleteSession,
1738
+ onNew: handleCreateSession,
1739
+ onOpen: handleOpenSession
1740
+ });
1741
+ break;
1528
1742
  case SCREEN.CHAT:
1529
1743
  screenContent = /* @__PURE__ */ jsx(Chat, {
1744
+ initialMessages: activeSession.messages,
1530
1745
  model: appConfig.model,
1531
1746
  onCommand: handleCommand,
1747
+ onMessagesChange: handleMessagesChange,
1532
1748
  mode,
1533
1749
  onModeChange: setMode,
1534
- sessionId
1750
+ sessionId: activeSession.metadata.id
1535
1751
  });
1536
1752
  break;
1537
1753
  }
@@ -1553,15 +1769,15 @@ function App() {
1553
1769
  }
1554
1770
  //#endregion
1555
1771
  //#region src/tui.tsx
1556
- function renderApp() {
1772
+ function renderApp(sessionId) {
1557
1773
  let resetKey = 0;
1558
- const app = render(/* @__PURE__ */ jsx(App, {}, resetKey), {
1774
+ const app = render(/* @__PURE__ */ jsx(App, { sessionId }, resetKey), {
1559
1775
  exitOnCtrlC: false,
1560
1776
  maxFps: 60
1561
1777
  });
1562
- setClearHandler(() => {
1778
+ setClearHandler((nextSessionId) => {
1563
1779
  reset();
1564
- app.rerender(/* @__PURE__ */ jsx(App, {}, ++resetKey));
1780
+ app.rerender(/* @__PURE__ */ jsx(App, { sessionId: nextSessionId ?? sessionId }, ++resetKey));
1565
1781
  });
1566
1782
  }
1567
1783
  //#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.1";
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-CoX71F7Y.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.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",
@@ -42,20 +42,21 @@
42
42
  "@inkjs/ui": "2.0.0",
43
43
  "@shikijs/cli": "4.0.2",
44
44
  "cac": "7.0.0",
45
- "ink": "7.0.2",
45
+ "ink": "7.0.3",
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/"