@sean.holung/minicode 0.3.7 → 0.3.8

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.
Files changed (30) hide show
  1. package/dist/src/agent/config.js +25 -0
  2. package/dist/src/model-utils.js +18 -1
  3. package/dist/src/serve/agent-bridge.js +85 -14
  4. package/dist/src/serve/server.js +137 -3
  5. package/dist/src/session/session-store.js +18 -0
  6. package/dist/src/web/app.js +559 -90
  7. package/dist/src/web/index.html +112 -8
  8. package/dist/src/web/style.css +141 -7
  9. package/dist/tests/agent.test.js +16 -0
  10. package/dist/tests/config-integration.test.js +91 -1
  11. package/dist/tests/file-tools.test.js +12 -0
  12. package/dist/tests/graph-onboarding.test.js +8 -0
  13. package/dist/tests/model-client-openai.test.js +41 -0
  14. package/dist/tests/model-dropdown-ui.test.js +23 -0
  15. package/dist/tests/model-utils.test.js +26 -1
  16. package/dist/tests/serve.integration.test.js +163 -0
  17. package/dist/tests/session-store.test.js +15 -1
  18. package/dist/tests/settings-ui.test.js +11 -0
  19. package/dist/tests/setup-overlay-state.test.js +49 -0
  20. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  21. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +10 -1
  22. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  23. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  24. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +21 -0
  25. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  26. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  27. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +60 -6
  28. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  29. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +1 -1
@@ -1678,6 +1678,7 @@ var activeAnalysisFilter = "all";
1678
1678
  var analysisExplanationCache = /* @__PURE__ */ new Map();
1679
1679
  var filePreviewModalInitialized = false;
1680
1680
  var latestFilePreviewRequestId = 0;
