code-ollama 0.18.0 → 0.18.2

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,4 +1,4 @@
1
- import { A as SYSTEM, B as REJECT, C as resetSystemMessage, D as LIST$1, E as WARNING, F as AUTO, H as LIST, I as LABEL, L as PLAN, M as PLAN_GENERATION_INSTRUCTION, N as BACK, O as getTheme, P as CATALOG, R as SAFE, S as saveConfig, T as HEADER_PREFIX, V as VERSION, _ as deleteModel, a as color, b as streamChat, c as createSession, d as listSessions, f as loadSession, g as setClearHandler, h as reset, i as WRITE_TOOLS, j as USER, k as ASSISTANT, l as deleteSession, m as clear, n as READ_TOOLS, o as write, p as updateSessionModel, r as TOOLS, s as appendMessage, t as executeTool, u as deleteSessionIfEmpty, v as listModels, w as withSystemMessage, x as loadConfig, y as pullModel, z as APPROVE } from "../cli.js";
1
+ import { A as ASSISTANT, B as APPROVE, C as saveConfig, D as WARNING, E as HEADER_PREFIX, F as CATALOG, H as VERSION, I as AUTO, L as LABEL, M as USER, N as PLAN_GENERATION_INSTRUCTION, O as LIST$1, P as BACK, R as PLAN, S as loadConfig, T as withSystemMessage, U as LIST, V as REJECT, _ as checkHealth, a as color, b as pullModel, c as createSession, d as listSessions, f as loadSession, g as setClearHandler, h as reset, i as WRITE_TOOLS, j as SYSTEM, k as getTheme, l as deleteSession, m as clear, n as READ_TOOLS, o as write, p as updateSessionModel, r as TOOLS, s as appendMessage, t as executeTool, u as deleteSessionIfEmpty, v as deleteModel, w as resetSystemMessage, x as streamChat, y as listModels, z as SAFE } from "../cli.js";
2
2
  import { readdirSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join, relative } from "node:path";
@@ -520,9 +520,13 @@ function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
520
520
  }
521
521
  //#endregion
522
522
  //#region src/components/SelectPrompt/SelectPromptHint.tsx
523
+ /**
524
+ * Select prompt hint component that displays:
525
+ * Select option (↑↓ + Enter to confirm, Esc/Ctrl+C to cancel)
526
+ */
523
527
  function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" }) {
