code-ollama 0.17.0 → 0.18.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,4 +1,4 @@
1
- import { A as PLAN_GENERATION_INSTRUCTION, C as withSystemMessage, D as ASSISTANT, E as getTheme, F as APPROVE, I as REJECT, L as VERSION, M as LABEL, N as PLAN, O as SYSTEM, P as SAFE, R as LIST, S as resetSystemMessage, T as LIST$1, _ 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 AUTO, k as USER, l as createSession, 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";
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";
2
2
  import { readdirSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join, relative } from "node:path";
@@ -6,7 +6,7 @@ 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";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
- import { Select, Spinner } from "@inkjs/ui";
9
+ import { ProgressBar, Select, Spinner } from "@inkjs/ui";
10
10
  import { Marked } from "marked";
11
11
  import { markedTerminal } from "marked-terminal";
12
12
  //#region src/components/CodeBlock/CodeBlock.tsx
@@ -630,9 +630,13 @@ function ToolApproval({ toolCall, onDecision, theme = getTheme() }) {
630
630
  onChange: handleChange,
631
631
  onCancel: handleEscape,
632
632
  children: [
633
- /* @__PURE__ */ jsx(Text, {
633
+ /* @__PURE__ */ jsxs(Text, {
634
634
  color: theme.colors.warning,
635
- children: "Tool requires approval ⚠️ "
635
+ children: [
636
+ "Tool requires approval ",
637
+ WARNING,
638
+ " "
639
+ ]
636
640
  }),
637
641
  /* @__PURE__ */ jsxs(Box, {
638
642
  flexDirection: "column",
@@ -817,8 +821,57 @@ function CommandMenu({ input, onSubmit }) {
817
821
  });
818
822
  }
819
823
  //#endregion
824
+ //#region src/components/Suggestions.tsx
825
+ var DEFAULT_MAX_VISIBLE_OPTIONS = 5;
826
+ function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_MAX_VISIBLE_OPTIONS, resetKey, onHighlight, onSelect }) {
827
+ const [focusedIndex, setFocusedIndex] = useState(0);
828
+ useEffect(() => {
829
+ setFocusedIndex(0);
830
+ }, [resetKey]);
831
+ useEffect(() => {
832
+ if (!options.length) {
833
+ setFocusedIndex(0);
834
+ onHighlight?.(null);
835
+ return;
836
+ }
837
+ setFocusedIndex((currentIndex) => Math.min(currentIndex, options.length - 1));
838
+ }, [onHighlight, options]);
839
+ useEffect(() => {
840
+ onHighlight?.(options[focusedIndex] ?? null);
841
+ }, [
842
+ focusedIndex,
843
+ onHighlight,
844
+ options
845
+ ]);
846
+ useInput((input, key) => {
847
+ if (isDisabled || !options.length) return;
848
+ if (key.downArrow || input === "\x1B[B") {
849
+ setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
850
+ return;
851
+ }
852
+ if (key.upArrow || input === "\x1B[A") {
853
+ setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
854
+ return;
855
+ }
856
+ if (key.tab || key.return || input === " " || input === "\r") onSelect(options[focusedIndex]);
857
+ });
858
+ if (!options.length) return null;
859
+ const visibleStart = Math.min(Math.max(0, focusedIndex - maxVisibleOptions + 1), Math.max(0, options.length - maxVisibleOptions));
860
+ return /* @__PURE__ */ jsx(Box, {
861
+ flexDirection: "column",
862
+ children: options.slice(visibleStart, visibleStart + maxVisibleOptions).map((option, index) => {
863
+ return /* @__PURE__ */ jsx(Box, {
864
+ marginLeft: 2,
865
+ children: /* @__PURE__ */ jsx(Text, {
866
+ color: visibleStart + index === focusedIndex ? "cyan" : void 0,
867
+ children: option.label
868
+ })
869
+ }, option.label);
870
+ })
871
+ });
872
+ }
873
+ //#endregion
820
874
  //#region src/components/Chat/FileSuggestions.tsx
821
- var MAX_VISIBLE_OPTIONS = 5;
822
875
  var MENTION_PATTERN = /(^|.)@(\S+)/;
823
876
  var RIPGREP_MAX_BUFFER = 10 * 1024 * 1024;
824
877
  function normalizePath(filePath) {
@@ -897,7 +950,6 @@ async function listProjectFiles(rootDir) {
897
950
  }
898
951
  function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
899
952
  const [filePaths, setFilePaths] = useState([]);
900
- const [focusedIndex, setFocusedIndex] = useState(0);
901
953
  useEffect(() => {
902
954
  async function loadProjectFiles() {
903
955
  setFilePaths(await listProjectFiles(process.cwd()));
@@ -910,55 +962,29 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
910
962
  const normalizedQuery = mentionMatch.query.toLowerCase();
911
963
  return filePaths.filter((filePath) => filePath.toLowerCase().includes(normalizedQuery));
912
964
  }, [filePaths, mentionMatch]);
913
- useEffect(() => {
914
- setFocusedIndex(0);
915
- }, [input]);
916
- useEffect(() => {
917
- if (!options.length) {
918
- setFocusedIndex(0);
919
- return;
920
- }
921
- setFocusedIndex((currentIndex) => Math.min(currentIndex, options.length - 1));
922
- }, [options]);
923
965
  useEffect(() => {
924
966
  if (!onChange) return;
925
- if (!mentionMatch || !options.length) {
926
- onChange(null);
927
- return;
928
- }
929
- onChange(buildNextInput(input, options[focusedIndex]).value);
967
+ if (!mentionMatch || !options.length) onChange(null);
930
968
  }, [
931
- focusedIndex,
932
- input,
933
969
  mentionMatch,
934
970
  onChange,
935
971
  options
936
972
  ]);
937
- useInput((_, key) => {
938
- if (isDisabled || !options.length) return;
939
- if (key.downArrow) {
940
- setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
941
- return;
942
- }
943
- if (key.upArrow) {
944
- setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
945
- return;
946
- }
947
- if (key.tab || key.return) onSelect(buildNextInput(input, options[focusedIndex]));
948
- });
949
973
  if (!mentionMatch || !options.length) return null;
950
- const visibleStart = Math.min(Math.max(0, focusedIndex - MAX_VISIBLE_OPTIONS + 1), Math.max(0, options.length - MAX_VISIBLE_OPTIONS));
951
- return /* @__PURE__ */ jsx(Box, {
952
- flexDirection: "column",
953
- children: options.slice(visibleStart, visibleStart + MAX_VISIBLE_OPTIONS).map((option, index) => {
954
- return /* @__PURE__ */ jsx(Box, {
955
- marginLeft: 2,
956
- children: /* @__PURE__ */ jsx(Text, {
957
- color: visibleStart + index === focusedIndex ? "cyan" : void 0,
958
- children: option
959
- })
960
- }, option);
961
- })
974
+ return /* @__PURE__ */ jsx(Suggestions, {
975
+ isDisabled,
976
+ options: options.map((option) => ({
977
+ label: option,
978
+ value: option
979
+ })),
980
+ resetKey: input,
981
+ onHighlight: (option) => {
982
+ // v8 ignore next
983
+ onChange?.(option ? buildNextInput(input, option.value).value : null);
984
+ },
985
+ onSelect: (option) => {
986
+ onSelect(buildNextInput(input, option.value));
987
+ }
962
988
  });
963
989
  }
964
990
  //#endregion
@@ -1119,10 +1145,10 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1119
1145
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1120
1146
  var PLAN_CHECKLIST_REMINDER = "Then display the execution plan as an unchecked Markdown checklist only";
1121
1147
  var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1122
- var INTERRUPT_REASON = /* @__PURE__ */ function(INTERRUPT_REASON) {
1123
- INTERRUPT_REASON["INTERRUPTED"] = "interrupted";
1124
- INTERRUPT_REASON["REJECTED"] = "rejected";
1125
- return INTERRUPT_REASON;
1148
+ var InterruptReason = /* @__PURE__ */ function(InterruptReason) {
1149
+ InterruptReason["Interrupted"] = "interrupted";
1150
+ InterruptReason["Rejected"] = "rejected";
1151
+ return InterruptReason;
1126
1152
  }({});
1127
1153
  //#endregion
1128
1154
  //#region src/components/Chat/plan.ts
@@ -1190,13 +1216,15 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1190
1216
  abortControllerRef.current = null;
1191
1217
  setIsLoading(false);
1192
1218
  setStreamingMessage(null);
1193
- setInterruptReason(INTERRUPT_REASON.INTERRUPTED);
1219
+ setInterruptReason(InterruptReason.Interrupted);
1194
1220
  setMessages((prev) => [...prev, {
1195
1221
  role: USER,
1196
1222
  content: TURN_ABORTED_MESSAGE
1197
1223
  }]);
1198
1224
  }, []);
1199
1225
  const processStream = useCallback(async (currentMessages, executionMode = mode) => {
1226
+ // v8 ignore next
1227
+ if (!model) throw new Error("Model is required");
1200
1228
  const controller = new AbortController();
1201
1229
  abortControllerRef.current = controller;
1202
1230
  const assistantMessage = {
@@ -1271,6 +1299,9 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1271
1299
  theme
1272
1300
  ]);
1273
1301
  const processStreamReadOnly = useCallback(async (currentMessages) => {
1302
+ const modelName = model;
1303
+ // v8 ignore next
1304
+ if (!modelName) throw new Error("Model is required");
1274
1305
  const controller = new AbortController();
1275
1306
  abortControllerRef.current = controller;
1276
1307
  const assistantMessage = {
@@ -1301,7 +1332,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1301
1332
  setStreamingMessage(assistantMessage);
1302
1333
  try {
1303
1334
  const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
1304
- for await (const chunk of streamChat(withSystemMessage(currentMessages), model, readOnlyTools, controller.signal)) {
1335
+ for await (const chunk of streamChat(withSystemMessage(currentMessages), modelName, readOnlyTools, controller.signal)) {
1305
1336
  // v8 ignore next 3
1306
1337
  if (controller.signal.aborted) return;
1307
1338
  if (chunk.type === "content") {
@@ -1337,7 +1368,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1337
1368
  };
1338
1369
  setStreamingMessage(planAssistantMessage);
1339
1370
  try {
1340
- for await (const chunk of streamChat(withSystemMessage(planMessages), model, [], controller.signal)) {
1371
+ for await (const chunk of streamChat(withSystemMessage(planMessages), modelName, [], controller.signal)) {
1341
1372
  // v8 ignore next 3
1342
1373
  if (controller.signal.aborted) return;
1343
1374
  if (chunk.type === "content") {
@@ -1429,7 +1460,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1429
1460
  content: TURN_ABORTED_MESSAGE
1430
1461
  }]);
1431
1462
  setIsLoading(false);
1432
- setInterruptReason(INTERRUPT_REASON.REJECTED);
1463
+ setInterruptReason(InterruptReason.Rejected);
1433
1464
  break;
1434
1465
  }
1435
1466
  }, [
@@ -1485,7 +1516,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1485
1516
  marginBottom: 1,
1486
1517
  children: /* @__PURE__ */ jsx(Text, {
1487
1518
  color: theme.colors.error,
1488
- children: interruptReason === INTERRUPT_REASON.REJECTED ? "❗ Tool call rejected." : "❗ Execution interrupted."
1519
+ children: interruptReason === InterruptReason.Rejected ? `❗ Tool call rejected.` : `❗ Execution interrupted.`
1489
1520
  })
1490
1521
  }),
1491
1522
  !pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Box, {
@@ -1512,6 +1543,7 @@ function getModeColor(mode, theme) {
1512
1543
  }
1513
1544
  }
1514
1545
  function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
1546
+ const modelLabel = model || "not configured";
1515
1547
  useInput((_, key) => {
1516
1548
  if (key.tab && key.shift) onToggleMode();
1517
1549
  });
@@ -1535,7 +1567,7 @@ function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
1535
1567
  " Model: ",
1536
1568
  /* @__PURE__ */ jsx(Text, {
1537
1569
  color: theme.colors.model,
1538
- children: model
1570
+ children: modelLabel
1539
1571
  })
1540
1572
  ]
1541
1573
  })
@@ -1549,6 +1581,7 @@ function abbreviatePath(dir) {
1549
1581
  }
1550
1582
  function Header({ model, onLoad, theme = getTheme() }) {
1551
1583
  const directory = abbreviatePath(process.cwd());
1584
+ const modelLabel = model || "not configured";
1552
1585
  useEffect(() => {
1553
1586
  onLoad();
1554
1587
  }, []);
@@ -1580,7 +1613,7 @@ function Header({ model, onLoad, theme = getTheme() }) {
1580
1613
  dimColor: true,
1581
1614
  children: "model:".padEnd(11)
1582
1615
  }),
1583
- /* @__PURE__ */ jsx(Text, { children: model.padEnd(model.length + 3) }),
1616
+ /* @__PURE__ */ jsx(Text, { children: modelLabel.padEnd(modelLabel.length + 3) }),
1584
1617
  /* @__PURE__ */ jsx(Text, {
1585
1618
  color: theme.colors.command,
1586
1619
  children: "/model"
@@ -1588,7 +1621,7 @@ function Header({ model, onLoad, theme = getTheme() }) {
1588
1621
  /* @__PURE__ */ jsxs(Text, {
1589
1622
  color: theme.colors.secondary,
1590
1623
  dimColor: true,
1591
- children: [" ", "to switch"]
1624
+ children: [" ", "to manage"]
1592
1625
  })
1593
1626
  ]
1594
1627
  }),
@@ -1602,48 +1635,611 @@ function Header({ model, onLoad, theme = getTheme() }) {
1602
1635
  });
1603
1636
  }
1604
1637
  //#endregion
1605
- //#region src/components/ModelPicker.tsx
1606
- function ModelPicker({ currentModel, onSelect, onClose, theme = getTheme() }) {
1607
- const [options, setOptions] = useState([]);
1608
- const [error, setError] = useState(null);
1609
- const handleChange = useCallback((model) => {
1610
- onSelect({ model });
1611
- }, [onSelect]);
1612
- useInput(async (_input, key) => {
1613
- if (!error && options.length && key.return) {
1614
- await tick();
1615
- onClose();
1638
+ //#region src/components/ModelManager/ModelSuggestions.tsx
1639
+ function rankCatalogMatch(entry, normalizedInput) {
1640
+ const normalizedValue = entry.value.toLowerCase();
1641
+ const normalizedLabel = entry.label.toLowerCase();
1642
+ // v8 ignore start
1643
+ switch (true) {
1644
+ case normalizedValue.startsWith(normalizedInput): return 0;
1645
+ case normalizedLabel.startsWith(normalizedInput): return 1;
1646
+ case normalizedValue.includes(normalizedInput): return 2;
1647
+ case normalizedLabel.includes(normalizedInput): return 3;
1648
+ default: return Number.MAX_SAFE_INTEGER;
1649
+ }
1650
+ // v8 ignore stop
1651
+ }
1652
+ function ModelSuggestions({ catalog, input, isDisabled = false, onHighlight, onSelect }) {
1653
+ const normalizedInput = input.trim().toLowerCase();
1654
+ return /* @__PURE__ */ jsx(Suggestions, {
1655
+ isDisabled,
1656
+ options: useMemo(() => {
1657
+ if (!normalizedInput) return [];
1658
+ return catalog.filter((entry) => entry.value.toLowerCase().includes(normalizedInput) || entry.label.toLowerCase().includes(normalizedInput)).sort((left, right) => rankCatalogMatch(left, normalizedInput) - rankCatalogMatch(right, normalizedInput) || left.label.localeCompare(right.label)).map((entry) => ({
1659
+ label: entry.value,
1660
+ value: entry.value
1661
+ }));
1662
+ }, [catalog, normalizedInput]),
1663
+ resetKey: input,
1664
+ /* v8 ignore next */
1665
+ onHighlight: (option) => onHighlight?.(option?.value ?? null),
1666
+ onSelect: (option) => {
1667
+ onSelect(option.value);
1668
+ }
1669
+ });
1670
+ }
1671
+ //#endregion
1672
+ //#region src/components/ModelManager/types.ts
1673
+ var View = /* @__PURE__ */ function(View) {
1674
+ View["Menu"] = "menu";
1675
+ View["Switch"] = "switch";
1676
+ View["Download"] = "download";
1677
+ View["CustomDownload"] = "custom-download";
1678
+ View["Downloading"] = "downloading";
1679
+ View["Delete"] = "delete";
1680
+ View["DeleteConfirm"] = "delete-confirm";
1681
+ return View;
1682
+ }({});
1683
+ var MenuAction = /* @__PURE__ */ function(MenuAction) {
1684
+ MenuAction["Switch"] = "switch";
1685
+ MenuAction["Download"] = "download";
1686
+ MenuAction["Delete"] = "delete";
1687
+ MenuAction["Cancel"] = "cancel";
1688
+ return MenuAction;
1689
+ }({});
1690
+ var DownloadAction = /* @__PURE__ */ function(DownloadAction) {
1691
+ DownloadAction["Custom"] = "custom";
1692
+ return DownloadAction;
1693
+ }({});
1694
+ var ConfirmDeleteAction = /* @__PURE__ */ function(ConfirmDeleteAction) {
1695
+ ConfirmDeleteAction["Delete"] = "delete";
1696
+ return ConfirmDeleteAction;
1697
+ }({});
1698
+ //#endregion
1699
+ //#region src/components/ModelManager/utils.ts
1700
+ function buildMenuOptions() {
1701
+ return [
1702
+ {
1703
+ label: "Switch model",
1704
+ value: MenuAction.Switch
1705
+ },
1706
+ {
1707
+ label: "Download model",
1708
+ value: MenuAction.Download
1709
+ },
1710
+ {
1711
+ label: "Delete model",
1712
+ value: MenuAction.Delete
1713
+ },
1714
+ {
1715
+ label: "Cancel",
1716
+ value: MenuAction.Cancel
1616
1717
  }
1718
+ ];
1719
+ }
1720
+ function buildInstalledModelOptions(models, currentModel) {
1721
+ const nextModels = [...models];
1722
+ if (nextModels.includes(currentModel)) {
1723
+ nextModels.splice(nextModels.indexOf(currentModel), 1);
1724
+ nextModels.unshift(currentModel);
1725
+ }
1726
+ return nextModels.map((model) => ({
1727
+ label: model === currentModel ? `${model} (current model)` : model,
1728
+ value: model
1729
+ }));
1730
+ }
1731
+ function buildDownloadOptions(installedModels) {
1732
+ const installedModelSet = new Set(installedModels);
1733
+ const availableCatalog = CATALOG.filter(({ value, alias }) => !installedModelSet.has(value) && !(alias && installedModelSet.has(alias)));
1734
+ return [
1735
+ {
1736
+ label: "Enter custom model...",
1737
+ value: DownloadAction.Custom
1738
+ },
1739
+ ...availableCatalog.map(({ label, value }) => ({
1740
+ label,
1741
+ value
1742
+ })),
1743
+ BACK
1744
+ ];
1745
+ }
1746
+ function getNoticeColor(tone, theme) {
1747
+ switch (tone) {
1748
+ case "error": return theme.colors.error;
1749
+ case "success": return theme.colors.status;
1750
+ default: return theme.colors.secondary;
1751
+ }
1752
+ }
1753
+ function formatBytes(bytes) {
1754
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
1755
+ const units = [
1756
+ "B",
1757
+ "KB",
1758
+ "MB",
1759
+ "GB",
1760
+ "TB"
1761
+ ];
1762
+ let value = bytes;
1763
+ let index = 0;
1764
+ while (value >= 1024 && index < units.length - 1) {
1765
+ value /= 1024;
1766
+ index += 1;
1767
+ }
1768
+ const fractionDigits = value >= 10 || index === 0 ? 0 : 1;
1769
+ return `${value.toFixed(fractionDigits)} ${units[index]}`;
1770
+ }
1771
+ function isAbortError(error) {
1772
+ return error instanceof Error && error.name === "AbortError";
1773
+ }
1774
+ function mergeDownloadProgress(previous, model, status, completed, total) {
1775
+ const nextCompleted = typeof completed === "number" && Number.isFinite(completed) && completed >= 0 ? completed : null;
1776
+ const nextTotal = typeof total === "number" && Number.isFinite(total) && total > 0 ? total : null;
1777
+ if (nextTotal !== null && nextCompleted !== null) return {
1778
+ model,
1779
+ status,
1780
+ completed: nextCompleted,
1781
+ total: nextTotal
1782
+ };
1783
+ const hasPreviousProgress = previous?.model === model && previous.total > 0;
1784
+ return {
1785
+ model,
1786
+ status,
1787
+ completed: hasPreviousProgress ? previous.completed : 0,
1788
+ total: hasPreviousProgress ? previous.total : 0
1789
+ };
1790
+ }
1791
+ //#endregion
1792
+ //#region src/components/ModelManager/ModelCustomDownloadView.tsx
1793
+ function ModelCustomDownloadView({ downloadDraft, notice, theme, onDraftChange, onHighlight, onSelectSuggestion, onSubmit }) {
1794
+ const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
1795
+ color: getNoticeColor(notice.tone, theme),
1796
+ children: notice.text
1797
+ }) : null;
1798
+ return /* @__PURE__ */ jsxs(Box, {
1799
+ flexDirection: "column",
1800
+ children: [
1801
+ /* @__PURE__ */ jsx(Text, { children: "Enter an Ollama model name to download." }),
1802
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
1803
+ value: downloadDraft,
1804
+ placeholder: "name:tag",
1805
+ wrapIndent: 2,
1806
+ onChange: onDraftChange,
1807
+ onSubmit
1808
+ })] }),
1809
+ /* @__PURE__ */ jsx(ModelSuggestions, {
1810
+ catalog: [],
1811
+ input: downloadDraft,
1812
+ onHighlight,
1813
+ onSelect: onSelectSuggestion
1814
+ }),
1815
+ renderNotice(),
1816
+ /* @__PURE__ */ jsx(Text, {
1817
+ color: theme.colors.secondary,
1818
+ dimColor: true,
1819
+ children: "Press Enter to download, Esc or Ctrl+C to go back."
1820
+ })
1821
+ ]
1822
+ });
1823
+ }
1824
+ //#endregion
1825
+ //#region src/components/ModelManager/ModelDeleteConfirmView.tsx
1826
+ function ModelDeleteConfirmView({ deleteCandidate, isDeleting, notice, theme, onCancel, onConfirm }) {
1827
+ if (isDeleting) return /* @__PURE__ */ jsx(Spinner, { label: `Deleting model ${deleteCandidate}...` });
1828
+ const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
1829
+ color: getNoticeColor(notice.tone, theme),
1830
+ children: notice.text
1831
+ }) : null;
1832
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1833
+ options: [{
1834
+ label: `Yes, delete ${deleteCandidate}`,
1835
+ value: ConfirmDeleteAction.Delete
1836
+ }, {
1837
+ ...BACK,
1838
+ label: "No"
1839
+ }],
1840
+ onCancel,
1841
+ onChange: onConfirm,
1842
+ children: [
1843
+ /* @__PURE__ */ jsxs(Text, { children: [
1844
+ WARNING,
1845
+ " Delete model",
1846
+ " ",
1847
+ /* @__PURE__ */ jsx(Text, {
1848
+ color: theme.colors.model,
1849
+ children: deleteCandidate
1850
+ }),
1851
+ "?"
1852
+ ] }),
1853
+ renderNotice(),
1854
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "This action cannot be undone" })
1855
+ ]
1856
+ });
1857
+ }
1858
+ //#endregion
1859
+ //#region src/components/ModelManager/ModelDeleteView.tsx
1860
+ function ModelDeleteView({ currentModel, installedModels, isLoading, notice, theme, onCancel, onSelect }) {
1861
+ if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1862
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1863
+ options: [...buildInstalledModelOptions(installedModels.filter((model) => model !== currentModel), currentModel), BACK],
1864
+ onCancel,
1865
+ onChange: (value) => {
1866
+ if (value === BACK.value) onCancel();
1867
+ else onSelect(value);
1868
+ },
1869
+ children: [
1870
+ /* @__PURE__ */ jsxs(Text, { children: [
1871
+ "Delete an installed model (current model",
1872
+ " ",
1873
+ /* @__PURE__ */ jsx(Text, {
1874
+ color: theme.colors.model,
1875
+ children: currentModel
1876
+ }),
1877
+ " cannot be deleted)."
1878
+ ] }),
1879
+ notice && /* @__PURE__ */ jsx(Text, {
1880
+ color: getNoticeColor(notice.tone, theme),
1881
+ children: notice.text
1882
+ }),
1883
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Delete models" })
1884
+ ]
1885
+ });
1886
+ }
1887
+ //#endregion
1888
+ //#region src/components/ModelManager/ModelDownloadingView.tsx
1889
+ function ModelDownloadingView({ progress, theme, onCancel }) {
1890
+ const percent = progress.total > 0 && Number.isFinite(progress.completed) && Number.isFinite(progress.total) ? Math.round(progress.completed / progress.total * 100) : null;
1891
+ return /* @__PURE__ */ jsxs(Box, {
1892
+ flexDirection: "column",
1893
+ children: [
1894
+ /* @__PURE__ */ jsxs(Text, { children: [
1895
+ "Downloading model:",
1896
+ " ",
1897
+ /* @__PURE__ */ jsx(Text, {
1898
+ color: theme.colors.model,
1899
+ children: progress.model
1900
+ })
1901
+ ] }),
1902
+ /* @__PURE__ */ jsx(Text, { children: progress.status }),
1903
+ percent !== null ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Text, { children: [
1904
+ percent,
1905
+ "% (",
1906
+ formatBytes(progress.completed),
1907
+ " /",
1908
+ " ",
1909
+ formatBytes(progress.total),
1910
+ ")"
1911
+ ] }), /* @__PURE__ */ jsx(ProgressBar, { value: Math.max(0, Math.min(100, percent)) })] }) : /* @__PURE__ */ jsx(Text, {
1912
+ color: theme.colors.secondary,
1913
+ dimColor: true,
1914
+ children: "Progress details unavailable. Waiting for Ollama updates..."
1915
+ }),
1916
+ /* @__PURE__ */ jsx(SelectPrompt, {
1917
+ options: [{
1918
+ label: "Cancel download",
1919
+ value: "cancel-download"
1920
+ }],
1921
+ onCancel,
1922
+ onChange: onCancel,
1923
+ children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Press Enter, Esc, or Ctrl+C to cancel" })
1924
+ })
1925
+ ]
1926
+ });
1927
+ }
1928
+ //#endregion
1929
+ //#region src/components/ModelManager/ModelDownloadView.tsx
1930
+ function ModelDownloadView({ installedModels, notice, theme, onCancel, onChange }) {
1931
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1932
+ options: buildDownloadOptions(installedModels),
1933
+ onCancel,
1934
+ onChange: (value) => {
1935
+ switch (value) {
1936
+ case "custom":
1937
+ onChange("custom");
1938
+ break;
1939
+ case "back":
1940
+ onCancel();
1941
+ break;
1942
+ default:
1943
+ onChange(value);
1944
+ break;
1945
+ }
1946
+ },
1947
+ children: [
1948
+ /* @__PURE__ */ jsx(Text, { children: "Choose a model to download or use a custom model name." }),
1949
+ notice && /* @__PURE__ */ jsx(Text, {
1950
+ color: getNoticeColor(notice.tone, theme),
1951
+ children: notice.text
1952
+ }),
1953
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Download models" })
1954
+ ]
1955
+ });
1956
+ }
1957
+ //#endregion
1958
+ //#region src/components/ModelManager/ModelSwitchView.tsx
1959
+ function ModelSwitchView({ currentModel, installedModels, isLoading, onCancel, onSelect }) {
1960
+ if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1961
+ return /* @__PURE__ */ jsx(SelectPrompt, {
1962
+ options: [...buildInstalledModelOptions(installedModels, currentModel), BACK],
1963
+ onCancel,
1964
+ onChange: (value) => {
1965
+ if (value === BACK.value) onCancel();
1966
+ else onSelect(value);
1967
+ },
1968
+ children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Switch models" })
1617
1969
  });
1970
+ }
1971
+ //#endregion
1972
+ //#region src/components/ModelManager/ModelManager.tsx
1973
+ function ModelManager({ currentModel, onSelect, onClose, theme = getTheme() }) {
1974
+ const [view, setView] = useState(View.Menu);
1975
+ const [installedModels, setInstalledModels] = useState([]);
1976
+ const [isLoadingModels, setIsLoadingModels] = useState(true);
1977
+ const [loadError, setLoadError] = useState(null);
1978
+ const [notice, setNotice] = useState(null);
1979
+ const [downloadDraft, setDownloadDraft] = useState("");
1980
+ const [highlightedSuggestion, setHighlightedSuggestion] = useState(null);
1981
+ const [downloadProgress, setDownloadProgress] = useState(null);
1982
+ const [deleteCandidate, setDeleteCandidate] = useState(null);
1983
+ const [isDeleting, setIsDeleting] = useState(false);
1984
+ const isDeletingRef = useRef(false);
1985
+ const pullRef = useRef(null);
1986
+ const loadInstalledModels = useCallback(async () => {
1987
+ setIsLoadingModels(true);
1988
+ setLoadError(null);
1989
+ try {
1990
+ setInstalledModels(await listModels());
1991
+ } catch (error) {
1992
+ setLoadError(error instanceof Error ? error.message : /* v8 ignore next */ String(error));
1993
+ } finally {
1994
+ setIsLoadingModels(false);
1995
+ }
1996
+ }, []);
1618
1997
  useEffect(() => {
1619
- async function load() {
1620
- try {
1621
- const models = await listModels();
1622
- if (models.includes(currentModel)) {
1623
- models.splice(models.indexOf(currentModel), 1);
1624
- models.unshift(currentModel);
1625
- }
1626
- setOptions(models.map((model) => ({
1627
- label: model,
1628
- value: model
1629
- })));
1630
- } catch (error) {
1631
- setError(error instanceof Error ? error.message : String(error));
1998
+ loadInstalledModels();
1999
+ }, [loadInstalledModels]);
2000
+ const resetDownloadState = useCallback(() => {
2001
+ setDownloadDraft("");
2002
+ setHighlightedSuggestion(null);
2003
+ setDownloadProgress(null);
2004
+ pullRef.current = null;
2005
+ }, []);
2006
+ const handleBackToMenu = useCallback(() => {
2007
+ setNotice(null);
2008
+ setDeleteCandidate(null);
2009
+ setIsDeleting(false);
2010
+ isDeletingRef.current = false;
2011
+ resetDownloadState();
2012
+ setView(View.Menu);
2013
+ }, [resetDownloadState]);
2014
+ const cancelActivePull = useCallback(() => {
2015
+ pullRef.current?.abort();
2016
+ }, []);
2017
+ useInput((input, key) => {
2018
+ const isEscape = key.escape || input === "\x1B\x1B";
2019
+ const isCtrlC = key.ctrl && input === "c" || input === "";
2020
+ if (view === View.CustomDownload && (isEscape || isCtrlC)) {
2021
+ setNotice(null);
2022
+ setHighlightedSuggestion(null);
2023
+ setView(View.Download);
2024
+ return;
2025
+ }
2026
+ // v8 ignore next
2027
+ if (view === View.Downloading && (isEscape || isCtrlC)) cancelActivePull();
2028
+ });
2029
+ const handleMenuChange = useCallback((value) => {
2030
+ setNotice(null);
2031
+ switch (value) {
2032
+ case "switch":
2033
+ setView(View.Switch);
2034
+ break;
2035
+ case "download":
2036
+ setView(View.Download);
2037
+ break;
2038
+ case "delete":
2039
+ setView(View.Delete);
2040
+ break;
2041
+ default: onClose();
2042
+ }
2043
+ }, [onClose]);
2044
+ const handleSwitchChange = useCallback((model) => {
2045
+ onSelect({ model });
2046
+ }, [onSelect]);
2047
+ const startPull = useCallback(async (model) => {
2048
+ const normalizedModel = model.trim();
2049
+ if (!normalizedModel) {
2050
+ setNotice({
2051
+ tone: "error",
2052
+ text: `❗ Enter a model name to download`
2053
+ });
2054
+ return;
2055
+ }
2056
+ if (installedModels.includes(normalizedModel)) {
2057
+ setNotice({
2058
+ tone: "info",
2059
+ text: `${normalizedModel} is already installed`
2060
+ });
2061
+ return;
2062
+ }
2063
+ setNotice(null);
2064
+ setDownloadProgress({
2065
+ model: normalizedModel,
2066
+ status: "Starting download...",
2067
+ completed: 0,
2068
+ total: 0
2069
+ });
2070
+ setView(View.Downloading);
2071
+ try {
2072
+ const pull = await pullModel(normalizedModel);
2073
+ pullRef.current = pull;
2074
+ for await (const update of pull) setDownloadProgress((previous) => {
2075
+ return mergeDownloadProgress(previous, normalizedModel, update.status, update.completed, update.total);
2076
+ });
2077
+ pullRef.current = null;
2078
+ resetDownloadState();
2079
+ await loadInstalledModels();
2080
+ setNotice({
2081
+ tone: "success",
2082
+ text: `✅ ${normalizedModel} downloaded successfully`
2083
+ });
2084
+ setView(View.Menu);
2085
+ } catch (error) {
2086
+ pullRef.current = null;
2087
+ if (isAbortError(error)) {
2088
+ setNotice({
2089
+ tone: "error",
2090
+ text: `❌ Download canceled for ${normalizedModel}`
2091
+ });
2092
+ setDownloadProgress(null);
2093
+ setView(View.Download);
2094
+ return;
1632
2095
  }
2096
+ setNotice({
2097
+ tone: "error",
2098
+ text: `❗ Error downloading model: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`
2099
+ });
2100
+ setDownloadProgress(null);
2101
+ setView(View.CustomDownload);
2102
+ }
2103
+ }, [
2104
+ installedModels,
2105
+ loadInstalledModels,
2106
+ resetDownloadState
2107
+ ]);
2108
+ const handleDownloadChange = useCallback((value) => {
2109
+ if (value === "custom") {
2110
+ setNotice(null);
2111
+ setView(View.CustomDownload);
2112
+ return;
2113
+ }
2114
+ // v8 ignore next 3
2115
+ if (value === "back") {
2116
+ handleBackToMenu();
2117
+ return;
2118
+ }
2119
+ setDownloadDraft(value);
2120
+ startPull(value);
2121
+ }, [handleBackToMenu, startPull]);
2122
+ const handleCustomDownloadSubmit = useCallback((value) => {
2123
+ const nextValue = highlightedSuggestion ?? value.trim();
2124
+ setDownloadDraft(nextValue);
2125
+ startPull(nextValue);
2126
+ }, [highlightedSuggestion, startPull]);
2127
+ const handleDeleteChange = useCallback((model) => {
2128
+ setNotice(null);
2129
+ setDeleteCandidate(model);
2130
+ setView(View.DeleteConfirm);
2131
+ }, []);
2132
+ const handleDeleteConfirm = useCallback(async (value) => {
2133
+ if (isDeletingRef.current) return;
2134
+ if (value === "back") {
2135
+ setView(View.Delete);
2136
+ return;
2137
+ }
2138
+ // v8 ignore next 3
2139
+ if (!deleteCandidate) {
2140
+ setView(View.Delete);
2141
+ return;
2142
+ }
2143
+ try {
2144
+ isDeletingRef.current = true;
2145
+ setIsDeleting(true);
2146
+ await deleteModel(deleteCandidate);
2147
+ await loadInstalledModels();
2148
+ setNotice({
2149
+ tone: "success",
2150
+ text: `✅ ${deleteCandidate} deleted successfully`
2151
+ });
2152
+ isDeletingRef.current = false;
2153
+ setIsDeleting(false);
2154
+ setDeleteCandidate(null);
2155
+ setView(View.Delete);
2156
+ } catch (error) {
2157
+ isDeletingRef.current = false;
2158
+ setIsDeleting(false);
2159
+ setNotice({
2160
+ tone: "error",
2161
+ text: `❗ Error deleting model: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`
2162
+ });
2163
+ setView(View.Delete);
1633
2164
  }
1634
- load();
1635
- }, [currentModel]);
1636
- if (error) return /* @__PURE__ */ jsxs(Text, {
1637
- color: theme.colors.error,
1638
- children: ["Error loading models: ", error]
2165
+ }, [deleteCandidate, loadInstalledModels]);
2166
+ const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
2167
+ color: notice.tone === "error" ? theme.colors.error : notice.tone === "success" ? theme.colors.status : theme.colors.secondary,
2168
+ children: notice.text
2169
+ }) : null;
2170
+ if (loadError && view !== View.Menu) return /* @__PURE__ */ jsxs(Box, {
2171
+ flexDirection: "column",
2172
+ children: [/* @__PURE__ */ jsxs(Text, {
2173
+ color: theme.colors.error,
2174
+ children: ["Error loading models: ", loadError]
2175
+ }), /* @__PURE__ */ jsx(Text, {
2176
+ color: theme.colors.secondary,
2177
+ dimColor: true,
2178
+ children: "Press Esc to go back."
2179
+ })]
1639
2180
  });
1640
- if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
1641
- return /* @__PURE__ */ jsx(SelectPrompt, {
1642
- options,
1643
- defaultValue: currentModel,
1644
- onChange: handleChange,
2181
+ if (view === View.Downloading && downloadProgress) return /* @__PURE__ */ jsx(ModelDownloadingView, {
2182
+ progress: downloadProgress,
2183
+ theme,
2184
+ onCancel: cancelActivePull
2185
+ });
2186
+ if (view === View.CustomDownload) return /* @__PURE__ */ jsx(ModelCustomDownloadView, {
2187
+ downloadDraft,
2188
+ notice,
2189
+ theme,
2190
+ onDraftChange: setDownloadDraft,
2191
+ onHighlight: setHighlightedSuggestion,
2192
+ onSelectSuggestion: (value) => {
2193
+ setDownloadDraft(value);
2194
+ setHighlightedSuggestion(value);
2195
+ },
2196
+ onSubmit: handleCustomDownloadSubmit
2197
+ });
2198
+ if (view === View.Switch) return /* @__PURE__ */ jsx(ModelSwitchView, {
2199
+ currentModel,
2200
+ installedModels,
2201
+ isLoading: isLoadingModels,
2202
+ onCancel: handleBackToMenu,
2203
+ onSelect: handleSwitchChange
2204
+ });
2205
+ if (view === View.Download) return /* @__PURE__ */ jsx(ModelDownloadView, {
2206
+ installedModels,
2207
+ notice,
2208
+ theme,
2209
+ onCancel: handleBackToMenu,
2210
+ onChange: handleDownloadChange
2211
+ });
2212
+ if (view === View.Delete) return /* @__PURE__ */ jsx(ModelDeleteView, {
2213
+ currentModel,
2214
+ installedModels,
2215
+ isLoading: isLoadingModels,
2216
+ notice,
2217
+ theme,
2218
+ onCancel: handleBackToMenu,
2219
+ onSelect: handleDeleteChange
2220
+ });
2221
+ if (view === View.DeleteConfirm && deleteCandidate) return /* @__PURE__ */ jsx(ModelDeleteConfirmView, {
2222
+ deleteCandidate,
2223
+ isDeleting,
2224
+ notice,
2225
+ theme,
2226
+ onCancel: () => {
2227
+ setView(View.Delete);
2228
+ },
2229
+ onConfirm: handleDeleteConfirm
2230
+ });
2231
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
2232
+ options: buildMenuOptions(),
1645
2233
  onCancel: onClose,
1646
- children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select a model" })
2234
+ onChange: handleMenuChange,
2235
+ children: [
2236
+ /* @__PURE__ */ jsxs(Text, { children: ["Current model: ", /* @__PURE__ */ jsx(Text, {
2237
+ color: theme.colors.model,
2238
+ children: currentModel
2239
+ })] }),
2240
+ renderNotice(),
2241
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage models" })
2242
+ ]
1647
2243
  });
1648
2244
  }