1681
+ var pendingGraphRefreshTimer = null;
1681
1682
  var LAYOUT_OPTIONS = {
1682
1683
  name: "cose",
1683
1684
  nodeRepulsion: function() {
@@ -1704,39 +1705,8 @@ async function initGraph() {
1704
1705
  const cyEl = document.getElementById("cy");
1705
1706
  const detailEl = document.getElementById("symbol-detail");
1706
1707
  setupFilePreviewModal();
1708
+ cyEl.innerHTML = "";
1707
1709
  try {
1708
- const [graphRes, symbolsRes, focusRes] = await Promise.all([
1709
- fetch("/api/graph"),
1710
- fetch("/api/symbols"),
1711
- fetch("/api/focus")
1712
- ]);
1713
- if (!graphRes.ok || !symbolsRes.ok) {
1714
- cyEl.innerHTML = '<div class="graph-empty">No index available. Run minicode with a project to generate the code graph.</div>';
1715
- return;
1716
- }
1717
- const graphData = await graphRes.json();
1718
- const focusData = focusRes.ok ? await focusRes.json() : { pinned: [] };
1719
- if (!graphData.nodes || graphData.nodes.length === 0) {
1720
- cyEl.innerHTML = '<div class="graph-empty">No index available. Run minicode with a project to generate the code graph.</div>';
1721
- return;
1722
- }
1723
- for (const node of graphData.nodes) {
1724
- const id = node.qualifiedName || node.id || node.name || "";
1725
- graphNodes.set(id, node);
1726
- }
1727
- graphEdges = (graphData.edges || []).map((e) => ({
1728
- source: e.source || e.from || "",
1729
- target: e.target || e.to || "",
1730
- kind: (e.kind || e.type || "references").toLowerCase()
1731
- }));
1732
- edgeIndex = buildGraphEdgeIndex(graphEdges);
1733
- fileToSymbolIds = buildGraphFileIndex(graphNodes);
1734
- allSymbolNames = Array.from(graphNodes.keys()).sort();
1735
- const pinned = focusData.pinned || [];
1736
- for (const f of pinned) {
1737
- const name = typeof f === "string" ? f : f.name || f.qualifiedName;
1738
- if (name) pinnedNames.add(name);
1739
- }
1740
1710
  cy = cytoscape({
1741
1711
  container: cyEl,
1742
1712
  elements: [],
@@ -1746,20 +1716,64 @@ async function initGraph() {
1746
1716
  });
1747
1717
  setupInteractions(cy, detailEl);
1748
1718
  setupToolbar();
1749
- if (pinnedNames.size > 0) {
1750
- for (const name of pinnedNames) {
1751
- addNodeNeighborhood(name, 1);
1752
- }
1753
- runLayout();
1754
- } else {
1755
- showOnboardingHint(cyEl);
1719
+ const loaded = await loadGraphSnapshot(false);
1720
+ if (!loaded) {
1721
+ showOnboardingHint(cyEl, "No project index is available yet. Refresh after files are available, or restart minicode serve.");
1722
+ return;
1756
1723
  }
1724
+ seedPinnedSymbolsOrOnboarding(
1725
+ graphNodes.size === 0 ? "No JavaScript or TypeScript symbols are indexed yet. Create a file, then refresh the graph." : void 0
1726
+ );
1757
1727
  } catch (err) {
1758
1728
  console.error("Graph init failed:", err);
1759
1729
  const msg = err instanceof Error ? err.message : String(err);
1760
1730
  cyEl.innerHTML = `<div class="graph-empty">Failed to load graph: ${msg}</div>`;
1761
1731
  }
1762
1732
  }
1733
+ async function refreshGraphData(options = {}) {
1734
+ if (!initialized || !cy) return;
1735
+ const {
1736
+ refreshIndex = false,
1737
+ preserveVisible = true,
1738
+ showFeedback = false
1739
+ } = options;
1740
+ const refreshBtn = document.getElementById("graph-refresh");
1741
+ const visibleIds = preserveVisible ? cy.nodes().map((node) => node.id()) : [];
1742
+ if (showFeedback && refreshBtn) {
1743
+ refreshBtn.disabled = true;
1744
+ refreshBtn.textContent = "Refreshing...";
1745
+ }
1746
+ try {
1747
+ const loaded = await loadGraphSnapshot(refreshIndex);
1748
+ if (!loaded) {
1749
+ throw new Error("No project index available");
1750
+ }
1751
+ renderGraphAfterDataRefresh(visibleIds, preserveVisible);
1752
+ } catch (err) {
1753
+ console.error("Graph refresh failed:", err);
1754
+ if (showFeedback) {
1755
+ const cyEl = document.getElementById("cy");
1756
+ if (cyEl && graphNodes.size === 0) {
1757
+ showOnboardingHint(cyEl, "Could not refresh the project index. Check the minicode serve logs, then try again.");
1758
+ }
1759
+ }
1760
+ } finally {
1761
+ if (showFeedback && refreshBtn) {
1762
+ refreshBtn.disabled = false;
1763
+ refreshBtn.textContent = "Refresh";
1764
+ }
1765
+ }
1766
+ }
1767
+ function scheduleGraphDataRefresh() {
1768
+ if (!initialized) return;
1769
+ if (pendingGraphRefreshTimer) {
1770
+ clearTimeout(pendingGraphRefreshTimer);
1771
+ }
1772
+ pendingGraphRefreshTimer = setTimeout(() => {
1773
+ pendingGraphRefreshTimer = null;
1774
+ void refreshGraphData({ preserveVisible: true });
1775
+ }, 250);
1776
+ }
1763
1777
  function highlightAgentActivity(symbolName) {
1764
1778
  if (!cy) return;
1765
1779
  void focusResolvedSymbolsInGraph(symbolName, {
@@ -1773,11 +1787,106 @@ function highlightAgentActivity(symbolName) {
1773
1787
  function resizeGraph() {
1774
1788
  if (cy) cy.resize();
1775
1789
  }
1776
- function showOnboardingHint(container) {
1777
- if (container.querySelector(".graph-onboarding")) return;
1790
+ async function loadGraphSnapshot(refreshIndex) {
1791
+ if (refreshIndex) {
1792
+ const refreshRes = await fetch("/api/index/refresh", { method: "POST" });
1793
+ if (!refreshRes.ok) {
1794
+ return false;
1795
+ }
1796
+ }
1797
+ const [graphRes, focusRes] = await Promise.all([
1798
+ fetch("/api/graph"),
1799
+ fetch("/api/focus")
1800
+ ]);
1801
+ if (!graphRes.ok) {
1802
+ return false;
1803
+ }
1804
+ const graphData = await graphRes.json();
1805
+ const focusData = focusRes.ok ? await focusRes.json() : { pinned: [] };
1806
+ applyGraphSnapshot(graphData, focusData);
1807
+ return true;
1808
+ }
1809
+ function applyGraphSnapshot(graphData, focusData) {
1810
+ graphNodes.clear();
1811
+ for (const node of graphData.nodes || []) {
1812
+ const id = node.qualifiedName || node.id || node.name || "";
1813
+ if (id) {
1814
+ graphNodes.set(id, node);
1815
+ }
1816
+ }
1817
+ graphEdges = (graphData.edges || []).map((e) => ({
1818
+ source: e.source || e.from || "",
1819
+ target: e.target || e.to || "",
1820
+ kind: (e.kind || e.type || "references").toLowerCase()
1821
+ })).filter((edge) => edge.source && edge.target);
1822
+ edgeIndex = buildGraphEdgeIndex(graphEdges);
1823
+ fileToSymbolIds = buildGraphFileIndex(graphNodes);
1824
+ allSymbolNames = Array.from(graphNodes.keys()).sort();
1825
+ pinnedNames.clear();
1826
+ for (const pinned of focusData.pinned || []) {
1827
+ const name = typeof pinned === "string" ? pinned : pinned.name || pinned.qualifiedName;
1828
+ if (name) pinnedNames.add(name);
1829
+ }
1830
+ resetAnalysisForGraphRefresh();
1831
+ }
1832
+ function resetAnalysisForGraphRefresh() {
1833
+ analysisReport = null;
1834
+ activeAnalysisFindingId = null;
1835
+ activeAnalysisFilter = "all";
1836
+ analysisExplanationCache.clear();
1837
+ clearAnalysisGraphClasses();
1838
+ const panel = document.getElementById("analysis-panel");
1839
+ if (!panel || panel.classList.contains("hidden")) {
1840
+ return;
1841
+ }
1842
+ const { summary, findings } = getAnalysisPanelEls();
1843
+ summary.innerHTML = "";
1844
+ findings.innerHTML = '<div class="analysis-empty">Graph refreshed. Re-run analysis to inspect the latest dependency graph.</div>';
1845
+ setAnalysisStatus("Graph refreshed. Re-run analysis to inspect the latest snapshot.");
1846
+ }
1847
+ function renderGraphAfterDataRefresh(previousVisibleIds, preserveVisible) {
1848
+ if (!cy) return;
1849
+ cy.elements().remove();
1850
+ document.getElementById("symbol-detail")?.classList.add("hidden");
1851
+ const visibleIds = preserveVisible ? [...new Set(previousVisibleIds)].filter((id) => graphNodes.has(id)) : [];
1852
+ for (const id of visibleIds) {
1853
+ addNodeToGraph(id);
1854
+ }
1855
+ connectExistingNodes();
1856
+ if (cy.nodes().length > 0) {
1857
+ refreshAnalysisGraphState();
1858
+ runLayout();
1859
+ return;
1860
+ }
1861
+ seedPinnedSymbolsOrOnboarding(
1862
+ graphNodes.size === 0 ? "No JavaScript or TypeScript symbols are indexed yet. Create a file, then refresh the graph." : void 0
1863
+ );
1864
+ }
1865
+ function seedPinnedSymbolsOrOnboarding(subtitle) {
1866
+ if (!cy) return;
1867
+ for (const name of pinnedNames) {
1868
+ addNodeNeighborhood(name, 1);
1869
+ }
1870
+ connectExistingNodes();
1871
+ if (cy.nodes().length > 0) {
1872
+ refreshAnalysisGraphState();
1873
+ runLayout();
1874
+ return;
1875
+ }
1876
+ const cyEl = document.getElementById("cy");
1877
+ if (cyEl) showOnboardingHint(cyEl, subtitle);
1878
+ refreshAnalysisGraphState();
1879
+ }
1880
+ function showOnboardingHint(container, subtitle = "Search for a symbol or file above to start exploring.<br/>Nodes expand on click to reveal connections.") {
1881
+ const existing = container.querySelector(".graph-onboarding");
1882
+ if (existing) {
1883
+ const subtitleEl = existing.querySelector(".graph-onboarding-subtitle");
1884
+ if (subtitleEl) subtitleEl.innerHTML = subtitle;
1885
+ return;
1886
+ }
1778
1887
  const hint = document.createElement("div");
1779
1888
  hint.className = "graph-onboarding";
1780
- hint.innerHTML = '<div class="graph-onboarding-icon">&#9670; &#8212; &#9670;</div><div class="graph-onboarding-title">Code dependency graph</div><div class="graph-onboarding-subtitle">Search for a symbol or file above to start exploring.<br/>Nodes expand on click to reveal connections.</div>';
1889
+ hint.innerHTML = `<div class="graph-onboarding-icon">&#9670; &#8212; &#9670;</div><div class="graph-onboarding-title">Code dependency graph</div><div class="graph-onboarding-subtitle">${subtitle}</div>`;
1781
1890
  container.appendChild(hint);
1782
1891
  }
1783
1892
  function removeOnboardingHint() {
@@ -2660,6 +2769,7 @@ async function togglePin(name, node, btnEl) {
2660
2769
  function setupToolbar() {
2661
2770
  const searchInput = document.getElementById("graph-search");
2662
2771
  const analyzeBtn = document.getElementById("graph-analyze");
2772
+ const refreshBtn = document.getElementById("graph-refresh");
2663
2773
  const fitBtn = document.getElementById("graph-fit");
2664
2774
  const relayoutBtn = document.getElementById("graph-relayout");
2665
2775
  const clearBtn = document.getElementById("graph-clear");
@@ -2671,7 +2781,6 @@ function setupToolbar() {
2671
2781
  dropdown.className = "search-dropdown hidden";
2672
2782
  searchInput.parentNode.style.position = "relative";
2673
2783
  searchInput.parentNode.appendChild(dropdown);
2674
- const rankedSymbols = allSymbolNames.slice().sort((a, b2) => compareGraphNodeIds(a, b2, graphNodes));
2675
2784
  function showDropdownResults(results) {
2676
2785
  if (results.length === 0) {
2677
2786
  dropdown.classList.add("hidden");
@@ -2710,6 +2819,7 @@ function setupToolbar() {
2710
2819
  });
2711
2820
  }
2712
2821
  function updateDropdownResults() {
2822
+ const rankedSymbols = allSymbolNames.slice().sort((a, b2) => compareGraphNodeIds(a, b2, graphNodes));
2713
2823
  const results = buildGraphSearchResults({
2714
2824
  query: searchInput.value,
2715
2825
  symbolIds: rankedSymbols,
@@ -2743,6 +2853,9 @@ function setupToolbar() {
2743
2853
  cy.fit(40);
2744
2854
  }
2745
2855
  });
2856
+ refreshBtn.addEventListener("click", () => {
2857
+ void refreshGraphData({ refreshIndex: true, preserveVisible: true, showFeedback: true });
2858
+ });
2746
2859
  analyzeBtn.addEventListener("click", () => {
2747
2860
  void runStructuralAnalysis();
2748
2861
  });
@@ -2769,6 +2882,24 @@ function setupToolbar() {
2769
2882
  });
2770
2883
  }
2771
2884
 
2885
+ // src/model-utils.ts
2886
+ function getModelDisplayName(model) {
2887
+ return (model.name ?? model.id).trim();
2888
+ }
2889
+ function normalizeModelSearchValue(value) {
2890
+ return value.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
2891
+ }
2892
+ function filterModelsByQuery(models, query) {
2893
+ const tokens = normalizeModelSearchValue(query);
2894
+ if (tokens.length === 0) {
2895
+ return [...models];
2896
+ }
2897
+ return models.filter((model) => {
2898
+ const haystack = `${getModelDisplayName(model)} ${model.id}`.toLocaleLowerCase();
2899
+ return tokens.every((token) => haystack.includes(token));
2900
+ });
2901
+ }
2902
+
2772
2903
  // src/web/request-tracker.ts
2773
2904
  function createLatestRequestTracker() {
2774
2905
  let latestToken = 0;
@@ -2783,6 +2914,25 @@ function createLatestRequestTracker() {
2783
2914
  };
2784
2915
  }
2785
2916
 
2917
+ // src/web/setup-overlay-state.ts
2918
+ var DEFAULT_SETUP_INTRO = "minicode needs a model provider to run. Configure one of the following:";
2919
+ function deriveSetupOverlayState(input) {
2920
+ const missingItems = input.missing ?? [];
2921
+ const configuredProvider = input.configuredProvider ?? null;
2922
+ const isOnlyModelMissing = missingItems.length === 1 && typeof missingItems[0] === "string" && missingItems[0].includes("MODEL");
2923
+ const hasConfiguredProvider = isOnlyModelMissing && configuredProvider !== null;
2924
+ const filteredMissingItems = hasConfiguredProvider ? missingItems : missingItems.filter((item) => !item.includes("MODEL"));
2925
+ const introText = configuredProvider === "openrouter" && isOnlyModelMissing ? "OpenRouter is already configured. Select a model to continue:" : configuredProvider === "openai-compatible" && isOnlyModelMissing ? "An OpenAI-compatible provider is already configured. Select a model to continue:" : configuredProvider === "anthropic" && isOnlyModelMissing ? "Anthropic is already configured. Select a model to continue:" : DEFAULT_SETUP_INTRO;
2926
+ return {
2927
+ introText,
2928
+ hideQuickConnects: hasConfiguredProvider,
2929
+ hideOpenRouterSpotlight: configuredProvider === "openrouter" && isOnlyModelMissing,
2930
+ missingItems: filteredMissingItems,
2931
+ showModelSelectionHint: hasConfiguredProvider,
2932
+ modelSelectionNote: configuredProvider === "openrouter" && isOnlyModelMissing ? 'If you are on the OpenRouter free tier, search "free" in the model dropdown to find supported free models.' : null
2933
+ };
2934
+ }
2935
+
2786
2936
  // src/web/app.ts
2787
2937
  var messagesEl = document.getElementById("messages");
2788
2938
  var chatForm = document.getElementById("chat-form");
@@ -2793,6 +2943,7 @@ var statusBadge = document.getElementById("status-badge");
2793
2943
  var modelInfo = document.getElementById("model-info");
2794
2944
  var modelBtn = document.getElementById("model-btn");
2795
2945
  var modelDropdown = document.getElementById("model-dropdown");
2946
+ var modelSearchInput = document.getElementById("model-search");
2796
2947
  var modelList = document.getElementById("model-list");
2797
2948
  var sessionBtn = document.getElementById("session-btn");
2798
2949
  var sessionDropdown = document.getElementById("session-dropdown");
@@ -2813,17 +2964,36 @@ var openRouterConnectCloseBtn = document.getElementById("openrouter-connect-clos
2813
2964
  var openRouterConnectCancelBtn = document.getElementById("openrouter-connect-cancel");
2814
2965
  var openRouterConnectContinueBtn = document.getElementById("openrouter-connect-continue");
2815
2966
  var openRouterPersistCheckbox = document.getElementById("openrouter-persist-checkbox");
2967
+ var openAiCompatibleConnectModal = document.getElementById("openai-compatible-connect-modal");
2968
+ var openAiCompatibleConnectBackdrop = document.getElementById("openai-compatible-connect-backdrop");
2969
+ var openAiCompatibleConnectCloseBtn = document.getElementById("openai-compatible-connect-close");
2970
+ var openAiCompatibleConnectCancelBtn = document.getElementById("openai-compatible-connect-cancel");
2971
+ var openAiCompatibleConnectContinueBtn = document.getElementById("openai-compatible-connect-continue");
2972
+ var openAiCompatiblePresetSelect = document.getElementById("openai-compatible-preset");
2973
+ var openAiCompatiblePresetHelp = document.getElementById("openai-compatible-preset-help");
2974
+ var openAiCompatibleBaseUrlInput = document.getElementById("openai-compatible-base-url");
2975
+ var openAiCompatibleApiKeyInput = document.getElementById("openai-compatible-api-key");
2976
+ var openAiCompatiblePersistCheckbox = document.getElementById("openai-compatible-persist-checkbox");
2977
+ var openAiCompatibleConnectStatus = document.getElementById("openai-compatible-connect-status");
2816
2978
  var settingsPath = document.getElementById("settings-path");
2817
2979
  var settingsList = document.getElementById("settings-list");
2818
2980
  var settingsBanner = document.getElementById("settings-banner");
2819
2981
  var settingsOpenRouterSession = document.getElementById("settings-openrouter-session");
2820
2982
  var settingsOpenRouterSessionMeta = document.getElementById("settings-openrouter-session-meta");
2983
+ var settingsOpenAiCompatibleSession = document.getElementById("settings-openai-compatible-session");
2984
+ var settingsOpenAiCompatibleSessionMeta = document.getElementById("settings-openai-compatible-session-meta");
2821
2985
  var settingsSaveBtn = document.getElementById("settings-save");
2822
2986
  var settingsResetBtn = document.getElementById("settings-reset");
2823
2987
  var disconnectOpenRouterBtn = document.getElementById("disconnect-openrouter-btn");
2988
+ var disconnectOpenAiCompatibleBtn = document.getElementById("disconnect-openai-compatible-btn");
2824
2989
  var connectOpenRouterButtons = Array.from(
2825
2990
  document.querySelectorAll("[data-openrouter-connect]")
2826
2991
  );
2992
+ var connectOpenAiCompatibleButtons = Array.from(
2993
+ document.querySelectorAll("[data-openai-compatible-connect]")
2994
+ );
2995
+ var GRAPH_REFRESH_TOOL_NAMES = /* @__PURE__ */ new Set(["write_file", "edit_file", "run_command"]);
2996
+ var configOverlayQuickConnects = document.getElementById("config-overlay-quick-connects");
2827
2997
  var configOverlaySpotlight = document.getElementById("config-overlay-spotlight");
2828
2998
  var configOverlayIntro = document.getElementById("config-overlay-intro");
2829
2999
  var configConnectStatus = document.getElementById("config-connect-status");
@@ -2835,10 +3005,33 @@ var settingsPayload = null;
2835
3005
  var activeSavedSession = null;
2836
3006
  var activeBaseUrl = "";
2837
3007
  var sessionOpenRouterConnected = false;
3008
+ var sessionOpenAiCompatibleConnected = false;
2838
3009
  var sessionRefreshTracker = createLatestRequestTracker();
2839
3010
  var TOOL_RESULT_MAX = 500;
2840
3011
  var OPENROUTER_PKCE_VERIFIER_KEY = "minicode:openrouter:pkce-verifier";
2841
3012
  var OPENROUTER_PERSIST_TO_ENV_KEY = "minicode:openrouter:persist-to-env";
3013
+ var OPENAI_COMPATIBLE_PRESETS = {
3014
+ lmstudio: {
3015
+ baseUrl: "http://localhost:1234/v1",
3016
+ helpText: "LM Studio pre-fills the default local server endpoint at http://localhost:1234/v1.",
3017
+ apiKeyPlaceholder: "Leave blank for LM Studio unless local auth is enabled"
3018
+ },
3019
+ openai: {
3020
+ baseUrl: "https://api.openai.com/v1",
3021
+ helpText: "OpenAI uses https://api.openai.com/v1 and typically requires an API key.",
3022
+ apiKeyPlaceholder: "Enter your OpenAI API key"
3023
+ },
3024
+ ollama: {
3025
+ baseUrl: "http://localhost:11434/v1",
3026
+ helpText: "Ollama pre-fills the default local server endpoint at http://localhost:11434/v1.",
3027
+ apiKeyPlaceholder: "Leave blank for Ollama unless your proxy requires auth"
3028
+ },
3029
+ custom: {
3030
+ baseUrl: "",
3031
+ helpText: "Custom leaves the endpoint fully editable so you can point minicode at any OpenAI-compatible API.",
3032
+ apiKeyPlaceholder: "Add an API key only if this endpoint requires auth"
3033
+ }
3034
+ };
2842
3035
  function connect() {
2843
3036
  const protocol = location.protocol === "https:" ? "wss:" : "ws:";
2844
3037
  ws = new WebSocket(`${protocol}//${location.host}`);
@@ -2886,6 +3079,51 @@ function clearConfigConnectStatus() {
2886
3079
  configConnectStatus.textContent = "";
2887
3080
  configConnectStatus.className = "config-connect-status hidden";
2888
3081
  }
3082
+ function setOpenAiCompatibleConnectStatus(message, tone) {
3083
+ openAiCompatibleConnectStatus.textContent = message;
3084
+ openAiCompatibleConnectStatus.className = `config-connect-status ${tone}`;
3085
+ }
3086
+ function clearOpenAiCompatibleConnectStatus() {
3087
+ openAiCompatibleConnectStatus.textContent = "";
3088
+ openAiCompatibleConnectStatus.className = "config-connect-status hidden";
3089
+ }
3090
+ function normalizeBaseUrl(value) {
3091
+ return value.trim().replace(/\/+$/, "");
3092
+ }
3093
+ function inferOpenAiCompatiblePreset(baseUrl) {
3094
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl).toLowerCase();
3095
+ if (normalizedBaseUrl === OPENAI_COMPATIBLE_PRESETS.lmstudio.baseUrl) {
3096
+ return "lmstudio";
3097
+ }
3098
+ if (normalizedBaseUrl === OPENAI_COMPATIBLE_PRESETS.openai.baseUrl) {
3099
+ return "openai";
3100
+ }
3101
+ if (normalizedBaseUrl === OPENAI_COMPATIBLE_PRESETS.ollama.baseUrl) {
3102
+ return "ollama";
3103
+ }
3104
+ return "custom";
3105
+ }
3106
+ function applyOpenAiCompatiblePreset(preset, options = {}) {
3107
+ const presetConfig = OPENAI_COMPATIBLE_PRESETS[preset];
3108
+ openAiCompatiblePresetHelp.textContent = presetConfig.helpText;
3109
+ openAiCompatibleApiKeyInput.placeholder = presetConfig.apiKeyPlaceholder;
3110
+ if (preset === "custom" && options.preserveCustomValue) {
3111
+ return;
3112
+ }
3113
+ openAiCompatibleBaseUrlInput.value = presetConfig.baseUrl;
3114
+ }
3115
+ function isModalOpen(modal) {
3116
+ return !modal.classList.contains("hidden");
3117
+ }
3118
+ function isSettingsModalOpen() {
3119
+ return isModalOpen(settingsModal);
3120
+ }
3121
+ function isOpenRouterConnectModalOpen() {
3122
+ return isModalOpen(openRouterConnectModal);
3123
+ }
3124
+ function isOpenAiCompatibleConnectModalOpen() {
3125
+ return isModalOpen(openAiCompatibleConnectModal);
3126
+ }
2889
3127
  function encodeBase64Url(bytes) {
2890
3128
  let binary = "";
2891
3129
  for (const byte of bytes) {
@@ -2958,6 +3196,7 @@ async function maybeHandleOpenRouterCallback() {
2958
3196
  if (onlyModelMissing) {
2959
3197
  modelDropdown.classList.remove("hidden");
2960
3198
  sessionDropdown.classList.add("hidden");
3199
+ focusModelSearchInput();
2961
3200
  }
2962
3201
  } catch (error) {
2963
3202
  const message = error instanceof Error ? error.message : "Failed to connect OpenRouter";
@@ -2993,6 +3232,76 @@ async function disconnectOpenRouter() {
2993
3232
  disconnectOpenRouterBtn.disabled = false;
2994
3233
  }
2995
3234
  }
3235
+ async function connectOpenAiCompatible() {
3236
+ const baseUrl = normalizeBaseUrl(openAiCompatibleBaseUrlInput.value);
3237
+ const apiKey = openAiCompatibleApiKeyInput.value.trim();
3238
+ if (!baseUrl) {
3239
+ setOpenAiCompatibleConnectStatus("Endpoint is required.", "error");
3240
+ return;
3241
+ }
3242
+ clearOpenAiCompatibleConnectStatus();
3243
+ clearSettingsBanner();
3244
+ openAiCompatibleConnectContinueBtn.disabled = true;
3245
+ try {
3246
+ const res = await fetch("/api/openai-compatible/connect", {
3247
+ method: "POST",
3248
+ headers: { "Content-Type": "application/json" },
3249
+ body: JSON.stringify({
3250
+ baseUrl,
3251
+ apiKey,
3252
+ persistToEnv: openAiCompatiblePersistCheckbox.checked
3253
+ })
3254
+ });
3255
+ const body = await res.json();
3256
+ if (!res.ok) {
3257
+ throw new Error("error" in body ? body.error : `Failed to connect OpenAI-compatible provider (${res.status})`);
3258
+ }
3259
+ activeBaseUrl = body.baseUrl;
3260
+ addMessage(body.message, "thinking");
3261
+ const tone = body.persistWarning ? "info" : body.needsSetup ? "info" : "success";
3262
+ setConfigConnectStatus(body.message, tone);
3263
+ setSettingsBanner(body.message, tone === "success" ? "success" : "info");
3264
+ closeOpenAiCompatibleConnectModal();
3265
+ await fetchStatus();
3266
+ await refreshModelList();
3267
+ const onlyModelMissing = body.needsSetup && body.missing.length === 1 && body.missing[0]?.includes("MODEL");
3268
+ if (onlyModelMissing) {
3269
+ modelDropdown.classList.remove("hidden");
3270
+ sessionDropdown.classList.add("hidden");
3271
+ focusModelSearchInput();
3272
+ }
3273
+ } catch (error) {
3274
+ const message = error instanceof Error ? error.message : "Failed to connect OpenAI-compatible provider";
3275
+ setOpenAiCompatibleConnectStatus(message, "error");
3276
+ } finally {
3277
+ openAiCompatibleConnectContinueBtn.disabled = false;
3278
+ }
3279
+ }
3280
+ async function disconnectOpenAiCompatible() {
3281
+ disconnectOpenAiCompatibleBtn.disabled = true;
3282
+ clearSettingsBanner();
3283
+ try {
3284
+ const res = await fetch("/api/openai-compatible/disconnect", {
3285
+ method: "POST",
3286
+ headers: { "Content-Type": "application/json" }
3287
+ });
3288
+ const body = await res.json();
3289
+ if (!res.ok) {
3290
+ throw new Error("error" in body ? body.error : `Failed to disconnect OpenAI-compatible provider (${res.status})`);
3291
+ }
3292
+ activeBaseUrl = body.baseUrl;
3293
+ addMessage(body.message, "thinking");
3294
+ setSettingsBanner(body.message, body.disconnected ? "success" : "info");
3295
+ clearConfigConnectStatus();
3296
+ await fetchStatus();
3297
+ await refreshModelList();
3298
+ } catch (error) {
3299
+ const message = error instanceof Error ? error.message : "Failed to disconnect OpenAI-compatible provider";
3300
+ setSettingsBanner(message, "error");
3301
+ } finally {
3302
+ disconnectOpenAiCompatibleBtn.disabled = false;
3303
+ }
3304
+ }
2996
3305
  async function fetchStatus() {
2997
3306
  try {
2998
3307
  const res = await fetch("/api/status");
@@ -3002,22 +3311,32 @@ async function fetchStatus() {
3002
3311
  activeModel = data.model;
3003
3312
  activeBaseUrl = data.baseUrl ?? "";
3004
3313
  sessionOpenRouterConnected = data.sessionOpenRouterConnected ?? false;
3005
- renderOpenRouterSessionControls();
3314
+ sessionOpenAiCompatibleConnected = data.sessionOpenAiCompatibleConnected ?? false;
3315
+ renderSessionProviderControls();
3006
3316
  if (data.needsSetup) {
3007
3317
  configOverlay.classList.remove("hidden");
3008
3318
  chatInput.disabled = true;
3009
3319
  sendBtn.disabled = true;
3010
3320
  const missingEl = document.getElementById("config-missing");
3011
- if (missingEl && data.missing && data.missing.length > 0) {
3012
- const isOnlyModelMissing = data.missing.length === 1 && data.missing[0].includes("MODEL");
3013
- const hasPersistedOpenRouter = isOnlyModelMissing && (data.baseUrl ?? "").includes("openrouter");
3014
- if (configOverlayIntro) {
3015
- configOverlayIntro.textContent = hasPersistedOpenRouter ? "OpenRouter is already configured. Select a model to continue:" : "minicode needs a model provider to run. Configure one of the following:";
3321
+ const overlayState = deriveSetupOverlayState({
3322
+ configuredProvider: data.configuredProvider ?? null,
3323
+ missing: data.missing ?? []
3324
+ });
3325
+ if (configOverlayIntro) {
3326
+ configOverlayIntro.textContent = overlayState.introText;
3327
+ }
3328
+ configOverlayQuickConnects?.classList.toggle("hidden", overlayState.hideQuickConnects);
3329
+ configOverlaySpotlight?.classList.toggle("hidden", overlayState.hideOpenRouterSpotlight);
3330
+ if (missingEl) {
3331
+ if (overlayState.missingItems.length > 0) {
3332
+ const hint = overlayState.showModelSelectionHint ? ` \u2014 select one from the <strong>model dropdown</strong> above, or set it in config` : "";
3333
+ const note = overlayState.modelSelectionNote ? ` ${escapeHtml(overlayState.modelSelectionNote)}` : "";
3334
+ missingEl.innerHTML = `<strong>Missing:</strong> ${overlayState.missingItems.map(escapeHtml).join(", ")}${hint}${note}`;
3335
+ missingEl.classList.remove("hidden");
3336
+ } else {
3337
+ missingEl.classList.add("hidden");
3338
+ missingEl.innerHTML = "";
3016
3339
  }
3017
- configOverlaySpotlight?.classList.toggle("hidden", hasPersistedOpenRouter);
3018
- const hint = isOnlyModelMissing ? ` \u2014 select one from the <strong>model dropdown</strong> above, or set it in config` : "";
3019
- missingEl.innerHTML = `<strong>Missing:</strong> ${data.missing.map(escapeHtml).join(", ")}${hint}`;
3020
- missingEl.classList.remove("hidden");
3021
3340
  }
3022
3341
  } else {
3023
3342
  configOverlay.classList.add("hidden");
@@ -3029,8 +3348,9 @@ async function fetchStatus() {
3029
3348
  missingEl.innerHTML = "";
3030
3349
  }
3031
3350
  if (configOverlayIntro) {
3032
- configOverlayIntro.textContent = "minicode needs a model provider to run. Configure one of the following:";
3351
+ configOverlayIntro.textContent = DEFAULT_SETUP_INTRO;
3033
3352
  }
3353
+ configOverlayQuickConnects?.classList.remove("hidden");
3034
3354
  configOverlaySpotlight?.classList.remove("hidden");
3035
3355
  }
3036
3356
  } catch {
@@ -3079,6 +3399,9 @@ function handleServerMessage(msg) {
3079
3399
  break;
3080
3400
  case "tool_call_end":
3081
3401
  finalizeToolCall(msg.name || "", msg.result || "", msg.elapsedMs || 0);
3402
+ if (GRAPH_REFRESH_TOOL_NAMES.has(msg.name || "")) {
3403
+ scheduleGraphDataRefresh();
3404
+ }
3082
3405
  break;
3083
3406
  case "turn_end":
3084
3407
  if (hadToolCalls && msg.text) {
@@ -3264,8 +3587,12 @@ function addUsageInfo(usage) {
3264
3587
  function scrollToBottom() {
3265
3588
  messagesEl.scrollTop = messagesEl.scrollHeight;
3266
3589
  }
3267
- function closeHeaderMenus() {
3590
+ function closeModelDropdown() {
3268
3591
  modelDropdown.classList.add("hidden");
3592
+ modelSearchInput.value = "";
3593
+ }
3594
+ function closeHeaderMenus() {
3595
+ closeModelDropdown();
3269
3596
  sessionDropdown.classList.add("hidden");
3270
3597
  }
3271
3598
  function formatSettingsValue(value) {
@@ -3279,16 +3606,31 @@ function clearSettingsBanner() {
3279
3606
  settingsBanner.textContent = "";
3280
3607
  settingsBanner.className = "settings-banner hidden";
3281
3608
  }
3282
- function renderOpenRouterSessionControls() {
3609
+ function renderSessionProviderControls() {
3283
3610
  if (sessionOpenRouterConnected) {
3284
3611
  settingsOpenRouterSession.classList.remove("hidden");
3285
3612
  settingsOpenRouterSessionMeta.textContent = activeBaseUrl ? `Endpoint: ${activeBaseUrl}. This session-only connection overrides your original provider settings until you disconnect or restart serve.` : "This session-only connection overrides your original provider settings until you disconnect or restart serve.";
3613
+ settingsOpenAiCompatibleSession.classList.add("hidden");
3614
+ settingsOpenAiCompatibleSessionMeta.textContent = "";
3615
+ disconnectOpenRouterBtn.disabled = false;
3616
+ disconnectOpenAiCompatibleBtn.disabled = false;
3617
+ return;
3618
+ }
3619
+ if (sessionOpenAiCompatibleConnected) {
3620
+ settingsOpenAiCompatibleSession.classList.remove("hidden");
3621
+ settingsOpenAiCompatibleSessionMeta.textContent = activeBaseUrl ? `Endpoint: ${activeBaseUrl}. This session-only connection overrides your original provider settings until you disconnect or restart serve.` : "This session-only connection overrides your original provider settings until you disconnect or restart serve.";
3622
+ settingsOpenRouterSession.classList.add("hidden");
3623
+ settingsOpenRouterSessionMeta.textContent = "";
3286
3624
  disconnectOpenRouterBtn.disabled = false;
3625
+ disconnectOpenAiCompatibleBtn.disabled = false;
3287
3626
  return;
3288
3627
  }
3289
3628
  settingsOpenRouterSession.classList.add("hidden");
3290
3629
  settingsOpenRouterSessionMeta.textContent = "";
3630
+ settingsOpenAiCompatibleSession.classList.add("hidden");
3631
+ settingsOpenAiCompatibleSessionMeta.textContent = "";
3291
3632
  disconnectOpenRouterBtn.disabled = false;
3633
+ disconnectOpenAiCompatibleBtn.disabled = false;
3292
3634
  }
3293
3635
  function createSettingsControl(entry, inputId) {
3294
3636
  const value = entry.persistedValue;
@@ -3499,6 +3841,27 @@ function closeOpenRouterConnectModal() {
3499
3841
  closeModal(openRouterConnectModal);
3500
3842
  openRouterConnectContinueBtn.disabled = false;
3501
3843
  }
3844
+ function openOpenAiCompatibleConnectModal() {
3845
+ closeHeaderMenus();
3846
+ openModal(openAiCompatibleConnectModal);
3847
+ const preset = inferOpenAiCompatiblePreset(activeBaseUrl);
3848
+ openAiCompatiblePresetSelect.value = preset;
3849
+ openAiCompatibleBaseUrlInput.value = preset === "custom" ? activeBaseUrl : OPENAI_COMPATIBLE_PRESETS[preset].baseUrl;
3850
+ openAiCompatibleApiKeyInput.value = "";
3851
+ openAiCompatiblePersistCheckbox.checked = false;
3852
+ openAiCompatibleConnectContinueBtn.disabled = false;
3853
+ applyOpenAiCompatiblePreset(preset, { preserveCustomValue: preset === "custom" });
3854
+ clearOpenAiCompatibleConnectStatus();
3855
+ requestAnimationFrame(() => {
3856
+ openAiCompatibleBaseUrlInput.focus();
3857
+ openAiCompatibleBaseUrlInput.select();
3858
+ });
3859
+ }
3860
+ function closeOpenAiCompatibleConnectModal() {
3861
+ closeModal(openAiCompatibleConnectModal);
3862
+ openAiCompatibleConnectContinueBtn.disabled = false;
3863
+ clearOpenAiCompatibleConnectStatus();
3864
+ }
3502
3865
  chatForm.addEventListener("submit", (e) => {
3503
3866
  e.preventDefault();
3504
3867
  const message = chatInput.value.trim();
@@ -3522,30 +3885,99 @@ chatInput.addEventListener("keydown", (e) => {
3522
3885
  }
3523
3886
  });
3524
3887
  var activeModel = "";
3888
+ var availableModels = [];
3889
+ function focusModelSearchInput() {
3890
+ requestAnimationFrame(() => {
3891
+ modelSearchInput.focus();
3892
+ modelSearchInput.select();
3893
+ });
3894
+ }
3895
+ function renderModelList() {
3896
+ const filteredModels = filterModelsByQuery(availableModels, modelSearchInput.value);
3897
+ if (availableModels.length === 0) {
3898
+ modelList.innerHTML = '<div class="dropdown-empty">No models available</div>';
3899
+ return;
3900
+ }
3901
+ if (filteredModels.length === 0) {
3902
+ modelList.innerHTML = '<div class="dropdown-empty">No matching models</div>';
3903
+ return;
3904
+ }
3905
+ modelList.innerHTML = "";
3906
+ for (const model of filteredModels) {
3907
+ const el = document.createElement("button");
3908
+ el.type = "button";
3909
+ el.className = "model-item" + (model.id === activeModel ? " active" : "");
3910
+ el.title = model.id;
3911
+ const body = document.createElement("div");
3912
+ body.className = "model-item-body";
3913
+ const name = document.createElement("span");
3914
+ name.className = "model-item-name";
3915
+ name.textContent = getModelDisplayName(model);
3916
+ body.appendChild(name);
3917
+ if ((model.name ?? "").trim() && getModelDisplayName(model) !== model.id) {
3918
+ const subtitle = document.createElement("span");
3919
+ subtitle.className = "model-item-subtitle";
3920
+ subtitle.textContent = model.id;
3921
+ body.appendChild(subtitle);
3922
+ }
3923
+ el.appendChild(body);
3924
+ if (model.id === activeModel) {
3925
+ const badge = document.createElement("span");
3926
+ badge.className = "model-item-badge";
3927
+ badge.textContent = "Active";
3928
+ el.appendChild(badge);
3929
+ }
3930
+ el.addEventListener("click", () => {
3931
+ void switchModel(model.id);
3932
+ });
3933
+ modelList.appendChild(el);
3934
+ }
3935
+ }
3525
3936
  modelBtn.addEventListener("click", (e) => {
3526
3937
  e.stopPropagation();
3527
3938
  const isOpen = !modelDropdown.classList.contains("hidden");
3528
- modelDropdown.classList.toggle("hidden");
3529
- sessionDropdown.classList.add("hidden");
3530
- if (!isOpen) {
3531
- refreshModelList();
3939
+ if (isOpen) {
3940
+ closeModelDropdown();
3941
+ return;
3532
3942
  }
3943
+ closeHeaderMenus();
3944
+ modelDropdown.classList.remove("hidden");
3945
+ modelSearchInput.value = "";
3946
+ void refreshModelList({ focusSearch: true });
3533
3947
  });
3534
3948
  document.addEventListener("click", (e) => {
3535
3949
  if (!modelDropdown.contains(e.target) && e.target !== modelBtn) {
3536
- modelDropdown.classList.add("hidden");
3950
+ closeModelDropdown();
3951
+ }
3952
+ });
3953
+ modelSearchInput.addEventListener("input", () => {
3954
+ renderModelList();
3955
+ });
3956
+ modelSearchInput.addEventListener("keydown", (e) => {
3957
+ if (e.key === "Escape") {
3958
+ if (modelSearchInput.value) {
3959
+ modelSearchInput.value = "";
3960
+ renderModelList();
3961
+ return;
3962
+ }
3963
+ closeModelDropdown();
3537
3964
  }
3538
3965
  });
3539
- async function refreshModelList() {
3966
+ modelSearchInput.addEventListener("click", (e) => {
3967
+ e.stopPropagation();
3968
+ });
3969
+ async function refreshModelList(options = {}) {
3540
3970
  try {
3541
3971
  const res = await fetch("/api/models");
3542
3972
  const data = await res.json();
3543
3973
  activeModel = data.activeModel;
3974
+ availableModels = data.models ?? [];
3544
3975
  const hasActiveModel = !!activeModel && data.models.some((model) => model.id === activeModel);
3545
3976
  if (!data.models || data.models.length === 0) {
3546
3977
  modelInfo.textContent = "Select model";
3547
3978
  modelInfo.classList.add("placeholder");
3548
- modelList.innerHTML = '<div class="dropdown-empty">No models available</div>';
3979
+ availableModels = [];
3980
+ renderModelList();
3549
3981
  return;
3550
3982
  }
3551
3983
  if (hasActiveModel) {
@@ -3555,18 +3987,12 @@ async function refreshModelList() {
3555
3987
  modelInfo.textContent = "Select model";
3556
3988
  modelInfo.classList.add("placeholder");
3557
3989
  }
3558
- modelList.innerHTML = "";
3559
- for (const m2 of data.models) {
3560
- const el = document.createElement("div");
3561
- el.className = "model-item" + (m2.id === activeModel ? " active" : "");
3562
- el.textContent = m2.name ?? m2.id;
3563
- el.title = m2.id;
3564
- el.addEventListener("click", () => {
3565
- void switchModel(m2.id);
3566
- });
3567
- modelList.appendChild(el);
3990
+ renderModelList();
3991
+ if (options.focusSearch && !modelDropdown.classList.contains("hidden")) {
3992
+ focusModelSearchInput();
3568
3993
  }
3569
3994
  } catch {
3995
+ availableModels = [];
3570
3996
  modelList.innerHTML = '<div class="dropdown-empty">Failed to load models</div>';
3571
3997
  }
3572
3998
  }
@@ -3587,7 +4013,8 @@ async function switchModel(modelId) {
3587
4013
  modelInfo.textContent = modelId || "Select model";
3588
4014
  modelInfo.classList.toggle("placeholder", !modelId);
3589
4015
  activeModel = modelId;
3590
- modelDropdown.classList.add("hidden");
4016
+ renderModelList();
4017
+ closeModelDropdown();
3591
4018
  if (body.persistedToEnv) {
3592
4019
  addMessage(`Model switched to: ${modelId}. Saved as the default in ~/.minicode/.env.`, "thinking");
3593
4020
  } else {
@@ -3603,7 +4030,7 @@ sessionBtn.addEventListener("click", (e) => {
3603
4030
  e.stopPropagation();
3604
4031
  const isOpen = !sessionDropdown.classList.contains("hidden");
3605
4032
  sessionDropdown.classList.toggle("hidden");
3606
- modelDropdown.classList.add("hidden");
4033
+ closeModelDropdown();
3607
4034
  if (!isOpen) {
3608
4035
  refreshSessionList();
3609
4036
  }
@@ -3624,16 +4051,20 @@ saveBtn.addEventListener("click", async () => {
3624
4051
  headers: { "Content-Type": "application/json" },
3625
4052
  body: JSON.stringify({ label })
3626
4053
  });
3627
- if (res.ok) {
3628
- const data = await res.json();
3629
- saveLabelInput.value = "";
3630
- addMessage(
3631
- `${isUpdatingCurrentSession ? "Session updated" : "Session saved"}: "${data.label}"`,
3632
- "thinking"
3633
- );
3634
- await refreshSessionList();
4054
+ const body = await res.json();
4055
+ if (!res.ok) {
4056
+ throw new Error("error" in body ? body.error : `Failed to save session (${res.status})`);
3635
4057
  }
3636
- } catch {
4058
+ const data = body;
4059
+ saveLabelInput.value = "";
4060
+ addMessage(
4061
+ `${isUpdatingCurrentSession ? "Session updated" : "Session saved"}: "${data.label}"`,
4062
+ "thinking"
4063
+ );
4064
+ await refreshSessionList();
4065
+ } catch (error) {
4066
+ const message = error instanceof Error ? error.message : "Failed to save session";
4067
+ addMessage(message, "error");
3637
4068
  } finally {
3638
4069
  saveBtn.removeAttribute("disabled");
3639
4070
  }
@@ -3715,12 +4146,16 @@ sessionUpdateBtn.addEventListener("click", async () => {
3715
4146
  headers: { "Content-Type": "application/json" },
3716
4147
  body: JSON.stringify({ label: activeSavedSession.label })
3717
4148
  });
3718
- if (res.ok) {
3719
- const data = await res.json();
3720
- addMessage(`Session updated: "${data.label}"`, "thinking");
3721
- await refreshSessionList();
4149
+ const body = await res.json();
4150
+ if (!res.ok) {
4151
+ throw new Error("error" in body ? body.error : `Failed to update session (${res.status})`);
3722
4152
  }
3723
- } catch {
4153
+ const data = body;
4154
+ addMessage(`Session updated: "${data.label}"`, "thinking");
4155
+ await refreshSessionList();
4156
+ } catch (error) {
4157
+ const message = error instanceof Error ? error.message : "Failed to update session";
4158
+ addMessage(message, "error");
3724
4159
  } finally {
3725
4160
  sessionUpdateBtn.disabled = false;
3726
4161
  }
@@ -3743,10 +4178,19 @@ openRouterConnectCloseBtn.addEventListener("click", () => {
3743
4178
  openRouterConnectCancelBtn.addEventListener("click", () => {
3744
4179
  closeOpenRouterConnectModal();
3745
4180
  });
4181
+ openAiCompatibleConnectBackdrop.addEventListener("click", () => {
4182
+ closeOpenAiCompatibleConnectModal();
4183
+ });
4184
+ openAiCompatibleConnectCloseBtn.addEventListener("click", () => {
4185
+ closeOpenAiCompatibleConnectModal();
4186
+ });
4187
+ openAiCompatibleConnectCancelBtn.addEventListener("click", () => {
4188
+ closeOpenAiCompatibleConnectModal();
4189
+ });
3746
4190
  settingsResetBtn.addEventListener("click", () => {
3747
4191
  clearSettingsBanner();
3748
4192
  renderSettings();
3749
- renderOpenRouterSessionControls();
4193
+ renderSessionProviderControls();
3750
4194
  });
3751
4195
  settingsSaveBtn.addEventListener("click", async () => {
3752
4196
  if (!settingsPayload) {
@@ -3798,6 +4242,9 @@ settingsList.addEventListener("change", () => {
3798
4242
  disconnectOpenRouterBtn.addEventListener("click", () => {
3799
4243
  void disconnectOpenRouter();
3800
4244
  });
4245
+ disconnectOpenAiCompatibleBtn.addEventListener("click", () => {
4246
+ void disconnectOpenAiCompatible();
4247
+ });
3801
4248
  document.addEventListener("keydown", (event) => {
3802
4249
  if (event.key !== "Escape") {
3803
4250
  return;
@@ -3806,6 +4253,10 @@ document.addEventListener("keydown", (event) => {
3806
4253
  closeOpenRouterConnectModal();
3807
4254
  return;
3808
4255
  }
4256
+ if (isOpenAiCompatibleConnectModalOpen()) {
4257
+ closeOpenAiCompatibleConnectModal();
4258
+ return;
4259
+ }
3809
4260
  if (isSettingsModalOpen()) {
3810
4261
  closeSettings();
3811
4262
  }
@@ -3815,11 +4266,29 @@ for (const button of connectOpenRouterButtons) {
3815
4266
  openOpenRouterConnectModal();
3816
4267
  });
3817
4268
  }
4269
+ for (const button of connectOpenAiCompatibleButtons) {
4270
+ button.addEventListener("click", () => {
4271
+ openOpenAiCompatibleConnectModal();
4272
+ });
4273
+ }
4274
+ openAiCompatiblePresetSelect.addEventListener("change", () => {
4275
+ applyOpenAiCompatiblePreset(openAiCompatiblePresetSelect.value);
4276
+ clearOpenAiCompatibleConnectStatus();
4277
+ });
4278
+ openAiCompatibleBaseUrlInput.addEventListener("input", () => {
4279
+ clearOpenAiCompatibleConnectStatus();
4280
+ });
4281
+ openAiCompatibleApiKeyInput.addEventListener("input", () => {
4282
+ clearOpenAiCompatibleConnectStatus();
4283
+ });
3818
4284
  openRouterConnectContinueBtn.addEventListener("click", () => {
3819
4285
  openRouterConnectContinueBtn.disabled = true;
3820
4286
  closeOpenRouterConnectModal();
3821
4287
  void startOpenRouterConnect(openRouterPersistCheckbox.checked);
3822
4288
  });
4289
+ openAiCompatibleConnectContinueBtn.addEventListener("click", () => {
4290
+ void connectOpenAiCompatible();
4291
+ });
3823
4292
  var chatPane = document.getElementById("chat-pane");
3824
4293
  var divider = document.getElementById("pane-divider");
3825
4294
  divider.addEventListener("mousedown", (e) => {