524
- return /* @__PURE__ */ jsxs(Box, {
525
- flexDirection: "row",
528
+ return /* @__PURE__ */ jsxs(Text, {
529
+ color: getTheme().colors.secondary,
526
530
  children: [
527
531
  /* @__PURE__ */ jsxs(Text, {
528
532
  dimColor: true,
@@ -548,6 +552,14 @@ function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" })
548
552
  bold: true,
549
553
  children: "Esc"
550
554
  }),
555
+ /* @__PURE__ */ jsx(Text, {
556
+ dimColor: true,
557
+ children: "/"
558
+ }),
559
+ /* @__PURE__ */ jsx(Text, {
560
+ bold: true,
561
+ children: "Ctrl+C"
562
+ }),
551
563
  /* @__PURE__ */ jsxs(Text, {
552
564
  dimColor: true,
553
565
  children: [
@@ -560,7 +572,7 @@ function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" })
560
572
  });
561
573
  }
562
574
  //#endregion
563
- //#region src/components/PlanApproval.tsx
575
+ //#region src/components/PlanApproval/PlanApproval.tsx
564
576
  var options$1 = [
565
577
  {
566
578
  label: "Auto - Execute tools automatically",
@@ -606,7 +618,7 @@ function PlanApproval({ planContent, onModeChange, theme = getTheme() }) {
606
618
  });
607
619
  }
608
620
  //#endregion
609
- //#region src/components/ToolApproval.tsx
621
+ //#region src/components/ToolApproval/ToolApproval.tsx
610
622
  var options = [{
611
623
  label: "Approve tool call",
612
624
  value: APPROVE
@@ -821,7 +833,7 @@ function CommandMenu({ input, onSubmit }) {
821
833
  });
822
834
  }
823
835
  //#endregion
824
- //#region src/components/Suggestions.tsx
836
+ //#region src/components/Suggestions/Suggestions.tsx
825
837
  var DEFAULT_MAX_VISIBLE_OPTIONS = 5;
826
838
  function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_MAX_VISIBLE_OPTIONS, resetKey, onHighlight, onSelect }) {
827
839
  const [focusedIndex, setFocusedIndex] = useState(0);
@@ -843,17 +855,17 @@ function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_
843
855
  onHighlight,
844
856
  options
845
857
  ]);
846
- useInput((_, key) => {
858
+ useInput((input, key) => {
847
859
  if (isDisabled || !options.length) return;
848
- if (key.downArrow) {
860
+ if (key.downArrow || input === "\x1B[B") {
849
861
  setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
850
862
  return;
851
863
  }
852
- if (key.upArrow) {
864
+ if (key.upArrow || input === "\x1B[A") {
853
865
  setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
854
866
  return;
855
867
  }
856
- if (key.tab || key.return) onSelect(options[focusedIndex]);
868
+ if (key.tab || key.return || input === " " || input === "\r") onSelect(options[focusedIndex]);
857
869
  });
858
870
  if (!options.length) return null;
859
871
  const visibleStart = Math.min(Math.max(0, focusedIndex - maxVisibleOptions + 1), Math.max(0, options.length - maxVisibleOptions));
@@ -1145,10 +1157,10 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1145
1157
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1146
1158
  var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
1147
1159
  var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1148
- var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
1149
- INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
1150
- INTERRUPT_REASON["REJECTED"] = "rejected";
1151
- return INTERRUPT_REASON;
1160
+ var InterruptReason = /* @__PURE__ */ function(InterruptReason) {
1161
+ InterruptReason["Interrupted"] = "interrupted";
1162
+ InterruptReason["Rejected"] = "rejected";
1163
+ return InterruptReason;
1152
1164
  }({});
1153
1165
  //#endregion
1154
1166
  //#region src/components/Chat/plan.ts
@@ -1216,13 +1228,15 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1216
1228
  abortControllerRef.current = null;
1217
1229
  setIsLoading(false);
1218
1230
  setStreamingMessage(null);
1219
- setInterruptReason(INTERRUPT_REASON.INTERRUPTED);
1231
+ setInterruptReason(InterruptReason.Interrupted);
1220
1232
  setMessages((prev) => [...prev, {
1221
1233
  role: USER,
1222
1234
  content: TURN_ABORTED_MESSAGE
1223
1235
  }]);
1224
1236
  }, []);
1225
1237
  const processStream = useCallback(async (currentMessages, executionMode = mode) => {
1238
+ // v8 ignore next
1239
+ if (!model) throw new Error("Model is required");
1226
1240
  const controller = new AbortController();
1227
1241
  abortControllerRef.current = controller;
1228
1242
  const assistantMessage = {
@@ -1297,6 +1311,9 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1297
1311
  theme
1298
1312
  ]);
1299
1313
  const processStreamReadOnly = useCallback(async (currentMessages) => {
1314
+ const modelName = model;
1315
+ // v8 ignore next
1316
+ if (!modelName) throw new Error("Model is required");
1300
1317
  const controller = new AbortController();
1301
1318
  abortControllerRef.current = controller;
1302
1319
  const assistantMessage = {
@@ -1327,7 +1344,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1327
1344
  setStreamingMessage(assistantMessage);
1328
1345
  try {
1329
1346
  const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
1330
- for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools, controller.signal)) {
1347
+ for await (const chunk of streamChat(withSystemMessage(currentMessages), modelName, readOnlyTools, controller.signal)) {
1331
1348
  // v8 ignore next 3
1332
1349
  if (controller.signal.aborted) return;
1333
1350
  if (chunk.type === "content") {
@@ -1363,7 +1380,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1363
1380
  };
1364
1381
  setStreamingMessage(planAssistantMessage);
1365
1382
  try {
1366
- for await (const chunk of streamChat(withSystemMessage(planMessages), model, [], controller.signal)) {
1383
+ for await (const chunk of streamChat(withSystemMessage(planMessages), modelName, [], controller.signal)) {
1367
1384
  // v8 ignore next 3
1368
1385
  if (controller.signal.aborted) return;
1369
1386
  if (chunk.type === "content") {
@@ -1455,7 +1472,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1455
1472
  content: TURN_ABORTED_MESSAGE
1456
1473
  }]);
1457
1474
  setIsLoading(false);
1458
- setInterruptReason(INTERRUPT_REASON.REJECTED);
1475
+ setInterruptReason(InterruptReason.Rejected);
1459
1476
  break;
1460
1477
  }
1461
1478
  }, [
@@ -1511,7 +1528,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1511
1528
  marginBottom: 1,
1512
1529
  children: /* @__PURE__ */ jsx(Text, {
1513
1530
  color: theme.colors.error,
1514
- children: interruptReason === INTERRUPT_REASON.REJECTED ? `❗ Tool call rejected.` : `❗ Execution interrupted.`
1531
+ children: interruptReason === InterruptReason.Rejected ? `❗ Tool call rejected.` : `❗ Execution interrupted.`
1515
1532
  })
1516
1533
  }),
1517
1534
  !pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Box, {
@@ -1527,7 +1544,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1527
1544
  });
1528
1545
  }
1529
1546
  //#endregion
1530
- //#region src/components/Footer.tsx
1547
+ //#region src/components/Footer/Footer.tsx
1531
1548
  function getModeColor(mode, theme) {
1532
1549
  switch (mode) {
1533
1550
  case PLAN: return theme.colors.modePlan;
@@ -1538,6 +1555,7 @@ function getModeColor(mode, theme) {
1538
1555
  }
1539
1556
  }
1540
1557
  function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
1558
+ const modelLabel = model || "not configured";
1541
1559
  useInput((_, key) => {
1542
1560
  if (key.tab && key.shift) onToggleMode();
1543
1561
  });
@@ -1561,20 +1579,21 @@ function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
1561
1579
  " Model: ",
1562
1580
  /* @__PURE__ */ jsx(Text, {
1563
1581
  color: theme.colors.model,
1564
- children: model
1582
+ children: modelLabel
1565
1583
  })
1566
1584
  ]
1567
1585
  })
1568
1586
  });
1569
1587
  }
