@sean.holung/minicode 0.3.6 → 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 (50) hide show
  1. package/README.md +2 -1
  2. package/dist/scripts/run-benchmarks.js +1 -0
  3. package/dist/src/agent/config.js +27 -0
  4. package/dist/src/agent/editable-config.js +6 -0
  5. package/dist/src/model-utils.js +18 -1
  6. package/dist/src/serve/agent-bridge.js +85 -14
  7. package/dist/src/serve/mcp-server.js +19 -13
  8. package/dist/src/serve/server.js +166 -3
  9. package/dist/src/session/session-store.js +18 -0
  10. package/dist/src/shared/symbol-search.js +156 -0
  11. package/dist/src/tools/search-code-map.js +27 -35
  12. package/dist/src/web/app.js +662 -113
  13. package/dist/src/web/index.html +128 -8
  14. package/dist/src/web/style.css +189 -7
  15. package/dist/tests/agent.test.js +16 -0
  16. package/dist/tests/config-api.test.js +5 -0
  17. package/dist/tests/config-integration.test.js +91 -1
  18. package/dist/tests/config.test.js +9 -0
  19. package/dist/tests/file-tools.test.js +12 -0
  20. package/dist/tests/graph-onboarding.test.js +20 -0
  21. package/dist/tests/mcp-and-plugin.test.js +3 -0
  22. package/dist/tests/model-client-openai.test.js +41 -0
  23. package/dist/tests/model-dropdown-ui.test.js +23 -0
  24. package/dist/tests/model-utils.test.js +26 -1
  25. package/dist/tests/search-code-map.test.js +9 -0
  26. package/dist/tests/serve.integration.test.js +189 -0
  27. package/dist/tests/session-store.test.js +15 -1
  28. package/dist/tests/settings-ui.test.js +11 -0
  29. package/dist/tests/setup-overlay-state.test.js +49 -0
  30. package/dist/tests/system-prompt.test.js +1 -0
  31. package/dist/tests/test-utils.js +1 -0
  32. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  33. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +10 -1
  34. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  35. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +1 -0
  36. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
  37. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +8 -1
  38. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  39. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +164 -27
  40. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  41. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  42. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +60 -6
  43. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  44. package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js +87 -0
  45. package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js.map +1 -1
  46. package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.d.ts.map +1 -1
  47. package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js +1 -0
  48. package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js.map +1 -1
  49. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +1 -1
@@ -2,6 +2,22 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
+ // src/web/modal-state.ts
6
+ function syncBodyModalOpenState() {
7
+ const anyModalOpen = [...document.querySelectorAll(".modal")].some((modal) => !modal.classList.contains("hidden"));
8
+ document.body.classList.toggle("modal-open", anyModalOpen);
9
+ }
10
+ function openModal(modal) {
11
+ modal.classList.remove("hidden");
12
+ modal.setAttribute("aria-hidden", "false");
13
+ syncBodyModalOpenState();
14
+ }
15
+ function closeModal(modal) {
16
+ modal.classList.add("hidden");
17
+ modal.setAttribute("aria-hidden", "true");
18
+ syncBodyModalOpenState();
19
+ }
20
+
5
21
  // node_modules/marked/lib/marked.esm.js
