@sean.holung/minicode 0.3.5 → 0.3.7
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/README.md +22 -45
- package/dist/scripts/run-benchmarks.js +1 -0
- package/dist/src/agent/config.js +53 -66
- package/dist/src/agent/editable-config.js +56 -58
- package/dist/src/agent/home-env.js +74 -0
- package/dist/src/cli/config-slash-command.js +15 -13
- package/dist/src/serve/agent-bridge.js +87 -28
- package/dist/src/serve/mcp-server.js +19 -13
- package/dist/src/serve/server.js +190 -4
- package/dist/src/session/session-preview.js +14 -0
- package/dist/src/shared/graph-search.js +80 -0
- package/dist/src/shared/graph-selection.js +40 -0
- package/dist/src/shared/symbol-search.js +156 -0
- package/dist/src/tools/search-code-map.js +27 -35
- package/dist/src/web/app.js +582 -64
- package/dist/src/web/index.html +84 -6
- package/dist/src/web/style.css +256 -1
- package/dist/tests/config-api.test.js +10 -5
- package/dist/tests/config-integration.test.js +130 -56
- package/dist/tests/config-slash-command.test.js +12 -11
- package/dist/tests/config.test.js +21 -4
- package/dist/tests/editable-config.test.js +15 -12
- package/dist/tests/graph-onboarding.test.js +22 -1
- package/dist/tests/graph-search.test.js +66 -0
- package/dist/tests/graph-selection.test.js +58 -0
- package/dist/tests/home-env.test.js +56 -0
- package/dist/tests/mcp-and-plugin.test.js +3 -0
- package/dist/tests/search-code-map.test.js +9 -0
- package/dist/tests/serve.integration.test.js +255 -6
- package/dist/tests/session-preview.test.js +56 -0
- package/dist/tests/session-ui.test.js +2 -0
- package/dist/tests/settings-ui.test.js +18 -0
- package/dist/tests/system-prompt.test.js +1 -0
- package/dist/tests/test-utils.js +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +8 -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 +143 -27
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js +87 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js +1 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.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
|
@@ -2,6 +2,22 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
|
+
// src/web/modal-state.ts
|
|
6
|
+
function syncBodyModalOpenState() {
|
|
7
|
+
const anyModalOpen = [...document.querySelectorAll(".modal")].some((modal) => !modal.classList.contains("hidden"));
|
|
8
|
+
document.body.classList.toggle("modal-open", anyModalOpen);
|
|
9
|
+
}
|
|
10
|
+
function openModal(modal) {
|
|
11
|
+
modal.classList.remove("hidden");
|
|
12
|
+
modal.setAttribute("aria-hidden", "false");
|
|
13
|
+
syncBodyModalOpenState();
|
|
14
|
+
}
|
|
15
|
+
function closeModal(modal) {
|
|
16
|
+
modal.classList.add("hidden");
|
|
17
|
+
modal.setAttribute("aria-hidden", "true");
|
|
18
|
+
syncBodyModalOpenState();
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
// node_modules/marked/lib/marked.esm.js
|
|
6
22
|
function M() {
|
|
7
23
|
return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
|
|
@@ -1523,10 +1539,135 @@ function resolveGraphNodeIds(nodes, symbolName) {
|
|
|
1523
1539
|
return [...nodes.entries()].filter(([id, node]) => matchesGraphNodeQuery(query, node, id)).map(([id]) => id).sort((a, b2) => compareGraphNodeIds(a, b2, nodes));
|
|
1524
1540
|
}
|
|
1525
1541
|
|
|
1542
|
+
// src/shared/graph-search.ts
|
|
1543
|
+
function getGraphNodeFilePath(node) {
|
|
1544
|
+
return node.filePath || node.file || "";
|
|
1545
|
+
}
|
|
1546
|
+
function buildGraphFileIndex(nodes) {
|
|
1547
|
+
const files = /* @__PURE__ */ new Map();
|
|
1548
|
+
for (const [id, node] of nodes) {
|
|
1549
|
+
const filePath = getGraphNodeFilePath(node);
|
|
1550
|
+
if (!filePath) continue;
|
|
1551
|
+
const existing = files.get(filePath);
|
|
1552
|
+
if (existing) {
|
|
1553
|
+
existing.push(id);
|
|
1554
|
+
} else {
|
|
1555
|
+
files.set(filePath, [id]);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
for (const symbolIds of files.values()) {
|
|
1559
|
+
symbolIds.sort((a, b2) => compareGraphNodeIds(a, b2, nodes));
|
|
1560
|
+
}
|
|
1561
|
+
return files;
|
|
1562
|
+
}
|
|
1563
|
+
function compareGraphFilePaths(a, b2, fileIndex) {
|
|
1564
|
+
const countDifference = (fileIndex.get(b2)?.length ?? 0) - (fileIndex.get(a)?.length ?? 0);
|
|
1565
|
+
if (countDifference !== 0) {
|
|
1566
|
+
return countDifference;
|
|
1567
|
+
}
|
|
1568
|
+
return a.localeCompare(b2, void 0, { sensitivity: "base" });
|
|
1569
|
+
}
|
|
1570
|
+
function matchesGraphFileQuery(query, filePath) {
|
|
1571
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1572
|
+
if (normalizedQuery.length === 0) {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
return filePath.toLowerCase().includes(normalizedQuery);
|
|
1576
|
+
}
|
|
1577
|
+
function buildGraphSearchResults({
|
|
1578
|
+
query,
|
|
1579
|
+
symbolIds,
|
|
1580
|
+
nodes,
|
|
1581
|
+
fileIndex,
|
|
1582
|
+
symbolLimit = 12,
|
|
1583
|
+
fileLimit = 8
|
|
1584
|
+
}) {
|
|
1585
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1586
|
+
const showDefaultResults = normalizedQuery.length < 2;
|
|
1587
|
+
const rankedFiles = [...fileIndex.keys()].sort((a, b2) => compareGraphFilePaths(a, b2, fileIndex));
|
|
1588
|
+
const symbolResults = symbolIds.filter((id) => {
|
|
1589
|
+
if (showDefaultResults) {
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
return matchesGraphNodeQuery(normalizedQuery, nodes.get(id) || {}, id);
|
|
1593
|
+
}).slice(0, symbolLimit).map((id) => {
|
|
1594
|
+
const node = nodes.get(id) || {};
|
|
1595
|
+
return {
|
|
1596
|
+
type: "symbol",
|
|
1597
|
+
id,
|
|
1598
|
+
label: getGraphNodeLabel(node, id),
|
|
1599
|
+
subtitle: getGraphNodeFilePath(node),
|
|
1600
|
+
kind: (node.kind || "symbol").toLowerCase()
|
|
1601
|
+
};
|
|
1602
|
+
});
|
|
1603
|
+
const fileResults = rankedFiles.filter((filePath) => {
|
|
1604
|
+
if (showDefaultResults) {
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1607
|
+
return matchesGraphFileQuery(normalizedQuery, filePath);
|
|
1608
|
+
}).slice(0, fileLimit).map((filePath) => {
|
|
1609
|
+
const symbolCount = fileIndex.get(filePath)?.length ?? 0;
|
|
1610
|
+
return {
|
|
1611
|
+
type: "file",
|
|
1612
|
+
id: filePath,
|
|
1613
|
+
label: filePath,
|
|
1614
|
+
subtitle: `${symbolCount} symbol${symbolCount === 1 ? "" : "s"}`,
|
|
1615
|
+
kind: "file",
|
|
1616
|
+
symbolCount
|
|
1617
|
+
};
|
|
1618
|
+
});
|
|
1619
|
+
return [...symbolResults, ...fileResults];
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/shared/graph-selection.ts
|
|
1623
|
+
function buildGraphEdgeId(edge) {
|
|
1624
|
+
return `${edge.source}->${edge.target}:${edge.kind}`;
|
|
1625
|
+
}
|
|
1626
|
+
function buildGraphEdgeIndex(edges) {
|
|
1627
|
+
const edgeIndex2 = /* @__PURE__ */ new Map();
|
|
1628
|
+
for (const edge of edges) {
|
|
1629
|
+
const sourceEdges = edgeIndex2.get(edge.source);
|
|
1630
|
+
if (sourceEdges) {
|
|
1631
|
+
sourceEdges.push(edge);
|
|
1632
|
+
} else {
|
|
1633
|
+
edgeIndex2.set(edge.source, [edge]);
|
|
1634
|
+
}
|
|
1635
|
+
const targetEdges = edgeIndex2.get(edge.target);
|
|
1636
|
+
if (targetEdges) {
|
|
1637
|
+
targetEdges.push(edge);
|
|
1638
|
+
} else {
|
|
1639
|
+
edgeIndex2.set(edge.target, [edge]);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return edgeIndex2;
|
|
1643
|
+
}
|
|
1644
|
+
function buildFileFocusedSelection({
|
|
1645
|
+
filePath,
|
|
1646
|
+
fileIndex,
|
|
1647
|
+
edgeIndex: edgeIndex2
|
|
1648
|
+
}) {
|
|
1649
|
+
const fileSymbolIds = fileIndex.get(filePath) || [];
|
|
1650
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
1651
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1652
|
+
for (const symbolId of fileSymbolIds) {
|
|
1653
|
+
nodeIds.add(symbolId);
|
|
1654
|
+
for (const edge of edgeIndex2.get(symbolId) || []) {
|
|
1655
|
+
nodeIds.add(edge.source);
|
|
1656
|
+
nodeIds.add(edge.target);
|
|
1657
|
+
edges.set(buildGraphEdgeId(edge), edge);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
nodeIds: [...nodeIds],
|
|
1662
|
+
edges: [...edges.values()]
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1526
1666
|
// src/web/graph.ts
|
|
1527
1667
|
var cy = null;
|
|
1528
1668
|
var graphNodes = /* @__PURE__ */ new Map();
|
|
1529
1669
|
var graphEdges = [];
|
|
1670
|
+
var fileToSymbolIds = /* @__PURE__ */ new Map();
|
|
1530
1671
|
var edgeIndex = /* @__PURE__ */ new Map();
|
|
1531
1672
|
var pinnedNames = /* @__PURE__ */ new Set();
|
|
1532
1673
|
var allSymbolNames = [];
|
|
@@ -1535,6 +1676,8 @@ var analysisReport = null;
|
|
|
1535
1676
|
var activeAnalysisFindingId = null;
|
|
1536
1677
|
var activeAnalysisFilter = "all";
|
|
1537
1678
|
var analysisExplanationCache = /* @__PURE__ */ new Map();
|
|
1679
|
+
var filePreviewModalInitialized = false;
|
|
1680
|
+
var latestFilePreviewRequestId = 0;
|
|
1538
1681
|
var LAYOUT_OPTIONS = {
|
|
1539
1682
|
name: "cose",
|
|
1540
1683
|
nodeRepulsion: function() {
|
|
@@ -1560,6 +1703,7 @@ async function initGraph() {
|
|
|
1560
1703
|
initialized = true;
|
|
1561
1704
|
const cyEl = document.getElementById("cy");
|
|
1562
1705
|
const detailEl = document.getElementById("symbol-detail");
|
|
1706
|
+
setupFilePreviewModal();
|
|
1563
1707
|
try {
|
|
1564
1708
|
const [graphRes, symbolsRes, focusRes] = await Promise.all([
|
|
1565
1709
|
fetch("/api/graph"),
|
|
@@ -1585,7 +1729,8 @@ async function initGraph() {
|
|
|
1585
1729
|
target: e.target || e.to || "",
|
|
1586
1730
|
kind: (e.kind || e.type || "references").toLowerCase()
|
|
1587
1731
|
}));
|
|
1588
|
-
|
|
1732
|
+
edgeIndex = buildGraphEdgeIndex(graphEdges);
|
|
1733
|
+
fileToSymbolIds = buildGraphFileIndex(graphNodes);
|
|
1589
1734
|
allSymbolNames = Array.from(graphNodes.keys()).sort();
|
|
1590
1735
|
const pinned = focusData.pinned || [];
|
|
1591
1736
|
for (const f of pinned) {
|
|
@@ -1632,30 +1777,13 @@ function showOnboardingHint(container) {
|
|
|
1632
1777
|
if (container.querySelector(".graph-onboarding")) return;
|
|
1633
1778
|
const hint = document.createElement("div");
|
|
1634
1779
|
hint.className = "graph-onboarding";
|
|
1635
|
-
hint.innerHTML = '<div class="graph-onboarding-icon">◆ — ◆</div><div class="graph-onboarding-title">Code dependency graph</div><div class="graph-onboarding-subtitle">Search for a symbol above to start exploring.<br/>Nodes expand on click to reveal connections.</div>';
|
|
1780
|
+
hint.innerHTML = '<div class="graph-onboarding-icon">◆ — ◆</div><div class="graph-onboarding-title">Code dependency graph</div><div class="graph-onboarding-subtitle">Search for a symbol or file above to start exploring.<br/>Nodes expand on click to reveal connections.</div>';
|
|
1636
1781
|
container.appendChild(hint);
|
|
1637
1782
|
}
|
|
1638
1783
|
function removeOnboardingHint() {
|
|
1639
1784
|
const hint = document.querySelector(".graph-onboarding");
|
|
1640
1785
|
if (hint) hint.remove();
|
|
1641
1786
|
}
|
|
1642
|
-
function buildEdgeIndex() {
|
|
1643
|
-
edgeIndex.clear();
|
|
1644
|
-
for (const edge of graphEdges) {
|
|
1645
|
-
let srcList = edgeIndex.get(edge.source);
|
|
1646
|
-
if (!srcList) {
|
|
1647
|
-
srcList = [];
|
|
1648
|
-
edgeIndex.set(edge.source, srcList);
|
|
1649
|
-
}
|
|
1650
|
-
srcList.push(edge);
|
|
1651
|
-
let tgtList = edgeIndex.get(edge.target);
|
|
1652
|
-
if (!tgtList) {
|
|
1653
|
-
tgtList = [];
|
|
1654
|
-
edgeIndex.set(edge.target, tgtList);
|
|
1655
|
-
}
|
|
1656
|
-
tgtList.push(edge);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
1787
|
function addNodeNeighborhood(symbolId, maxDegrees = 1) {
|
|
1660
1788
|
const visited = /* @__PURE__ */ new Set();
|
|
1661
1789
|
let frontier = /* @__PURE__ */ new Set([symbolId]);
|
|
@@ -1690,6 +1818,102 @@ function renderNodeNeighborhoodAndLayout(symbolId, maxDegrees = 1) {
|
|
|
1690
1818
|
runLayout();
|
|
1691
1819
|
}
|
|
1692
1820
|
}
|
|
1821
|
+
function focusFileInGraph(filePath) {
|
|
1822
|
+
if (!cy) return;
|
|
1823
|
+
const selection = buildFileFocusedSelection({
|
|
1824
|
+
filePath,
|
|
1825
|
+
fileIndex: fileToSymbolIds,
|
|
1826
|
+
edgeIndex
|
|
1827
|
+
});
|
|
1828
|
+
cy.elements().remove();
|
|
1829
|
+
document.getElementById("symbol-detail")?.classList.add("hidden");
|
|
1830
|
+
if (selection.nodeIds.length === 0) {
|
|
1831
|
+
const cyEl = document.getElementById("cy");
|
|
1832
|
+
if (cyEl) showOnboardingHint(cyEl);
|
|
1833
|
+
refreshAnalysisGraphState();
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
for (const symbolId of selection.nodeIds) {
|
|
1837
|
+
addNodeToGraph(symbolId);
|
|
1838
|
+
}
|
|
1839
|
+
for (const edge of selection.edges) {
|
|
1840
|
+
addEdgeToGraph(edge);
|
|
1841
|
+
}
|
|
1842
|
+
refreshAnalysisGraphState();
|
|
1843
|
+
runLayout();
|
|
1844
|
+
}
|
|
1845
|
+
function getFilePreviewLanguage(filePath) {
|
|
1846
|
+
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
1847
|
+
const langMap = {
|
|
1848
|
+
ts: "typescript",
|
|
1849
|
+
tsx: "typescript",
|
|
1850
|
+
js: "javascript",
|
|
1851
|
+
jsx: "javascript",
|
|
1852
|
+
json: "json",
|
|
1853
|
+
md: "markdown",
|
|
1854
|
+
css: "css",
|
|
1855
|
+
html: "xml"
|
|
1856
|
+
};
|
|
1857
|
+
return langMap[ext] || "plaintext";
|
|
1858
|
+
}
|
|
1859
|
+
function closeFilePreview() {
|
|
1860
|
+
const modal = document.getElementById("file-preview-modal");
|
|
1861
|
+
if (!modal) return;
|
|
1862
|
+
closeModal(modal);
|
|
1863
|
+
}
|
|
1864
|
+
function setupFilePreviewModal() {
|
|
1865
|
+
if (filePreviewModalInitialized) return;
|
|
1866
|
+
filePreviewModalInitialized = true;
|
|
1867
|
+
const modal = document.getElementById("file-preview-modal");
|
|
1868
|
+
const backdrop = document.getElementById("file-preview-backdrop");
|
|
1869
|
+
const closeBtn = document.getElementById("file-preview-close");
|
|
1870
|
+
if (!modal || !backdrop || !closeBtn) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
backdrop.addEventListener("click", () => closeFilePreview());
|
|
1874
|
+
closeBtn.addEventListener("click", () => closeFilePreview());
|
|
1875
|
+
document.addEventListener("keydown", (event) => {
|
|
1876
|
+
if (event.key === "Escape" && !modal.classList.contains("hidden")) {
|
|
1877
|
+
closeFilePreview();
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
async function openFilePreview(filePath) {
|
|
1882
|
+
const modal = document.getElementById("file-preview-modal");
|
|
1883
|
+
const pathEl = document.getElementById("file-preview-path");
|
|
1884
|
+
const codeEl = document.getElementById("file-preview-code");
|
|
1885
|
+
if (!modal || !pathEl || !codeEl) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
latestFilePreviewRequestId += 1;
|
|
1889
|
+
const requestId = latestFilePreviewRequestId;
|
|
1890
|
+
pathEl.textContent = filePath;
|
|
1891
|
+
codeEl.className = "file-preview-code";
|
|
1892
|
+
codeEl.textContent = "Loading...";
|
|
1893
|
+
openModal(modal);
|
|
1894
|
+
try {
|
|
1895
|
+
const res = await fetch(`/api/file-source?path=${encodeURIComponent(filePath)}`);
|
|
1896
|
+
if (!res.ok) {
|
|
1897
|
+
codeEl.textContent = "(file unavailable)";
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
const data = await res.json();
|
|
1901
|
+
if (requestId !== latestFilePreviewRequestId) {
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
pathEl.textContent = data.filePath;
|
|
1905
|
+
codeEl.className = `file-preview-code language-${getFilePreviewLanguage(data.filePath)}`;
|
|
1906
|
+
codeEl.textContent = data.source;
|
|
1907
|
+
if (typeof hljs !== "undefined") {
|
|
1908
|
+
hljs.highlightElement(codeEl);
|
|
1909
|
+
}
|
|
1910
|
+
} catch {
|
|
1911
|
+
if (requestId !== latestFilePreviewRequestId) {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
codeEl.textContent = "(file unavailable)";
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1693
1917
|
async function focusSymbolInGraph(symbolId, options = {}) {
|
|
1694
1918
|
await focusSymbolsInGraph([symbolId], options);
|
|
1695
1919
|
}
|
|
@@ -2216,7 +2440,7 @@ async function showDetail(node, detailEl) {
|
|
|
2216
2440
|
<span class="detail-name">${escapeHtml(data.label)}</span>
|
|
2217
2441
|
<span class="detail-kind-badge" style="background:${kindColor}20;color:${kindColor}">${kind}</span>
|
|
2218
2442
|
</div>
|
|
2219
|
-
<div class="detail-file">${
|
|
2443
|
+
<div class="detail-file">${data.file ? `<button type="button" class="detail-file-link" data-file="${escapeHtml(data.file)}">${escapeHtml(data.file)}${data.startLine ? ":" + data.startLine : ""}</button>` : "unknown"}</div>
|
|
2220
2444
|
`;
|
|
2221
2445
|
html += `<div class="detail-actions">`;
|
|
2222
2446
|
html += `<button class="detail-pin header-btn" data-name="${escapeHtml(data.qualifiedName)}">${isPinned ? "Unpin" : "Pin to focus"}</button>`;
|
|
@@ -2258,6 +2482,12 @@ async function showDetail(node, detailEl) {
|
|
|
2258
2482
|
const name = pinBtn.dataset.name || "";
|
|
2259
2483
|
await togglePin(name, node, pinBtn);
|
|
2260
2484
|
});
|
|
2485
|
+
const fileLink = detailEl.querySelector(".detail-file-link");
|
|
2486
|
+
fileLink?.addEventListener("click", () => {
|
|
2487
|
+
const filePath = fileLink.dataset.file || "";
|
|
2488
|
+
if (!filePath) return;
|
|
2489
|
+
void openFilePreview(filePath);
|
|
2490
|
+
});
|
|
2261
2491
|
const explainBtn = detailEl.querySelector(".detail-explain-btn");
|
|
2262
2492
|
explainBtn.addEventListener("click", () => {
|
|
2263
2493
|
const name = explainBtn.dataset.name || "";
|
|
@@ -2442,27 +2672,33 @@ function setupToolbar() {
|
|
|
2442
2672
|
searchInput.parentNode.style.position = "relative";
|
|
2443
2673
|
searchInput.parentNode.appendChild(dropdown);
|
|
2444
2674
|
const rankedSymbols = allSymbolNames.slice().sort((a, b2) => compareGraphNodeIds(a, b2, graphNodes));
|
|
2445
|
-
function showDropdownResults(
|
|
2446
|
-
if (
|
|
2675
|
+
function showDropdownResults(results) {
|
|
2676
|
+
if (results.length === 0) {
|
|
2447
2677
|
dropdown.classList.add("hidden");
|
|
2448
2678
|
return;
|
|
2449
2679
|
}
|
|
2450
|
-
dropdown.innerHTML =
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
<span class="search-result-kind" style="color:${kindColor}">${kind}</span>
|
|
2680
|
+
dropdown.innerHTML = results.map((result) => {
|
|
2681
|
+
const kindColor = result.type === "file" ? "var(--accent)" : KIND_COLORS[result.kind] ? KIND_COLORS[result.kind].border : "#565f89";
|
|
2682
|
+
return `<div class="search-result" data-id="${escapeHtml(result.id)}" data-type="${result.type}">
|
|
2683
|
+
<div class="search-result-body">
|
|
2684
|
+
<span class="search-result-name">${escapeHtml(result.label)}</span>
|
|
2685
|
+
<span class="search-result-subtitle">${escapeHtml(result.subtitle)}</span>
|
|
2686
|
+
</div>
|
|
2687
|
+
<span class="search-result-kind${result.type === "file" ? " file" : ""}" style="color:${kindColor}">${escapeHtml(result.kind)}</span>
|
|
2458
2688
|
</div>`;
|
|
2459
2689
|
}).join("");
|
|
2460
2690
|
dropdown.classList.remove("hidden");
|
|
2461
2691
|
dropdown.querySelectorAll(".search-result").forEach((el) => {
|
|
2462
2692
|
el.addEventListener("click", () => {
|
|
2463
2693
|
const id = el.dataset.id || "";
|
|
2694
|
+
const type = el.dataset.type || "symbol";
|
|
2464
2695
|
searchInput.value = "";
|
|
2465
2696
|
dropdown.classList.add("hidden");
|
|
2697
|
+
if (type === "file") {
|
|
2698
|
+
focusFileInGraph(id);
|
|
2699
|
+
void openFilePreview(id);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2466
2702
|
void focusSymbolInGraph(id, {
|
|
2467
2703
|
maxDegrees: 1,
|
|
2468
2704
|
animate: true,
|
|
@@ -2473,23 +2709,22 @@ function setupToolbar() {
|
|
|
2473
2709
|
});
|
|
2474
2710
|
});
|
|
2475
2711
|
}
|
|
2712
|
+
function updateDropdownResults() {
|
|
2713
|
+
const results = buildGraphSearchResults({
|
|
2714
|
+
query: searchInput.value,
|
|
2715
|
+
symbolIds: rankedSymbols,
|
|
2716
|
+
nodes: graphNodes,
|
|
2717
|
+
fileIndex: fileToSymbolIds
|
|
2718
|
+
});
|
|
2719
|
+
showDropdownResults(results);
|
|
2720
|
+
}
|
|
2476
2721
|
searchInput.addEventListener("focus", () => {
|
|
2477
|
-
|
|
2478
|
-
showDropdownResults(rankedSymbols.slice(0, 20));
|
|
2479
|
-
}
|
|
2722
|
+
updateDropdownResults();
|
|
2480
2723
|
});
|
|
2481
2724
|
searchInput.addEventListener("input", () => {
|
|
2482
2725
|
clearTimeout(searchTimeout);
|
|
2483
2726
|
searchTimeout = setTimeout(() => {
|
|
2484
|
-
|
|
2485
|
-
if (query.length < 2) {
|
|
2486
|
-
showDropdownResults(rankedSymbols.slice(0, 20));
|
|
2487
|
-
return;
|
|
2488
|
-
}
|
|
2489
|
-
const matches = rankedSymbols.filter((name) => {
|
|
2490
|
-
return matchesGraphNodeQuery(query, graphNodes.get(name) || {}, name);
|
|
2491
|
-
}).slice(0, 15);
|
|
2492
|
-
showDropdownResults(matches);
|
|
2727
|
+
updateDropdownResults();
|
|
2493
2728
|
}, 150);
|
|
2494
2729
|
});
|
|
2495
2730
|
searchInput.addEventListener("keydown", (e) => {
|
|
@@ -2572,25 +2807,45 @@ var settingsBtn = document.getElementById("settings-btn");
|
|
|
2572
2807
|
var settingsModal = document.getElementById("settings-modal");
|
|
2573
2808
|
var settingsBackdrop = document.getElementById("settings-backdrop");
|
|
2574
2809
|
var settingsCloseBtn = document.getElementById("settings-close");
|
|
2810
|
+
var openRouterConnectModal = document.getElementById("openrouter-connect-modal");
|
|
2811
|
+
var openRouterConnectBackdrop = document.getElementById("openrouter-connect-backdrop");
|
|
2812
|
+
var openRouterConnectCloseBtn = document.getElementById("openrouter-connect-close");
|
|
2813
|
+
var openRouterConnectCancelBtn = document.getElementById("openrouter-connect-cancel");
|
|
2814
|
+
var openRouterConnectContinueBtn = document.getElementById("openrouter-connect-continue");
|
|
2815
|
+
var openRouterPersistCheckbox = document.getElementById("openrouter-persist-checkbox");
|
|
2575
2816
|
var settingsPath = document.getElementById("settings-path");
|
|
2576
2817
|
var settingsList = document.getElementById("settings-list");
|
|
2577
2818
|
var settingsBanner = document.getElementById("settings-banner");
|
|
2819
|
+
var settingsOpenRouterSession = document.getElementById("settings-openrouter-session");
|
|
2820
|
+
var settingsOpenRouterSessionMeta = document.getElementById("settings-openrouter-session-meta");
|
|
2578
2821
|
var settingsSaveBtn = document.getElementById("settings-save");
|
|
2579
2822
|
var settingsResetBtn = document.getElementById("settings-reset");
|
|
2823
|
+
var disconnectOpenRouterBtn = document.getElementById("disconnect-openrouter-btn");
|
|
2824
|
+
var connectOpenRouterButtons = Array.from(
|
|
2825
|
+
document.querySelectorAll("[data-openrouter-connect]")
|
|
2826
|
+
);
|
|
2827
|
+
var configOverlaySpotlight = document.getElementById("config-overlay-spotlight");
|
|
2828
|
+
var configOverlayIntro = document.getElementById("config-overlay-intro");
|
|
2829
|
+
var configConnectStatus = document.getElementById("config-connect-status");
|
|
2580
2830
|
var ws;
|
|
2581
2831
|
var currentAssistantEl = null;
|
|
2582
2832
|
var assistantText = "";
|
|
2583
2833
|
var hadToolCalls = false;
|
|
2584
2834
|
var settingsPayload = null;
|
|
2585
2835
|
var activeSavedSession = null;
|
|
2836
|
+
var activeBaseUrl = "";
|
|
2837
|
+
var sessionOpenRouterConnected = false;
|
|
2586
2838
|
var sessionRefreshTracker = createLatestRequestTracker();
|
|
2587
2839
|
var TOOL_RESULT_MAX = 500;
|
|
2840
|
+
var OPENROUTER_PKCE_VERIFIER_KEY = "minicode:openrouter:pkce-verifier";
|
|
2841
|
+
var OPENROUTER_PERSIST_TO_ENV_KEY = "minicode:openrouter:persist-to-env";
|
|
2588
2842
|
function connect() {
|
|
2589
2843
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
2590
2844
|
ws = new WebSocket(`${protocol}//${location.host}`);
|
|
2591
2845
|
ws.onopen = () => {
|
|
2592
2846
|
setStatus("ready");
|
|
2593
2847
|
fetchStatus();
|
|
2848
|
+
refreshModelList();
|
|
2594
2849
|
fetchContext();
|
|
2595
2850
|
};
|
|
2596
2851
|
ws.onclose = () => {
|
|
@@ -2617,6 +2872,127 @@ function setBusy(busy) {
|
|
|
2617
2872
|
}
|
|
2618
2873
|
}
|
|
2619
2874
|
var configOverlay = document.getElementById("config-overlay");
|
|
2875
|
+
function setConfigConnectStatus(message, tone) {
|
|
2876
|
+
if (!configConnectStatus) {
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
configConnectStatus.textContent = message;
|
|
2880
|
+
configConnectStatus.className = `config-connect-status ${tone}`;
|
|
2881
|
+
}
|
|
2882
|
+
function clearConfigConnectStatus() {
|
|
2883
|
+
if (!configConnectStatus) {
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
configConnectStatus.textContent = "";
|
|
2887
|
+
configConnectStatus.className = "config-connect-status hidden";
|
|
2888
|
+
}
|
|
2889
|
+
function encodeBase64Url(bytes) {
|
|
2890
|
+
let binary = "";
|
|
2891
|
+
for (const byte of bytes) {
|
|
2892
|
+
binary += String.fromCharCode(byte);
|
|
2893
|
+
}
|
|
2894
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2895
|
+
}
|
|
2896
|
+
function createPkceVerifier() {
|
|
2897
|
+
const bytes = new Uint8Array(32);
|
|
2898
|
+
crypto.getRandomValues(bytes);
|
|
2899
|
+
return encodeBase64Url(bytes);
|
|
2900
|
+
}
|
|
2901
|
+
async function createPkceChallenge(verifier) {
|
|
2902
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
2903
|
+
return encodeBase64Url(new Uint8Array(digest));
|
|
2904
|
+
}
|
|
2905
|
+
async function startOpenRouterConnect(persistToEnv) {
|
|
2906
|
+
const verifier = createPkceVerifier();
|
|
2907
|
+
const challenge = await createPkceChallenge(verifier);
|
|
2908
|
+
sessionStorage.setItem(OPENROUTER_PKCE_VERIFIER_KEY, verifier);
|
|
2909
|
+
sessionStorage.setItem(OPENROUTER_PERSIST_TO_ENV_KEY, persistToEnv ? "1" : "0");
|
|
2910
|
+
setConfigConnectStatus("Redirecting to OpenRouter\u2026", "info");
|
|
2911
|
+
const callbackUrl = new URL(location.pathname, location.origin).toString();
|
|
2912
|
+
const authUrl = new URL("https://openrouter.ai/auth");
|
|
2913
|
+
authUrl.searchParams.set("callback_url", callbackUrl);
|
|
2914
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
2915
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
2916
|
+
location.assign(authUrl.toString());
|
|
2917
|
+
}
|
|
2918
|
+
async function maybeHandleOpenRouterCallback() {
|
|
2919
|
+
const url = new URL(location.href);
|
|
2920
|
+
const code = url.searchParams.get("code");
|
|
2921
|
+
if (!code) {
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
const cleanedUrl = `${url.pathname}${url.hash}`;
|
|
2925
|
+
history.replaceState({}, document.title, cleanedUrl);
|
|
2926
|
+
const codeVerifier = sessionStorage.getItem(OPENROUTER_PKCE_VERIFIER_KEY);
|
|
2927
|
+
const persistToEnv = sessionStorage.getItem(OPENROUTER_PERSIST_TO_ENV_KEY) === "1";
|
|
2928
|
+
sessionStorage.removeItem(OPENROUTER_PKCE_VERIFIER_KEY);
|
|
2929
|
+
sessionStorage.removeItem(OPENROUTER_PERSIST_TO_ENV_KEY);
|
|
2930
|
+
if (!codeVerifier) {
|
|
2931
|
+
setConfigConnectStatus(
|
|
2932
|
+
"OpenRouter sign-in could not be completed because the local PKCE verifier was missing. Start the connect flow again.",
|
|
2933
|
+
"error"
|
|
2934
|
+
);
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
for (const button of connectOpenRouterButtons) {
|
|
2938
|
+
button.disabled = true;
|
|
2939
|
+
}
|
|
2940
|
+
setConfigConnectStatus("Connecting OpenRouter to this serve session\u2026", "info");
|
|
2941
|
+
try {
|
|
2942
|
+
const res = await fetch("/api/openrouter/connect", {
|
|
2943
|
+
method: "POST",
|
|
2944
|
+
headers: { "Content-Type": "application/json" },
|
|
2945
|
+
body: JSON.stringify({ code, codeVerifier, persistToEnv })
|
|
2946
|
+
});
|
|
2947
|
+
const body = await res.json();
|
|
2948
|
+
if (!res.ok) {
|
|
2949
|
+
throw new Error("error" in body ? body.error : `Failed to connect OpenRouter (${res.status})`);
|
|
2950
|
+
}
|
|
2951
|
+
activeBaseUrl = body.baseUrl;
|
|
2952
|
+
addMessage(body.message, "thinking");
|
|
2953
|
+
const statusTone = body.persistWarning ? "info" : body.needsSetup ? "info" : "success";
|
|
2954
|
+
setConfigConnectStatus(body.message, statusTone);
|
|
2955
|
+
await fetchStatus();
|
|
2956
|
+
await refreshModelList();
|
|
2957
|
+
const onlyModelMissing = body.needsSetup && body.missing.length === 1 && body.missing[0]?.includes("MODEL");
|
|
2958
|
+
if (onlyModelMissing) {
|
|
2959
|
+
modelDropdown.classList.remove("hidden");
|
|
2960
|
+
sessionDropdown.classList.add("hidden");
|
|
2961
|
+
}
|
|
2962
|
+
} catch (error) {
|
|
2963
|
+
const message = error instanceof Error ? error.message : "Failed to connect OpenRouter";
|
|
2964
|
+
setConfigConnectStatus(message, "error");
|
|
2965
|
+
} finally {
|
|
2966
|
+
for (const button of connectOpenRouterButtons) {
|
|
2967
|
+
button.disabled = false;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
async function disconnectOpenRouter() {
|
|
2972
|
+
disconnectOpenRouterBtn.disabled = true;
|
|
2973
|
+
clearSettingsBanner();
|
|
2974
|
+
try {
|
|
2975
|
+
const res = await fetch("/api/openrouter/disconnect", {
|
|
2976
|
+
method: "POST",
|
|
2977
|
+
headers: { "Content-Type": "application/json" }
|
|
2978
|
+
});
|
|
2979
|
+
const body = await res.json();
|
|
2980
|
+
if (!res.ok) {
|
|
2981
|
+
throw new Error("error" in body ? body.error : `Failed to disconnect OpenRouter (${res.status})`);
|
|
2982
|
+
}
|
|
2983
|
+
activeBaseUrl = body.baseUrl;
|
|
2984
|
+
addMessage(body.message, "thinking");
|
|
2985
|
+
setSettingsBanner(body.message, body.disconnected ? "success" : "info");
|
|
2986
|
+
clearConfigConnectStatus();
|
|
2987
|
+
await fetchStatus();
|
|
2988
|
+
await refreshModelList();
|
|
2989
|
+
} catch (error) {
|
|
2990
|
+
const message = error instanceof Error ? error.message : "Failed to disconnect OpenRouter";
|
|
2991
|
+
setSettingsBanner(message, "error");
|
|
2992
|
+
} finally {
|
|
2993
|
+
disconnectOpenRouterBtn.disabled = false;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2620
2996
|
async function fetchStatus() {
|
|
2621
2997
|
try {
|
|
2622
2998
|
const res = await fetch("/api/status");
|
|
@@ -2624,6 +3000,9 @@ async function fetchStatus() {
|
|
|
2624
3000
|
modelInfo.textContent = data.model || "Select model";
|
|
2625
3001
|
modelInfo.classList.toggle("placeholder", !data.model);
|
|
2626
3002
|
activeModel = data.model;
|
|
3003
|
+
activeBaseUrl = data.baseUrl ?? "";
|
|
3004
|
+
sessionOpenRouterConnected = data.sessionOpenRouterConnected ?? false;
|
|
3005
|
+
renderOpenRouterSessionControls();
|
|
2627
3006
|
if (data.needsSetup) {
|
|
2628
3007
|
configOverlay.classList.remove("hidden");
|
|
2629
3008
|
chatInput.disabled = true;
|
|
@@ -2631,6 +3010,11 @@ async function fetchStatus() {
|
|
|
2631
3010
|
const missingEl = document.getElementById("config-missing");
|
|
2632
3011
|
if (missingEl && data.missing && data.missing.length > 0) {
|
|
2633
3012
|
const isOnlyModelMissing = data.missing.length === 1 && data.missing[0].includes("MODEL");
|
|
3013
|
+
const hasPersistedOpenRouter = isOnlyModelMissing && (data.baseUrl ?? "").includes("openrouter");
|
|
3014
|
+
if (configOverlayIntro) {
|
|
3015
|
+
configOverlayIntro.textContent = hasPersistedOpenRouter ? "OpenRouter is already configured. Select a model to continue:" : "minicode needs a model provider to run. Configure one of the following:";
|
|
3016
|
+
}
|
|
3017
|
+
configOverlaySpotlight?.classList.toggle("hidden", hasPersistedOpenRouter);
|
|
2634
3018
|
const hint = isOnlyModelMissing ? ` \u2014 select one from the <strong>model dropdown</strong> above, or set it in config` : "";
|
|
2635
3019
|
missingEl.innerHTML = `<strong>Missing:</strong> ${data.missing.map(escapeHtml).join(", ")}${hint}`;
|
|
2636
3020
|
missingEl.classList.remove("hidden");
|
|
@@ -2638,6 +3022,16 @@ async function fetchStatus() {
|
|
|
2638
3022
|
} else {
|
|
2639
3023
|
configOverlay.classList.add("hidden");
|
|
2640
3024
|
chatInput.disabled = false;
|
|
3025
|
+
sendBtn.disabled = false;
|
|
3026
|
+
const missingEl = document.getElementById("config-missing");
|
|
3027
|
+
if (missingEl) {
|
|
3028
|
+
missingEl.classList.add("hidden");
|
|
3029
|
+
missingEl.innerHTML = "";
|
|
3030
|
+
}
|
|
3031
|
+
if (configOverlayIntro) {
|
|
3032
|
+
configOverlayIntro.textContent = "minicode needs a model provider to run. Configure one of the following:";
|
|
3033
|
+
}
|
|
3034
|
+
configOverlaySpotlight?.classList.remove("hidden");
|
|
2641
3035
|
}
|
|
2642
3036
|
} catch {
|
|
2643
3037
|
}
|
|
@@ -2746,6 +3140,50 @@ function addMessage(text, type, markdown = false) {
|
|
|
2746
3140
|
scrollToBottom();
|
|
2747
3141
|
return el;
|
|
2748
3142
|
}
|
|
3143
|
+
function clearChatTranscript() {
|
|
3144
|
+
messagesEl.innerHTML = "";
|
|
3145
|
+
currentAssistantEl = null;
|
|
3146
|
+
assistantText = "";
|
|
3147
|
+
hadToolCalls = false;
|
|
3148
|
+
}
|
|
3149
|
+
function stringifyToolInput(input) {
|
|
3150
|
+
const entries = Object.entries(input).flatMap(([key, value]) => {
|
|
3151
|
+
if (value === void 0 || value === null) {
|
|
3152
|
+
return [];
|
|
3153
|
+
}
|
|
3154
|
+
if (typeof value === "string") {
|
|
3155
|
+
return [[key, value]];
|
|
3156
|
+
}
|
|
3157
|
+
return [[key, JSON.stringify(value)]];
|
|
3158
|
+
});
|
|
3159
|
+
return Object.fromEntries(entries);
|
|
3160
|
+
}
|
|
3161
|
+
function addToolResultPreview(name, result) {
|
|
3162
|
+
const toolEls = messagesEl.querySelectorAll(`.tool-call[data-tool-name="${name}"]`);
|
|
3163
|
+
if (toolEls.length === 0) {
|
|
3164
|
+
addToolCall(name, {});
|
|
3165
|
+
}
|
|
3166
|
+
finalizeToolCall(name, result);
|
|
3167
|
+
}
|
|
3168
|
+
function renderLoadedSessionMessages(messages) {
|
|
3169
|
+
clearChatTranscript();
|
|
3170
|
+
for (const message of messages) {
|
|
3171
|
+
if (message.role === "user") {
|
|
3172
|
+
addMessage(message.content, "user");
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
3175
|
+
if (message.role === "assistant") {
|
|
3176
|
+
if (message.content.trim().length > 0) {
|
|
3177
|
+
addMessage(message.content, "assistant", true);
|
|
3178
|
+
}
|
|
3179
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
3180
|
+
addToolCall(toolCall.name, stringifyToolInput(toolCall.input));
|
|
3181
|
+
}
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
addToolResultPreview(message.toolName, message.content);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
2749
3187
|
function summarizeToolInput(name, input) {
|
|
2750
3188
|
const key = input.path ?? input.file_path ?? input.command ?? input.query ?? input.pattern ?? input.name ?? input.old_string;
|
|
2751
3189
|
if (typeof key === "string") {
|
|
@@ -2786,7 +3224,7 @@ function finalizeToolCall(name, result, elapsedMs) {
|
|
|
2786
3224
|
if (!el) return;
|
|
2787
3225
|
const timeEl = el.querySelector(".tool-time");
|
|
2788
3226
|
if (timeEl) {
|
|
2789
|
-
timeEl.textContent = `${elapsedMs}ms
|
|
3227
|
+
timeEl.textContent = elapsedMs && elapsedMs > 0 ? `${elapsedMs}ms` : "";
|
|
2790
3228
|
}
|
|
2791
3229
|
const resultEl = el.querySelector(".tool-result");
|
|
2792
3230
|
if (resultEl && result) {
|
|
@@ -2830,9 +3268,6 @@ function closeHeaderMenus() {
|
|
|
2830
3268
|
modelDropdown.classList.add("hidden");
|
|
2831
3269
|
sessionDropdown.classList.add("hidden");
|
|
2832
3270
|
}
|
|
2833
|
-
function isSettingsModalOpen() {
|
|
2834
|
-
return !settingsModal.classList.contains("hidden");
|
|
2835
|
-
}
|
|
2836
3271
|
function formatSettingsValue(value) {
|
|
2837
3272
|
return value === null ? "(unset)" : String(value);
|
|
2838
3273
|
}
|
|
@@ -2844,6 +3279,17 @@ function clearSettingsBanner() {
|
|
|
2844
3279
|
settingsBanner.textContent = "";
|
|
2845
3280
|
settingsBanner.className = "settings-banner hidden";
|
|
2846
3281
|
}
|
|
3282
|
+
function renderOpenRouterSessionControls() {
|
|
3283
|
+
if (sessionOpenRouterConnected) {
|
|
3284
|
+
settingsOpenRouterSession.classList.remove("hidden");
|
|
3285
|
+
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.";
|
|
3286
|
+
disconnectOpenRouterBtn.disabled = false;
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
settingsOpenRouterSession.classList.add("hidden");
|
|
3290
|
+
settingsOpenRouterSessionMeta.textContent = "";
|
|
3291
|
+
disconnectOpenRouterBtn.disabled = false;
|
|
3292
|
+
}
|
|
2847
3293
|
function createSettingsControl(entry, inputId) {
|
|
2848
3294
|
const value = entry.persistedValue;
|
|
2849
3295
|
if (entry.type === "boolean") {
|
|
@@ -3036,17 +3482,23 @@ function updateSettingsActions() {
|
|
|
3036
3482
|
}
|
|
3037
3483
|
function openSettings() {
|
|
3038
3484
|
closeHeaderMenus();
|
|
3039
|
-
settingsModal
|
|
3040
|
-
settingsModal.setAttribute("aria-hidden", "false");
|
|
3041
|
-
document.body.classList.add("modal-open");
|
|
3485
|
+
openModal(settingsModal);
|
|
3042
3486
|
void loadSettings();
|
|
3043
3487
|
}
|
|
3044
3488
|
function closeSettings() {
|
|
3045
|
-
settingsModal
|
|
3046
|
-
settingsModal.setAttribute("aria-hidden", "true");
|
|
3047
|
-
document.body.classList.remove("modal-open");
|
|
3489
|
+
closeModal(settingsModal);
|
|
3048
3490
|
clearSettingsBanner();
|
|
3049
3491
|
}
|
|
3492
|
+
function openOpenRouterConnectModal() {
|
|
3493
|
+
closeHeaderMenus();
|
|
3494
|
+
openModal(openRouterConnectModal);
|
|
3495
|
+
openRouterPersistCheckbox.checked = false;
|
|
3496
|
+
openRouterConnectContinueBtn.disabled = false;
|
|
3497
|
+
}
|
|
3498
|
+
function closeOpenRouterConnectModal() {
|
|
3499
|
+
closeModal(openRouterConnectModal);
|
|
3500
|
+
openRouterConnectContinueBtn.disabled = false;
|
|
3501
|
+
}
|
|
3050
3502
|
chatForm.addEventListener("submit", (e) => {
|
|
3051
3503
|
e.preventDefault();
|
|
3052
3504
|
const message = chatInput.value.trim();
|
|
@@ -3089,31 +3541,63 @@ async function refreshModelList() {
|
|
|
3089
3541
|
const res = await fetch("/api/models");
|
|
3090
3542
|
const data = await res.json();
|
|
3091
3543
|
activeModel = data.activeModel;
|
|
3544
|
+
const hasActiveModel = !!activeModel && data.models.some((model) => model.id === activeModel);
|
|
3092
3545
|
if (!data.models || data.models.length === 0) {
|
|
3546
|
+
modelInfo.textContent = "Select model";
|
|
3547
|
+
modelInfo.classList.add("placeholder");
|
|
3093
3548
|
modelList.innerHTML = '<div class="dropdown-empty">No models available</div>';
|
|
3094
3549
|
return;
|
|
3095
3550
|
}
|
|
3551
|
+
if (hasActiveModel) {
|
|
3552
|
+
modelInfo.textContent = activeModel;
|
|
3553
|
+
modelInfo.classList.remove("placeholder");
|
|
3554
|
+
} else {
|
|
3555
|
+
modelInfo.textContent = "Select model";
|
|
3556
|
+
modelInfo.classList.add("placeholder");
|
|
3557
|
+
}
|
|
3096
3558
|
modelList.innerHTML = "";
|
|
3097
3559
|
for (const m2 of data.models) {
|
|
3098
3560
|
const el = document.createElement("div");
|
|
3099
3561
|
el.className = "model-item" + (m2.id === activeModel ? " active" : "");
|
|
3100
3562
|
el.textContent = m2.name ?? m2.id;
|
|
3101
3563
|
el.title = m2.id;
|
|
3102
|
-
el.addEventListener("click", () =>
|
|
3564
|
+
el.addEventListener("click", () => {
|
|
3565
|
+
void switchModel(m2.id);
|
|
3566
|
+
});
|
|
3103
3567
|
modelList.appendChild(el);
|
|
3104
3568
|
}
|
|
3105
3569
|
} catch {
|
|
3106
3570
|
modelList.innerHTML = '<div class="dropdown-empty">Failed to load models</div>';
|
|
3107
3571
|
}
|
|
3108
3572
|
}
|
|
3109
|
-
function switchModel(modelId) {
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3573
|
+
async function switchModel(modelId) {
|
|
3574
|
+
try {
|
|
3575
|
+
const res = await fetch("/api/model", {
|
|
3576
|
+
method: "POST",
|
|
3577
|
+
headers: { "Content-Type": "application/json" },
|
|
3578
|
+
body: JSON.stringify({
|
|
3579
|
+
model: modelId,
|
|
3580
|
+
persistToHomeEnv: true
|
|
3581
|
+
})
|
|
3582
|
+
});
|
|
3583
|
+
const body = await res.json();
|
|
3584
|
+
if (!res.ok) {
|
|
3585
|
+
throw new Error("error" in body ? body.error : `Failed to switch model (${res.status})`);
|
|
3586
|
+
}
|
|
3587
|
+
modelInfo.textContent = modelId || "Select model";
|
|
3588
|
+
modelInfo.classList.toggle("placeholder", !modelId);
|
|
3589
|
+
activeModel = modelId;
|
|
3590
|
+
modelDropdown.classList.add("hidden");
|
|
3591
|
+
if (body.persistedToEnv) {
|
|
3592
|
+
addMessage(`Model switched to: ${modelId}. Saved as the default in ~/.minicode/.env.`, "thinking");
|
|
3593
|
+
} else {
|
|
3594
|
+
addMessage(`Model switched to: ${modelId}`, "thinking");
|
|
3595
|
+
}
|
|
3596
|
+
await fetchStatus();
|
|
3597
|
+
} catch (error) {
|
|
3598
|
+
const message = error instanceof Error ? error.message : "Failed to switch model";
|
|
3599
|
+
addMessage(message, "error");
|
|
3600
|
+
}
|
|
3117
3601
|
}
|
|
3118
3602
|
sessionBtn.addEventListener("click", (e) => {
|
|
3119
3603
|
e.stopPropagation();
|
|
@@ -3209,9 +3693,12 @@ async function loadSession(label) {
|
|
|
3209
3693
|
body: JSON.stringify({ label })
|
|
3210
3694
|
});
|
|
3211
3695
|
if (res.ok) {
|
|
3696
|
+
const body = await res.json();
|
|
3212
3697
|
sessionDropdown.classList.add("hidden");
|
|
3213
|
-
|
|
3214
|
-
|
|
3698
|
+
renderLoadedSessionMessages(body.messages);
|
|
3699
|
+
if (body.messages.length === 0) {
|
|
3700
|
+
addMessage(`Session "${body.label}" restored`, "thinking");
|
|
3701
|
+
}
|
|
3215
3702
|
void refreshSessionList();
|
|
3216
3703
|
}
|
|
3217
3704
|
} catch {
|
|
@@ -3247,9 +3734,19 @@ settingsCloseBtn.addEventListener("click", () => {
|
|
|
3247
3734
|
settingsBackdrop.addEventListener("click", () => {
|
|
3248
3735
|
closeSettings();
|
|
3249
3736
|
});
|
|
3737
|
+
openRouterConnectBackdrop.addEventListener("click", () => {
|
|
3738
|
+
closeOpenRouterConnectModal();
|
|
3739
|
+
});
|
|
3740
|
+
openRouterConnectCloseBtn.addEventListener("click", () => {
|
|
3741
|
+
closeOpenRouterConnectModal();
|
|
3742
|
+
});
|
|
3743
|
+
openRouterConnectCancelBtn.addEventListener("click", () => {
|
|
3744
|
+
closeOpenRouterConnectModal();
|
|
3745
|
+
});
|
|
3250
3746
|
settingsResetBtn.addEventListener("click", () => {
|
|
3251
3747
|
clearSettingsBanner();
|
|
3252
3748
|
renderSettings();
|
|
3749
|
+
renderOpenRouterSessionControls();
|
|
3253
3750
|
});
|
|
3254
3751
|
settingsSaveBtn.addEventListener("click", async () => {
|
|
3255
3752
|
if (!settingsPayload) {
|
|
@@ -3298,11 +3795,31 @@ settingsList.addEventListener("change", () => {
|
|
|
3298
3795
|
clearSettingsBanner();
|
|
3299
3796
|
updateSettingsActions();
|
|
3300
3797
|
});
|
|
3798
|
+
disconnectOpenRouterBtn.addEventListener("click", () => {
|
|
3799
|
+
void disconnectOpenRouter();
|
|
3800
|
+
});
|
|
3301
3801
|
document.addEventListener("keydown", (event) => {
|
|
3302
|
-
if (event.key
|
|
3802
|
+
if (event.key !== "Escape") {
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
if (isOpenRouterConnectModalOpen()) {
|
|
3806
|
+
closeOpenRouterConnectModal();
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
if (isSettingsModalOpen()) {
|
|
3303
3810
|
closeSettings();
|
|
3304
3811
|
}
|
|
3305
3812
|
});
|
|
3813
|
+
for (const button of connectOpenRouterButtons) {
|
|
3814
|
+
button.addEventListener("click", () => {
|
|
3815
|
+
openOpenRouterConnectModal();
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
openRouterConnectContinueBtn.addEventListener("click", () => {
|
|
3819
|
+
openRouterConnectContinueBtn.disabled = true;
|
|
3820
|
+
closeOpenRouterConnectModal();
|
|
3821
|
+
void startOpenRouterConnect(openRouterPersistCheckbox.checked);
|
|
3822
|
+
});
|
|
3306
3823
|
var chatPane = document.getElementById("chat-pane");
|
|
3307
3824
|
var divider = document.getElementById("pane-divider");
|
|
3308
3825
|
divider.addEventListener("mousedown", (e) => {
|
|
@@ -3337,3 +3854,4 @@ graphToggle.addEventListener("click", () => {
|
|
|
3337
3854
|
});
|
|
3338
3855
|
connect();
|
|
3339
3856
|
initGraph();
|
|
3857
|
+
void maybeHandleOpenRouterCallback();
|