1649
2245
  //#endregion
@@ -1751,7 +2347,6 @@ function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
1751
2347
  //#endregion
1752
2348
  //#region src/components/SessionManager.tsx
1753
2349
  var ACTION = {
1754
- BACK: "back",
1755
2350
  CLOSE: "close",
1756
2351
  DELETE_MENU: "delete-menu",
1757
2352
  DELETE_PREFIX: "delete:",
@@ -1779,16 +2374,10 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, th
1779
2374
  const options = view === "open" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1780
2375
  label: formatSessionLabel(session, maxLabelWidth),
1781
2376
  value: `${ACTION.OPEN_PREFIX}${session.id}`
1782
- })), {
1783
- label: "Back",
1784
- value: ACTION.BACK
1785
- }] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
2377
+ })), BACK] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
1786
2378
  label: formatSessionLabel(session, maxLabelWidth, "Delete "),
1787
2379
  value: `${ACTION.DELETE_PREFIX}${session.id}`
1788
- })), {
1789
- label: "Back",
1790
- value: ACTION.BACK
1791
- }] : [
2380
+ })), BACK] : [
1792
2381
  {
1793
2382
  label: "New session",
1794
2383
  value: ACTION.NEW
@@ -1820,7 +2409,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, th
1820
2409
  case value === ACTION.OPEN_MENU:
1821
2410
  setView("open");
1822
2411
  break;
1823
- case value === ACTION.BACK:
2412
+ case value === BACK.value:
1824
2413
  setView("main");
1825
2414
  break;
1826
2415
  case value.startsWith(ACTION.DELETE_PREFIX):
@@ -1970,45 +2559,45 @@ function ThemeSettings({ currentTheme, onClose, onPreview, onSave }) {
1970
2559
  }
1971
2560
  //#endregion
1972
2561
  //#region src/components/App/constants.ts
1973
- var SCREEN = /* @__PURE__ */ function(SCREEN) {
1974
- SCREEN["CHAT"] = "chat";
1975
- SCREEN["MODEL_PICKER"] = "model-picker";
1976
- SCREEN["SEARCH_SETTINGS"] = "search-settings";
1977
- SCREEN["SESSION_MANAGER"] = "session-manager";
1978
- SCREEN["THEME_SETTINGS"] = "theme-settings";
1979
- return SCREEN;
2562
+ var Screen = /* @__PURE__ */ function(Screen) {
2563
+ Screen["Chat"] = "chat";
2564
+ Screen["ModelManager"] = "model-manager";
2565
+ Screen["SearchSettings"] = "search-settings";
2566
+ Screen["SessionManager"] = "session-manager";
2567
+ Screen["ThemeSettings"] = "theme-settings";
2568
+ return Screen;
1980
2569
  }({});
1981
2570
  //#endregion
1982
2571
  //#region src/components/App/hooks/useScreenRouter.ts
1983
2572
  function useScreenRouter() {
1984
2573
  const { exit } = useApp();
1985
- const [currentScreen, setScreen] = useState(SCREEN.CHAT);
2574
+ const [currentScreen, setScreen] = useState(Screen.Chat);
1986
2575
  return {
1987
2576
  currentScreen,
1988
2577
  setScreen,
1989
2578
  handleClose: useCallback(() => {
1990
- setScreen(SCREEN.CHAT);
2579
+ setScreen(Screen.Chat);
1991
2580
  }, []),
1992
2581
  handleCommand: useCallback((command, callbacks) => {
1993
2582
  const { onCreateSession, onSetPreviewThemeId, model, theme } = callbacks;
1994
2583
  switch (command) {
1995
2584
  case "/session":
1996
- setScreen(SCREEN.SESSION_MANAGER);
2585
+ setScreen(Screen.SessionManager);
1997
2586
  break;
1998
2587
  case "/model":
1999
- setScreen(SCREEN.MODEL_PICKER);
2588
+ setScreen(Screen.ModelManager);
2000
2589
  break;
2001
2590
  case "/search":
2002
- setScreen(SCREEN.SEARCH_SETTINGS);
2591
+ setScreen(Screen.SearchSettings);
2003
2592
  break;
2004
2593
  case "/theme":
2005
2594
  onSetPreviewThemeId(theme);
2006
- setScreen(SCREEN.THEME_SETTINGS);
2595
+ setScreen(Screen.ThemeSettings);
2007
2596
  break;
2008
2597
  case "/clear": {
2009
2598
  resetSystemMessage();
2010
2599
  const nextSession = onCreateSession(model);
2011
- setScreen(SCREEN.CHAT);
2600
+ setScreen(Screen.Chat);
2012
2601
  clear(nextSession.metadata.id);
2013
2602
  break;
2014
2603
  }
@@ -2049,8 +2638,6 @@ function useSessionManager({ sessionId, model, commandColor }) {
2049
2638
  }, []);
2050
2639
  return {
2051
2640
  activeSession,
2052
- sessionRef,
2053
- setActiveSession,
2054
2641
  setSession,
2055
2642
  handleCreateSession: useCallback(() => {
2056
2643
  const nextSession = createSession(modelRef.current);
@@ -2089,39 +2676,137 @@ function useSessionManager({ sessionId, model, commandColor }) {
2089
2676
  //#region src/components/App/hooks/useThemeSettings.ts
2090
2677
  function useThemeSettings({ currentTheme, onUpdateConfig, setScreen }) {
2091
2678
  const [previewThemeId, setPreviewThemeId] = useState(null);
2092
- const activeThemeId = previewThemeId ?? currentTheme;
2093
- const activeTheme = getTheme(activeThemeId);
2679
+ const activeTheme = getTheme(previewThemeId ?? currentTheme);
2094
2680
  const handleThemePreview = useCallback((themeId) => {
2095
2681
  setPreviewThemeId(themeId);
2096
2682
  }, []);
2097
2683
  return {
2098
2684
  activeTheme,
2099
- activeThemeId,
2100
2685
  handleThemeClose: useCallback(() => {
2101
2686
  setPreviewThemeId(null);
2102
- setScreen(SCREEN.CHAT);
2687
+ setScreen(Screen.Chat);
2103
2688
  }, [setScreen]),
2104
2689
  handleThemePreview,
2105
2690
  handleThemeSave: useCallback((themeId) => {
2106
2691
  setPreviewThemeId(null);
2107
2692
  onUpdateConfig({ theme: themeId });
2108
2693
  }, [onUpdateConfig]),
2109
- previewThemeId,
2110
2694
  setPreviewThemeId
2111
2695
  };
2112
2696
  }
2113
2697
  //#endregion
2698
+ //#region src/components/App/ReadinessCheck.tsx
2699
+ var ReadinessState = /* @__PURE__ */ function(ReadinessState) {
2700
+ ReadinessState["Checking"] = "checking";
2701
+ ReadinessState["Ready"] = "ready";
2702
+ ReadinessState["MissingModelConfig"] = "missing-model-config";
2703
+ ReadinessState["NoInstalledModels"] = "no-installed-models";
2704
+ ReadinessState["ModelLoadError"] = "model-load-error";
2705
+ return ReadinessState;
2706
+ }({});
2707
+ function getTitle(setupState) {
2708
+ switch (setupState) {
2709
+ case "model-load-error": return "Connection Error";
2710
+ case "missing-model-config": return "No Model Configured";
2711
+ case "no-installed-models": return "No Model Installed";
2712
+ }
2713
+ }
2714
+ function getMessage(setupState, errorMessage) {
2715
+ const theme = getTheme();
2716
+ switch (setupState) {
2717
+ case "checking": return /* @__PURE__ */ jsx(Text, { children: "Checking model setup..." });
2718
+ case "missing-model-config": return /* @__PURE__ */ jsxs(Text, { children: [
2719
+ "Select or download a model with",
2720
+ " ",
2721
+ /* @__PURE__ */ jsx(Text, {
2722
+ color: theme.colors.command,
2723
+ children: "/model"
2724
+ })
2725
+ ] });
2726
+ case "no-installed-models": return /* @__PURE__ */ jsxs(Text, { children: ["Download a model with ", /* @__PURE__ */ jsx(Text, {
2727
+ color: theme.colors.command,
2728
+ children: "/model"
2729
+ })] });
2730
+ case "model-load-error": return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Text, { children: [
2731
+ "Error loading models",
2732
+ errorMessage ? `: ${errorMessage}` : "",
2733
+ "."
2734
+ ] }), /* @__PURE__ */ jsx(Text, { children: "Fix the connection and restart the app" })] });
2735
+ default: return null;
2736
+ }
2737
+ }
2738
+ function ReadinessCheck({ errorMessage, onCommand, setupState, theme = getTheme() }) {
2739
+ const title = getTitle(setupState);
2740
+ return /* @__PURE__ */ jsxs(Box, {
2741
+ flexDirection: "column",
2742
+ children: [/* @__PURE__ */ jsxs(Box, {
2743
+ borderStyle: "round",
2744
+ flexDirection: "column",
2745
+ marginBottom: 1,
2746
+ paddingX: 1,
2747
+ paddingY: 1,
2748
+ children: [title && /* @__PURE__ */ jsxs(Text, {
2749
+ bold: true,
2750
+ color: theme.colors.error,
2751
+ children: [
2752
+ "❗",
2753
+ " ",
2754
+ title
2755
+ ]
2756
+ }), getMessage(setupState, errorMessage)]
2757
+ }), /* @__PURE__ */ jsx(ChatInput, {
2758
+ history: [],
2759
+ onSubmit: onCommand
2760
+ })]
2761
+ });
2762
+ }
2763
+ //#endregion
2114
2764
  //#region src/components/App/App.tsx
2115
2765
  function App({ sessionId }) {
2116
2766
  const [appConfig, setConfig] = useState(() => loadConfig());
2117
2767
  const [mode, setMode] = useState(SAFE);
2118
2768
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
2769
+ const [setupState, setSetupState] = useState(() => appConfig.model ? ReadinessState.Ready : ReadinessState.MissingModelConfig);
2770
+ const [setupErrorMessage, setSetupErrorMessage] = useState(null);
2119
2771
  const { currentScreen, setScreen, handleClose, handleCommand } = useScreenRouter();
2120
2772
  const { activeSession, setSession, handleCreateSession, handleOpenSession, handleDeleteSession, handleMessagesChange } = useSessionManager({
2121
2773
  sessionId,
2122
- model: appConfig.model,
2774
+ model: appConfig.model ?? "",
2123
2775
  commandColor: getTheme(appConfig.theme).colors.command
2124
2776
  });
2777
+ useEffect(() => {
2778
+ let isMounted = true;
2779
+ async function refreshSetupState() {
2780
+ if (!appConfig.model) {
2781
+ // v8 ignore next
2782
+ if (isMounted) {
2783
+ setSetupErrorMessage(null);
2784
+ setSetupState(ReadinessState.MissingModelConfig);
2785
+ }
2786
+ return;
2787
+ }
2788
+ if (currentScreen !== Screen.Chat) return;
2789
+ // v8 ignore next
2790
+ if (isMounted) {
2791
+ setSetupErrorMessage(null);
2792
+ setSetupState(ReadinessState.Checking);
2793
+ }
2794
+ try {
2795
+ const installedModels = await listModels();
2796
+ if (!isMounted) return;
2797
+ setSetupState(installedModels.length > 0 ? ReadinessState.Ready : ReadinessState.NoInstalledModels);
2798
+ } catch (error) {
2799
+ // v8 ignore start
2800
+ if (!isMounted) return;
2801
+ setSetupErrorMessage(error instanceof Error ? error.message : String(error));
2802
+ setSetupState(ReadinessState.ModelLoadError);
2803
+ }
2804
+ }
2805
+ refreshSetupState();
2806
+ return () => {
2807
+ isMounted = false;
2808
+ };
2809
+ }, [appConfig.model, currentScreen]);
2125
2810
  const handleUpdateConfig = useCallback((update) => {
2126
2811
  setConfig((current) => ({
2127
2812
  ...current,
@@ -2133,7 +2818,7 @@ function App({ sessionId }) {
2133
2818
  ...current,
2134
2819
  metadata: updateSessionModel(current.metadata.id, newModel)
2135
2820
  }));
2136
- setScreen(SCREEN.CHAT);
2821
+ setScreen(Screen.Chat);
2137
2822
  }, [setScreen, setSession]);
2138
2823
  const { activeTheme, handleThemeClose, handleThemePreview, handleThemeSave, setPreviewThemeId } = useThemeSettings({
2139
2824
  currentTheme: appConfig.theme,
@@ -2155,7 +2840,7 @@ function App({ sessionId }) {
2155
2840
  }, []);
2156
2841
  const handleChatCommand = useCallback((command) => {
2157
2842
  handleCommand(command, {
2158
- model: appConfig.model,
2843
+ model: appConfig.model ?? "",
2159
2844
  theme: appConfig.theme,
2160
2845
  onCreateSession: handleCreateSession,
2161
2846
  onSetPreviewThemeId: setPreviewThemeId
@@ -2169,27 +2854,27 @@ function App({ sessionId }) {
2169
2854
  ]);
2170
2855
  const handleDeleteSessionAndStay = useCallback((sid) => {
2171
2856
  handleDeleteSession(sid);
2172
- setScreen(SCREEN.SESSION_MANAGER);
2857
+ setScreen(Screen.SessionManager);
2173
2858
  }, [handleDeleteSession, setScreen]);
2174
2859
  const handleOpenSessionAndNavigate = useCallback((sid) => {
2175
2860
  handleOpenSession(sid);
2176
- setScreen(SCREEN.CHAT);
2861
+ setScreen(Screen.Chat);
2177
2862
  }, [handleOpenSession, setScreen]);
2178
2863
  const handleCreateSessionAndNavigate = useCallback(() => {
2179
2864
  handleCreateSession();
2180
- setScreen(SCREEN.CHAT);
2865
+ setScreen(Screen.Chat);
2181
2866
  }, [handleCreateSession, setScreen]);
2182
2867
  let screenContent;
2183
2868
  switch (currentScreen) {
2184
- case SCREEN.MODEL_PICKER:
2185
- screenContent = /* @__PURE__ */ jsx(ModelPicker, {
2186
- currentModel: appConfig.model,
2869
+ case Screen.ModelManager:
2870
+ screenContent = /* @__PURE__ */ jsx(ModelManager, {
2871
+ currentModel: appConfig.model ?? "",
2187
2872
  onSelect: handleUpdateConfig,
2188
2873
  onClose: handleClose,
2189
2874
  theme: activeTheme
2190
2875
  });
2191
2876
  break;
2192
- case SCREEN.SEARCH_SETTINGS:
2877
+ case Screen.SearchSettings:
2193
2878
  screenContent = /* @__PURE__ */ jsx(SearchSettings, {
2194
2879
  currentUrl: appConfig.searxngBaseUrl,
2195
2880
  onSave: handleUpdateConfig,
@@ -2197,7 +2882,7 @@ function App({ sessionId }) {
2197
2882
  theme: activeTheme
2198
2883
  });
2199
2884
  break;
2200
- case SCREEN.SESSION_MANAGER:
2885
+ case Screen.SessionManager:
2201
2886
  screenContent = /* @__PURE__ */ jsx(SessionManager, {
2202
2887
  currentSessionId: activeSession.metadata.id,
2203
2888
  onClose: handleClose,
@@ -2207,7 +2892,7 @@ function App({ sessionId }) {
2207
2892
  theme: activeTheme
2208
2893
  });
2209
2894
  break;
2210
- case SCREEN.THEME_SETTINGS:
2895
+ case Screen.ThemeSettings:
2211
2896
  screenContent = /* @__PURE__ */ jsx(ThemeSettings, {
2212
2897
  currentTheme: appConfig.theme,
2213
2898
  onClose: handleThemeClose,
@@ -2215,8 +2900,8 @@ function App({ sessionId }) {
2215
2900
  onSave: handleThemeSave
2216
2901
  });
2217
2902
  break;
2218
- case SCREEN.CHAT:
2219
- screenContent = /* @__PURE__ */ jsx(Chat, {
2903
+ case Screen.Chat:
2904
+ screenContent = setupState === ReadinessState.Ready ? /* @__PURE__ */ jsx(Chat, {
2220
2905
  initialMessages: activeSession.messages,
2221
2906
  model: appConfig.model,
2222
2907
  onCommand: handleChatCommand,
@@ -2225,6 +2910,11 @@ function App({ sessionId }) {
2225
2910
  onModeChange: setMode,
2226
2911
  sessionId: activeSession.metadata.id,
2227
2912
  theme: activeTheme
2913
+ }) : /* @__PURE__ */ jsx(ReadinessCheck, {
2914
+ errorMessage: setupErrorMessage,
2915
+ onCommand: handleChatCommand,
2916
+ setupState,
2917
+ theme: activeTheme
2228
2918
  });
2229
2919
  break;
2230
2920
  }
@@ -2232,14 +2922,14 @@ function App({ sessionId }) {
2232
2922
  flexDirection: "column",
2233
2923
  children: [
2234
2924
  /* @__PURE__ */ jsx(Header, {
2235
- model: appConfig.model,
2925
+ model: appConfig.model ?? "",
2236
2926
  onLoad: handleHeaderLoad,
2237
2927
  theme: activeTheme
2238
2928
  }),
2239
2929
  isHeaderLoaded && screenContent,
2240
2930
  /* @__PURE__ */ jsx(Footer, {
2241
2931
  mode,
2242
- model: appConfig.model,
2932
+ model: appConfig.model ?? "",
2243
2933
  onToggleMode: handleToggleMode,
2244
2934
  theme: activeTheme
2245
2935
  })