6
22
  function M() {
7
23
  return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
@@ -1660,6 +1676,9 @@ var analysisReport = null;
1660
1676
  var activeAnalysisFindingId = null;
1661
1677
  var activeAnalysisFilter = "all";
1662
1678
  var analysisExplanationCache = /* @__PURE__ */ new Map();
1679
+ var filePreviewModalInitialized = false;
1680
+ var latestFilePreviewRequestId = 0;
1681
+ var pendingGraphRefreshTimer = null;
1663
1682
  var LAYOUT_OPTIONS = {
1664
1683
  name: "cose",
1665
1684
  nodeRepulsion: function() {
@@ -1685,39 +1704,9 @@ async function initGraph() {
1685
1704
  initialized = true;
1686
1705
  const cyEl = document.getElementById("cy");
1687
1706
  const detailEl = document.getElementById("symbol-detail");
1707
+ setupFilePreviewModal();
1708
+ cyEl.innerHTML = "";
1688
1709
  try {
1689
- const [graphRes, symbolsRes, focusRes] = await Promise.all([
1690
- fetch("/api/graph"),
1691
- fetch("/api/symbols"),
1692
- fetch("/api/focus")
1693
- ]);
1694
- if (!graphRes.ok || !symbolsRes.ok) {
1695
- cyEl.innerHTML = '<div class="graph-empty">No index available. Run minicode with a project to generate the code graph.</div>';
1696
- return;
1697
- }
1698
- const graphData = await graphRes.json();
1699
- const focusData = focusRes.ok ? await focusRes.json() : { pinned: [] };
1700
- if (!graphData.nodes || graphData.nodes.length === 0) {
1701
- cyEl.innerHTML = '<div class="graph-empty">No index available. Run minicode with a project to generate the code graph.</div>';
1702
- return;
1703
- }
1704
- for (const node of graphData.nodes) {
1705
- const id = node.qualifiedName || node.id || node.name || "";
1706
- graphNodes.set(id, node);
1707
- }
1708
- graphEdges = (graphData.edges || []).map((e) => ({
1709
- source: e.source || e.from || "",
1710
- target: e.target || e.to || "",
1711
- kind: (e.kind || e.type || "references").toLowerCase()
1712
- }));
1713
- edgeIndex = buildGraphEdgeIndex(graphEdges);
1714
- fileToSymbolIds = buildGraphFileIndex(graphNodes);
1715
- allSymbolNames = Array.from(graphNodes.keys()).sort();
1716
- const pinned = focusData.pinned || [];
1717
- for (const f of pinned) {
1718
- const name = typeof f === "string" ? f : f.name || f.qualifiedName;
1719
- if (name) pinnedNames.add(name);
1720
- }
1721
1710
  cy = cytoscape({
1722
1711
  container: cyEl,
1723
1712
  elements: [],
@@ -1727,20 +1716,64 @@ async function initGraph() {
1727
1716
  });
1728
1717
  setupInteractions(cy, detailEl);
1729
1718
  setupToolbar();
1730
- if (pinnedNames.size > 0) {
1731
- for (const name of pinnedNames) {
1732
- addNodeNeighborhood(name, 1);
1733
- }
1734
- runLayout();
1735
- } else {
1736
- 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;
1737
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
+ );
1738
1727
  } catch (err) {
1739
1728
  console.error("Graph init failed:", err);
1740
1729
  const msg = err instanceof Error ? err.message : String(err);
1741
1730
  cyEl.innerHTML = `<div class="graph-empty">Failed to load graph: ${msg}</div>`;
1742
1731
  }
1743
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
+ }
1744
1777
  function highlightAgentActivity(symbolName) {
1745
1778
  if (!cy) return;
1746
1779
  void focusResolvedSymbolsInGraph(symbolName, {
@@ -1754,11 +1787,106 @@ function highlightAgentActivity(symbolName) {
1754
1787
  function resizeGraph() {
1755
1788
  if (cy) cy.resize();
1756
1789
  }
1757
- function showOnboardingHint(container) {
1758
- 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
+ }
1759
1887
  const hint = document.createElement("div");
1760
1888
  hint.className = "graph-onboarding";
1761
- 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>`;
1762
1890
  container.appendChild(hint);
1763
1891
  }
1764
1892
  function removeOnboardingHint() {
@@ -1823,6 +1951,78 @@ function focusFileInGraph(filePath) {
1823
1951
  refreshAnalysisGraphState();
1824
1952
  runLayout();
1825
1953
  }
1954
+ function getFilePreviewLanguage(filePath) {
1955
+ const ext = filePath.split(".").pop()?.toLowerCase() || "";
1956
+ const langMap = {
1957
+ ts: "typescript",
1958
+ tsx: "typescript",
1959
+ js: "javascript",
1960
+ jsx: "javascript",
1961
+ json: "json",
1962
+ md: "markdown",
1963
+ css: "css",
1964
+ html: "xml"
1965
+ };
1966
+ return langMap[ext] || "plaintext";
1967
+ }
1968
+ function closeFilePreview() {
1969
+ const modal = document.getElementById("file-preview-modal");
1970
+ if (!modal) return;
1971
+ closeModal(modal);
1972
+ }
1973
+ function setupFilePreviewModal() {
1974
+ if (filePreviewModalInitialized) return;
1975
+ filePreviewModalInitialized = true;
1976
+ const modal = document.getElementById("file-preview-modal");
1977
+ const backdrop = document.getElementById("file-preview-backdrop");
1978
+ const closeBtn = document.getElementById("file-preview-close");
1979
+ if (!modal || !backdrop || !closeBtn) {
1980
+ return;
1981
+ }
1982
+ backdrop.addEventListener("click", () => closeFilePreview());
1983
+ closeBtn.addEventListener("click", () => closeFilePreview());
1984
+ document.addEventListener("keydown", (event) => {
1985
+ if (event.key === "Escape" && !modal.classList.contains("hidden")) {
1986
+ closeFilePreview();
1987
+ }
1988
+ });
1989
+ }
1990
+ async function openFilePreview(filePath) {
1991
+ const modal = document.getElementById("file-preview-modal");
1992
+ const pathEl = document.getElementById("file-preview-path");
1993
+ const codeEl = document.getElementById("file-preview-code");
1994
+ if (!modal || !pathEl || !codeEl) {
1995
+ return;
1996
+ }
1997
+ latestFilePreviewRequestId += 1;
1998
+ const requestId = latestFilePreviewRequestId;
1999
+ pathEl.textContent = filePath;
2000
+ codeEl.className = "file-preview-code";
2001
+ codeEl.textContent = "Loading...";
2002
+ openModal(modal);
2003
+ try {
2004
+ const res = await fetch(`/api/file-source?path=${encodeURIComponent(filePath)}`);
2005
+ if (!res.ok) {
2006
+ codeEl.textContent = "(file unavailable)";
2007
+ return;
2008
+ }
2009
+ const data = await res.json();
2010
+ if (requestId !== latestFilePreviewRequestId) {
2011
+ return;
2012
+ }
2013
+ pathEl.textContent = data.filePath;
2014
+ codeEl.className = `file-preview-code language-${getFilePreviewLanguage(data.filePath)}`;
2015
+ codeEl.textContent = data.source;
2016
+ if (typeof hljs !== "undefined") {
2017
+ hljs.highlightElement(codeEl);
2018
+ }
2019
+ } catch {
2020
+ if (requestId !== latestFilePreviewRequestId) {
2021
+ return;
2022
+ }
2023
+ codeEl.textContent = "(file unavailable)";
2024
+ }
2025
+ }
1826
2026
  async function focusSymbolInGraph(symbolId, options = {}) {
1827
2027
  await focusSymbolsInGraph([symbolId], options);
1828
2028
  }
@@ -2349,7 +2549,7 @@ async function showDetail(node, detailEl) {
2349
2549
  <span class="detail-name">${escapeHtml(data.label)}</span>
2350
2550
  <span class="detail-kind-badge" style="background:${kindColor}20;color:${kindColor}">${kind}</span>
2351
2551
  </div>
2352
- <div class="detail-file">${escapeHtml(data.file || "unknown")}${data.startLine ? ":" + data.startLine : ""}</div>
2552
+ <div class="detail-file">${data.file ? `<button type="button" class="detail-file-link" data-file="${escapeHtml(data.file)}">${escapeHtml(data.file)}${data.startLine ? ":" + data.startLine : ""}</button>` : "unknown"}</div>
2353
2553
  `;
2354
2554
  html += `<div class="detail-actions">`;
2355
2555
  html += `<button class="detail-pin header-btn" data-name="${escapeHtml(data.qualifiedName)}">${isPinned ? "Unpin" : "Pin to focus"}</button>`;
@@ -2391,6 +2591,12 @@ async function showDetail(node, detailEl) {
2391
2591
  const name = pinBtn.dataset.name || "";
2392
2592
  await togglePin(name, node, pinBtn);
2393
2593
  });
2594
+ const fileLink = detailEl.querySelector(".detail-file-link");
2595
+ fileLink?.addEventListener("click", () => {
2596
+ const filePath = fileLink.dataset.file || "";
2597
+ if (!filePath) return;
2598
+ void openFilePreview(filePath);
2599
+ });
2394
2600
  const explainBtn = detailEl.querySelector(".detail-explain-btn");
2395
2601
  explainBtn.addEventListener("click", () => {
2396
2602
  const name = explainBtn.dataset.name || "";
@@ -2563,6 +2769,7 @@ async function togglePin(name, node, btnEl) {
2563
2769
  function setupToolbar() {
2564
2770
  const searchInput = document.getElementById("graph-search");
2565
2771
  const analyzeBtn = document.getElementById("graph-analyze");
2772
+ const refreshBtn = document.getElementById("graph-refresh");
2566
2773
  const fitBtn = document.getElementById("graph-fit");
2567
2774
  const relayoutBtn = document.getElementById("graph-relayout");
2568
2775
  const clearBtn = document.getElementById("graph-clear");
@@ -2574,7 +2781,6 @@ function setupToolbar() {
2574
2781
  dropdown.className = "search-dropdown hidden";
2575
2782
  searchInput.parentNode.style.position = "relative";
2576
2783
  searchInput.parentNode.appendChild(dropdown);
2577
- const rankedSymbols = allSymbolNames.slice().sort((a, b2) => compareGraphNodeIds(a, b2, graphNodes));
2578
2784
  function showDropdownResults(results) {
2579
2785
  if (results.length === 0) {
2580
2786
  dropdown.classList.add("hidden");
@@ -2599,6 +2805,7 @@ function setupToolbar() {
2599
2805
  dropdown.classList.add("hidden");
2600
2806
  if (type === "file") {
2601
2807
  focusFileInGraph(id);
2808
+ void openFilePreview(id);
2602
2809
  return;
2603
2810
  }
2604
2811
  void focusSymbolInGraph(id, {
@@ -2612,6 +2819,7 @@ function setupToolbar() {
2612
2819
  });
2613
2820
  }
2614
2821
  function updateDropdownResults() {
2822
+ const rankedSymbols = allSymbolNames.slice().sort((a, b2) => compareGraphNodeIds(a, b2, graphNodes));
2615
2823
  const results = buildGraphSearchResults({
2616
2824
  query: searchInput.value,
2617
2825
  symbolIds: rankedSymbols,
@@ -2645,6 +2853,9 @@ function setupToolbar() {
2645
2853
  cy.fit(40);
2646
2854
  }
2647
2855
  });
2856
+ refreshBtn.addEventListener("click", () => {
2857
+ void refreshGraphData({ refreshIndex: true, preserveVisible: true, showFeedback: true });
2858
+ });
2648
2859
  analyzeBtn.addEventListener("click", () => {
2649
2860
  void runStructuralAnalysis();
2650
2861
  });
@@ -2671,6 +2882,24 @@ function setupToolbar() {
2671
2882
  });
2672
2883
  }
2673
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
+
2674
2903
  // src/web/request-tracker.ts
2675
2904
  function createLatestRequestTracker() {
2676
2905
  let latestToken = 0;
@@ -2685,6 +2914,25 @@ function createLatestRequestTracker() {
2685
2914
  };
2686
2915
  }
2687
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
+
2688
2936
  // src/web/app.ts
2689
2937
  var messagesEl = document.getElementById("messages");
2690
2938
  var chatForm = document.getElementById("chat-form");
@@ -2695,6 +2943,7 @@ var statusBadge = document.getElementById("status-badge");
2695
2943
  var modelInfo = document.getElementById("model-info");
2696
2944
  var modelBtn = document.getElementById("model-btn");
2697
2945
  var modelDropdown = document.getElementById("model-dropdown");
2946
+ var modelSearchInput = document.getElementById("model-search");
2698
2947
  var modelList = document.getElementById("model-list");
2699
2948
  var sessionBtn = document.getElementById("session-btn");
2700
2949
  var sessionDropdown = document.getElementById("session-dropdown");
@@ -2715,17 +2964,36 @@ var openRouterConnectCloseBtn = document.getElementById("openrouter-connect-clos
2715
2964
  var openRouterConnectCancelBtn = document.getElementById("openrouter-connect-cancel");
2716
2965
  var openRouterConnectContinueBtn = document.getElementById("openrouter-connect-continue");
2717
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");
2718
2978
  var settingsPath = document.getElementById("settings-path");
2719
2979
  var settingsList = document.getElementById("settings-list");
2720
2980
  var settingsBanner = document.getElementById("settings-banner");
2721
2981
  var settingsOpenRouterSession = document.getElementById("settings-openrouter-session");
2722
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");
2723
2985
  var settingsSaveBtn = document.getElementById("settings-save");
2724
2986
  var settingsResetBtn = document.getElementById("settings-reset");
2725
2987
  var disconnectOpenRouterBtn = document.getElementById("disconnect-openrouter-btn");
2988
+ var disconnectOpenAiCompatibleBtn = document.getElementById("disconnect-openai-compatible-btn");
2726
2989
  var connectOpenRouterButtons = Array.from(
2727
2990
  document.querySelectorAll("[data-openrouter-connect]")
2728
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");
2729
2997
  var configOverlaySpotlight = document.getElementById("config-overlay-spotlight");
2730
2998
  var configOverlayIntro = document.getElementById("config-overlay-intro");
2731
2999
  var configConnectStatus = document.getElementById("config-connect-status");
@@ -2737,10 +3005,33 @@ var settingsPayload = null;
2737
3005
  var activeSavedSession = null;
2738
3006
  var activeBaseUrl = "";
2739
3007
  var sessionOpenRouterConnected = false;
3008
+ var sessionOpenAiCompatibleConnected = false;
2740
3009
  var sessionRefreshTracker = createLatestRequestTracker();
2741
3010
  var TOOL_RESULT_MAX = 500;
2742
3011
  var OPENROUTER_PKCE_VERIFIER_KEY = "minicode:openrouter:pkce-verifier";
2743
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
+ };
2744
3035
  function connect() {
2745
3036
  const protocol = location.protocol === "https:" ? "wss:" : "ws:";
2746
3037
  ws = new WebSocket(`${protocol}//${location.host}`);
@@ -2788,6 +3079,51 @@ function clearConfigConnectStatus() {
2788
3079
  configConnectStatus.textContent = "";
2789
3080
  configConnectStatus.className = "config-connect-status hidden";
2790
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
+ }
2791
3127
  function encodeBase64Url(bytes) {
2792
3128
  let binary = "";
2793
3129
  for (const byte of bytes) {
@@ -2860,6 +3196,7 @@ async function maybeHandleOpenRouterCallback() {
2860
3196
  if (onlyModelMissing) {
2861
3197
  modelDropdown.classList.remove("hidden");
2862
3198
  sessionDropdown.classList.add("hidden");
3199
+ focusModelSearchInput();
2863
3200
  }
2864
3201
  } catch (error) {
2865
3202
  const message = error instanceof Error ? error.message : "Failed to connect OpenRouter";
@@ -2895,6 +3232,76 @@ async function disconnectOpenRouter() {
2895
3232
  disconnectOpenRouterBtn.disabled = false;
2896
3233
  }
2897
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
+ }
2898
3305
  async function fetchStatus() {
2899
3306
  try {
2900
3307
  const res = await fetch("/api/status");
@@ -2904,22 +3311,32 @@ async function fetchStatus() {
2904
3311
  activeModel = data.model;
2905
3312
  activeBaseUrl = data.baseUrl ?? "";
2906
3313
  sessionOpenRouterConnected = data.sessionOpenRouterConnected ?? false;
2907
- renderOpenRouterSessionControls();
3314
+ sessionOpenAiCompatibleConnected = data.sessionOpenAiCompatibleConnected ?? false;
3315
+ renderSessionProviderControls();
2908
3316
  if (data.needsSetup) {
2909
3317
  configOverlay.classList.remove("hidden");
2910
3318
  chatInput.disabled = true;
2911
3319
  sendBtn.disabled = true;
2912
3320
  const missingEl = document.getElementById("config-missing");
2913
- if (missingEl && data.missing && data.missing.length > 0) {
2914
- const isOnlyModelMissing = data.missing.length === 1 && data.missing[0].includes("MODEL");
2915
- const hasPersistedOpenRouter = isOnlyModelMissing && (data.baseUrl ?? "").includes("openrouter");
2916
- if (configOverlayIntro) {
2917
- 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 = "";
2918
3339
  }
2919
- configOverlaySpotlight?.classList.toggle("hidden", hasPersistedOpenRouter);
2920
- const hint = isOnlyModelMissing ? ` \u2014 select one from the <strong>model dropdown</strong> above, or set it in config` : "";
2921
- missingEl.innerHTML = `<strong>Missing:</strong> ${data.missing.map(escapeHtml).join(", ")}${hint}`;
2922
- missingEl.classList.remove("hidden");
2923
3340
  }
2924
3341
  } else {
2925
3342
  configOverlay.classList.add("hidden");
@@ -2931,8 +3348,9 @@ async function fetchStatus() {
2931
3348
  missingEl.innerHTML = "";
2932
3349
  }
2933
3350
  if (configOverlayIntro) {
2934
- configOverlayIntro.textContent = "minicode needs a model provider to run. Configure one of the following:";
3351
+ configOverlayIntro.textContent = DEFAULT_SETUP_INTRO;
2935
3352
  }
3353
+ configOverlayQuickConnects?.classList.remove("hidden");
2936
3354
  configOverlaySpotlight?.classList.remove("hidden");
2937
3355
  }
2938
3356
  } catch {
@@ -2981,6 +3399,9 @@ function handleServerMessage(msg) {
2981
3399
  break;
2982
3400
  case "tool_call_end":
2983
3401
  finalizeToolCall(msg.name || "", msg.result || "", msg.elapsedMs || 0);
3402
+ if (GRAPH_REFRESH_TOOL_NAMES.has(msg.name || "")) {
3403
+ scheduleGraphDataRefresh();
3404
+ }
2984
3405
  break;
2985
3406
  case "turn_end":
2986
3407
  if (hadToolCalls && msg.text) {
@@ -3166,19 +3587,13 @@ function addUsageInfo(usage) {
3166
3587
  function scrollToBottom() {
3167
3588
  messagesEl.scrollTop = messagesEl.scrollHeight;
3168
3589
  }
3169
- function closeHeaderMenus() {
3590
+ function closeModelDropdown() {
3170
3591
  modelDropdown.classList.add("hidden");
3171
- sessionDropdown.classList.add("hidden");
3172
- }
3173
- function isSettingsModalOpen() {
3174
- return !settingsModal.classList.contains("hidden");
3592
+ modelSearchInput.value = "";
3175
3593
  }
3176
- function isOpenRouterConnectModalOpen() {
3177
- return !openRouterConnectModal.classList.contains("hidden");
3178
- }
3179
- function syncModalOpenState() {
3180
- const anyModalOpen = isSettingsModalOpen() || isOpenRouterConnectModalOpen();
3181
- document.body.classList.toggle("modal-open", anyModalOpen);
3594
+ function closeHeaderMenus() {
3595
+ closeModelDropdown();
3596
+ sessionDropdown.classList.add("hidden");
3182
3597
  }
3183
3598
  function formatSettingsValue(value) {
3184
3599
  return value === null ? "(unset)" : String(value);
@@ -3191,16 +3606,31 @@ function clearSettingsBanner() {
3191
3606
  settingsBanner.textContent = "";
3192
3607
  settingsBanner.className = "settings-banner hidden";
3193
3608
  }
3194
- function renderOpenRouterSessionControls() {
3609
+ function renderSessionProviderControls() {
3195
3610
  if (sessionOpenRouterConnected) {
3196
3611
  settingsOpenRouterSession.classList.remove("hidden");
3197
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 = "";
3198
3624
  disconnectOpenRouterBtn.disabled = false;
3625
+ disconnectOpenAiCompatibleBtn.disabled = false;
3199
3626
  return;
3200
3627
  }
3201
3628
  settingsOpenRouterSession.classList.add("hidden");
3202
3629
  settingsOpenRouterSessionMeta.textContent = "";
3630
+ settingsOpenAiCompatibleSession.classList.add("hidden");
3631
+ settingsOpenAiCompatibleSessionMeta.textContent = "";
3203
3632
  disconnectOpenRouterBtn.disabled = false;
3633
+ disconnectOpenAiCompatibleBtn.disabled = false;
3204
3634
  }
3205
3635
  function createSettingsControl(entry, inputId) {
3206
3636
  const value = entry.persistedValue;
@@ -3394,30 +3824,43 @@ function updateSettingsActions() {
3394
3824
  }
3395
3825
  function openSettings() {
3396
3826
  closeHeaderMenus();
3397
- settingsModal.classList.remove("hidden");
3398
- settingsModal.setAttribute("aria-hidden", "false");
3399
- syncModalOpenState();
3827
+ openModal(settingsModal);
3400
3828
  void loadSettings();
3401
3829
  }
3402
3830
  function closeSettings() {
3403
- settingsModal.classList.add("hidden");
3404
- settingsModal.setAttribute("aria-hidden", "true");
3405
- syncModalOpenState();
3831
+ closeModal(settingsModal);
3406
3832
  clearSettingsBanner();
3407
3833
  }
3408
3834
  function openOpenRouterConnectModal() {
3409
3835
  closeHeaderMenus();
3410
- openRouterConnectModal.classList.remove("hidden");
3411
- openRouterConnectModal.setAttribute("aria-hidden", "false");
3836
+ openModal(openRouterConnectModal);
3412
3837
  openRouterPersistCheckbox.checked = false;
3413
3838
  openRouterConnectContinueBtn.disabled = false;
3414
- syncModalOpenState();
3415
3839
  }
3416
3840
  function closeOpenRouterConnectModal() {
3417
- openRouterConnectModal.classList.add("hidden");
3418
- openRouterConnectModal.setAttribute("aria-hidden", "true");
3841
+ closeModal(openRouterConnectModal);
3419
3842
  openRouterConnectContinueBtn.disabled = false;
3420
- syncModalOpenState();
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();
3421
3864
  }
3422
3865
  chatForm.addEventListener("submit", (e) => {
3423
3866
  e.preventDefault();
@@ -3442,30 +3885,99 @@ chatInput.addEventListener("keydown", (e) => {
3442
3885
  }
3443
3886
  });
3444
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
+ }
3445
3936
  modelBtn.addEventListener("click", (e) => {
3446
3937
  e.stopPropagation();
3447
3938
  const isOpen = !modelDropdown.classList.contains("hidden");
3448
- modelDropdown.classList.toggle("hidden");
3449
- sessionDropdown.classList.add("hidden");
3450
- if (!isOpen) {
3451
- refreshModelList();
3939
+ if (isOpen) {
3940
+ closeModelDropdown();
3941
+ return;
3452
3942
  }
3943
+ closeHeaderMenus();
3944
+ modelDropdown.classList.remove("hidden");
3945
+ modelSearchInput.value = "";
3946
+ void refreshModelList({ focusSearch: true });
3453
3947
  });
3454
3948
  document.addEventListener("click", (e) => {
3455
3949
  if (!modelDropdown.contains(e.target) && e.target !== modelBtn) {
3456
- modelDropdown.classList.add("hidden");
3950
+ closeModelDropdown();
3457
3951
  }
3458
3952
  });
3459
- async function refreshModelList() {
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();
3964
+ }
3965
+ });
3966
+ modelSearchInput.addEventListener("click", (e) => {
3967
+ e.stopPropagation();
3968
+ });
3969
+ async function refreshModelList(options = {}) {
3460
3970
  try {
3461
3971
  const res = await fetch("/api/models");
3462
3972
  const data = await res.json();
3463
3973
  activeModel = data.activeModel;
3974
+ availableModels = data.models ?? [];
3464
3975
  const hasActiveModel = !!activeModel && data.models.some((model) => model.id === activeModel);
3465
3976
  if (!data.models || data.models.length === 0) {
3466
3977
  modelInfo.textContent = "Select model";
3467
3978
  modelInfo.classList.add("placeholder");
3468
- modelList.innerHTML = '<div class="dropdown-empty">No models available</div>';
3979
+ availableModels = [];
3980
+ renderModelList();
3469
3981
  return;
3470
3982
  }
3471
3983
  if (hasActiveModel) {
@@ -3475,18 +3987,12 @@ async function refreshModelList() {
3475
3987
  modelInfo.textContent = "Select model";
3476
3988
  modelInfo.classList.add("placeholder");
3477
3989
  }
3478
- modelList.innerHTML = "";
3479
- for (const m2 of data.models) {
3480
- const el = document.createElement("div");
3481
- el.className = "model-item" + (m2.id === activeModel ? " active" : "");
3482
- el.textContent = m2.name ?? m2.id;
3483
- el.title = m2.id;
3484
- el.addEventListener("click", () => {
3485
- void switchModel(m2.id);
3486
- });
3487
- modelList.appendChild(el);
3990
+ renderModelList();
3991
+ if (options.focusSearch && !modelDropdown.classList.contains("hidden")) {
3992
+ focusModelSearchInput();
3488
3993
  }
3489
3994
  } catch {
3995
+ availableModels = [];
3490
3996
  modelList.innerHTML = '<div class="dropdown-empty">Failed to load models</div>';
3491
3997
  }
3492
3998
  }
@@ -3507,7 +4013,8 @@ async function switchModel(modelId) {
3507
4013
  modelInfo.textContent = modelId || "Select model";
3508
4014
  modelInfo.classList.toggle("placeholder", !modelId);
3509
4015
  activeModel = modelId;
3510
- modelDropdown.classList.add("hidden");
4016
+ renderModelList();
4017
+ closeModelDropdown();
3511
4018
  if (body.persistedToEnv) {
3512
4019
  addMessage(`Model switched to: ${modelId}. Saved as the default in ~/.minicode/.env.`, "thinking");
3513
4020
  } else {
@@ -3523,7 +4030,7 @@ sessionBtn.addEventListener("click", (e) => {
3523
4030
  e.stopPropagation();
3524
4031
  const isOpen = !sessionDropdown.classList.contains("hidden");
3525
4032
  sessionDropdown.classList.toggle("hidden");
3526
- modelDropdown.classList.add("hidden");
4033
+ closeModelDropdown();
3527
4034
  if (!isOpen) {
3528
4035
  refreshSessionList();
3529
4036
  }
@@ -3544,16 +4051,20 @@ saveBtn.addEventListener("click", async () => {
3544
4051
  headers: { "Content-Type": "application/json" },
3545
4052
  body: JSON.stringify({ label })
3546
4053
  });
3547
- if (res.ok) {
3548
- const data = await res.json();
3549
- saveLabelInput.value = "";
3550
- addMessage(
3551
- `${isUpdatingCurrentSession ? "Session updated" : "Session saved"}: "${data.label}"`,
3552
- "thinking"
3553
- );
3554
- 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})`);
3555
4057
  }
3556
- } 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");
3557
4068
  } finally {
3558
4069
  saveBtn.removeAttribute("disabled");
3559
4070
  }
@@ -3635,12 +4146,16 @@ sessionUpdateBtn.addEventListener("click", async () => {
3635
4146
  headers: { "Content-Type": "application/json" },
3636
4147
  body: JSON.stringify({ label: activeSavedSession.label })
3637
4148
  });
3638
- if (res.ok) {
3639
- const data = await res.json();
3640
- addMessage(`Session updated: "${data.label}"`, "thinking");
3641
- 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})`);
3642
4152
  }
3643
- } 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");
3644
4159
  } finally {
3645
4160
  sessionUpdateBtn.disabled = false;
3646
4161
  }
@@ -3663,10 +4178,19 @@ openRouterConnectCloseBtn.addEventListener("click", () => {
3663
4178
  openRouterConnectCancelBtn.addEventListener("click", () => {
3664
4179
  closeOpenRouterConnectModal();
3665
4180
  });
4181
+ openAiCompatibleConnectBackdrop.addEventListener("click", () => {
4182
+ closeOpenAiCompatibleConnectModal();
4183
+ });
4184
+ openAiCompatibleConnectCloseBtn.addEventListener("click", () => {
4185
+ closeOpenAiCompatibleConnectModal();
4186
+ });
4187
+ openAiCompatibleConnectCancelBtn.addEventListener("click", () => {
4188
+ closeOpenAiCompatibleConnectModal();
4189
+ });
3666
4190
  settingsResetBtn.addEventListener("click", () => {
3667
4191
  clearSettingsBanner();
3668
4192
  renderSettings();
3669
- renderOpenRouterSessionControls();
4193
+ renderSessionProviderControls();
3670
4194
  });
3671
4195
  settingsSaveBtn.addEventListener("click", async () => {
3672
4196
  if (!settingsPayload) {
@@ -3718,6 +4242,9 @@ settingsList.addEventListener("change", () => {
3718
4242
  disconnectOpenRouterBtn.addEventListener("click", () => {
3719
4243
  void disconnectOpenRouter();
3720
4244
  });
4245
+ disconnectOpenAiCompatibleBtn.addEventListener("click", () => {
4246
+ void disconnectOpenAiCompatible();
4247
+ });
3721
4248
  document.addEventListener("keydown", (event) => {
3722
4249
  if (event.key !== "Escape") {
3723
4250
  return;
@@ -3726,6 +4253,10 @@ document.addEventListener("keydown", (event) => {
3726
4253
  closeOpenRouterConnectModal();
3727
4254
  return;
3728
4255
  }
4256
+ if (isOpenAiCompatibleConnectModalOpen()) {
4257
+ closeOpenAiCompatibleConnectModal();
4258
+ return;
4259
+ }
3729
4260
  if (isSettingsModalOpen()) {
3730
4261
  closeSettings();
3731
4262
  }
@@ -3735,11 +4266,29 @@ for (const button of connectOpenRouterButtons) {
3735
4266
  openOpenRouterConnectModal();
3736
4267
  });
3737
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
+ });
3738
4284
  openRouterConnectContinueBtn.addEventListener("click", () => {
3739
4285
  openRouterConnectContinueBtn.disabled = true;
3740
4286
  closeOpenRouterConnectModal();
3741
4287
  void startOpenRouterConnect(openRouterPersistCheckbox.checked);
3742
4288
  });
4289
+ openAiCompatibleConnectContinueBtn.addEventListener("click", () => {
4290
+ void connectOpenAiCompatible();
4291
+ });
3743
4292
  var chatPane = document.getElementById("chat-pane");
3744
4293
  var divider = document.getElementById("pane-divider");
3745
4294
  divider.addEventListener("mousedown", (e) => {