1570
1588
  //#endregion
1571
- //#region src/components/Header.tsx
1589
+ //#region src/components/Header/Header.tsx
1572
1590
  function abbreviatePath(dir) {
1573
1591
  const home = homedir();
1574
1592
  return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
1575
1593
  }
1576
1594
  function Header({ model, onLoad, theme = getTheme() }) {
1577
1595
  const directory = abbreviatePath(process.cwd());
1596
+ const modelLabel = model || "not configured";
1578
1597
  useEffect(() => {
1579
1598
  onLoad();
1580
1599
  }, []);
@@ -1606,7 +1625,7 @@ function Header({ model, onLoad, theme = getTheme() }) {
1606
1625
  dimColor: true,
1607
1626
  children: "model:".padEnd(11)
1608
1627
  }),
1609
- /* @__PURE__ */ jsx(Text, { children: model.padEnd(model.length + 3) }),
1628
+ /* @__PURE__ */ jsx(Text, { children: modelLabel.padEnd(modelLabel.length + 3) }),
1610
1629
  /* @__PURE__ */ jsx(Text, {
1611
1630
  color: theme.colors.command,
1612
1631
  children: "/model"
@@ -1806,11 +1825,15 @@ function ModelCustomDownloadView({ downloadDraft, notice, theme, onDraftChange,
1806
1825
  onSelect: onSelectSuggestion
1807
1826
  }),
1808
1827
  renderNotice(),
1809
- /* @__PURE__ */ jsx(Text, {
1810
- color: theme.colors.secondary,
1811
- dimColor: true,
1812
- children: "Press Enter to download, Esc or Ctrl+C to go back."
1813
- })
1828
+ /* @__PURE__ */ jsxs(Text, { children: [
1829
+ /* @__PURE__ */ jsx(Text, {
1830
+ color: theme.colors.secondary,
1831
+ dimColor: true,
1832
+ children: "Press Enter to download."
1833
+ }),
1834
+ " ",
1835
+ /* @__PURE__ */ jsx(ExitHint, {})
1836
+ ] })
1814
1837
  ]
1815
1838
  });
1816
1839
  }
@@ -2008,13 +2031,20 @@ function ModelManager({ currentModel, onSelect, onClose, theme = getTheme() }) {
2008
2031
  pullRef.current?.abort();
2009
2032
  }, []);
2010
2033
  useInput((input, key) => {
2011
- if (view === View.CustomDownload && (key.escape || key.ctrl && input === "c")) {
2034
+ const isEscape = key.escape || input === "\x1B\x1B";
2035
+ const isCtrlC = key.ctrl && input === "c" || input === "";
2036
+ if (loadError && view !== View.Menu && (isEscape || isCtrlC)) {
2037
+ handleBackToMenu();
2038
+ return;
2039
+ }
2040
+ if (view === View.CustomDownload && (isEscape || isCtrlC)) {
2012
2041
  setNotice(null);
2013
2042
  setHighlightedSuggestion(null);
2014
2043
  setView(View.Download);
2015
2044
  return;
2016
2045
  }
2017
- if (view === View.Downloading && (key.escape || key.ctrl && input === "c")) cancelActivePull();
2046
+ // v8 ignore next
2047
+ if (view === View.Downloading && (isEscape || isCtrlC)) cancelActivePull();
2018
2048
  });
2019
2049
  const handleMenuChange = useCallback((value) => {
2020
2050
  setNotice(null);
@@ -2157,17 +2187,10 @@ function ModelManager({ currentModel, onSelect, onClose, theme = getTheme() }) {
2157
2187
  color: notice.tone === "error" ? theme.colors.error : notice.tone === "success" ? theme.colors.status : theme.colors.secondary,
2158
2188
  children: notice.text
2159
2189
  }) : null;
2160
- if (loadError && view !== View.Menu) return /* @__PURE__ */ jsxs(Box, {
2161
- flexDirection: "column",
2162
- children: [/* @__PURE__ */ jsxs(Text, {
2163
- color: theme.colors.error,
2164
- children: ["Error loading models: ", loadError]
2165
- }), /* @__PURE__ */ jsx(Text, {
2166
- color: theme.colors.secondary,
2167
- dimColor: true,
2168
- children: "Press Esc to go back."
2169
- })]
2170
- });
2190
+ if (loadError && view !== View.Menu) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Text, {
2191
+ color: theme.colors.error,
2192
+ children: ["Error loading models: ", loadError]
2193
+ }), /* @__PURE__ */ jsx(ExitHint, {})] });
2171
2194
  if (view === View.Downloading && downloadProgress) return /* @__PURE__ */ jsx(ModelDownloadingView, {
2172
2195
  progress: downloadProgress,
2173
2196
  theme,
@@ -2233,7 +2256,7 @@ function ModelManager({ currentModel, onSelect, onClose, theme = getTheme() }) {
2233
2256
  });
2234
2257
  }
