@sean.holung/minicode 0.3.7 → 0.3.9
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 +89 -15
- package/dist/src/serve/server.js +151 -3
- package/dist/src/session/session-store.js +29 -1
- package/dist/src/web/app.js +691 -105
- package/dist/src/web/index.html +117 -9
- package/dist/src/web/style.css +198 -10
- package/dist/tests/agent.test.js +16 -0
- package/dist/tests/config-integration.test.js +91 -1
- package/dist/tests/context-indicator.test.js +9 -0
- 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 +194 -0
- package/dist/tests/session-store.test.js +32 -1
- package/dist/tests/session-ui.test.js +6 -0
- 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,12 +2943,14 @@ 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");
|
|
2799
2950
|
var sessionList = document.getElementById("session-list");
|
|
2800
2951
|
var sessionUpdateRow = document.getElementById("session-update-row");
|
|
2801
2952
|
var sessionUpdateBtn = document.getElementById("session-update-btn");
|
|
2953
|
+
var sessionAutoSaveToggle = document.getElementById("session-autosave-toggle");
|
|
2802
2954
|
var saveBtn = document.getElementById("save-btn");
|
|
2803
2955
|
var saveLabelInput = document.getElementById("save-label");
|
|
2804
2956
|
var contextFill = document.getElementById("context-fill");
|
|
@@ -2813,17 +2965,36 @@ var openRouterConnectCloseBtn = document.getElementById("openrouter-connect-clos
|
|
|
2813
2965
|
var openRouterConnectCancelBtn = document.getElementById("openrouter-connect-cancel");
|
|
2814
2966
|
var openRouterConnectContinueBtn = document.getElementById("openrouter-connect-continue");
|
|
2815
2967
|
var openRouterPersistCheckbox = document.getElementById("openrouter-persist-checkbox");
|
|
2968
|
+
var openAiCompatibleConnectModal = document.getElementById("openai-compatible-connect-modal");
|
|
2969
|
+
var openAiCompatibleConnectBackdrop = document.getElementById("openai-compatible-connect-backdrop");
|
|
2970
|
+
var openAiCompatibleConnectCloseBtn = document.getElementById("openai-compatible-connect-close");
|
|
2971
|
+
var openAiCompatibleConnectCancelBtn = document.getElementById("openai-compatible-connect-cancel");
|
|
2972
|
+
var openAiCompatibleConnectContinueBtn = document.getElementById("openai-compatible-connect-continue");
|
|
2973
|
+
var openAiCompatiblePresetSelect = document.getElementById("openai-compatible-preset");
|
|
2974
|
+
var openAiCompatiblePresetHelp = document.getElementById("openai-compatible-preset-help");
|
|
2975
|
+
var openAiCompatibleBaseUrlInput = document.getElementById("openai-compatible-base-url");
|
|
2976
|
+
var openAiCompatibleApiKeyInput = document.getElementById("openai-compatible-api-key");
|
|
2977
|
+
var openAiCompatiblePersistCheckbox = document.getElementById("openai-compatible-persist-checkbox");
|
|
2978
|
+
var openAiCompatibleConnectStatus = document.getElementById("openai-compatible-connect-status");
|
|
2816
2979
|
var settingsPath = document.getElementById("settings-path");
|
|
2817
2980
|
var settingsList = document.getElementById("settings-list");
|
|
2818
2981
|
var settingsBanner = document.getElementById("settings-banner");
|
|
2819
2982
|
var settingsOpenRouterSession = document.getElementById("settings-openrouter-session");
|
|
2820
2983
|
var settingsOpenRouterSessionMeta = document.getElementById("settings-openrouter-session-meta");
|
|
2984
|
+
var settingsOpenAiCompatibleSession = document.getElementById("settings-openai-compatible-session");
|
|
2985
|
+
var settingsOpenAiCompatibleSessionMeta = document.getElementById("settings-openai-compatible-session-meta");
|
|
2821
2986
|
var settingsSaveBtn = document.getElementById("settings-save");
|
|
2822
2987
|
var settingsResetBtn = document.getElementById("settings-reset");
|
|
2823
2988
|
var disconnectOpenRouterBtn = document.getElementById("disconnect-openrouter-btn");
|
|
2989
|
+
var disconnectOpenAiCompatibleBtn = document.getElementById("disconnect-openai-compatible-btn");
|
|
2824
2990
|
var connectOpenRouterButtons = Array.from(
|
|
2825
2991
|
document.querySelectorAll("[data-openrouter-connect]")
|
|
2826
2992
|
);
|
|
2993
|
+
var connectOpenAiCompatibleButtons = Array.from(
|
|
2994
|
+
document.querySelectorAll("[data-openai-compatible-connect]")
|
|
2995
|
+
);
|
|
2996
|
+
var GRAPH_REFRESH_TOOL_NAMES = /* @__PURE__ */ new Set(["write_file", "edit_file", "run_command"]);
|
|
2997
|
+
var configOverlayQuickConnects = document.getElementById("config-overlay-quick-connects");
|
|
2827
2998
|
var configOverlaySpotlight = document.getElementById("config-overlay-spotlight");
|
|
2828
2999
|
var configOverlayIntro = document.getElementById("config-overlay-intro");
|
|
2829
3000
|
var configConnectStatus = document.getElementById("config-connect-status");
|
|
@@ -2835,10 +3006,40 @@ var settingsPayload = null;
|
|
|
2835
3006
|
var activeSavedSession = null;
|
|
2836
3007
|
var activeBaseUrl = "";
|
|
2837
3008
|
var sessionOpenRouterConnected = false;
|
|
3009
|
+
var sessionOpenAiCompatibleConnected = false;
|
|
2838
3010
|
var sessionRefreshTracker = createLatestRequestTracker();
|
|
2839
3011
|
var TOOL_RESULT_MAX = 500;
|
|
2840
3012
|
var OPENROUTER_PKCE_VERIFIER_KEY = "minicode:openrouter:pkce-verifier";
|
|
2841
3013
|
var OPENROUTER_PERSIST_TO_ENV_KEY = "minicode:openrouter:persist-to-env";
|
|
3014
|
+
var SESSION_AUTOSAVE_KEY = "minicode:session:auto-save";
|
|
3015
|
+
var SESSION_AUTOSAVE_LABEL_PREFIX = "Autosave";
|
|
3016
|
+
var OPENAI_COMPATIBLE_PRESETS = {
|
|
3017
|
+
lmstudio: {
|
|
3018
|
+
baseUrl: "http://localhost:1234/v1",
|
|
3019
|
+
helpText: "LM Studio pre-fills the default local server endpoint at http://localhost:1234/v1.",
|
|
3020
|
+
apiKeyPlaceholder: "Leave blank for LM Studio unless local auth is enabled"
|
|
3021
|
+
},
|
|
3022
|
+
openai: {
|
|
3023
|
+
baseUrl: "https://api.openai.com/v1",
|
|
3024
|
+
helpText: "OpenAI uses https://api.openai.com/v1 and typically requires an API key.",
|
|
3025
|
+
apiKeyPlaceholder: "Enter your OpenAI API key"
|
|
3026
|
+
},
|
|
3027
|
+
ollama: {
|
|
3028
|
+
baseUrl: "http://localhost:11434/v1",
|
|
3029
|
+
helpText: "Ollama pre-fills the default local server endpoint at http://localhost:11434/v1.",
|
|
3030
|
+
apiKeyPlaceholder: "Leave blank for Ollama unless your proxy requires auth"
|
|
3031
|
+
},
|
|
3032
|
+
custom: {
|
|
3033
|
+
baseUrl: "",
|
|
3034
|
+
helpText: "Custom leaves the endpoint fully editable so you can point minicode at any OpenAI-compatible API.",
|
|
3035
|
+
apiKeyPlaceholder: "Add an API key only if this endpoint requires auth"
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
var sessionAutoSaveEnabled = loadSessionAutoSavePreference();
|
|
3039
|
+
var pendingAutoSaveLabel = null;
|
|
3040
|
+
var autoSaveInFlight = null;
|
|
3041
|
+
var autoSaveQueued = false;
|
|
3042
|
+
sessionAutoSaveToggle.checked = sessionAutoSaveEnabled;
|
|
2842
3043
|
function connect() {
|
|
2843
3044
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
2844
3045
|
ws = new WebSocket(`${protocol}//${location.host}`);
|
|
@@ -2886,6 +3087,51 @@ function clearConfigConnectStatus() {
|
|
|
2886
3087
|
configConnectStatus.textContent = "";
|
|
2887
3088
|
configConnectStatus.className = "config-connect-status hidden";
|
|
2888
3089
|
}
|
|
3090
|
+
function setOpenAiCompatibleConnectStatus(message, tone) {
|
|
3091
|
+
openAiCompatibleConnectStatus.textContent = message;
|
|
3092
|
+
openAiCompatibleConnectStatus.className = `config-connect-status ${tone}`;
|
|
3093
|
+
}
|
|
3094
|
+
function clearOpenAiCompatibleConnectStatus() {
|
|
3095
|
+
openAiCompatibleConnectStatus.textContent = "";
|
|
3096
|
+
openAiCompatibleConnectStatus.className = "config-connect-status hidden";
|
|
3097
|
+
}
|
|
3098
|
+
function normalizeBaseUrl(value) {
|
|
3099
|
+
return value.trim().replace(/\/+$/, "");
|
|
3100
|
+
}
|
|
3101
|
+
function inferOpenAiCompatiblePreset(baseUrl) {
|
|
3102
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl).toLowerCase();
|
|
3103
|
+
if (normalizedBaseUrl === OPENAI_COMPATIBLE_PRESETS.lmstudio.baseUrl) {
|
|
3104
|
+
return "lmstudio";
|
|
3105
|
+
}
|
|
3106
|
+
if (normalizedBaseUrl === OPENAI_COMPATIBLE_PRESETS.openai.baseUrl) {
|
|
3107
|
+
return "openai";
|
|
3108
|
+
}
|
|
3109
|
+
if (normalizedBaseUrl === OPENAI_COMPATIBLE_PRESETS.ollama.baseUrl) {
|
|
3110
|
+
return "ollama";
|
|
3111
|
+
}
|
|
3112
|
+
return "custom";
|
|
3113
|
+
}
|
|
3114
|
+
function applyOpenAiCompatiblePreset(preset, options = {}) {
|
|
3115
|
+
const presetConfig = OPENAI_COMPATIBLE_PRESETS[preset];
|
|
3116
|
+
openAiCompatiblePresetHelp.textContent = presetConfig.helpText;
|
|
3117
|
+
openAiCompatibleApiKeyInput.placeholder = presetConfig.apiKeyPlaceholder;
|
|
3118
|
+
if (preset === "custom" && options.preserveCustomValue) {
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
openAiCompatibleBaseUrlInput.value = presetConfig.baseUrl;
|
|
3122
|
+
}
|
|
3123
|
+
function isModalOpen(modal) {
|
|
3124
|
+
return !modal.classList.contains("hidden");
|
|
3125
|
+
}
|
|
3126
|
+
function isSettingsModalOpen() {
|
|
3127
|
+
return isModalOpen(settingsModal);
|
|
3128
|
+
}
|
|
3129
|
+
function isOpenRouterConnectModalOpen() {
|
|
3130
|
+
return isModalOpen(openRouterConnectModal);
|
|
3131
|
+
}
|
|
3132
|
+
function isOpenAiCompatibleConnectModalOpen() {
|
|
3133
|
+
return isModalOpen(openAiCompatibleConnectModal);
|
|
3134
|
+
}
|
|
2889
3135
|
function encodeBase64Url(bytes) {
|
|
2890
3136
|
let binary = "";
|
|
2891
3137
|
for (const byte of bytes) {
|
|
@@ -2958,6 +3204,7 @@ async function maybeHandleOpenRouterCallback() {
|
|
|
2958
3204
|
if (onlyModelMissing) {
|
|
2959
3205
|
modelDropdown.classList.remove("hidden");
|
|
2960
3206
|
sessionDropdown.classList.add("hidden");
|
|
3207
|
+
focusModelSearchInput();
|
|
2961
3208
|
}
|
|
2962
3209
|
} catch (error) {
|
|
2963
3210
|
const message = error instanceof Error ? error.message : "Failed to connect OpenRouter";
|
|
@@ -2993,6 +3240,76 @@ async function disconnectOpenRouter() {
|
|
|
2993
3240
|
disconnectOpenRouterBtn.disabled = false;
|
|
2994
3241
|
}
|
|
2995
3242
|
}
|
|
3243
|
+
async function connectOpenAiCompatible() {
|
|
3244
|
+
const baseUrl = normalizeBaseUrl(openAiCompatibleBaseUrlInput.value);
|
|
3245
|
+
const apiKey = openAiCompatibleApiKeyInput.value.trim();
|
|
3246
|
+
if (!baseUrl) {
|
|
3247
|
+
setOpenAiCompatibleConnectStatus("Endpoint is required.", "error");
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
clearOpenAiCompatibleConnectStatus();
|
|
3251
|
+
clearSettingsBanner();
|
|
3252
|
+
openAiCompatibleConnectContinueBtn.disabled = true;
|
|
3253
|
+
try {
|
|
3254
|
+
const res = await fetch("/api/openai-compatible/connect", {
|
|
3255
|
+
method: "POST",
|
|
3256
|
+
headers: { "Content-Type": "application/json" },
|
|
3257
|
+
body: JSON.stringify({
|
|
3258
|
+
baseUrl,
|
|
3259
|
+
apiKey,
|
|
3260
|
+
persistToEnv: openAiCompatiblePersistCheckbox.checked
|
|
3261
|
+
})
|
|
3262
|
+
});
|
|
3263
|
+
const body = await res.json();
|
|
3264
|
+
if (!res.ok) {
|
|
3265
|
+
throw new Error("error" in body ? body.error : `Failed to connect OpenAI-compatible provider (${res.status})`);
|
|
3266
|
+
}
|
|
3267
|
+
activeBaseUrl = body.baseUrl;
|
|
3268
|
+
addMessage(body.message, "thinking");
|
|
3269
|
+
const tone = body.persistWarning ? "info" : body.needsSetup ? "info" : "success";
|
|
3270
|
+
setConfigConnectStatus(body.message, tone);
|
|
3271
|
+
setSettingsBanner(body.message, tone === "success" ? "success" : "info");
|
|
3272
|
+
closeOpenAiCompatibleConnectModal();
|
|
3273
|
+
await fetchStatus();
|
|
3274
|
+
await refreshModelList();
|
|
3275
|
+
const onlyModelMissing = body.needsSetup && body.missing.length === 1 && body.missing[0]?.includes("MODEL");
|
|
3276
|
+
if (onlyModelMissing) {
|
|
3277
|
+
modelDropdown.classList.remove("hidden");
|
|
3278
|
+
sessionDropdown.classList.add("hidden");
|
|
3279
|
+
focusModelSearchInput();
|
|
3280
|
+
}
|
|
3281
|
+
} catch (error) {
|
|
3282
|
+
const message = error instanceof Error ? error.message : "Failed to connect OpenAI-compatible provider";
|
|
3283
|
+
setOpenAiCompatibleConnectStatus(message, "error");
|
|
3284
|
+
} finally {
|
|
3285
|
+
openAiCompatibleConnectContinueBtn.disabled = false;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
async function disconnectOpenAiCompatible() {
|
|
3289
|
+
disconnectOpenAiCompatibleBtn.disabled = true;
|
|
3290
|
+
clearSettingsBanner();
|
|
3291
|
+
try {
|
|
3292
|
+
const res = await fetch("/api/openai-compatible/disconnect", {
|
|
3293
|
+
method: "POST",
|
|
3294
|
+
headers: { "Content-Type": "application/json" }
|
|
3295
|
+
});
|
|
3296
|
+
const body = await res.json();
|
|
3297
|
+
if (!res.ok) {
|
|
3298
|
+
throw new Error("error" in body ? body.error : `Failed to disconnect OpenAI-compatible provider (${res.status})`);
|
|
3299
|
+
}
|
|
3300
|
+
activeBaseUrl = body.baseUrl;
|
|
3301
|
+
addMessage(body.message, "thinking");
|
|
3302
|
+
setSettingsBanner(body.message, body.disconnected ? "success" : "info");
|
|
3303
|
+
clearConfigConnectStatus();
|
|
3304
|
+
await fetchStatus();
|
|
3305
|
+
await refreshModelList();
|
|
3306
|
+
} catch (error) {
|
|
3307
|
+
const message = error instanceof Error ? error.message : "Failed to disconnect OpenAI-compatible provider";
|
|
3308
|
+
setSettingsBanner(message, "error");
|
|
3309
|
+
} finally {
|
|
3310
|
+
disconnectOpenAiCompatibleBtn.disabled = false;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
2996
3313
|
async function fetchStatus() {
|
|
2997
3314
|
try {
|
|
2998
3315
|
const res = await fetch("/api/status");
|
|
@@ -3002,22 +3319,32 @@ async function fetchStatus() {
|
|
|
3002
3319
|
activeModel = data.model;
|
|
3003
3320
|
activeBaseUrl = data.baseUrl ?? "";
|
|
3004
3321
|
sessionOpenRouterConnected = data.sessionOpenRouterConnected ?? false;
|
|
3005
|
-
|
|
3322
|
+
sessionOpenAiCompatibleConnected = data.sessionOpenAiCompatibleConnected ?? false;
|
|
3323
|
+
renderSessionProviderControls();
|
|
3006
3324
|
if (data.needsSetup) {
|
|
3007
3325
|
configOverlay.classList.remove("hidden");
|
|
3008
3326
|
chatInput.disabled = true;
|
|
3009
3327
|
sendBtn.disabled = true;
|
|
3010
3328
|
const missingEl = document.getElementById("config-missing");
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3329
|
+
const overlayState = deriveSetupOverlayState({
|
|
3330
|
+
configuredProvider: data.configuredProvider ?? null,
|
|
3331
|
+
missing: data.missing ?? []
|
|
3332
|
+
});
|
|
3333
|
+
if (configOverlayIntro) {
|
|
3334
|
+
configOverlayIntro.textContent = overlayState.introText;
|
|
3335
|
+
}
|
|
3336
|
+
configOverlayQuickConnects?.classList.toggle("hidden", overlayState.hideQuickConnects);
|
|
3337
|
+
configOverlaySpotlight?.classList.toggle("hidden", overlayState.hideOpenRouterSpotlight);
|
|
3338
|
+
if (missingEl) {
|
|
3339
|
+
if (overlayState.missingItems.length > 0) {
|
|
3340
|
+
const hint = overlayState.showModelSelectionHint ? ` \u2014 select one from the <strong>model dropdown</strong> above, or set it in config` : "";
|
|
3341
|
+
const note = overlayState.modelSelectionNote ? ` ${escapeHtml(overlayState.modelSelectionNote)}` : "";
|
|
3342
|
+
missingEl.innerHTML = `<strong>Missing:</strong> ${overlayState.missingItems.map(escapeHtml).join(", ")}${hint}${note}`;
|
|
3343
|
+
missingEl.classList.remove("hidden");
|
|
3344
|
+
} else {
|
|
3345
|
+
missingEl.classList.add("hidden");
|
|
3346
|
+
missingEl.innerHTML = "";
|
|
3016
3347
|
}
|
|
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
3348
|
}
|
|
3022
3349
|
} else {
|
|
3023
3350
|
configOverlay.classList.add("hidden");
|
|
@@ -3029,8 +3356,9 @@ async function fetchStatus() {
|
|
|
3029
3356
|
missingEl.innerHTML = "";
|
|
3030
3357
|
}
|
|
3031
3358
|
if (configOverlayIntro) {
|
|
3032
|
-
configOverlayIntro.textContent =
|
|
3359
|
+
configOverlayIntro.textContent = DEFAULT_SETUP_INTRO;
|
|
3033
3360
|
}
|
|
3361
|
+
configOverlayQuickConnects?.classList.remove("hidden");
|
|
3034
3362
|
configOverlaySpotlight?.classList.remove("hidden");
|
|
3035
3363
|
}
|
|
3036
3364
|
} catch {
|
|
@@ -3079,6 +3407,9 @@ function handleServerMessage(msg) {
|
|
|
3079
3407
|
break;
|
|
3080
3408
|
case "tool_call_end":
|
|
3081
3409
|
finalizeToolCall(msg.name || "", msg.result || "", msg.elapsedMs || 0);
|
|
3410
|
+
if (GRAPH_REFRESH_TOOL_NAMES.has(msg.name || "")) {
|
|
3411
|
+
scheduleGraphDataRefresh();
|
|
3412
|
+
}
|
|
3082
3413
|
break;
|
|
3083
3414
|
case "turn_end":
|
|
3084
3415
|
if (hadToolCalls && msg.text) {
|
|
@@ -3099,6 +3430,7 @@ function handleServerMessage(msg) {
|
|
|
3099
3430
|
if (msg.usage) {
|
|
3100
3431
|
addUsageInfo(msg.usage);
|
|
3101
3432
|
}
|
|
3433
|
+
queueSessionAutoSave();
|
|
3102
3434
|
break;
|
|
3103
3435
|
case "error":
|
|
3104
3436
|
addMessage(`Error: ${msg.message || ""}`, "error");
|
|
@@ -3245,7 +3577,8 @@ function updateContextIndicator(contextTokens, maxContextTokens) {
|
|
|
3245
3577
|
}
|
|
3246
3578
|
contextLabel.textContent = pct + "%";
|
|
3247
3579
|
const indicator = document.getElementById("context-indicator");
|
|
3248
|
-
indicator.title = `Context: ~${contextTokens.toLocaleString()} / ${maxContextTokens.toLocaleString()} tokens (${pct}%)
|
|
3580
|
+
indicator.title = `Context: ~${contextTokens.toLocaleString()} / ${maxContextTokens.toLocaleString()} tokens (${pct}%)
|
|
3581
|
+
Adjust context size in Settings if you want it larger or smaller.`;
|
|
3249
3582
|
}
|
|
3250
3583
|
async function fetchContext() {
|
|
3251
3584
|
try {
|
|
@@ -3264,8 +3597,12 @@ function addUsageInfo(usage) {
|
|
|
3264
3597
|
function scrollToBottom() {
|
|
3265
3598
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3266
3599
|
}
|
|
3267
|
-
function
|
|
3600
|
+
function closeModelDropdown() {
|
|
3268
3601
|
modelDropdown.classList.add("hidden");
|
|
3602
|
+
modelSearchInput.value = "";
|
|
3603
|
+
}
|
|
3604
|
+
function closeHeaderMenus() {
|
|
3605
|
+
closeModelDropdown();
|
|
3269
3606
|
sessionDropdown.classList.add("hidden");
|
|
3270
3607
|
}
|
|
3271
3608
|
function formatSettingsValue(value) {
|
|
@@ -3279,16 +3616,31 @@ function clearSettingsBanner() {
|
|
|
3279
3616
|
settingsBanner.textContent = "";
|
|
3280
3617
|
settingsBanner.className = "settings-banner hidden";
|
|
3281
3618
|
}
|
|
3282
|
-
function
|
|
3619
|
+
function renderSessionProviderControls() {
|
|
3283
3620
|
if (sessionOpenRouterConnected) {
|
|
3284
3621
|
settingsOpenRouterSession.classList.remove("hidden");
|
|
3285
3622
|
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.";
|
|
3623
|
+
settingsOpenAiCompatibleSession.classList.add("hidden");
|
|
3624
|
+
settingsOpenAiCompatibleSessionMeta.textContent = "";
|
|
3286
3625
|
disconnectOpenRouterBtn.disabled = false;
|
|
3626
|
+
disconnectOpenAiCompatibleBtn.disabled = false;
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
if (sessionOpenAiCompatibleConnected) {
|
|
3630
|
+
settingsOpenAiCompatibleSession.classList.remove("hidden");
|
|
3631
|
+
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.";
|
|
3632
|
+
settingsOpenRouterSession.classList.add("hidden");
|
|
3633
|
+
settingsOpenRouterSessionMeta.textContent = "";
|
|
3634
|
+
disconnectOpenRouterBtn.disabled = false;
|
|
3635
|
+
disconnectOpenAiCompatibleBtn.disabled = false;
|
|
3287
3636
|
return;
|
|
3288
3637
|
}
|
|
3289
3638
|
settingsOpenRouterSession.classList.add("hidden");
|
|
3290
3639
|
settingsOpenRouterSessionMeta.textContent = "";
|
|
3640
|
+
settingsOpenAiCompatibleSession.classList.add("hidden");
|
|
3641
|
+
settingsOpenAiCompatibleSessionMeta.textContent = "";
|
|
3291
3642
|
disconnectOpenRouterBtn.disabled = false;
|
|
3643
|
+
disconnectOpenAiCompatibleBtn.disabled = false;
|
|
3292
3644
|
}
|
|
3293
3645
|
function createSettingsControl(entry, inputId) {
|
|
3294
3646
|
const value = entry.persistedValue;
|
|
@@ -3499,6 +3851,27 @@ function closeOpenRouterConnectModal() {
|
|
|
3499
3851
|
closeModal(openRouterConnectModal);
|
|
3500
3852
|
openRouterConnectContinueBtn.disabled = false;
|
|
3501
3853
|
}
|
|
3854
|
+
function openOpenAiCompatibleConnectModal() {
|
|
3855
|
+
closeHeaderMenus();
|
|
3856
|
+
openModal(openAiCompatibleConnectModal);
|
|
3857
|
+
const preset = inferOpenAiCompatiblePreset(activeBaseUrl);
|
|
3858
|
+
openAiCompatiblePresetSelect.value = preset;
|
|
3859
|
+
openAiCompatibleBaseUrlInput.value = preset === "custom" ? activeBaseUrl : OPENAI_COMPATIBLE_PRESETS[preset].baseUrl;
|
|
3860
|
+
openAiCompatibleApiKeyInput.value = "";
|
|
3861
|
+
openAiCompatiblePersistCheckbox.checked = false;
|
|
3862
|
+
openAiCompatibleConnectContinueBtn.disabled = false;
|
|
3863
|
+
applyOpenAiCompatiblePreset(preset, { preserveCustomValue: preset === "custom" });
|
|
3864
|
+
clearOpenAiCompatibleConnectStatus();
|
|
3865
|
+
requestAnimationFrame(() => {
|
|
3866
|
+
openAiCompatibleBaseUrlInput.focus();
|
|
3867
|
+
openAiCompatibleBaseUrlInput.select();
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
function closeOpenAiCompatibleConnectModal() {
|
|
3871
|
+
closeModal(openAiCompatibleConnectModal);
|
|
3872
|
+
openAiCompatibleConnectContinueBtn.disabled = false;
|
|
3873
|
+
clearOpenAiCompatibleConnectStatus();
|
|
3874
|
+
}
|
|
3502
3875
|
chatForm.addEventListener("submit", (e) => {
|
|
3503
3876
|
e.preventDefault();
|
|
3504
3877
|
const message = chatInput.value.trim();
|
|
@@ -3522,30 +3895,99 @@ chatInput.addEventListener("keydown", (e) => {
|
|
|
3522
3895
|
}
|
|
3523
3896
|
});
|
|
3524
3897
|
var activeModel = "";
|
|
3898
|
+
var availableModels = [];
|
|
3899
|
+
function focusModelSearchInput() {
|
|
3900
|
+
requestAnimationFrame(() => {
|
|
3901
|
+
modelSearchInput.focus();
|
|
3902
|
+
modelSearchInput.select();
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
function renderModelList() {
|
|
3906
|
+
const filteredModels = filterModelsByQuery(availableModels, modelSearchInput.value);
|
|
3907
|
+
if (availableModels.length === 0) {
|
|
3908
|
+
modelList.innerHTML = '<div class="dropdown-empty">No models available</div>';
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
if (filteredModels.length === 0) {
|
|
3912
|
+
modelList.innerHTML = '<div class="dropdown-empty">No matching models</div>';
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
modelList.innerHTML = "";
|
|
3916
|
+
for (const model of filteredModels) {
|
|
3917
|
+
const el = document.createElement("button");
|
|
3918
|
+
el.type = "button";
|
|
3919
|
+
el.className = "model-item" + (model.id === activeModel ? " active" : "");
|
|
3920
|
+
el.title = model.id;
|
|
3921
|
+
const body = document.createElement("div");
|
|
3922
|
+
body.className = "model-item-body";
|
|
3923
|
+
const name = document.createElement("span");
|
|
3924
|
+
name.className = "model-item-name";
|
|
3925
|
+
name.textContent = getModelDisplayName(model);
|
|
3926
|
+
body.appendChild(name);
|
|
3927
|
+
if ((model.name ?? "").trim() && getModelDisplayName(model) !== model.id) {
|
|
3928
|
+
const subtitle = document.createElement("span");
|
|
3929
|
+
subtitle.className = "model-item-subtitle";
|
|
3930
|
+
subtitle.textContent = model.id;
|
|
3931
|
+
body.appendChild(subtitle);
|
|
3932
|
+
}
|
|
3933
|
+
el.appendChild(body);
|
|
3934
|
+
if (model.id === activeModel) {
|
|
3935
|
+
const badge = document.createElement("span");
|
|
3936
|
+
badge.className = "model-item-badge";
|
|
3937
|
+
badge.textContent = "Active";
|
|
3938
|
+
el.appendChild(badge);
|
|
3939
|
+
}
|
|
3940
|
+
el.addEventListener("click", () => {
|
|
3941
|
+
void switchModel(model.id);
|
|
3942
|
+
});
|
|
3943
|
+
modelList.appendChild(el);
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3525
3946
|
modelBtn.addEventListener("click", (e) => {
|
|
3526
3947
|
e.stopPropagation();
|
|
3527
3948
|
const isOpen = !modelDropdown.classList.contains("hidden");
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
refreshModelList();
|
|
3949
|
+
if (isOpen) {
|
|
3950
|
+
closeModelDropdown();
|
|
3951
|
+
return;
|
|
3532
3952
|
}
|
|
3953
|
+
closeHeaderMenus();
|
|
3954
|
+
modelDropdown.classList.remove("hidden");
|
|
3955
|
+
modelSearchInput.value = "";
|
|
3956
|
+
void refreshModelList({ focusSearch: true });
|
|
3533
3957
|
});
|
|
3534
3958
|
document.addEventListener("click", (e) => {
|
|
3535
3959
|
if (!modelDropdown.contains(e.target) && e.target !== modelBtn) {
|
|
3536
|
-
|
|
3960
|
+
closeModelDropdown();
|
|
3961
|
+
}
|
|
3962
|
+
});
|
|
3963
|
+
modelSearchInput.addEventListener("input", () => {
|
|
3964
|
+
renderModelList();
|
|
3965
|
+
});
|
|
3966
|
+
modelSearchInput.addEventListener("keydown", (e) => {
|
|
3967
|
+
if (e.key === "Escape") {
|
|
3968
|
+
if (modelSearchInput.value) {
|
|
3969
|
+
modelSearchInput.value = "";
|
|
3970
|
+
renderModelList();
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
closeModelDropdown();
|
|
3537
3974
|
}
|
|
3538
3975
|
});
|
|
3539
|
-
|
|
3976
|
+
modelSearchInput.addEventListener("click", (e) => {
|
|
3977
|
+
e.stopPropagation();
|
|
3978
|
+
});
|
|
3979
|
+
async function refreshModelList(options = {}) {
|
|
3540
3980
|
try {
|
|
3541
3981
|
const res = await fetch("/api/models");
|
|
3542
3982
|
const data = await res.json();
|
|
3543
3983
|
activeModel = data.activeModel;
|
|
3984
|
+
availableModels = data.models ?? [];
|
|
3544
3985
|
const hasActiveModel = !!activeModel && data.models.some((model) => model.id === activeModel);
|
|
3545
3986
|
if (!data.models || data.models.length === 0) {
|
|
3546
3987
|
modelInfo.textContent = "Select model";
|
|
3547
3988
|
modelInfo.classList.add("placeholder");
|
|
3548
|
-
|
|
3989
|
+
availableModels = [];
|
|
3990
|
+
renderModelList();
|
|
3549
3991
|
return;
|
|
3550
3992
|
}
|
|
3551
3993
|
if (hasActiveModel) {
|
|
@@ -3555,18 +3997,12 @@ async function refreshModelList() {
|
|
|
3555
3997
|
modelInfo.textContent = "Select model";
|
|
3556
3998
|
modelInfo.classList.add("placeholder");
|
|
3557
3999
|
}
|
|
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);
|
|
4000
|
+
renderModelList();
|
|
4001
|
+
if (options.focusSearch && !modelDropdown.classList.contains("hidden")) {
|
|
4002
|
+
focusModelSearchInput();
|
|
3568
4003
|
}
|
|
3569
4004
|
} catch {
|
|
4005
|
+
availableModels = [];
|
|
3570
4006
|
modelList.innerHTML = '<div class="dropdown-empty">Failed to load models</div>';
|
|
3571
4007
|
}
|
|
3572
4008
|
}
|
|
@@ -3587,7 +4023,8 @@ async function switchModel(modelId) {
|
|
|
3587
4023
|
modelInfo.textContent = modelId || "Select model";
|
|
3588
4024
|
modelInfo.classList.toggle("placeholder", !modelId);
|
|
3589
4025
|
activeModel = modelId;
|
|
3590
|
-
|
|
4026
|
+
renderModelList();
|
|
4027
|
+
closeModelDropdown();
|
|
3591
4028
|
if (body.persistedToEnv) {
|
|
3592
4029
|
addMessage(`Model switched to: ${modelId}. Saved as the default in ~/.minicode/.env.`, "thinking");
|
|
3593
4030
|
} else {
|
|
@@ -3603,7 +4040,7 @@ sessionBtn.addEventListener("click", (e) => {
|
|
|
3603
4040
|
e.stopPropagation();
|
|
3604
4041
|
const isOpen = !sessionDropdown.classList.contains("hidden");
|
|
3605
4042
|
sessionDropdown.classList.toggle("hidden");
|
|
3606
|
-
|
|
4043
|
+
closeModelDropdown();
|
|
3607
4044
|
if (!isOpen) {
|
|
3608
4045
|
refreshSessionList();
|
|
3609
4046
|
}
|
|
@@ -3613,27 +4050,130 @@ document.addEventListener("click", (e) => {
|
|
|
3613
4050
|
sessionDropdown.classList.add("hidden");
|
|
3614
4051
|
}
|
|
3615
4052
|
});
|
|
4053
|
+
function loadSessionAutoSavePreference() {
|
|
4054
|
+
try {
|
|
4055
|
+
return localStorage.getItem(SESSION_AUTOSAVE_KEY) === "1";
|
|
4056
|
+
} catch {
|
|
4057
|
+
return false;
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
function persistSessionAutoSavePreference(enabled) {
|
|
4061
|
+
try {
|
|
4062
|
+
if (enabled) {
|
|
4063
|
+
localStorage.setItem(SESSION_AUTOSAVE_KEY, "1");
|
|
4064
|
+
} else {
|
|
4065
|
+
localStorage.removeItem(SESSION_AUTOSAVE_KEY);
|
|
4066
|
+
}
|
|
4067
|
+
} catch {
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
function buildAutoSaveLabel() {
|
|
4071
|
+
return `${SESSION_AUTOSAVE_LABEL_PREFIX} ${(/* @__PURE__ */ new Date()).toLocaleString()}`;
|
|
4072
|
+
}
|
|
4073
|
+
async function persistCurrentSession(label) {
|
|
4074
|
+
const res = await fetch("/api/sessions/save", {
|
|
4075
|
+
method: "POST",
|
|
4076
|
+
headers: { "Content-Type": "application/json" },
|
|
4077
|
+
body: JSON.stringify({ label })
|
|
4078
|
+
});
|
|
4079
|
+
const body = await res.json();
|
|
4080
|
+
if (!res.ok) {
|
|
4081
|
+
throw new Error("error" in body ? body.error : `Failed to save session (${res.status})`);
|
|
4082
|
+
}
|
|
4083
|
+
return body;
|
|
4084
|
+
}
|
|
4085
|
+
async function deleteSavedSession(session) {
|
|
4086
|
+
const isCurrentSavedSession = activeSavedSession?.id === session.id;
|
|
4087
|
+
const confirmed = window.confirm(`Delete saved session "${session.label}"?`);
|
|
4088
|
+
if (!confirmed) {
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
try {
|
|
4092
|
+
const res = await fetch(`/api/sessions/${encodeURIComponent(session.id)}`, {
|
|
4093
|
+
method: "DELETE"
|
|
4094
|
+
});
|
|
4095
|
+
const body = await res.json();
|
|
4096
|
+
if (!res.ok) {
|
|
4097
|
+
throw new Error("error" in body ? body.error : `Failed to delete session (${res.status})`);
|
|
4098
|
+
}
|
|
4099
|
+
if (isCurrentSavedSession) {
|
|
4100
|
+
activeSavedSession = null;
|
|
4101
|
+
}
|
|
4102
|
+
if (pendingAutoSaveLabel === session.label) {
|
|
4103
|
+
pendingAutoSaveLabel = null;
|
|
4104
|
+
}
|
|
4105
|
+
addMessage(
|
|
4106
|
+
isCurrentSavedSession ? `Deleted saved session "${session.label}". The current chat stays open until you load another session or refresh.` : `Session deleted: "${session.label}"`,
|
|
4107
|
+
"thinking"
|
|
4108
|
+
);
|
|
4109
|
+
await refreshSessionList();
|
|
4110
|
+
} catch (error) {
|
|
4111
|
+
const message = error instanceof Error ? error.message : "Failed to delete session";
|
|
4112
|
+
addMessage(message, "error");
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
async function maybeAutoSaveSession() {
|
|
4116
|
+
if (!sessionAutoSaveEnabled) {
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
const label = activeSavedSession?.label ?? pendingAutoSaveLabel ?? buildAutoSaveLabel();
|
|
4120
|
+
try {
|
|
4121
|
+
const data = await persistCurrentSession(label);
|
|
4122
|
+
pendingAutoSaveLabel = data.label;
|
|
4123
|
+
await refreshSessionList();
|
|
4124
|
+
} catch (error) {
|
|
4125
|
+
const message = error instanceof Error ? error.message : "Failed to auto-save session";
|
|
4126
|
+
addMessage(message, "error");
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
function queueSessionAutoSave() {
|
|
4130
|
+
if (!sessionAutoSaveEnabled) {
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
if (autoSaveInFlight) {
|
|
4134
|
+
autoSaveQueued = true;
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
autoSaveInFlight = (async () => {
|
|
4138
|
+
await maybeAutoSaveSession();
|
|
4139
|
+
})();
|
|
4140
|
+
void autoSaveInFlight.finally(() => {
|
|
4141
|
+
autoSaveInFlight = null;
|
|
4142
|
+
if (autoSaveQueued) {
|
|
4143
|
+
autoSaveQueued = false;
|
|
4144
|
+
queueSessionAutoSave();
|
|
4145
|
+
}
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
sessionAutoSaveToggle.addEventListener("change", () => {
|
|
4149
|
+
sessionAutoSaveEnabled = sessionAutoSaveToggle.checked;
|
|
4150
|
+
persistSessionAutoSavePreference(sessionAutoSaveEnabled);
|
|
4151
|
+
if (sessionAutoSaveEnabled) {
|
|
4152
|
+
addMessage(
|
|
4153
|
+
activeSavedSession ? `Auto-save enabled. minicode will update "${activeSavedSession.label}" after each completed turn.` : "Auto-save enabled. minicode will save this chat after the next completed turn.",
|
|
4154
|
+
"thinking"
|
|
4155
|
+
);
|
|
4156
|
+
} else {
|
|
4157
|
+
addMessage("Auto-save disabled.", "thinking");
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
3616
4160
|
saveBtn.addEventListener("click", async () => {
|
|
3617
4161
|
const requestedLabel = saveLabelInput.value.trim();
|
|
3618
4162
|
const label = requestedLabel || activeSavedSession?.label || void 0;
|
|
3619
4163
|
const isUpdatingCurrentSession = !!activeSavedSession && (requestedLabel.length === 0 || requestedLabel === activeSavedSession.label);
|
|
3620
4164
|
try {
|
|
3621
4165
|
saveBtn.setAttribute("disabled", "true");
|
|
3622
|
-
const
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
);
|
|
3634
|
-
await refreshSessionList();
|
|
3635
|
-
}
|
|
3636
|
-
} catch {
|
|
4166
|
+
const data = await persistCurrentSession(label);
|
|
4167
|
+
saveLabelInput.value = "";
|
|
4168
|
+
pendingAutoSaveLabel = data.label;
|
|
4169
|
+
addMessage(
|
|
4170
|
+
`${isUpdatingCurrentSession ? "Session updated" : "Session saved"}: "${data.label}"`,
|
|
4171
|
+
"thinking"
|
|
4172
|
+
);
|
|
4173
|
+
await refreshSessionList();
|
|
4174
|
+
} catch (error) {
|
|
4175
|
+
const message = error instanceof Error ? error.message : "Failed to save session";
|
|
4176
|
+
addMessage(message, "error");
|
|
3637
4177
|
} finally {
|
|
3638
4178
|
saveBtn.removeAttribute("disabled");
|
|
3639
4179
|
}
|
|
@@ -3655,6 +4195,7 @@ async function refreshSessionList() {
|
|
|
3655
4195
|
const sessions = data.sessions;
|
|
3656
4196
|
activeSavedSession = sessions.find((session) => session.id === data.currentSessionId) ?? null;
|
|
3657
4197
|
if (activeSavedSession) {
|
|
4198
|
+
pendingAutoSaveLabel = activeSavedSession.label;
|
|
3658
4199
|
sessionUpdateRow.classList.remove("hidden");
|
|
3659
4200
|
sessionUpdateBtn.textContent = `Update "${activeSavedSession.label}"`;
|
|
3660
4201
|
sessionUpdateBtn.title = `Save changes back to "${activeSavedSession.label}"`;
|
|
@@ -3672,8 +4213,22 @@ async function refreshSessionList() {
|
|
|
3672
4213
|
const el = document.createElement("div");
|
|
3673
4214
|
const isActive = activeSavedSession?.id === s.id;
|
|
3674
4215
|
el.className = "session-item" + (isActive ? " active" : "");
|
|
3675
|
-
|
|
3676
|
-
|
|
4216
|
+
const loadBtn = document.createElement("button");
|
|
4217
|
+
loadBtn.type = "button";
|
|
4218
|
+
loadBtn.className = "session-load-btn";
|
|
4219
|
+
loadBtn.innerHTML = `<span class="session-label">${escapeHtml(s.label)}</span><span class="session-meta">${s.messageCount} msgs${isActive ? ' <span class="session-active-badge">\u2022 active</span>' : ""}</span>`;
|
|
4220
|
+
loadBtn.addEventListener("click", () => loadSession(s.label));
|
|
4221
|
+
const deleteBtn = document.createElement("button");
|
|
4222
|
+
deleteBtn.type = "button";
|
|
4223
|
+
deleteBtn.className = "session-delete-btn";
|
|
4224
|
+
deleteBtn.textContent = "Delete";
|
|
4225
|
+
deleteBtn.title = `Delete "${s.label}"`;
|
|
4226
|
+
deleteBtn.addEventListener("click", (event) => {
|
|
4227
|
+
event.stopPropagation();
|
|
4228
|
+
void deleteSavedSession(s);
|
|
4229
|
+
});
|
|
4230
|
+
el.appendChild(loadBtn);
|
|
4231
|
+
el.appendChild(deleteBtn);
|
|
3677
4232
|
sessionList.appendChild(el);
|
|
3678
4233
|
}
|
|
3679
4234
|
} catch {
|
|
@@ -3695,6 +4250,7 @@ async function loadSession(label) {
|
|
|
3695
4250
|
if (res.ok) {
|
|
3696
4251
|
const body = await res.json();
|
|
3697
4252
|
sessionDropdown.classList.add("hidden");
|
|
4253
|
+
pendingAutoSaveLabel = body.label;
|
|
3698
4254
|
renderLoadedSessionMessages(body.messages);
|
|
3699
4255
|
if (body.messages.length === 0) {
|
|
3700
4256
|
addMessage(`Session "${body.label}" restored`, "thinking");
|
|
@@ -3710,17 +4266,13 @@ sessionUpdateBtn.addEventListener("click", async () => {
|
|
|
3710
4266
|
}
|
|
3711
4267
|
try {
|
|
3712
4268
|
sessionUpdateBtn.disabled = true;
|
|
3713
|
-
const
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
addMessage(`Session updated: "${data.label}"`, "thinking");
|
|
3721
|
-
await refreshSessionList();
|
|
3722
|
-
}
|
|
3723
|
-
} catch {
|
|
4269
|
+
const data = await persistCurrentSession(activeSavedSession.label);
|
|
4270
|
+
pendingAutoSaveLabel = data.label;
|
|
4271
|
+
addMessage(`Session updated: "${data.label}"`, "thinking");
|
|
4272
|
+
await refreshSessionList();
|
|
4273
|
+
} catch (error) {
|
|
4274
|
+
const message = error instanceof Error ? error.message : "Failed to update session";
|
|
4275
|
+
addMessage(message, "error");
|
|
3724
4276
|
} finally {
|
|
3725
4277
|
sessionUpdateBtn.disabled = false;
|
|
3726
4278
|
}
|
|
@@ -3743,10 +4295,19 @@ openRouterConnectCloseBtn.addEventListener("click", () => {
|
|
|
3743
4295
|
openRouterConnectCancelBtn.addEventListener("click", () => {
|
|
3744
4296
|
closeOpenRouterConnectModal();
|
|
3745
4297
|
});
|
|
4298
|
+
openAiCompatibleConnectBackdrop.addEventListener("click", () => {
|
|
4299
|
+
closeOpenAiCompatibleConnectModal();
|
|
4300
|
+
});
|
|
4301
|
+
openAiCompatibleConnectCloseBtn.addEventListener("click", () => {
|
|
4302
|
+
closeOpenAiCompatibleConnectModal();
|
|
4303
|
+
});
|
|
4304
|
+
openAiCompatibleConnectCancelBtn.addEventListener("click", () => {
|
|
4305
|
+
closeOpenAiCompatibleConnectModal();
|
|
4306
|
+
});
|
|
3746
4307
|
settingsResetBtn.addEventListener("click", () => {
|
|
3747
4308
|
clearSettingsBanner();
|
|
3748
4309
|
renderSettings();
|
|
3749
|
-
|
|
4310
|
+
renderSessionProviderControls();
|
|
3750
4311
|
});
|
|
3751
4312
|
settingsSaveBtn.addEventListener("click", async () => {
|
|
3752
4313
|
if (!settingsPayload) {
|
|
@@ -3798,6 +4359,9 @@ settingsList.addEventListener("change", () => {
|
|
|
3798
4359
|
disconnectOpenRouterBtn.addEventListener("click", () => {
|
|
3799
4360
|
void disconnectOpenRouter();
|
|
3800
4361
|
});
|
|
4362
|
+
disconnectOpenAiCompatibleBtn.addEventListener("click", () => {
|
|
4363
|
+
void disconnectOpenAiCompatible();
|
|
4364
|
+
});
|
|
3801
4365
|
document.addEventListener("keydown", (event) => {
|
|
3802
4366
|
if (event.key !== "Escape") {
|
|
3803
4367
|
return;
|
|
@@ -3806,6 +4370,10 @@ document.addEventListener("keydown", (event) => {
|
|
|
3806
4370
|
closeOpenRouterConnectModal();
|
|
3807
4371
|
return;
|
|
3808
4372
|
}
|
|
4373
|
+
if (isOpenAiCompatibleConnectModalOpen()) {
|
|
4374
|
+
closeOpenAiCompatibleConnectModal();
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
3809
4377
|
if (isSettingsModalOpen()) {
|
|
3810
4378
|
closeSettings();
|
|
3811
4379
|
}
|
|
@@ -3815,11 +4383,29 @@ for (const button of connectOpenRouterButtons) {
|
|
|
3815
4383
|
openOpenRouterConnectModal();
|
|
3816
4384
|
});
|
|
3817
4385
|
}
|
|
4386
|
+
for (const button of connectOpenAiCompatibleButtons) {
|
|
4387
|
+
button.addEventListener("click", () => {
|
|
4388
|
+
openOpenAiCompatibleConnectModal();
|
|
4389
|
+
});
|
|
4390
|
+
}
|
|
4391
|
+
openAiCompatiblePresetSelect.addEventListener("change", () => {
|
|
4392
|
+
applyOpenAiCompatiblePreset(openAiCompatiblePresetSelect.value);
|
|
4393
|
+
clearOpenAiCompatibleConnectStatus();
|
|
4394
|
+
});
|
|
4395
|
+
openAiCompatibleBaseUrlInput.addEventListener("input", () => {
|
|
4396
|
+
clearOpenAiCompatibleConnectStatus();
|
|
4397
|
+
});
|
|
4398
|
+
openAiCompatibleApiKeyInput.addEventListener("input", () => {
|
|
4399
|
+
clearOpenAiCompatibleConnectStatus();
|
|
4400
|
+
});
|
|
3818
4401
|
openRouterConnectContinueBtn.addEventListener("click", () => {
|
|
3819
4402
|
openRouterConnectContinueBtn.disabled = true;
|
|
3820
4403
|
closeOpenRouterConnectModal();
|
|
3821
4404
|
void startOpenRouterConnect(openRouterPersistCheckbox.checked);
|
|
3822
4405
|
});
|
|
4406
|
+
openAiCompatibleConnectContinueBtn.addEventListener("click", () => {
|
|
4407
|
+
void connectOpenAiCompatible();
|
|
4408
|
+
});
|
|
3823
4409
|
var chatPane = document.getElementById("chat-pane");
|
|
3824
4410
|
var divider = document.getElementById("pane-divider");
|
|
3825
4411
|
divider.addEventListener("mousedown", (e) => {
|