@sean.holung/minicode 0.3.5 → 0.3.6
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 +21 -45
- package/dist/src/agent/config.js +51 -66
- package/dist/src/agent/editable-config.js +50 -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/server.js +161 -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/web/app.js +494 -56
- package/dist/src/web/index.html +68 -6
- package/dist/src/web/style.css +208 -1
- package/dist/tests/config-api.test.js +5 -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 +12 -4
- package/dist/tests/editable-config.test.js +15 -12
- package/dist/tests/graph-onboarding.test.js +10 -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/serve.integration.test.js +229 -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/package.json +1 -1
package/dist/src/web/app.js
CHANGED
|
@@ -1523,10 +1523,135 @@ function resolveGraphNodeIds(nodes, symbolName) {
|
|
|
1523
1523
|
return [...nodes.entries()].filter(([id, node]) => matchesGraphNodeQuery(query, node, id)).map(([id]) => id).sort((a, b2) => compareGraphNodeIds(a, b2, nodes));
|
|
1524
1524
|
}
|
|
1525
1525
|
|
|
1526
|
+
// src/shared/graph-search.ts
|
|
1527
|
+
function getGraphNodeFilePath(node) {
|
|
1528
|
+
return node.filePath || node.file || "";
|
|
1529
|
+
}
|
|
1530
|
+
function buildGraphFileIndex(nodes) {
|
|
1531
|
+
const files = /* @__PURE__ */ new Map();
|
|
1532
|
+
for (const [id, node] of nodes) {
|
|
1533
|
+
const filePath = getGraphNodeFilePath(node);
|
|
1534
|
+
if (!filePath) continue;
|
|
1535
|
+
const existing = files.get(filePath);
|
|
1536
|
+
if (existing) {
|
|
1537
|
+
existing.push(id);
|
|
1538
|
+
} else {
|
|
1539
|
+
files.set(filePath, [id]);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
for (const symbolIds of files.values()) {
|
|
1543
|
+
symbolIds.sort((a, b2) => compareGraphNodeIds(a, b2, nodes));
|
|
1544
|
+
}
|
|
1545
|
+
return files;
|
|
1546
|
+
}
|
|
1547
|
+
function compareGraphFilePaths(a, b2, fileIndex) {
|
|
1548
|
+
const countDifference = (fileIndex.get(b2)?.length ?? 0) - (fileIndex.get(a)?.length ?? 0);
|
|
1549
|
+
if (countDifference !== 0) {
|
|
1550
|
+
return countDifference;
|
|
1551
|
+
}
|
|
1552
|
+
return a.localeCompare(b2, void 0, { sensitivity: "base" });
|
|
1553
|
+
}
|
|
1554
|
+
function matchesGraphFileQuery(query, filePath) {
|
|
1555
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1556
|
+
if (normalizedQuery.length === 0) {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
return filePath.toLowerCase().includes(normalizedQuery);
|
|
1560
|
+
}
|
|
1561
|
+
function buildGraphSearchResults({
|
|
1562
|
+
query,
|
|
1563
|
+
symbolIds,
|
|
1564
|
+
nodes,
|
|
1565
|
+
fileIndex,
|
|
1566
|
+
symbolLimit = 12,
|
|
1567
|
+
fileLimit = 8
|
|
1568
|
+
}) {
|
|
1569
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1570
|
+
const showDefaultResults = normalizedQuery.length < 2;
|
|
1571
|
+
const rankedFiles = [...fileIndex.keys()].sort((a, b2) => compareGraphFilePaths(a, b2, fileIndex));
|
|
1572
|
+
const symbolResults = symbolIds.filter((id) => {
|
|
1573
|
+
if (showDefaultResults) {
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
return matchesGraphNodeQuery(normalizedQuery, nodes.get(id) || {}, id);
|
|
1577
|
+
}).slice(0, symbolLimit).map((id) => {
|
|
1578
|
+
const node = nodes.get(id) || {};
|
|
1579
|
+
return {
|
|
1580
|
+
type: "symbol",
|
|
1581
|
+
id,
|
|
1582
|
+
label: getGraphNodeLabel(node, id),
|
|
1583
|
+
subtitle: getGraphNodeFilePath(node),
|
|
1584
|
+
kind: (node.kind || "symbol").toLowerCase()
|
|
1585
|
+
};
|
|
1586
|
+
});
|
|
1587
|
+
const fileResults = rankedFiles.filter((filePath) => {
|
|
1588
|
+
if (showDefaultResults) {
|
|
1589
|
+
return true;
|
|
1590
|
+
}
|
|
1591
|
+
return matchesGraphFileQuery(normalizedQuery, filePath);
|
|
1592
|
+
}).slice(0, fileLimit).map((filePath) => {
|
|
1593
|
+
const symbolCount = fileIndex.get(filePath)?.length ?? 0;
|
|
1594
|
+
return {
|
|
1595
|
+
type: "file",
|
|
1596
|
+
id: filePath,
|
|
1597
|
+
label: filePath,
|
|
1598
|
+
subtitle: `${symbolCount} symbol${symbolCount === 1 ? "" : "s"}`,
|
|
1599
|
+
kind: "file",
|
|
1600
|
+
symbolCount
|
|
1601
|
+
};
|
|
1602
|
+
});
|
|
1603
|
+
return [...symbolResults, ...fileResults];
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/shared/graph-selection.ts
|
|
1607
|
+
function buildGraphEdgeId(edge) {
|
|
1608
|
+
return `${edge.source}->${edge.target}:${edge.kind}`;
|
|
1609
|
+
}
|
|
1610
|
+
function buildGraphEdgeIndex(edges) {
|
|
1611
|
+
const edgeIndex2 = /* @__PURE__ */ new Map();
|
|
1612
|
+
for (const edge of edges) {
|
|
1613
|
+
const sourceEdges = edgeIndex2.get(edge.source);
|
|
1614
|
+
if (sourceEdges) {
|
|
1615
|
+
sourceEdges.push(edge);
|
|
1616
|
+
} else {
|
|
1617
|
+
edgeIndex2.set(edge.source, [edge]);
|
|
1618
|
+
}
|
|
1619
|
+
const targetEdges = edgeIndex2.get(edge.target);
|
|
1620
|
+
if (targetEdges) {
|
|
1621
|
+
targetEdges.push(edge);
|
|
1622
|
+
} else {
|
|
1623
|
+
edgeIndex2.set(edge.target, [edge]);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return edgeIndex2;
|
|
1627
|
+
}
|
|
1628
|
+
function buildFileFocusedSelection({
|
|
1629
|
+
filePath,
|
|
1630
|
+
fileIndex,
|
|
1631
|
+
edgeIndex: edgeIndex2
|
|
1632
|
+
}) {
|
|
1633
|
+
const fileSymbolIds = fileIndex.get(filePath) || [];
|
|
1634
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
1635
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1636
|
+
for (const symbolId of fileSymbolIds) {
|
|
1637
|
+
nodeIds.add(symbolId);
|
|
1638
|
+
for (const edge of edgeIndex2.get(symbolId) || []) {
|
|
1639
|
+
nodeIds.add(edge.source);
|
|
1640
|
+
nodeIds.add(edge.target);
|
|
1641
|
+
edges.set(buildGraphEdgeId(edge), edge);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
nodeIds: [...nodeIds],
|
|
1646
|
+
edges: [...edges.values()]
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1526
1650
|
// src/web/graph.ts
|
|
1527
1651
|
var cy = null;
|
|
1528
1652
|
var graphNodes = /* @__PURE__ */ new Map();
|
|
1529
1653
|
var graphEdges = [];
|
|
1654
|
+
var fileToSymbolIds = /* @__PURE__ */ new Map();
|
|
1530
1655
|
var edgeIndex = /* @__PURE__ */ new Map();
|
|
1531
1656
|
var pinnedNames = /* @__PURE__ */ new Set();
|
|
1532
1657
|
var allSymbolNames = [];
|
|
@@ -1585,7 +1710,8 @@ async function initGraph() {
|
|
|
1585
1710
|
target: e.target || e.to || "",
|
|
1586
1711
|
kind: (e.kind || e.type || "references").toLowerCase()
|
|
1587
1712
|
}));
|
|
1588
|
-
|
|
1713
|
+
edgeIndex = buildGraphEdgeIndex(graphEdges);
|
|
1714
|
+
fileToSymbolIds = buildGraphFileIndex(graphNodes);
|
|
1589
1715
|
allSymbolNames = Array.from(graphNodes.keys()).sort();
|
|
1590
1716
|
const pinned = focusData.pinned || [];
|
|
1591
1717
|
for (const f of pinned) {
|
|
@@ -1632,30 +1758,13 @@ function showOnboardingHint(container) {
|
|
|
1632
1758
|
if (container.querySelector(".graph-onboarding")) return;
|
|
1633
1759
|
const hint = document.createElement("div");
|
|
1634
1760
|
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>';
|
|
1761
|
+
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
1762
|
container.appendChild(hint);
|
|
1637
1763
|
}
|
|
1638
1764
|
function removeOnboardingHint() {
|
|
1639
1765
|
const hint = document.querySelector(".graph-onboarding");
|
|
1640
1766
|
if (hint) hint.remove();
|
|
1641
1767
|
}
|
|
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
1768
|
function addNodeNeighborhood(symbolId, maxDegrees = 1) {
|
|
1660
1769
|
const visited = /* @__PURE__ */ new Set();
|
|
1661
1770
|
let frontier = /* @__PURE__ */ new Set([symbolId]);
|
|
@@ -1690,6 +1799,30 @@ function renderNodeNeighborhoodAndLayout(symbolId, maxDegrees = 1) {
|
|
|
1690
1799
|
runLayout();
|
|
1691
1800
|
}
|
|
1692
1801
|
}
|
|
1802
|
+
function focusFileInGraph(filePath) {
|
|
1803
|
+
if (!cy) return;
|
|
1804
|
+
const selection = buildFileFocusedSelection({
|
|
1805
|
+
filePath,
|
|
1806
|
+
fileIndex: fileToSymbolIds,
|
|
1807
|
+
edgeIndex
|
|
1808
|
+
});
|
|
1809
|
+
cy.elements().remove();
|
|
1810
|
+
document.getElementById("symbol-detail")?.classList.add("hidden");
|
|
1811
|
+
if (selection.nodeIds.length === 0) {
|
|
1812
|
+
const cyEl = document.getElementById("cy");
|
|
1813
|
+
if (cyEl) showOnboardingHint(cyEl);
|
|
1814
|
+
refreshAnalysisGraphState();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
for (const symbolId of selection.nodeIds) {
|
|
1818
|
+
addNodeToGraph(symbolId);
|
|
1819
|
+
}
|
|
1820
|
+
for (const edge of selection.edges) {
|
|
1821
|
+
addEdgeToGraph(edge);
|
|
1822
|
+
}
|
|
1823
|
+
refreshAnalysisGraphState();
|
|
1824
|
+
runLayout();
|
|
1825
|
+
}
|
|
1693
1826
|
async function focusSymbolInGraph(symbolId, options = {}) {
|
|
1694
1827
|
await focusSymbolsInGraph([symbolId], options);
|
|
1695
1828
|
}
|
|
@@ -2442,27 +2575,32 @@ function setupToolbar() {
|
|
|
2442
2575
|
searchInput.parentNode.style.position = "relative";
|
|
2443
2576
|
searchInput.parentNode.appendChild(dropdown);
|
|
2444
2577
|
const rankedSymbols = allSymbolNames.slice().sort((a, b2) => compareGraphNodeIds(a, b2, graphNodes));
|
|
2445
|
-
function showDropdownResults(
|
|
2446
|
-
if (
|
|
2578
|
+
function showDropdownResults(results) {
|
|
2579
|
+
if (results.length === 0) {
|
|
2447
2580
|
dropdown.classList.add("hidden");
|
|
2448
2581
|
return;
|
|
2449
2582
|
}
|
|
2450
|
-
dropdown.innerHTML =
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
<span class="search-result-kind" style="color:${kindColor}">${kind}</span>
|
|
2583
|
+
dropdown.innerHTML = results.map((result) => {
|
|
2584
|
+
const kindColor = result.type === "file" ? "var(--accent)" : KIND_COLORS[result.kind] ? KIND_COLORS[result.kind].border : "#565f89";
|
|
2585
|
+
return `<div class="search-result" data-id="${escapeHtml(result.id)}" data-type="${result.type}">
|
|
2586
|
+
<div class="search-result-body">
|
|
2587
|
+
<span class="search-result-name">${escapeHtml(result.label)}</span>
|
|
2588
|
+
<span class="search-result-subtitle">${escapeHtml(result.subtitle)}</span>
|
|
2589
|
+
</div>
|
|
2590
|
+
<span class="search-result-kind${result.type === "file" ? " file" : ""}" style="color:${kindColor}">${escapeHtml(result.kind)}</span>
|
|
2458
2591
|
</div>`;
|
|
2459
2592
|
}).join("");
|
|
2460
2593
|
dropdown.classList.remove("hidden");
|
|
2461
2594
|
dropdown.querySelectorAll(".search-result").forEach((el) => {
|
|
2462
2595
|
el.addEventListener("click", () => {
|
|
2463
2596
|
const id = el.dataset.id || "";
|
|
2597
|
+
const type = el.dataset.type || "symbol";
|
|
2464
2598
|
searchInput.value = "";
|
|
2465
2599
|
dropdown.classList.add("hidden");
|
|
2600
|
+
if (type === "file") {
|
|
2601
|
+
focusFileInGraph(id);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2466
2604
|
void focusSymbolInGraph(id, {
|
|
2467
2605
|
maxDegrees: 1,
|
|
2468
2606
|
animate: true,
|
|
@@ -2473,23 +2611,22 @@ function setupToolbar() {
|
|
|
2473
2611
|
});
|
|
2474
2612
|
});
|
|
2475
2613
|
}
|
|
2614
|
+
function updateDropdownResults() {
|
|
2615
|
+
const results = buildGraphSearchResults({
|
|
2616
|
+
query: searchInput.value,
|
|
2617
|
+
symbolIds: rankedSymbols,
|
|
2618
|
+
nodes: graphNodes,
|
|
2619
|
+
fileIndex: fileToSymbolIds
|
|
2620
|
+
});
|
|
2621
|
+
showDropdownResults(results);
|
|
2622
|
+
}
|
|
2476
2623
|
searchInput.addEventListener("focus", () => {
|
|
2477
|
-
|
|
2478
|
-
showDropdownResults(rankedSymbols.slice(0, 20));
|
|
2479
|
-
}
|
|
2624
|
+
updateDropdownResults();
|
|
2480
2625
|
});
|
|
2481
2626
|
searchInput.addEventListener("input", () => {
|
|
2482
2627
|
clearTimeout(searchTimeout);
|
|
2483
2628
|
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);
|
|
2629
|
+
updateDropdownResults();
|
|
2493
2630
|
}, 150);
|
|
2494
2631
|
});
|
|
2495
2632
|
searchInput.addEventListener("keydown", (e) => {
|
|
@@ -2572,25 +2709,45 @@ var settingsBtn = document.getElementById("settings-btn");
|
|
|
2572
2709
|
var settingsModal = document.getElementById("settings-modal");
|
|
2573
2710
|
var settingsBackdrop = document.getElementById("settings-backdrop");
|
|
2574
2711
|
var settingsCloseBtn = document.getElementById("settings-close");
|
|
2712
|
+
var openRouterConnectModal = document.getElementById("openrouter-connect-modal");
|
|
2713
|
+
var openRouterConnectBackdrop = document.getElementById("openrouter-connect-backdrop");
|
|
2714
|
+
var openRouterConnectCloseBtn = document.getElementById("openrouter-connect-close");
|
|
2715
|
+
var openRouterConnectCancelBtn = document.getElementById("openrouter-connect-cancel");
|
|
2716
|
+
var openRouterConnectContinueBtn = document.getElementById("openrouter-connect-continue");
|
|
2717
|
+
var openRouterPersistCheckbox = document.getElementById("openrouter-persist-checkbox");
|
|
2575
2718
|
var settingsPath = document.getElementById("settings-path");
|
|
2576
2719
|
var settingsList = document.getElementById("settings-list");
|
|
2577
2720
|
var settingsBanner = document.getElementById("settings-banner");
|
|
2721
|
+
var settingsOpenRouterSession = document.getElementById("settings-openrouter-session");
|
|
2722
|
+
var settingsOpenRouterSessionMeta = document.getElementById("settings-openrouter-session-meta");
|
|
2578
2723
|
var settingsSaveBtn = document.getElementById("settings-save");
|
|
2579
2724
|
var settingsResetBtn = document.getElementById("settings-reset");
|
|
2725
|
+
var disconnectOpenRouterBtn = document.getElementById("disconnect-openrouter-btn");
|
|
2726
|
+
var connectOpenRouterButtons = Array.from(
|
|
2727
|
+
document.querySelectorAll("[data-openrouter-connect]")
|
|
2728
|
+
);
|
|
2729
|
+
var configOverlaySpotlight = document.getElementById("config-overlay-spotlight");
|
|
2730
|
+
var configOverlayIntro = document.getElementById("config-overlay-intro");
|
|
2731
|
+
var configConnectStatus = document.getElementById("config-connect-status");
|
|
2580
2732
|
var ws;
|
|
2581
2733
|
var currentAssistantEl = null;
|
|
2582
2734
|
var assistantText = "";
|
|
2583
2735
|
var hadToolCalls = false;
|
|
2584
2736
|
var settingsPayload = null;
|
|
2585
2737
|
var activeSavedSession = null;
|
|
2738
|
+
var activeBaseUrl = "";
|
|
2739
|
+
var sessionOpenRouterConnected = false;
|
|
2586
2740
|
var sessionRefreshTracker = createLatestRequestTracker();
|
|
2587
2741
|
var TOOL_RESULT_MAX = 500;
|
|
2742
|
+
var OPENROUTER_PKCE_VERIFIER_KEY = "minicode:openrouter:pkce-verifier";
|
|
2743
|
+
var OPENROUTER_PERSIST_TO_ENV_KEY = "minicode:openrouter:persist-to-env";
|
|
2588
2744
|
function connect() {
|
|
2589
2745
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
2590
2746
|
ws = new WebSocket(`${protocol}//${location.host}`);
|
|
2591
2747
|
ws.onopen = () => {
|
|
2592
2748
|
setStatus("ready");
|
|
2593
2749
|
fetchStatus();
|
|
2750
|
+
refreshModelList();
|
|
2594
2751
|
fetchContext();
|
|
2595
2752
|
};
|
|
2596
2753
|
ws.onclose = () => {
|
|
@@ -2617,6 +2774,127 @@ function setBusy(busy) {
|
|
|
2617
2774
|
}
|
|
2618
2775
|
}
|
|
2619
2776
|
var configOverlay = document.getElementById("config-overlay");
|
|
2777
|
+
function setConfigConnectStatus(message, tone) {
|
|
2778
|
+
if (!configConnectStatus) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
configConnectStatus.textContent = message;
|
|
2782
|
+
configConnectStatus.className = `config-connect-status ${tone}`;
|
|
2783
|
+
}
|
|
2784
|
+
function clearConfigConnectStatus() {
|
|
2785
|
+
if (!configConnectStatus) {
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
configConnectStatus.textContent = "";
|
|
2789
|
+
configConnectStatus.className = "config-connect-status hidden";
|
|
2790
|
+
}
|
|
2791
|
+
function encodeBase64Url(bytes) {
|
|
2792
|
+
let binary = "";
|
|
2793
|
+
for (const byte of bytes) {
|
|
2794
|
+
binary += String.fromCharCode(byte);
|
|
2795
|
+
}
|
|
2796
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2797
|
+
}
|
|
2798
|
+
function createPkceVerifier() {
|
|
2799
|
+
const bytes = new Uint8Array(32);
|
|
2800
|
+
crypto.getRandomValues(bytes);
|
|
2801
|
+
return encodeBase64Url(bytes);
|
|
2802
|
+
}
|
|
2803
|
+
async function createPkceChallenge(verifier) {
|
|
2804
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
2805
|
+
return encodeBase64Url(new Uint8Array(digest));
|
|
2806
|
+
}
|
|
2807
|
+
async function startOpenRouterConnect(persistToEnv) {
|
|
2808
|
+
const verifier = createPkceVerifier();
|
|
2809
|
+
const challenge = await createPkceChallenge(verifier);
|
|
2810
|
+
sessionStorage.setItem(OPENROUTER_PKCE_VERIFIER_KEY, verifier);
|
|
2811
|
+
sessionStorage.setItem(OPENROUTER_PERSIST_TO_ENV_KEY, persistToEnv ? "1" : "0");
|
|
2812
|
+
setConfigConnectStatus("Redirecting to OpenRouter\u2026", "info");
|
|
2813
|
+
const callbackUrl = new URL(location.pathname, location.origin).toString();
|
|
2814
|
+
const authUrl = new URL("https://openrouter.ai/auth");
|
|
2815
|
+
authUrl.searchParams.set("callback_url", callbackUrl);
|
|
2816
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
2817
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
2818
|
+
location.assign(authUrl.toString());
|
|
2819
|
+
}
|
|
2820
|
+
async function maybeHandleOpenRouterCallback() {
|
|
2821
|
+
const url = new URL(location.href);
|
|
2822
|
+
const code = url.searchParams.get("code");
|
|
2823
|
+
if (!code) {
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
const cleanedUrl = `${url.pathname}${url.hash}`;
|
|
2827
|
+
history.replaceState({}, document.title, cleanedUrl);
|
|
2828
|
+
const codeVerifier = sessionStorage.getItem(OPENROUTER_PKCE_VERIFIER_KEY);
|
|
2829
|
+
const persistToEnv = sessionStorage.getItem(OPENROUTER_PERSIST_TO_ENV_KEY) === "1";
|
|
2830
|
+
sessionStorage.removeItem(OPENROUTER_PKCE_VERIFIER_KEY);
|
|
2831
|
+
sessionStorage.removeItem(OPENROUTER_PERSIST_TO_ENV_KEY);
|
|
2832
|
+
if (!codeVerifier) {
|
|
2833
|
+
setConfigConnectStatus(
|
|
2834
|
+
"OpenRouter sign-in could not be completed because the local PKCE verifier was missing. Start the connect flow again.",
|
|
2835
|
+
"error"
|
|
2836
|
+
);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
for (const button of connectOpenRouterButtons) {
|
|
2840
|
+
button.disabled = true;
|
|
2841
|
+
}
|
|
2842
|
+
setConfigConnectStatus("Connecting OpenRouter to this serve session\u2026", "info");
|
|
2843
|
+
try {
|
|
2844
|
+
const res = await fetch("/api/openrouter/connect", {
|
|
2845
|
+
method: "POST",
|
|
2846
|
+
headers: { "Content-Type": "application/json" },
|
|
2847
|
+
body: JSON.stringify({ code, codeVerifier, persistToEnv })
|
|
2848
|
+
});
|
|
2849
|
+
const body = await res.json();
|
|
2850
|
+
if (!res.ok) {
|
|
2851
|
+
throw new Error("error" in body ? body.error : `Failed to connect OpenRouter (${res.status})`);
|
|
2852
|
+
}
|
|
2853
|
+
activeBaseUrl = body.baseUrl;
|
|
2854
|
+
addMessage(body.message, "thinking");
|
|
2855
|
+
const statusTone = body.persistWarning ? "info" : body.needsSetup ? "info" : "success";
|
|
2856
|
+
setConfigConnectStatus(body.message, statusTone);
|
|
2857
|
+
await fetchStatus();
|
|
2858
|
+
await refreshModelList();
|
|
2859
|
+
const onlyModelMissing = body.needsSetup && body.missing.length === 1 && body.missing[0]?.includes("MODEL");
|
|
2860
|
+
if (onlyModelMissing) {
|
|
2861
|
+
modelDropdown.classList.remove("hidden");
|
|
2862
|
+
sessionDropdown.classList.add("hidden");
|
|
2863
|
+
}
|
|
2864
|
+
} catch (error) {
|
|
2865
|
+
const message = error instanceof Error ? error.message : "Failed to connect OpenRouter";
|
|
2866
|
+
setConfigConnectStatus(message, "error");
|
|
2867
|
+
} finally {
|
|
2868
|
+
for (const button of connectOpenRouterButtons) {
|
|
2869
|
+
button.disabled = false;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
async function disconnectOpenRouter() {
|
|
2874
|
+
disconnectOpenRouterBtn.disabled = true;
|
|
2875
|
+
clearSettingsBanner();
|
|
2876
|
+
try {
|
|
2877
|
+
const res = await fetch("/api/openrouter/disconnect", {
|
|
2878
|
+
method: "POST",
|
|
2879
|
+
headers: { "Content-Type": "application/json" }
|
|
2880
|
+
});
|
|
2881
|
+
const body = await res.json();
|
|
2882
|
+
if (!res.ok) {
|
|
2883
|
+
throw new Error("error" in body ? body.error : `Failed to disconnect OpenRouter (${res.status})`);
|
|
2884
|
+
}
|
|
2885
|
+
activeBaseUrl = body.baseUrl;
|
|
2886
|
+
addMessage(body.message, "thinking");
|
|
2887
|
+
setSettingsBanner(body.message, body.disconnected ? "success" : "info");
|
|
2888
|
+
clearConfigConnectStatus();
|
|
2889
|
+
await fetchStatus();
|
|
2890
|
+
await refreshModelList();
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
const message = error instanceof Error ? error.message : "Failed to disconnect OpenRouter";
|
|
2893
|
+
setSettingsBanner(message, "error");
|
|
2894
|
+
} finally {
|
|
2895
|
+
disconnectOpenRouterBtn.disabled = false;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2620
2898
|
async function fetchStatus() {
|
|
2621
2899
|
try {
|
|
2622
2900
|
const res = await fetch("/api/status");
|
|
@@ -2624,6 +2902,9 @@ async function fetchStatus() {
|
|
|
2624
2902
|
modelInfo.textContent = data.model || "Select model";
|
|
2625
2903
|
modelInfo.classList.toggle("placeholder", !data.model);
|
|
2626
2904
|
activeModel = data.model;
|
|
2905
|
+
activeBaseUrl = data.baseUrl ?? "";
|
|
2906
|
+
sessionOpenRouterConnected = data.sessionOpenRouterConnected ?? false;
|
|
2907
|
+
renderOpenRouterSessionControls();
|
|
2627
2908
|
if (data.needsSetup) {
|
|
2628
2909
|
configOverlay.classList.remove("hidden");
|
|
2629
2910
|
chatInput.disabled = true;
|
|
@@ -2631,6 +2912,11 @@ async function fetchStatus() {
|
|
|
2631
2912
|
const missingEl = document.getElementById("config-missing");
|
|
2632
2913
|
if (missingEl && data.missing && data.missing.length > 0) {
|
|
2633
2914
|
const isOnlyModelMissing = data.missing.length === 1 && data.missing[0].includes("MODEL");
|
|
2915
|
+
const hasPersistedOpenRouter = isOnlyModelMissing && (data.baseUrl ?? "").includes("openrouter");
|
|
2916
|
+
if (configOverlayIntro) {
|
|
2917
|
+
configOverlayIntro.textContent = hasPersistedOpenRouter ? "OpenRouter is already configured. Select a model to continue:" : "minicode needs a model provider to run. Configure one of the following:";
|
|
2918
|
+
}
|
|
2919
|
+
configOverlaySpotlight?.classList.toggle("hidden", hasPersistedOpenRouter);
|
|
2634
2920
|
const hint = isOnlyModelMissing ? ` \u2014 select one from the <strong>model dropdown</strong> above, or set it in config` : "";
|
|
2635
2921
|
missingEl.innerHTML = `<strong>Missing:</strong> ${data.missing.map(escapeHtml).join(", ")}${hint}`;
|
|
2636
2922
|
missingEl.classList.remove("hidden");
|
|
@@ -2638,6 +2924,16 @@ async function fetchStatus() {
|
|
|
2638
2924
|
} else {
|
|
2639
2925
|
configOverlay.classList.add("hidden");
|
|
2640
2926
|
chatInput.disabled = false;
|
|
2927
|
+
sendBtn.disabled = false;
|
|
2928
|
+
const missingEl = document.getElementById("config-missing");
|
|
2929
|
+
if (missingEl) {
|
|
2930
|
+
missingEl.classList.add("hidden");
|
|
2931
|
+
missingEl.innerHTML = "";
|
|
2932
|
+
}
|
|
2933
|
+
if (configOverlayIntro) {
|
|
2934
|
+
configOverlayIntro.textContent = "minicode needs a model provider to run. Configure one of the following:";
|
|
2935
|
+
}
|
|
2936
|
+
configOverlaySpotlight?.classList.remove("hidden");
|
|
2641
2937
|
}
|
|
2642
2938
|
} catch {
|
|
2643
2939
|
}
|
|
@@ -2746,6 +3042,50 @@ function addMessage(text, type, markdown = false) {
|
|
|
2746
3042
|
scrollToBottom();
|
|
2747
3043
|
return el;
|
|
2748
3044
|
}
|
|
3045
|
+
function clearChatTranscript() {
|
|
3046
|
+
messagesEl.innerHTML = "";
|
|
3047
|
+
currentAssistantEl = null;
|
|
3048
|
+
assistantText = "";
|
|
3049
|
+
hadToolCalls = false;
|
|
3050
|
+
}
|
|
3051
|
+
function stringifyToolInput(input) {
|
|
3052
|
+
const entries = Object.entries(input).flatMap(([key, value]) => {
|
|
3053
|
+
if (value === void 0 || value === null) {
|
|
3054
|
+
return [];
|
|
3055
|
+
}
|
|
3056
|
+
if (typeof value === "string") {
|
|
3057
|
+
return [[key, value]];
|
|
3058
|
+
}
|
|
3059
|
+
return [[key, JSON.stringify(value)]];
|
|
3060
|
+
});
|
|
3061
|
+
return Object.fromEntries(entries);
|
|
3062
|
+
}
|
|
3063
|
+
function addToolResultPreview(name, result) {
|
|
3064
|
+
const toolEls = messagesEl.querySelectorAll(`.tool-call[data-tool-name="${name}"]`);
|
|
3065
|
+
if (toolEls.length === 0) {
|
|
3066
|
+
addToolCall(name, {});
|
|
3067
|
+
}
|
|
3068
|
+
finalizeToolCall(name, result);
|
|
3069
|
+
}
|
|
3070
|
+
function renderLoadedSessionMessages(messages) {
|
|
3071
|
+
clearChatTranscript();
|
|
3072
|
+
for (const message of messages) {
|
|
3073
|
+
if (message.role === "user") {
|
|
3074
|
+
addMessage(message.content, "user");
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
if (message.role === "assistant") {
|
|
3078
|
+
if (message.content.trim().length > 0) {
|
|
3079
|
+
addMessage(message.content, "assistant", true);
|
|
3080
|
+
}
|
|
3081
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
3082
|
+
addToolCall(toolCall.name, stringifyToolInput(toolCall.input));
|
|
3083
|
+
}
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
addToolResultPreview(message.toolName, message.content);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
2749
3089
|
function summarizeToolInput(name, input) {
|
|
2750
3090
|
const key = input.path ?? input.file_path ?? input.command ?? input.query ?? input.pattern ?? input.name ?? input.old_string;
|
|
2751
3091
|
if (typeof key === "string") {
|
|
@@ -2786,7 +3126,7 @@ function finalizeToolCall(name, result, elapsedMs) {
|
|
|
2786
3126
|
if (!el) return;
|
|
2787
3127
|
const timeEl = el.querySelector(".tool-time");
|
|
2788
3128
|
if (timeEl) {
|
|
2789
|
-
timeEl.textContent = `${elapsedMs}ms
|
|
3129
|
+
timeEl.textContent = elapsedMs && elapsedMs > 0 ? `${elapsedMs}ms` : "";
|
|
2790
3130
|
}
|
|
2791
3131
|
const resultEl = el.querySelector(".tool-result");
|
|
2792
3132
|
if (resultEl && result) {
|
|
@@ -2833,6 +3173,13 @@ function closeHeaderMenus() {
|
|
|
2833
3173
|
function isSettingsModalOpen() {
|
|
2834
3174
|
return !settingsModal.classList.contains("hidden");
|
|
2835
3175
|
}
|
|
3176
|
+
function isOpenRouterConnectModalOpen() {
|
|
3177
|
+
return !openRouterConnectModal.classList.contains("hidden");
|
|
3178
|
+
}
|
|
3179
|
+
function syncModalOpenState() {
|
|
3180
|
+
const anyModalOpen = isSettingsModalOpen() || isOpenRouterConnectModalOpen();
|
|
3181
|
+
document.body.classList.toggle("modal-open", anyModalOpen);
|
|
3182
|
+
}
|
|
2836
3183
|
function formatSettingsValue(value) {
|
|
2837
3184
|
return value === null ? "(unset)" : String(value);
|
|
2838
3185
|
}
|
|
@@ -2844,6 +3191,17 @@ function clearSettingsBanner() {
|
|
|
2844
3191
|
settingsBanner.textContent = "";
|
|
2845
3192
|
settingsBanner.className = "settings-banner hidden";
|
|
2846
3193
|
}
|
|
3194
|
+
function renderOpenRouterSessionControls() {
|
|
3195
|
+
if (sessionOpenRouterConnected) {
|
|
3196
|
+
settingsOpenRouterSession.classList.remove("hidden");
|
|
3197
|
+
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.";
|
|
3198
|
+
disconnectOpenRouterBtn.disabled = false;
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
settingsOpenRouterSession.classList.add("hidden");
|
|
3202
|
+
settingsOpenRouterSessionMeta.textContent = "";
|
|
3203
|
+
disconnectOpenRouterBtn.disabled = false;
|
|
3204
|
+
}
|
|
2847
3205
|
function createSettingsControl(entry, inputId) {
|
|
2848
3206
|
const value = entry.persistedValue;
|
|
2849
3207
|
if (entry.type === "boolean") {
|
|
@@ -3038,15 +3396,29 @@ function openSettings() {
|
|
|
3038
3396
|
closeHeaderMenus();
|
|
3039
3397
|
settingsModal.classList.remove("hidden");
|
|
3040
3398
|
settingsModal.setAttribute("aria-hidden", "false");
|
|
3041
|
-
|
|
3399
|
+
syncModalOpenState();
|
|
3042
3400
|
void loadSettings();
|
|
3043
3401
|
}
|
|
3044
3402
|
function closeSettings() {
|
|
3045
3403
|
settingsModal.classList.add("hidden");
|
|
3046
3404
|
settingsModal.setAttribute("aria-hidden", "true");
|
|
3047
|
-
|
|
3405
|
+
syncModalOpenState();
|
|
3048
3406
|
clearSettingsBanner();
|
|
3049
3407
|
}
|
|
3408
|
+
function openOpenRouterConnectModal() {
|
|
3409
|
+
closeHeaderMenus();
|
|
3410
|
+
openRouterConnectModal.classList.remove("hidden");
|
|
3411
|
+
openRouterConnectModal.setAttribute("aria-hidden", "false");
|
|
3412
|
+
openRouterPersistCheckbox.checked = false;
|
|
3413
|
+
openRouterConnectContinueBtn.disabled = false;
|
|
3414
|
+
syncModalOpenState();
|
|
3415
|
+
}
|
|
3416
|
+
function closeOpenRouterConnectModal() {
|
|
3417
|
+
openRouterConnectModal.classList.add("hidden");
|
|
3418
|
+
openRouterConnectModal.setAttribute("aria-hidden", "true");
|
|
3419
|
+
openRouterConnectContinueBtn.disabled = false;
|
|
3420
|
+
syncModalOpenState();
|
|
3421
|
+
}
|
|
3050
3422
|
chatForm.addEventListener("submit", (e) => {
|
|
3051
3423
|
e.preventDefault();
|
|
3052
3424
|
const message = chatInput.value.trim();
|
|
@@ -3089,31 +3461,63 @@ async function refreshModelList() {
|
|
|
3089
3461
|
const res = await fetch("/api/models");
|
|
3090
3462
|
const data = await res.json();
|
|
3091
3463
|
activeModel = data.activeModel;
|
|
3464
|
+
const hasActiveModel = !!activeModel && data.models.some((model) => model.id === activeModel);
|
|
3092
3465
|
if (!data.models || data.models.length === 0) {
|
|
3466
|
+
modelInfo.textContent = "Select model";
|
|
3467
|
+
modelInfo.classList.add("placeholder");
|
|
3093
3468
|
modelList.innerHTML = '<div class="dropdown-empty">No models available</div>';
|
|
3094
3469
|
return;
|
|
3095
3470
|
}
|
|
3471
|
+
if (hasActiveModel) {
|
|
3472
|
+
modelInfo.textContent = activeModel;
|
|
3473
|
+
modelInfo.classList.remove("placeholder");
|
|
3474
|
+
} else {
|
|
3475
|
+
modelInfo.textContent = "Select model";
|
|
3476
|
+
modelInfo.classList.add("placeholder");
|
|
3477
|
+
}
|
|
3096
3478
|
modelList.innerHTML = "";
|
|
3097
3479
|
for (const m2 of data.models) {
|
|
3098
3480
|
const el = document.createElement("div");
|
|
3099
3481
|
el.className = "model-item" + (m2.id === activeModel ? " active" : "");
|
|
3100
3482
|
el.textContent = m2.name ?? m2.id;
|
|
3101
3483
|
el.title = m2.id;
|
|
3102
|
-
el.addEventListener("click", () =>
|
|
3484
|
+
el.addEventListener("click", () => {
|
|
3485
|
+
void switchModel(m2.id);
|
|
3486
|
+
});
|
|
3103
3487
|
modelList.appendChild(el);
|
|
3104
3488
|
}
|
|
3105
3489
|
} catch {
|
|
3106
3490
|
modelList.innerHTML = '<div class="dropdown-empty">Failed to load models</div>';
|
|
3107
3491
|
}
|
|
3108
3492
|
}
|
|
3109
|
-
function switchModel(modelId) {
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3493
|
+
async function switchModel(modelId) {
|
|
3494
|
+
try {
|
|
3495
|
+
const res = await fetch("/api/model", {
|
|
3496
|
+
method: "POST",
|
|
3497
|
+
headers: { "Content-Type": "application/json" },
|
|
3498
|
+
body: JSON.stringify({
|
|
3499
|
+
model: modelId,
|
|
3500
|
+
persistToHomeEnv: true
|
|
3501
|
+
})
|
|
3502
|
+
});
|
|
3503
|
+
const body = await res.json();
|
|
3504
|
+
if (!res.ok) {
|
|
3505
|
+
throw new Error("error" in body ? body.error : `Failed to switch model (${res.status})`);
|
|
3506
|
+
}
|
|
3507
|
+
modelInfo.textContent = modelId || "Select model";
|
|
3508
|
+
modelInfo.classList.toggle("placeholder", !modelId);
|
|
3509
|
+
activeModel = modelId;
|
|
3510
|
+
modelDropdown.classList.add("hidden");
|
|
3511
|
+
if (body.persistedToEnv) {
|
|
3512
|
+
addMessage(`Model switched to: ${modelId}. Saved as the default in ~/.minicode/.env.`, "thinking");
|
|
3513
|
+
} else {
|
|
3514
|
+
addMessage(`Model switched to: ${modelId}`, "thinking");
|
|
3515
|
+
}
|
|
3516
|
+
await fetchStatus();
|
|
3517
|
+
} catch (error) {
|
|
3518
|
+
const message = error instanceof Error ? error.message : "Failed to switch model";
|
|
3519
|
+
addMessage(message, "error");
|
|
3520
|
+
}
|
|
3117
3521
|
}
|
|
3118
3522
|
sessionBtn.addEventListener("click", (e) => {
|
|
3119
3523
|
e.stopPropagation();
|
|
@@ -3209,9 +3613,12 @@ async function loadSession(label) {
|
|
|
3209
3613
|
body: JSON.stringify({ label })
|
|
3210
3614
|
});
|
|
3211
3615
|
if (res.ok) {
|
|
3616
|
+
const body = await res.json();
|
|
3212
3617
|
sessionDropdown.classList.add("hidden");
|
|
3213
|
-
|
|
3214
|
-
|
|
3618
|
+
renderLoadedSessionMessages(body.messages);
|
|
3619
|
+
if (body.messages.length === 0) {
|
|
3620
|
+
addMessage(`Session "${body.label}" restored`, "thinking");
|
|
3621
|
+
}
|
|
3215
3622
|
void refreshSessionList();
|
|
3216
3623
|
}
|
|
3217
3624
|
} catch {
|
|
@@ -3247,9 +3654,19 @@ settingsCloseBtn.addEventListener("click", () => {
|
|
|
3247
3654
|
settingsBackdrop.addEventListener("click", () => {
|
|
3248
3655
|
closeSettings();
|
|
3249
3656
|
});
|
|
3657
|
+
openRouterConnectBackdrop.addEventListener("click", () => {
|
|
3658
|
+
closeOpenRouterConnectModal();
|
|
3659
|
+
});
|
|
3660
|
+
openRouterConnectCloseBtn.addEventListener("click", () => {
|
|
3661
|
+
closeOpenRouterConnectModal();
|
|
3662
|
+
});
|
|
3663
|
+
openRouterConnectCancelBtn.addEventListener("click", () => {
|
|
3664
|
+
closeOpenRouterConnectModal();
|
|
3665
|
+
});
|
|
3250
3666
|
settingsResetBtn.addEventListener("click", () => {
|
|
3251
3667
|
clearSettingsBanner();
|
|
3252
3668
|
renderSettings();
|
|
3669
|
+
renderOpenRouterSessionControls();
|
|
3253
3670
|
});
|
|
3254
3671
|
settingsSaveBtn.addEventListener("click", async () => {
|
|
3255
3672
|
if (!settingsPayload) {
|
|
@@ -3298,11 +3715,31 @@ settingsList.addEventListener("change", () => {
|
|
|
3298
3715
|
clearSettingsBanner();
|
|
3299
3716
|
updateSettingsActions();
|
|
3300
3717
|
});
|
|
3718
|
+
disconnectOpenRouterBtn.addEventListener("click", () => {
|
|
3719
|
+
void disconnectOpenRouter();
|
|
3720
|
+
});
|
|
3301
3721
|
document.addEventListener("keydown", (event) => {
|
|
3302
|
-
if (event.key
|
|
3722
|
+
if (event.key !== "Escape") {
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
if (isOpenRouterConnectModalOpen()) {
|
|
3726
|
+
closeOpenRouterConnectModal();
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
if (isSettingsModalOpen()) {
|
|
3303
3730
|
closeSettings();
|
|
3304
3731
|
}
|
|
3305
3732
|
});
|
|
3733
|
+
for (const button of connectOpenRouterButtons) {
|
|
3734
|
+
button.addEventListener("click", () => {
|
|
3735
|
+
openOpenRouterConnectModal();
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
openRouterConnectContinueBtn.addEventListener("click", () => {
|
|
3739
|
+
openRouterConnectContinueBtn.disabled = true;
|
|
3740
|
+
closeOpenRouterConnectModal();
|
|
3741
|
+
void startOpenRouterConnect(openRouterPersistCheckbox.checked);
|
|
3742
|
+
});
|
|
3306
3743
|
var chatPane = document.getElementById("chat-pane");
|
|
3307
3744
|
var divider = document.getElementById("pane-divider");
|
|
3308
3745
|
divider.addEventListener("mousedown", (e) => {
|
|
@@ -3337,3 +3774,4 @@ graphToggle.addEventListener("click", () => {
|
|
|
3337
3774
|
});
|
|
3338
3775
|
connect();
|
|
3339
3776
|
initGraph();
|
|
3777
|
+
void maybeHandleOpenRouterCallback();
|