2235
2258
  //#endregion
2236
- //#region src/components/SearchSettings.tsx
2259
+ //#region src/components/SearchSettings/SearchSettings.tsx
2237
2260
  function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
2238
2261
  const [view, setView] = useState("menu");
2239
2262
  const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
@@ -2309,11 +2332,15 @@ function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
2309
2332
  color: theme.colors.error,
2310
2333
  children: error
2311
2334
  }),
2312
- /* @__PURE__ */ jsx(Text, {
2313
- color: theme.colors.secondary,
2314
- dimColor: true,
2315
- children: "Press Enter to save, Esc to go back."
2316
- })
2335
+ /* @__PURE__ */ jsxs(Text, { children: [
2336
+ /* @__PURE__ */ jsx(Text, {
2337
+ color: theme.colors.secondary,
2338
+ dimColor: true,
2339
+ children: "Press Enter to save."
2340
+ }),
2341
+ " ",
2342
+ /* @__PURE__ */ jsx(ExitHint, {})
2343
+ ] })
2317
2344
  ]
2318
2345
  });
2319
2346
  return /* @__PURE__ */ jsxs(SelectPrompt, {
@@ -2335,7 +2362,7 @@ function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
2335
2362
  });
2336
2363
  }
2337
2364
  //#endregion
2338
- //#region src/components/SessionManager.tsx
2365
+ //#region src/components/SessionManager/SessionManager.tsx
2339
2366
  var ACTION = {
2340
2367
  CLOSE: "close",
2341
2368
  DELETE_MENU: "delete-menu",
@@ -2447,7 +2474,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, th
2447
2474
  });
2448
2475
  }
2449
2476
  //#endregion
