@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.
- package/dist/src/agent/config.js +25 -0
- package/dist/src/model-utils.js +18 -1
- package/dist/src/serve/agent-bridge.js +85 -14
- package/dist/src/serve/server.js +137 -3
- package/dist/src/session/session-store.js +18 -0
- package/dist/src/web/app.js +559 -90
- package/dist/src/web/index.html +112 -8
- package/dist/src/web/style.css +141 -7
- package/dist/tests/agent.test.js +16 -0
- package/dist/tests/config-integration.test.js +91 -1
- package/dist/tests/file-tools.test.js +12 -0
- package/dist/tests/graph-onboarding.test.js +8 -0
- package/dist/tests/model-client-openai.test.js +41 -0
- package/dist/tests/model-dropdown-ui.test.js +23 -0
- package/dist/tests/model-utils.test.js +26 -1
- package/dist/tests/serve.integration.test.js +163 -0
- package/dist/tests/session-store.test.js +15 -1
- package/dist/tests/settings-ui.test.js +11 -0
- package/dist/tests/setup-overlay-state.test.js +49 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +10 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +21 -0
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +60 -6
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/src/web/app.js
CHANGED
|
@@ -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
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
|
1777
|
-
if (
|
|
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 =
|
|
1889
|
+
hint.innerHTML = `<div class="graph-onboarding-icon">◆ — ◆</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
|
-
|
|
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
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|