2450
- //#region src/components/ThemeSettings.tsx
2477
+ //#region src/components/ThemeSettings/ThemeSettings.tsx
2451
2478
  function ThemeSettings({ currentTheme, onClose, onPreview, onSave }) {
2452
2479
  const [selectedIndex, setSelectedIndex] = useState(() => {
2453
2480
  const initialIndex = LIST$1.findIndex(({ id }) => id === currentTheme);
@@ -2549,45 +2576,45 @@ function ThemeSettings({ currentTheme, onClose, onPreview, onSave }) {
2549
2576
  }
2550
2577
  //#endregion
2551
2578
  //#region src/components/App/constants.ts
2552
- var SCREEN = /* @__PURE__ */ function(SCREEN) {
2553
- SCREEN["CHAT"] = "chat";
2554
- SCREEN["MODEL_MANAGER"] = "model-manager";
2555
- SCREEN["SEARCH_SETTINGS"] = "search-settings";
2556
- SCREEN["SESSION_MANAGER"] = "session-manager";
2557
- SCREEN["THEME_SETTINGS"] = "theme-settings";
2558
- return SCREEN;
2579
+ var Screen = /* @__PURE__ */ function(Screen) {
2580
+ Screen["Chat"] = "chat";
2581
+ Screen["ModelManager"] = "model-manager";
2582
+ Screen["SearchSettings"] = "search-settings";
2583
+ Screen["SessionManager"] = "session-manager";
2584
+ Screen["ThemeSettings"] = "theme-settings";
2585
+ return Screen;
2559
2586
  }({});
2560
2587
  //#endregion
2561
2588
  //#region src/components/App/hooks/useScreenRouter.ts
2562
2589
  function useScreenRouter() {
2563
2590
  const { exit } = useApp();
2564
- const [currentScreen, setScreen] = useState(SCREEN.CHAT);
2591
+ const [currentScreen, setScreen] = useState(Screen.Chat);
2565
2592
  return {
2566
2593
  currentScreen,
2567
2594
  setScreen,
2568
2595
  handleClose: useCallback(() => {
2569
- setScreen(SCREEN.CHAT);
2596
+ setScreen(Screen.Chat);
2570
2597
  }, []),
2571
2598
  handleCommand: useCallback((command, callbacks) => {
2572
2599
  const { onCreateSession, onSetPreviewThemeId, model, theme } = callbacks;
2573
2600
  switch (command) {
2574
2601
  case "/session":
2575
- setScreen(SCREEN.SESSION_MANAGER);
2602
+ setScreen(Screen.SessionManager);
2576
2603
  break;
2577
2604
  case "/model":
2578
- setScreen(SCREEN.MODEL_MANAGER);
2605
+ setScreen(Screen.ModelManager);
2579
2606
  break;
2580
2607
  case "/search":
2581
- setScreen(SCREEN.SEARCH_SETTINGS);
2608
+ setScreen(Screen.SearchSettings);
2582
2609
  break;
2583
2610
  case "/theme":
2584
2611
  onSetPreviewThemeId(theme);
2585
- setScreen(SCREEN.THEME_SETTINGS);
2612
+ setScreen(Screen.ThemeSettings);
2586
2613
  break;
2587
2614
  case "/clear": {
2588
2615
  resetSystemMessage();
2589
2616
  const nextSession = onCreateSession(model);
2590
- setScreen(SCREEN.CHAT);
2617
+ setScreen(Screen.Chat);
2591
2618
  clear(nextSession.metadata.id);
2592
2619
  break;
2593
2620
  }
@@ -2674,7 +2701,7 @@ function useThemeSettings({ currentTheme, onUpdateConfig, setScreen }) {
2674
2701
  activeTheme,
2675
2702
  handleThemeClose: useCallback(() => {
2676
2703
  setPreviewThemeId(null);
2677
- setScreen(SCREEN.CHAT);
2704
+ setScreen(Screen.Chat);
2678
2705
  }, [setScreen]),
2679
2706
  handleThemePreview,
2680
2707
  handleThemeSave: useCallback((themeId) => {
@@ -2685,17 +2712,133 @@ function useThemeSettings({ currentTheme, onUpdateConfig, setScreen }) {
2685
2712
  };
2686
2713
  }
2687
2714
  //#endregion
2715
+ //#region src/components/App/ReadinessCheck.tsx
2716
+ var ReadinessState = /* @__PURE__ */ function(ReadinessState) {
2717
+ ReadinessState["Checking"] = "checking";
2718
+ ReadinessState["Ready"] = "ready";
2719
+ ReadinessState["MissingModelConfig"] = "missing-model-config";
2720
+ ReadinessState["NoInstalledModels"] = "no-installed-models";
2721
+ ReadinessState["ServerUnavailable"] = "server-unavailable";
2722
+ ReadinessState["ModelLoadError"] = "model-load-error";
2723
+ return ReadinessState;
2724
+ }({});
2725
+ function getTitle(setupState) {
2726
+ switch (setupState) {
2727
+ case "server-unavailable": return "Ollama Server Unavailable";
2728
+ case "model-load-error": return "Connection Error";
2729
+ case "missing-model-config": return "No Model Configured";
2730
+ case "no-installed-models": return "No Model Installed";
2731
+ }
2732
+ }
2733
+ function getMessage(setupState, errorMessage) {
2734
+ const theme = getTheme();
2735
+ switch (setupState) {
2736
+ case "checking": return /* @__PURE__ */ jsx(Text, { children: "Checking Ollama server and model setup..." });
2737
+ case "missing-model-config": return /* @__PURE__ */ jsxs(Text, { children: [
2738
+ "Select or download a model with",
2739
+ " ",
2740
+ /* @__PURE__ */ jsx(Text, {
2741
+ color: theme.colors.command,
2742
+ children: "/model"
2743
+ })
2744
+ ] });
2745
+ case "no-installed-models": return /* @__PURE__ */ jsxs(Text, { children: ["Download a model with ", /* @__PURE__ */ jsx(Text, {
2746
+ color: theme.colors.command,
2747
+ children: "/model"
2748
+ })] });
2749
+ case "server-unavailable": return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, { children: "Ollama server is not running or unreachable." }), /* @__PURE__ */ jsxs(Text, { children: [
2750
+ "Start it with ",
2751
+ /* @__PURE__ */ jsx(Text, {
2752
+ color: theme.colors.command,
2753
+ children: "ollama serve"
2754
+ }),
2755
+ " ",
2756
+ "and restart the app"
2757
+ ] })] });
2758
+ case "model-load-error": return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Text, { children: [
2759
+ "Error loading models",
2760
+ errorMessage ? `: ${errorMessage}` : "",
2761
+ "."
2762
+ ] }), /* @__PURE__ */ jsx(Text, { children: "Fix the connection and restart the app" })] });
2763
+ default: return null;
2764
+ }
2765
+ }
2766
+ function ReadinessCheck({ errorMessage, onCommand, setupState, theme = getTheme() }) {
2767
+ const title = getTitle(setupState);
2768
+ return /* @__PURE__ */ jsxs(Box, {
2769
+ flexDirection: "column",
2770
+ children: [/* @__PURE__ */ jsxs(Box, {
2771
+ borderStyle: "round",
2772
+ flexDirection: "column",
2773
+ marginBottom: 1,
2774
+ paddingX: 1,
2775
+ paddingY: 1,
2776
+ children: [title && /* @__PURE__ */ jsxs(Text, {
2777
+ bold: true,
2778
+ color: theme.colors.error,
2779
+ children: [
2780
+ "❗",
2781
+ " ",
2782
+ title
2783
+ ]
2784
+ }), getMessage(setupState, errorMessage)]
2785
+ }), /* @__PURE__ */ jsx(ChatInput, {
2786
+ history: [],
2787
+ onSubmit: onCommand
2788
+ })]
2789
+ });
2790
+ }
2791
+ //#endregion
2688
2792
  //#region src/components/App/App.tsx
2689
2793
  function App({ sessionId }) {
2690
2794
  const [appConfig, setConfig] = useState(() => loadConfig());
2691
2795
  const [mode, setMode] = useState(SAFE);
2692
2796
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
2797
+ const [setupState, setSetupState] = useState(() => appConfig.model ? ReadinessState.Ready : ReadinessState.MissingModelConfig);
2798
+ const [setupErrorMessage, setSetupErrorMessage] = useState(null);
2693
2799
  const { currentScreen, setScreen, handleClose, handleCommand } = useScreenRouter();
2694
2800
  const { activeSession, setSession, handleCreateSession, handleOpenSession, handleDeleteSession, handleMessagesChange } = useSessionManager({
2695
2801
  sessionId,
2696
- model: appConfig.model,
2802
+ model: appConfig.model ?? "",
2697
2803
  commandColor: getTheme(appConfig.theme).colors.command
2698
2804
  });
2805
+ useEffect(() => {
2806
+ let isMounted = true;
2807
+ async function refreshSetupState() {
2808
+ if (!appConfig.model) {
2809
+ // v8 ignore next
2810
+ if (isMounted) {
2811
+ setSetupErrorMessage(null);
2812
+ setSetupState(ReadinessState.MissingModelConfig);
2813
+ }
2814
+ return;
2815
+ }
2816
+ if (currentScreen !== Screen.Chat) return;
2817
+ // v8 ignore next
2818
+ if (isMounted) {
2819
+ setSetupErrorMessage(null);
2820
+ setSetupState(ReadinessState.Checking);
2821
+ }
2822
+ try {
2823
+ const isHealthy = await checkHealth();
2824
+ if (!isMounted) return;
2825
+ if (!isHealthy) {
2826
+ setSetupState(ReadinessState.ServerUnavailable);
2827
+ return;
2828
+ }
2829
+ setSetupState((await listModels()).length > 0 ? ReadinessState.Ready : ReadinessState.NoInstalledModels);
2830
+ } catch (error) {
2831
+ // v8 ignore start
2832
+ if (!isMounted) return;
2833
+ setSetupErrorMessage(error instanceof Error ? error.message : String(error));
2834
+ setSetupState(ReadinessState.ModelLoadError);
2835
+ }
2836
+ }
2837
+ refreshSetupState();
2838
+ return () => {
2839
+ isMounted = false;
2840
+ };
2841
+ }, [appConfig.model, currentScreen]);
2699
2842
  const handleUpdateConfig = useCallback((update) => {
2700
2843
  setConfig((current) => ({
2701
2844
  ...current,
@@ -2707,7 +2850,7 @@ function App({ sessionId }) {
2707
2850
  ...current,
2708
2851
  metadata: updateSessionModel(current.metadata.id, newModel)
2709
2852
  }));
2710
- setScreen(SCREEN.CHAT);
2853
+ setScreen(Screen.Chat);
2711
2854
  }, [setScreen, setSession]);
2712
2855
  const { activeTheme, handleThemeClose, handleThemePreview, handleThemeSave, setPreviewThemeId } = useThemeSettings({
2713
2856
  currentTheme: appConfig.theme,
@@ -2729,7 +2872,7 @@ function App({ sessionId }) {
2729
2872
  }, []);
2730
2873
  const handleChatCommand = useCallback((command) => {
2731
2874
  handleCommand(command, {
2732
- model: appConfig.model,
2875
+ model: appConfig.model ?? "",
2733
2876
  theme: appConfig.theme,
2734
2877
  onCreateSession: handleCreateSession,
2735
2878
  onSetPreviewThemeId: setPreviewThemeId
@@ -2743,27 +2886,27 @@ function App({ sessionId }) {
2743
2886
  ]);
2744
2887
  const handleDeleteSessionAndStay = useCallback((sid) => {
2745
2888
  handleDeleteSession(sid);
2746
- setScreen(SCREEN.SESSION_MANAGER);
2889
+ setScreen(Screen.SessionManager);
2747
2890
  }, [handleDeleteSession, setScreen]);
2748
2891
  const handleOpenSessionAndNavigate = useCallback((sid) => {
2749
2892
  handleOpenSession(sid);
2750
- setScreen(SCREEN.CHAT);
2893
+ setScreen(Screen.Chat);
2751
2894
  }, [handleOpenSession, setScreen]);
2752
2895
  const handleCreateSessionAndNavigate = useCallback(() => {
2753
2896
  handleCreateSession();
2754
- setScreen(SCREEN.CHAT);
2897
+ setScreen(Screen.Chat);
2755
2898
  }, [handleCreateSession, setScreen]);
2756
2899
  let screenContent;
2757
2900
  switch (currentScreen) {
2758
- case SCREEN.MODEL_MANAGER:
2901
+ case Screen.ModelManager:
2759
2902
  screenContent = /* @__PURE__ */ jsx(ModelManager, {
2760
- currentModel: appConfig.model,
2903
+ currentModel: appConfig.model ?? "",
2761
2904
  onSelect: handleUpdateConfig,
2762
2905
  onClose: handleClose,
2763
2906
  theme: activeTheme
2764
2907
  });
2765
2908
  break;
2766
- case SCREEN.SEARCH_SETTINGS:
2909
+ case Screen.SearchSettings:
2767
2910
  screenContent = /* @__PURE__ */ jsx(SearchSettings, {
2768
2911
  currentUrl: appConfig.searxngBaseUrl,
2769
2912
  onSave: handleUpdateConfig,
@@ -2771,7 +2914,7 @@ function App({ sessionId }) {
2771
2914
  theme: activeTheme
2772
2915
  });
2773
2916
  break;
2774
- case SCREEN.SESSION_MANAGER:
2917
+ case Screen.SessionManager:
2775
2918
  screenContent = /* @__PURE__ */ jsx(SessionManager, {
2776
2919
  currentSessionId: activeSession.metadata.id,
2777
2920
  onClose: handleClose,
@@ -2781,7 +2924,7 @@ function App({ sessionId }) {
2781
2924
  theme: activeTheme
2782
2925
  });
2783
2926
  break;
2784
- case SCREEN.THEME_SETTINGS:
2927
+ case Screen.ThemeSettings:
2785
2928
  screenContent = /* @__PURE__ */ jsx(ThemeSettings, {
2786
2929
  currentTheme: appConfig.theme,
2787
2930
  onClose: handleThemeClose,
@@ -2789,8 +2932,8 @@ function App({ sessionId }) {
2789
2932
  onSave: handleThemeSave
2790
2933
  });
2791
2934
  break;
2792
- case SCREEN.CHAT:
2793
- screenContent = /* @__PURE__ */ jsx(Chat, {
2935
+ case Screen.Chat:
2936
+ screenContent = setupState === ReadinessState.Ready ? /* @__PURE__ */ jsx(Chat, {
2794
2937
  initialMessages: activeSession.messages,
2795
2938
  model: appConfig.model,
2796
2939
  onCommand: handleChatCommand,
@@ -2799,6 +2942,11 @@ function App({ sessionId }) {
2799
2942
  onModeChange: setMode,
2800
2943
  sessionId: activeSession.metadata.id,
2801
2944
  theme: activeTheme
2945
+ }) : /* @__PURE__ */ jsx(ReadinessCheck, {
2946
+ errorMessage: setupErrorMessage,
2947
+ onCommand: handleChatCommand,
2948
+ setupState,
2949
+ theme: activeTheme
2802
2950
  });
2803
2951
  break;
2804
2952
  }
@@ -2806,14 +2954,14 @@ function App({ sessionId }) {
2806
2954
  flexDirection: "column",
2807
2955
  children: [
2808
2956
  /* @__PURE__ */ jsx(Header, {
2809
- model: appConfig.model,
2957
+ model: appConfig.model ?? "",
2810
2958
  onLoad: handleHeaderLoad,
2811
2959
  theme: activeTheme
2812
2960
  }),
2813
2961
  isHeaderLoaded && screenContent,
2814
2962
  /* @__PURE__ */ jsx(Footer, {
2815
2963
  mode,
2816
- model: appConfig.model,
2964
+ model: appConfig.model ?? "",
2817
2965
  onToggleMode: handleToggleMode,
2818
2966
  theme: activeTheme
2819
2967
  })
@@ -2821,6 +2969,43 @@ function App({ sessionId }) {
2821
2969
  });
2822
2970
  }
2823
2971
  //#endregion
2972
+ //#region src/components/ExitHint/ExitHint.tsx
2973
+ /**
2974
+ * Exit hint component that displays:
2975
+ * Press Esc/Ctrl+C to go back.
2976
+ */
2977
+ function ExitHint({ action = "go back" }) {
2978
+ return /* @__PURE__ */ jsxs(Text, {
2979
+ color: getTheme().colors.secondary,
2980
+ children: [
2981
+ /* @__PURE__ */ jsx(Text, {
2982
+ dimColor: true,
2983
+ children: "Press "
2984
+ }),
2985
+ /* @__PURE__ */ jsx(Text, {
2986
+ bold: true,
2987
+ children: "Esc"
2988
+ }),
2989
+ /* @__PURE__ */ jsx(Text, {
2990
+ dimColor: true,
2991
+ children: "/"
2992
+ }),
2993
+ /* @__PURE__ */ jsx(Text, {
2994
+ bold: true,
2995
+ children: "Ctrl+C"
2996
+ }),
2997
+ /* @__PURE__ */ jsxs(Text, {
2998
+ dimColor: true,
2999
+ children: [
3000
+ " to ",
3001
+ action,
3002
+ "."
3003
+ ]
3004
+ })
3005
+ ]
3006
+ });
3007
+ }
3008
+ //#endregion
2824
3009
  //#region src/tui.tsx
2825
3010
  function renderApp(sessionId) {
2826
3011
  let resetKey = 0;
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ var LIST$1 = [
37
37
  //#endregion
38
38
  //#region package.json
39
39
  var name = "code-ollama";
40
- var version = "0.18.0";
40
+ var version = "0.18.2";
41
41
  //#endregion
42
42
  //#region src/constants/package.ts
43
43
  var NAME = name;
@@ -329,7 +329,6 @@ function withSystemMessage(messages) {
329
329
  //#region src/utils/config.ts
330
330
  var CONFIG_PATH = join(DIRECTORY, "config.json");
331
331
  var DEFAULT_HOST = "http://localhost:11434";
332
- var DEFAULT_MODEL$1 = "gemma4";
333
332
  function readFile$1() {
334
333
  if (!existsSync(CONFIG_PATH)) return {};
335
334
  try {
@@ -342,7 +341,7 @@ function loadConfig() {
342
341
  const file = readFile$1();
343
342
  return {
344
343
  host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULT_HOST,
345
- model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULT_MODEL$1,
344
+ model: file.model,
346
345
  searxngBaseUrl: file.searxngBaseUrl,
347
346
  theme: file.theme ?? "github-dark"
348
347
  };
@@ -357,9 +356,16 @@ function saveConfig(patch) {
357
356
  }
358
357
  //#endregion
359
358
  //#region src/utils/ollama.ts
360
- var { host, model: DEFAULT_MODEL } = loadConfig();
359
+ var { host } = loadConfig();
361
360
  var client = new Ollama({ host });
362
- async function* streamChat(messages, model = DEFAULT_MODEL, tools, signal) {
361
+ async function checkHealth() {
362
+ try {
363
+ return (await fetch(host)).ok;
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
368
+ async function* streamChat(messages, model, tools, signal) {
363
369
  const response = await client.chat({
364
370
  model,
365
371
  messages,
@@ -1124,7 +1130,7 @@ async function main(args = process.argv.slice(2)) {
1124
1130
  else await launchTui();
1125
1131
  }
1126
1132
  async function launchTui(sessionId) {
1127
- const { renderApp } = await import("./assets/tui-4cX-bKvd.js");
1133
+ const { renderApp } = await import("./assets/tui-XD0Ekj5m.js");
1128
1134
  reset();
1129
1135
  renderApp(sessionId);
1130
1136
  }
@@ -1140,4 +1146,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
1140
1146
  if (isEntrypoint()) main();
1141
1147
  // v8 ignore stop
1142
1148
  //#endregion
1143
- export { SYSTEM as A, REJECT as B, resetSystemMessage as C, LIST as D, WARNING as E, AUTO as F, LIST$1 as H, LABEL as I, PLAN as L, PLAN_GENERATION_INSTRUCTION as M, BACK as N, getTheme as O, CATALOG as P, SAFE as R, saveConfig as S, HEADER_PREFIX as T, VERSION as V, deleteModel as _, color as a, streamChat as b, createSession as c, listSessions as d, loadSession as f, setClearHandler as g, reset as h, WRITE_TOOLS as i, USER as j, ASSISTANT as k, deleteSession as l, clear as m, main, READ_TOOLS as n, write as o, updateSessionModel as p, TOOLS as r, appendMessage as s, executeTool as t, deleteSessionIfEmpty as u, listModels as v, withSystemMessage as w, loadConfig as x, pullModel as y, APPROVE as z };
1149
+ export { ASSISTANT as A, APPROVE as B, saveConfig as C, WARNING as D, HEADER_PREFIX as E, CATALOG as F, VERSION as H, AUTO as I, LABEL as L, USER as M, PLAN_GENERATION_INSTRUCTION as N, LIST as O, BACK as P, PLAN as R, loadConfig as S, withSystemMessage as T, LIST$1 as U, REJECT as V, checkHealth as _, color as a, pullModel as b, createSession as c, listSessions as d, loadSession as f, setClearHandler as g, reset as h, WRITE_TOOLS as i, SYSTEM as j, getTheme as k, deleteSession as l, clear as m, main, READ_TOOLS as n, write as o, updateSessionModel as p, TOOLS as r, appendMessage as s, executeTool as t, deleteSessionIfEmpty as u, deleteModel as v, resetSystemMessage as w, streamChat as x, listModels as y, SAFE as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
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",
@@ -53,7 +53,7 @@
53
53
  "@commitlint/cli": "21.0.1",
54
54
  "@commitlint/config-conventional": "21.0.1",
55
55
  "@eslint/js": "10.0.1",
56
- "@types/node": "25.8.0",
56
+ "@types/node": "25.9.0",
57
57
  "@types/react": "19.2.14",
58
58
  "@vitest/coverage-v8": "4.1.6",
59
59
  "eslint": "10.4.0",
@@ -65,9 +65,9 @@
65
65
  "lint-staged": "17.0.5",
66
66
  "prettier": "3.8.3",
67
67
  "publint": "0.3.21",
68
- "tsx": "4.22.1",
68
+ "tsx": "4.22.2",
69
69
  "typescript": "6.0.3",
70
- "typescript-eslint": "8.59.3",
70
+ "typescript-eslint": "8.59.4",
71
71
  "vite": "8.0.13",
72
72
  "vitest": "4.1.6"
73
73
  },