@sean.holung/minicode 0.3.2 → 0.3.3
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 +48 -43
- package/dist/scripts/run-benchmarks.js +147 -0
- package/dist/src/agent/config.js +149 -40
- package/dist/src/agent/editable-config.js +314 -0
- package/dist/src/analysis/structural-analysis.js +379 -0
- package/dist/src/benchmark/evaluator.js +79 -0
- package/dist/src/benchmark/index.js +4 -0
- package/dist/src/benchmark/reporter.js +177 -0
- package/dist/src/benchmark/runner.js +100 -0
- package/dist/src/benchmark/task-loader.js +78 -0
- package/dist/src/benchmark/types.js +5 -0
- package/dist/src/cli/args.js +10 -0
- package/dist/src/cli/config-slash-command.js +135 -0
- package/dist/src/cli/plugin-install.js +69 -0
- package/dist/src/index.js +76 -6
- package/dist/src/indexer/cache.js +6 -4
- package/dist/src/indexer/code-map.js +41 -13
- package/dist/src/indexer/plugins/typescript.js +70 -23
- package/dist/src/indexer/project-index.js +175 -36
- package/dist/src/indexer/symbol-names.js +92 -0
- package/dist/src/model-utils.js +18 -0
- package/dist/src/serve/agent-bridge.js +203 -24
- package/dist/src/serve/mcp-server.js +405 -0
- package/dist/src/serve/server.js +165 -10
- package/dist/src/serve/websocket.js +8 -0
- package/dist/src/shared/graph-styles.js +119 -0
- package/dist/src/tools/find-path.js +75 -0
- package/dist/src/tools/find-references.js +7 -2
- package/dist/src/tools/get-dependencies.js +3 -2
- package/dist/src/tools/read-symbol.js +12 -5
- package/dist/src/tools/registry.js +3 -1
- package/dist/src/tools/search-code-map.js +4 -2
- package/dist/src/ui/app.js +1 -1
- package/dist/src/ui/cli-ink.js +79 -4
- package/dist/src/ui/components/header-bar.js +6 -2
- package/dist/src/ui/state/ui-store.js +5 -0
- package/dist/src/web/app.js +1124 -176
- package/dist/src/web/index.html +113 -3
- package/dist/src/web/style.css +973 -55
- package/dist/tests/agent.test.js +31 -0
- package/dist/tests/analysis-helpers.test.js +89 -0
- package/dist/tests/analysis-ui.test.js +29 -0
- package/dist/tests/benchmark-harness.test.js +527 -0
- package/dist/tests/config-api.test.js +143 -0
- package/dist/tests/config-integration.test.js +751 -0
- package/dist/tests/config-slash-command.test.js +106 -0
- package/dist/tests/config.test.js +42 -1
- package/dist/tests/context-indicator.test.js +220 -0
- package/dist/tests/editable-config.test.js +109 -0
- package/dist/tests/find-path.test.js +183 -0
- package/dist/tests/focus-tracker.test.js +62 -0
- package/dist/tests/graph-onboarding.test.js +55 -0
- package/dist/tests/graph-styles.test.js +65 -0
- package/dist/tests/indexer.test.js +137 -0
- package/dist/tests/mcp-and-plugin.test.js +186 -0
- package/dist/tests/model-client-openai.test.js +29 -0
- package/dist/tests/model-selection.test.js +136 -0
- package/dist/tests/model-utils.test.js +22 -0
- package/dist/tests/reasoning-effort.test.js +264 -0
- package/dist/tests/run-benchmarks.test.js +161 -0
- package/dist/tests/search-code-map.test.js +18 -0
- package/dist/tests/serve.integration.test.js +218 -2
- package/dist/tests/session-ui.test.js +21 -0
- package/dist/tests/session.test.js +50 -0
- package/dist/tests/settings-ui.test.js +30 -0
- package/dist/tests/structural-analysis.test.js +218 -0
- package/node_modules/@minicode/agent-sdk/README.md +80 -51
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +16 -5
- 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 +51 -33
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +14 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +3 -2
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.js +2 -0
- package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts +35 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts.map +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js +64 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js.map +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +7 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +5 -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 +83 -11
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js +8 -1
- package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js +3 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js +8 -2
- package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -5
- package/plugin/.claude-plugin/plugin.json +12 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/CLAUDE.md +26 -0
- package/plugin/skills/analyze/SKILL.md +12 -0
- package/plugin/skills/focus/SKILL.md +20 -0
- package/plugin/skills/graph/SKILL.md +13 -0
- package/plugin/skills/symbols/SKILL.md +13 -0
package/dist/src/web/app.js
CHANGED
|
@@ -1231,14 +1231,100 @@ function renderMarkdownInto(el, text) {
|
|
|
1231
1231
|
}
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
|
-
// src/web/
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1234
|
+
// src/web/analysis-helpers.ts
|
|
1235
|
+
function findingTypeLabel(type) {
|
|
1236
|
+
switch (type) {
|
|
1237
|
+
case "cycle":
|
|
1238
|
+
return "Cycle";
|
|
1239
|
+
case "fanInOutlier":
|
|
1240
|
+
return "High fan-in";
|
|
1241
|
+
case "fanOutOutlier":
|
|
1242
|
+
return "High fan-out";
|
|
1243
|
+
case "hotspot":
|
|
1244
|
+
return "Hotspot";
|
|
1245
|
+
case "fileCoupling":
|
|
1246
|
+
return "File coupling";
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function findingSeverityLabel(severity) {
|
|
1250
|
+
switch (severity) {
|
|
1251
|
+
case "high":
|
|
1252
|
+
return "High";
|
|
1253
|
+
case "warning":
|
|
1254
|
+
return "Warning";
|
|
1255
|
+
case "info":
|
|
1256
|
+
return "Info";
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function buildFindingMetricChips(finding) {
|
|
1260
|
+
switch (finding.type) {
|
|
1261
|
+
case "cycle":
|
|
1262
|
+
return [
|
|
1263
|
+
`${Number(finding.metrics.cycleSize ?? finding.symbols.length)} symbols`,
|
|
1264
|
+
`${Number(finding.metrics.edgeCount ?? 0)} edges`,
|
|
1265
|
+
`${Number(finding.metrics.fileCount ?? finding.files.length)} files`
|
|
1266
|
+
];
|
|
1267
|
+
case "fanInOutlier":
|
|
1268
|
+
return [
|
|
1269
|
+
`fan-in ${Number(finding.metrics.fanIn ?? 0)}`,
|
|
1270
|
+
`threshold ${Number(finding.metrics.threshold ?? 0)}`
|
|
1271
|
+
];
|
|
1272
|
+
case "fanOutOutlier":
|
|
1273
|
+
return [
|
|
1274
|
+
`fan-out ${Number(finding.metrics.fanOut ?? 0)}`,
|
|
1275
|
+
`threshold ${Number(finding.metrics.threshold ?? 0)}`
|
|
1276
|
+
];
|
|
1277
|
+
case "hotspot":
|
|
1278
|
+
return [
|
|
1279
|
+
`degree ${Number(finding.metrics.totalDegree ?? 0)}`,
|
|
1280
|
+
`${Number(finding.metrics.fanIn ?? 0)} in`,
|
|
1281
|
+
`${Number(finding.metrics.fanOut ?? 0)} out`
|
|
1282
|
+
];
|
|
1283
|
+
case "fileCoupling":
|
|
1284
|
+
return [
|
|
1285
|
+
`coupling ${Number(finding.metrics.totalCoupling ?? 0)}`,
|
|
1286
|
+
`instability ${Number(finding.metrics.instability ?? 0)}`
|
|
1287
|
+
];
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function buildFindingGraphContext(finding, edges) {
|
|
1291
|
+
const selectedSymbols = new Set(finding.symbols);
|
|
1292
|
+
const edgeIds = /* @__PURE__ */ new Set();
|
|
1293
|
+
for (const edge of edges) {
|
|
1294
|
+
const touchesSelected = selectedSymbols.has(edge.source) || selectedSymbols.has(edge.target);
|
|
1295
|
+
const internalToSelection = selectedSymbols.has(edge.source) && selectedSymbols.has(edge.target);
|
|
1296
|
+
if (finding.type === "cycle" && internalToSelection || finding.type !== "cycle" && touchesSelected) {
|
|
1297
|
+
edgeIds.add(`${edge.source}->${edge.target}:${edge.kind}`);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
nodes: [...selectedSymbols].sort((a, b2) => a.localeCompare(b2)),
|
|
1302
|
+
edgeIds: [...edgeIds].sort((a, b2) => a.localeCompare(b2))
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function countFindingsByType(findings) {
|
|
1306
|
+
return findings.reduce(
|
|
1307
|
+
(counts, finding) => {
|
|
1308
|
+
counts[finding.type] += 1;
|
|
1309
|
+
return counts;
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
cycle: 0,
|
|
1313
|
+
fanInOutlier: 0,
|
|
1314
|
+
fanOutOutlier: 0,
|
|
1315
|
+
hotspot: 0,
|
|
1316
|
+
fileCoupling: 0
|
|
1317
|
+
}
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
function filterFindings(findings, filter) {
|
|
1321
|
+
if (filter === "all") {
|
|
1322
|
+
return findings;
|
|
1323
|
+
}
|
|
1324
|
+
return findings.filter((finding) => finding.type === filter);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/shared/graph-styles.ts
|
|
1242
1328
|
var KIND_COLORS = {
|
|
1243
1329
|
function: { border: "#7aa2f7", bg: "rgba(122,162,247,0.15)" },
|
|
1244
1330
|
class: { border: "#bb9af7", bg: "rgba(187,154,247,0.15)" },
|
|
@@ -1254,6 +1340,121 @@ var EDGE_STYLES = {
|
|
|
1254
1340
|
implements: { lineStyle: "dashed", opacity: 0.6, color: "#2ac3de", width: 1.5 },
|
|
1255
1341
|
references: { lineStyle: "dotted", opacity: 0.3, color: "#565f89", width: 1 }
|
|
1256
1342
|
};
|
|
1343
|
+
function buildStylesheet() {
|
|
1344
|
+
const styles = [
|
|
1345
|
+
{
|
|
1346
|
+
selector: "node",
|
|
1347
|
+
style: {
|
|
1348
|
+
"label": "data(label)",
|
|
1349
|
+
"font-size": 11,
|
|
1350
|
+
"color": "#c0caf5",
|
|
1351
|
+
"text-valign": "bottom",
|
|
1352
|
+
"text-halign": "center",
|
|
1353
|
+
"text-margin-y": 5,
|
|
1354
|
+
"width": 20,
|
|
1355
|
+
"height": 20,
|
|
1356
|
+
"shape": "roundrectangle",
|
|
1357
|
+
"border-width": 1.5,
|
|
1358
|
+
"border-color": "#565f89",
|
|
1359
|
+
"background-color": "rgba(34,35,54,0.8)",
|
|
1360
|
+
"font-family": "'JetBrains Mono', monospace",
|
|
1361
|
+
"text-wrap": "none"
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
selector: "edge",
|
|
1366
|
+
style: {
|
|
1367
|
+
"width": 1,
|
|
1368
|
+
"line-color": "#565f89",
|
|
1369
|
+
"target-arrow-color": "#565f89",
|
|
1370
|
+
"target-arrow-shape": "triangle",
|
|
1371
|
+
"arrow-scale": 0.6,
|
|
1372
|
+
"curve-style": "bezier",
|
|
1373
|
+
"opacity": 0.4
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
];
|
|
1377
|
+
for (const [kind, colors] of Object.entries(KIND_COLORS)) {
|
|
1378
|
+
styles.push({
|
|
1379
|
+
selector: `node.${kind}`,
|
|
1380
|
+
style: {
|
|
1381
|
+
"color": colors.border,
|
|
1382
|
+
"border-color": colors.border,
|
|
1383
|
+
"background-color": colors.bg
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
for (const [kind, s] of Object.entries(EDGE_STYLES)) {
|
|
1388
|
+
styles.push({
|
|
1389
|
+
selector: `edge.${kind}`,
|
|
1390
|
+
style: {
|
|
1391
|
+
"line-style": s.lineStyle,
|
|
1392
|
+
"line-color": s.color,
|
|
1393
|
+
"target-arrow-color": s.color,
|
|
1394
|
+
"opacity": s.opacity,
|
|
1395
|
+
"width": s.width
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
styles.push({ selector: "node.pinned", style: { "border-color": "#e0af68", "border-width": 3 } });
|
|
1400
|
+
styles.push({ selector: "node.faded", style: { "opacity": 0.15 } });
|
|
1401
|
+
styles.push({ selector: "edge.faded", style: { "opacity": 0.05 } });
|
|
1402
|
+
styles.push({ selector: "node.highlighted", style: { "border-width": 2.5, "z-index": 10 } });
|
|
1403
|
+
styles.push({ selector: "edge.highlighted", style: { "opacity": 0.9, "width": 2, "z-index": 10 } });
|
|
1404
|
+
styles.push({
|
|
1405
|
+
selector: "node.analysis-flagged",
|
|
1406
|
+
style: {
|
|
1407
|
+
"border-style": "double",
|
|
1408
|
+
"border-width": 3,
|
|
1409
|
+
"overlay-color": "#7dcfff",
|
|
1410
|
+
"overlay-opacity": 0.05
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
styles.push({
|
|
1414
|
+
selector: "node.analysis-selected",
|
|
1415
|
+
style: {
|
|
1416
|
+
"border-color": "#f7768e",
|
|
1417
|
+
"border-width": 4,
|
|
1418
|
+
"background-color": "rgba(247,118,142,0.18)",
|
|
1419
|
+
"z-index": 30
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
styles.push({
|
|
1423
|
+
selector: "edge.analysis-flagged",
|
|
1424
|
+
style: {
|
|
1425
|
+
"opacity": 0.55,
|
|
1426
|
+
"width": 1.75
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
styles.push({
|
|
1430
|
+
selector: "edge.analysis-selected",
|
|
1431
|
+
style: {
|
|
1432
|
+
"line-color": "#f7768e",
|
|
1433
|
+
"target-arrow-color": "#f7768e",
|
|
1434
|
+
"opacity": 0.95,
|
|
1435
|
+
"width": 3,
|
|
1436
|
+
"z-index": 30
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
styles.push({ selector: "node.agent-pulse", style: { "border-color": "#ff9e64", "border-width": 4, "background-color": "rgba(255,158,100,0.25)" } });
|
|
1440
|
+
styles.push({ selector: "node.search-match", style: { "border-color": "#e0af68", "border-width": 2.5 } });
|
|
1441
|
+
styles.push({ selector: "node.expanded", style: { "border-width": 2.5 } });
|
|
1442
|
+
styles.push({ selector: "node.hover-target", style: { "border-width": 3, "width": 24, "height": 24, "overlay-color": "#c0caf5", "overlay-opacity": 0.08, "z-index": 20 } });
|
|
1443
|
+
return styles;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/web/graph.ts
|
|
1447
|
+
var cy = null;
|
|
1448
|
+
var graphNodes = /* @__PURE__ */ new Map();
|
|
1449
|
+
var graphEdges = [];
|
|
1450
|
+
var edgeIndex = /* @__PURE__ */ new Map();
|
|
1451
|
+
var pinnedNames = /* @__PURE__ */ new Set();
|
|
1452
|
+
var allSymbolNames = [];
|
|
1453
|
+
var initialized = false;
|
|
1454
|
+
var analysisReport = null;
|
|
1455
|
+
var activeAnalysisFindingId = null;
|
|
1456
|
+
var activeAnalysisFilter = "all";
|
|
1457
|
+
var analysisExplanationCache = /* @__PURE__ */ new Map();
|
|
1257
1458
|
var LAYOUT_OPTIONS = {
|
|
1258
1459
|
name: "cose",
|
|
1259
1460
|
nodeRepulsion: function() {
|
|
@@ -1322,9 +1523,11 @@ async function initGraph() {
|
|
|
1322
1523
|
setupToolbar();
|
|
1323
1524
|
if (pinnedNames.size > 0) {
|
|
1324
1525
|
for (const name of pinnedNames) {
|
|
1325
|
-
|
|
1526
|
+
addNodeNeighborhood(name, 1);
|
|
1326
1527
|
}
|
|
1327
1528
|
runLayout();
|
|
1529
|
+
} else {
|
|
1530
|
+
showOnboardingHint(cyEl);
|
|
1328
1531
|
}
|
|
1329
1532
|
} catch (err) {
|
|
1330
1533
|
console.error("Graph init failed:", err);
|
|
@@ -1334,22 +1537,28 @@ async function initGraph() {
|
|
|
1334
1537
|
}
|
|
1335
1538
|
function highlightAgentActivity(symbolName) {
|
|
1336
1539
|
if (!cy) return;
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
const added = findNode(symbolName);
|
|
1345
|
-
if (added) {
|
|
1346
|
-
added.addClass("agent-pulse");
|
|
1347
|
-
setTimeout(() => added.removeClass("agent-pulse"), 2e3);
|
|
1348
|
-
}
|
|
1540
|
+
void focusSymbolInGraph(symbolName, {
|
|
1541
|
+
maxDegrees: 0,
|
|
1542
|
+
pulse: true,
|
|
1543
|
+
pulseDuration: 2e3,
|
|
1544
|
+
animate: false,
|
|
1545
|
+
openDetail: true
|
|
1546
|
+
});
|
|
1349
1547
|
}
|
|
1350
1548
|
function resizeGraph() {
|
|
1351
1549
|
if (cy) cy.resize();
|
|
1352
1550
|
}
|
|
1551
|
+
function showOnboardingHint(container) {
|
|
1552
|
+
if (container.querySelector(".graph-onboarding")) return;
|
|
1553
|
+
const hint = document.createElement("div");
|
|
1554
|
+
hint.className = "graph-onboarding";
|
|
1555
|
+
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>';
|
|
1556
|
+
container.appendChild(hint);
|
|
1557
|
+
}
|
|
1558
|
+
function removeOnboardingHint() {
|
|
1559
|
+
const hint = document.querySelector(".graph-onboarding");
|
|
1560
|
+
if (hint) hint.remove();
|
|
1561
|
+
}
|
|
1353
1562
|
function buildEdgeIndex() {
|
|
1354
1563
|
edgeIndex.clear();
|
|
1355
1564
|
for (const edge of graphEdges) {
|
|
@@ -1367,23 +1576,77 @@ function buildEdgeIndex() {
|
|
|
1367
1576
|
tgtList.push(edge);
|
|
1368
1577
|
}
|
|
1369
1578
|
}
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
for (
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1579
|
+
function addNodeNeighborhood(symbolId, maxDegrees = 1) {
|
|
1580
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1581
|
+
let frontier = /* @__PURE__ */ new Set([symbolId]);
|
|
1582
|
+
for (let degree = 0; degree <= maxDegrees; degree += 1) {
|
|
1583
|
+
const next = /* @__PURE__ */ new Set();
|
|
1584
|
+
for (const currentId of frontier) {
|
|
1585
|
+
if (visited.has(currentId)) continue;
|
|
1586
|
+
visited.add(currentId);
|
|
1587
|
+
addNodeToGraph(currentId);
|
|
1588
|
+
if (degree === maxDegrees) continue;
|
|
1589
|
+
const edges = edgeIndex.get(currentId) || [];
|
|
1590
|
+
for (const edge of edges) {
|
|
1591
|
+
const neighbor = edge.source === currentId ? edge.target : edge.source;
|
|
1592
|
+
addNodeToGraph(neighbor);
|
|
1593
|
+
addEdgeToGraph(edge);
|
|
1594
|
+
if (!visited.has(neighbor)) {
|
|
1595
|
+
next.add(neighbor);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
frontier = next;
|
|
1600
|
+
if (frontier.size === 0) break;
|
|
1377
1601
|
}
|
|
1378
1602
|
}
|
|
1379
|
-
function
|
|
1380
|
-
|
|
1603
|
+
function renderNodeNeighborhoodAndLayout(symbolId, maxDegrees = 1) {
|
|
1604
|
+
if (!cy) return;
|
|
1605
|
+
const beforeNodeCount = cy.nodes().length;
|
|
1606
|
+
addNodeNeighborhood(symbolId, maxDegrees);
|
|
1381
1607
|
connectExistingNodes();
|
|
1382
|
-
|
|
1608
|
+
refreshAnalysisGraphState();
|
|
1609
|
+
if (cy.nodes().length > beforeNodeCount) {
|
|
1610
|
+
runLayout();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
async function focusSymbolInGraph(symbolId, options = {}) {
|
|
1614
|
+
if (!cy) return;
|
|
1615
|
+
const {
|
|
1616
|
+
maxDegrees = 0,
|
|
1617
|
+
pulse = false,
|
|
1618
|
+
pulseDuration = 1500,
|
|
1619
|
+
animate = false,
|
|
1620
|
+
zoom = 1.2,
|
|
1621
|
+
flashDuration = 1200,
|
|
1622
|
+
openDetail = false
|
|
1623
|
+
} = options;
|
|
1624
|
+
renderNodeNeighborhoodAndLayout(symbolId, maxDegrees);
|
|
1625
|
+
const node = findNode(symbolId);
|
|
1626
|
+
if (!node) return;
|
|
1627
|
+
if (animate) {
|
|
1628
|
+
cy.animate({ center: { eles: node }, zoom }, { duration: 300 });
|
|
1629
|
+
}
|
|
1630
|
+
if (pulse) {
|
|
1631
|
+
node.addClass("agent-pulse");
|
|
1632
|
+
setTimeout(() => node.removeClass("agent-pulse"), pulseDuration);
|
|
1633
|
+
} else {
|
|
1634
|
+
node.flashClass("highlighted", flashDuration);
|
|
1635
|
+
}
|
|
1636
|
+
if (openDetail) {
|
|
1637
|
+
const detailEl = document.getElementById("symbol-detail");
|
|
1638
|
+
if (detailEl) {
|
|
1639
|
+
await showDetail(node, detailEl);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function expandNodeAndLayout(symbolId) {
|
|
1644
|
+
renderNodeNeighborhoodAndLayout(symbolId, 1);
|
|
1383
1645
|
}
|
|
1384
1646
|
function addNodeToGraph(id) {
|
|
1385
1647
|
if (!cy) return;
|
|
1386
1648
|
if (cy.getElementById(id).length > 0) return;
|
|
1649
|
+
removeOnboardingHint();
|
|
1387
1650
|
const nodeData = graphNodes.get(id);
|
|
1388
1651
|
if (!nodeData) return;
|
|
1389
1652
|
const kind = (nodeData.kind || "function").toLowerCase();
|
|
@@ -1435,71 +1698,348 @@ function findNode(name) {
|
|
|
1435
1698
|
});
|
|
1436
1699
|
return match.length > 0 ? match : null;
|
|
1437
1700
|
}
|
|
1438
|
-
function
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1701
|
+
function getAnalysisPanelEls() {
|
|
1702
|
+
return {
|
|
1703
|
+
panel: document.getElementById("analysis-panel"),
|
|
1704
|
+
status: document.getElementById("analysis-status"),
|
|
1705
|
+
summary: document.getElementById("analysis-summary"),
|
|
1706
|
+
findings: document.getElementById("analysis-findings")
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function setAnalysisStatus(message, tone = "info") {
|
|
1710
|
+
const { status } = getAnalysisPanelEls();
|
|
1711
|
+
status.textContent = message;
|
|
1712
|
+
status.classList.remove("hidden", "error");
|
|
1713
|
+
if (tone === "error") {
|
|
1714
|
+
status.classList.add("error");
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function clearAnalysisStatus() {
|
|
1718
|
+
const { status } = getAnalysisPanelEls();
|
|
1719
|
+
status.textContent = "";
|
|
1720
|
+
status.classList.add("hidden");
|
|
1721
|
+
status.classList.remove("error");
|
|
1722
|
+
}
|
|
1723
|
+
function clearAnalysisGraphClasses() {
|
|
1724
|
+
if (!cy) return;
|
|
1725
|
+
cy.elements().removeClass("analysis-flagged");
|
|
1726
|
+
cy.elements().removeClass("analysis-selected");
|
|
1727
|
+
}
|
|
1728
|
+
function refreshAnalysisGraphState() {
|
|
1729
|
+
const { panel } = getAnalysisPanelEls();
|
|
1730
|
+
if (!cy || !analysisReport || panel.classList.contains("hidden")) {
|
|
1731
|
+
clearAnalysisGraphClasses();
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
clearAnalysisGraphClasses();
|
|
1735
|
+
const candidateNodeIds = /* @__PURE__ */ new Set();
|
|
1736
|
+
const candidateEdgeIds = /* @__PURE__ */ new Set();
|
|
1737
|
+
for (const finding of getVisibleAnalysisFindings()) {
|
|
1738
|
+
const context = buildFindingGraphContext(finding, graphEdges);
|
|
1739
|
+
for (const nodeId of context.nodes) candidateNodeIds.add(nodeId);
|
|
1740
|
+
for (const edgeId of context.edgeIds) candidateEdgeIds.add(edgeId);
|
|
1741
|
+
}
|
|
1742
|
+
for (const nodeId of candidateNodeIds) {
|
|
1743
|
+
cy.getElementById(nodeId).addClass("analysis-flagged");
|
|
1744
|
+
}
|
|
1745
|
+
for (const edgeId of candidateEdgeIds) {
|
|
1746
|
+
cy.getElementById(edgeId).addClass("analysis-flagged");
|
|
1747
|
+
}
|
|
1748
|
+
if (!activeAnalysisFindingId) return;
|
|
1749
|
+
const activeFinding = getVisibleAnalysisFindings().find((finding) => finding.id === activeAnalysisFindingId);
|
|
1750
|
+
if (!activeFinding) return;
|
|
1751
|
+
const activeContext = buildFindingGraphContext(activeFinding, graphEdges);
|
|
1752
|
+
for (const nodeId of activeContext.nodes) {
|
|
1753
|
+
cy.getElementById(nodeId).addClass("analysis-selected");
|
|
1754
|
+
}
|
|
1755
|
+
for (const edgeId of activeContext.edgeIds) {
|
|
1756
|
+
cy.getElementById(edgeId).addClass("analysis-selected");
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
function getVisibleAnalysisFindings() {
|
|
1760
|
+
return analysisReport ? filterFindings(analysisReport.findings, activeAnalysisFilter) : [];
|
|
1761
|
+
}
|
|
1762
|
+
function renderAnalysisSummary(report) {
|
|
1763
|
+
const { summary } = getAnalysisPanelEls();
|
|
1764
|
+
const counts = countFindingsByType(report.findings);
|
|
1765
|
+
const cards = [
|
|
1766
|
+
{ label: "Findings", value: report.summary.findingCount, filter: "all" },
|
|
1767
|
+
{ label: "Cycles", value: counts.cycle, filter: "cycle" },
|
|
1768
|
+
{ label: "Hotspots", value: counts.hotspot, filter: "hotspot" },
|
|
1769
|
+
{ label: "Coupling", value: counts.fileCoupling, filter: "fileCoupling" }
|
|
1471
1770
|
];
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
}
|
|
1771
|
+
summary.innerHTML = cards.map((card) => `
|
|
1772
|
+
<button
|
|
1773
|
+
class="analysis-summary-card ${card.filter === activeAnalysisFilter ? "active" : ""}"
|
|
1774
|
+
data-filter="${card.filter}"
|
|
1775
|
+
type="button"
|
|
1776
|
+
>
|
|
1777
|
+
<span class="analysis-summary-value">${card.value}</span>
|
|
1778
|
+
<span class="analysis-summary-label">${escapeHtml(card.label)}</span>
|
|
1779
|
+
</button>
|
|
1780
|
+
`).join("");
|
|
1781
|
+
summary.querySelectorAll(".analysis-summary-card").forEach((el) => {
|
|
1782
|
+
el.addEventListener("click", () => {
|
|
1783
|
+
const filter = el.dataset.filter || "all";
|
|
1784
|
+
setAnalysisFilter(filter);
|
|
1480
1785
|
});
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
function renderAnalysisFindings(report) {
|
|
1789
|
+
const { findings } = getAnalysisPanelEls();
|
|
1790
|
+
const visibleFindings = filterFindings(report.findings, activeAnalysisFilter);
|
|
1791
|
+
if (visibleFindings.length === 0) {
|
|
1792
|
+
findings.innerHTML = `
|
|
1793
|
+
<div class="analysis-empty">
|
|
1794
|
+
No ${escapeHtml(activeAnalysisFilter === "all" ? "structural outliers" : findingTypeLabel(activeAnalysisFilter).toLowerCase())} cleared the current thresholds for this graph snapshot.
|
|
1795
|
+
</div>
|
|
1796
|
+
`;
|
|
1797
|
+
return;
|
|
1481
1798
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
"
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1799
|
+
findings.innerHTML = visibleFindings.map((finding) => {
|
|
1800
|
+
const metricChips = buildFindingMetricChips(finding).map((chip) => `<span class="analysis-metric-chip">${escapeHtml(chip)}</span>`).join("");
|
|
1801
|
+
const fileBadges = finding.files.slice(0, 3).map((file) => `<span class="analysis-file-badge">${escapeHtml(file)}</span>`).join("");
|
|
1802
|
+
const rationale = finding.rationale.slice(0, 2).map((item) => `<li>${escapeHtml(item)}</li>`).join("");
|
|
1803
|
+
return `
|
|
1804
|
+
<article class="analysis-finding" data-finding-id="${escapeHtml(finding.id)}">
|
|
1805
|
+
<div class="analysis-finding-header">
|
|
1806
|
+
<div>
|
|
1807
|
+
<h3 class="analysis-finding-title">${escapeHtml(finding.title)}</h3>
|
|
1808
|
+
</div>
|
|
1809
|
+
<div class="analysis-finding-meta">
|
|
1810
|
+
<span class="analysis-type-badge">${escapeHtml(findingTypeLabel(finding.type))}</span>
|
|
1811
|
+
<span class="analysis-severity-badge ${escapeHtml(finding.severity)}">${escapeHtml(findingSeverityLabel(finding.severity))}</span>
|
|
1812
|
+
</div>
|
|
1813
|
+
</div>
|
|
1814
|
+
<p class="analysis-finding-summary">${escapeHtml(finding.summary)}</p>
|
|
1815
|
+
<div class="analysis-chip-row">${metricChips}</div>
|
|
1816
|
+
${fileBadges ? `<div class="analysis-file-row">${fileBadges}</div>` : ""}
|
|
1817
|
+
${rationale ? `<ul class="analysis-rationale">${rationale}</ul>` : ""}
|
|
1818
|
+
<div class="analysis-finding-actions">
|
|
1819
|
+
<button class="header-btn analysis-select-btn" type="button">Inspect in graph</button>
|
|
1820
|
+
<button class="header-btn analysis-explain-btn" type="button">Explain this finding</button>
|
|
1821
|
+
</div>
|
|
1822
|
+
<div class="analysis-explanation hidden">
|
|
1823
|
+
<div class="analysis-explanation-label">AI interpretation</div>
|
|
1824
|
+
<div class="analysis-explanation-note">Advisory explanation grounded in the deterministic finding and affected symbols.</div>
|
|
1825
|
+
<div class="detail-explain-content analysis-explanation-content"></div>
|
|
1826
|
+
</div>
|
|
1827
|
+
</article>
|
|
1828
|
+
`;
|
|
1829
|
+
}).join("");
|
|
1830
|
+
findings.querySelectorAll(".analysis-finding").forEach((el) => {
|
|
1831
|
+
el.addEventListener("click", () => {
|
|
1832
|
+
const id = el.dataset.findingId || "";
|
|
1833
|
+
void selectAnalysisFinding(id);
|
|
1492
1834
|
});
|
|
1835
|
+
});
|
|
1836
|
+
findings.querySelectorAll(".analysis-select-btn").forEach((button) => {
|
|
1837
|
+
button.addEventListener("click", (event) => {
|
|
1838
|
+
event.stopPropagation();
|
|
1839
|
+
const parent = button.closest(".analysis-finding");
|
|
1840
|
+
const id = parent?.dataset.findingId || "";
|
|
1841
|
+
void selectAnalysisFinding(id);
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1844
|
+
findings.querySelectorAll(".analysis-explain-btn").forEach((button) => {
|
|
1845
|
+
button.addEventListener("click", (event) => {
|
|
1846
|
+
event.stopPropagation();
|
|
1847
|
+
const parent = button.closest(".analysis-finding");
|
|
1848
|
+
if (!parent) return;
|
|
1849
|
+
const id = parent.dataset.findingId || "";
|
|
1850
|
+
void explainAnalysisFinding(id, parent);
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
function setActiveFindingCard(findingId) {
|
|
1855
|
+
const { findings } = getAnalysisPanelEls();
|
|
1856
|
+
findings.querySelectorAll(".analysis-finding").forEach((el) => {
|
|
1857
|
+
el.classList.toggle("active", el.dataset.findingId === findingId);
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
function setAnalysisFilter(filter) {
|
|
1861
|
+
const nextFilter = activeAnalysisFilter === filter ? "all" : filter;
|
|
1862
|
+
activeAnalysisFilter = nextFilter;
|
|
1863
|
+
if (!analysisReport) {
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const visibleFindings = filterFindings(analysisReport.findings, activeAnalysisFilter);
|
|
1867
|
+
if (activeAnalysisFindingId && !visibleFindings.some((finding) => finding.id === activeAnalysisFindingId)) {
|
|
1868
|
+
activeAnalysisFindingId = null;
|
|
1869
|
+
}
|
|
1870
|
+
renderAnalysisSummary(analysisReport);
|
|
1871
|
+
renderAnalysisFindings(analysisReport);
|
|
1872
|
+
refreshAnalysisGraphState();
|
|
1873
|
+
setActiveFindingCard(activeAnalysisFindingId);
|
|
1874
|
+
}
|
|
1875
|
+
async function runStructuralAnalysis() {
|
|
1876
|
+
const { panel, findings } = getAnalysisPanelEls();
|
|
1877
|
+
panel.classList.remove("hidden");
|
|
1878
|
+
findings.innerHTML = '<div class="analysis-empty">Running structural analysis on the current dependency graph...</div>';
|
|
1879
|
+
setAnalysisStatus("Analyzing the current graph snapshot...");
|
|
1880
|
+
try {
|
|
1881
|
+
const res = await fetch("/api/analysis");
|
|
1882
|
+
if (!res.ok) {
|
|
1883
|
+
throw new Error(res.status === 404 ? "No project index available for analysis yet." : `Analysis request failed (${res.status})`);
|
|
1884
|
+
}
|
|
1885
|
+
const report = await res.json();
|
|
1886
|
+
analysisReport = report;
|
|
1887
|
+
activeAnalysisFindingId = null;
|
|
1888
|
+
activeAnalysisFilter = "all";
|
|
1889
|
+
analysisExplanationCache.clear();
|
|
1890
|
+
clearAnalysisStatus();
|
|
1891
|
+
setAnalysisStatus(
|
|
1892
|
+
`Analyzed ${report.summary.symbolCount} symbols across ${report.summary.fileCount} files. These are graph-derived structural signals.`
|
|
1893
|
+
);
|
|
1894
|
+
renderAnalysisSummary(report);
|
|
1895
|
+
renderAnalysisFindings(report);
|
|
1896
|
+
refreshAnalysisGraphState();
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
analysisReport = null;
|
|
1899
|
+
activeAnalysisFindingId = null;
|
|
1900
|
+
activeAnalysisFilter = "all";
|
|
1901
|
+
analysisExplanationCache.clear();
|
|
1902
|
+
clearAnalysisGraphClasses();
|
|
1903
|
+
getAnalysisPanelEls().summary.innerHTML = "";
|
|
1904
|
+
const message = error instanceof Error ? error.message : "Failed to run structural analysis.";
|
|
1905
|
+
findings.innerHTML = `<div class="analysis-empty">${escapeHtml(message)}</div>`;
|
|
1906
|
+
setAnalysisStatus(message, "error");
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
async function streamExplanationInto(request, content, loadingLabel, unavailableText, failureText, onComplete) {
|
|
1910
|
+
content.innerHTML = `<span class="explain-status"><span class="explain-spinner"></span> ${escapeHtml(loadingLabel)}</span>`;
|
|
1911
|
+
let streaming = false;
|
|
1912
|
+
let rawText = "";
|
|
1913
|
+
function showToolStatus(toolName, input) {
|
|
1914
|
+
if (streaming) return;
|
|
1915
|
+
const arg = input.name || input.path || input.symbol || input.query || "";
|
|
1916
|
+
content.innerHTML = `<span class="explain-status"><span class="explain-spinner"></span> ${escapeHtml(toolName)}(${escapeHtml(String(arg))})</span>`;
|
|
1917
|
+
}
|
|
1918
|
+
function startStreaming() {
|
|
1919
|
+
if (streaming) return;
|
|
1920
|
+
streaming = true;
|
|
1921
|
+
rawText = "";
|
|
1922
|
+
content.textContent = "";
|
|
1923
|
+
}
|
|
1924
|
+
function finalize() {
|
|
1925
|
+
if (!rawText) return;
|
|
1926
|
+
onComplete?.(rawText);
|
|
1927
|
+
renderMarkdownInto(content, rawText);
|
|
1928
|
+
}
|
|
1929
|
+
try {
|
|
1930
|
+
const res = await request();
|
|
1931
|
+
if (!res.ok || !res.body) {
|
|
1932
|
+
content.textContent = unavailableText;
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const reader = res.body.getReader();
|
|
1936
|
+
const decoder = new TextDecoder();
|
|
1937
|
+
let buffer = "";
|
|
1938
|
+
while (true) {
|
|
1939
|
+
const { done, value } = await reader.read();
|
|
1940
|
+
if (done) break;
|
|
1941
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1942
|
+
const lines = buffer.split("\n");
|
|
1943
|
+
buffer = lines.pop() || "";
|
|
1944
|
+
for (const line of lines) {
|
|
1945
|
+
if (!line.startsWith("data: ")) continue;
|
|
1946
|
+
const payload = line.slice(6);
|
|
1947
|
+
if (payload === "[DONE]") {
|
|
1948
|
+
finalize();
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
try {
|
|
1952
|
+
const event = JSON.parse(payload);
|
|
1953
|
+
if (event.type === "tool_call_start") {
|
|
1954
|
+
showToolStatus(event.name, event.input || {});
|
|
1955
|
+
} else if (event.type === "streaming_chunk" && event.content) {
|
|
1956
|
+
startStreaming();
|
|
1957
|
+
rawText += event.content;
|
|
1958
|
+
content.textContent = rawText;
|
|
1959
|
+
content.scrollTop = content.scrollHeight;
|
|
1960
|
+
} else if (event.type === "error") {
|
|
1961
|
+
startStreaming();
|
|
1962
|
+
rawText += `
|
|
1963
|
+
[Error: ${event.message}]`;
|
|
1964
|
+
content.textContent = rawText;
|
|
1965
|
+
}
|
|
1966
|
+
} catch {
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
finalize();
|
|
1971
|
+
} catch {
|
|
1972
|
+
if (!streaming) {
|
|
1973
|
+
content.textContent = failureText;
|
|
1974
|
+
} else {
|
|
1975
|
+
finalize();
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
async function selectAnalysisFinding(findingId) {
|
|
1980
|
+
if (!analysisReport || !cy) return;
|
|
1981
|
+
const finding = analysisReport.findings.find((item) => item.id === findingId);
|
|
1982
|
+
if (!finding) return;
|
|
1983
|
+
const beforeNodeCount = cy.nodes().length;
|
|
1984
|
+
for (const symbol of finding.symbols) {
|
|
1985
|
+
addNodeNeighborhood(symbol, 1);
|
|
1986
|
+
}
|
|
1987
|
+
connectExistingNodes();
|
|
1988
|
+
activeAnalysisFindingId = findingId;
|
|
1989
|
+
refreshAnalysisGraphState();
|
|
1990
|
+
setActiveFindingCard(findingId);
|
|
1991
|
+
const afterNodeCount = cy.nodes().length;
|
|
1992
|
+
if (afterNodeCount > beforeNodeCount) {
|
|
1993
|
+
runLayout();
|
|
1994
|
+
}
|
|
1995
|
+
const primarySymbol = finding.symbols[0];
|
|
1996
|
+
if (!primarySymbol) return;
|
|
1997
|
+
const primaryNode = cy.getElementById(primarySymbol);
|
|
1998
|
+
if (primaryNode.length > 0) {
|
|
1999
|
+
if (finding.symbols.length > 1) {
|
|
2000
|
+
cy.fit(80);
|
|
2001
|
+
} else {
|
|
2002
|
+
cy.animate({ center: { eles: primaryNode }, zoom: 1.35 }, { duration: 300 });
|
|
2003
|
+
}
|
|
2004
|
+
primaryNode.flashClass("highlighted", 1200);
|
|
2005
|
+
await showDetail(primaryNode, document.getElementById("symbol-detail"));
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
async function explainAnalysisFinding(findingId, findingEl) {
|
|
2009
|
+
await selectAnalysisFinding(findingId);
|
|
2010
|
+
const explanationEl = findingEl.querySelector(".analysis-explanation");
|
|
2011
|
+
const contentEl = findingEl.querySelector(".analysis-explanation-content");
|
|
2012
|
+
if (!explanationEl || !contentEl) return;
|
|
2013
|
+
explanationEl.classList.remove("hidden");
|
|
2014
|
+
const cached = analysisExplanationCache.get(findingId);
|
|
2015
|
+
if (cached) {
|
|
2016
|
+
renderMarkdownInto(contentEl, cached);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
if (findingEl.dataset.explaining === "true") {
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
findingEl.dataset.explaining = "true";
|
|
2023
|
+
try {
|
|
2024
|
+
await streamExplanationInto(
|
|
2025
|
+
() => fetch("/api/analysis/explain", {
|
|
2026
|
+
method: "POST",
|
|
2027
|
+
headers: { "Content-Type": "application/json" },
|
|
2028
|
+
body: JSON.stringify({ findingId })
|
|
2029
|
+
}),
|
|
2030
|
+
contentEl,
|
|
2031
|
+
"Interpreting deterministic finding...",
|
|
2032
|
+
"(analysis explanation unavailable)",
|
|
2033
|
+
"(analysis explanation failed)",
|
|
2034
|
+
(markdown) => {
|
|
2035
|
+
if (!markdown.includes("[Error:")) {
|
|
2036
|
+
analysisExplanationCache.set(findingId, markdown);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
} finally {
|
|
2041
|
+
delete findingEl.dataset.explaining;
|
|
1493
2042
|
}
|
|
1494
|
-
styles.push({ selector: "node.pinned", style: { "border-color": "#e0af68", "border-width": 3 } });
|
|
1495
|
-
styles.push({ selector: "node.faded", style: { "opacity": 0.15 } });
|
|
1496
|
-
styles.push({ selector: "edge.faded", style: { "opacity": 0.05 } });
|
|
1497
|
-
styles.push({ selector: "node.highlighted", style: { "border-width": 2.5, "z-index": 10 } });
|
|
1498
|
-
styles.push({ selector: "edge.highlighted", style: { "opacity": 0.9, "width": 2, "z-index": 10 } });
|
|
1499
|
-
styles.push({ selector: "node.agent-pulse", style: { "border-color": "#ff9e64", "border-width": 4, "background-color": "rgba(255,158,100,0.25)" } });
|
|
1500
|
-
styles.push({ selector: "node.search-match", style: { "border-color": "#e0af68", "border-width": 2.5 } });
|
|
1501
|
-
styles.push({ selector: "node.expanded", style: { "border-width": 2.5 } });
|
|
1502
|
-
return styles;
|
|
1503
2043
|
}
|
|
1504
2044
|
function setupInteractions(cyInst, detailEl) {
|
|
1505
2045
|
cyInst.on("tap", "node", function(evt) {
|
|
@@ -1514,16 +2054,22 @@ function setupInteractions(cyInst, detailEl) {
|
|
|
1514
2054
|
expandNodeAndLayout(id);
|
|
1515
2055
|
}
|
|
1516
2056
|
});
|
|
2057
|
+
const containerEl = cyInst.container();
|
|
1517
2058
|
cyInst.on("mouseover", "node", function(evt) {
|
|
1518
2059
|
if (!cy) return;
|
|
1519
2060
|
const node = evt.target;
|
|
1520
2061
|
const neighborhood = node.closedNeighborhood();
|
|
1521
2062
|
cy.elements().not(neighborhood).addClass("faded");
|
|
1522
2063
|
neighborhood.addClass("highlighted");
|
|
2064
|
+
node.addClass("hover-target");
|
|
2065
|
+
containerEl.style.cursor = "pointer";
|
|
1523
2066
|
});
|
|
1524
|
-
cyInst.on("mouseout", "node", function() {
|
|
2067
|
+
cyInst.on("mouseout", "node", function(evt) {
|
|
1525
2068
|
if (!cy) return;
|
|
2069
|
+
const node = evt.target;
|
|
2070
|
+
node.removeClass("hover-target");
|
|
1526
2071
|
cy.elements().removeClass("faded highlighted");
|
|
2072
|
+
containerEl.style.cursor = "";
|
|
1527
2073
|
});
|
|
1528
2074
|
cyInst.on("tap", function(evt) {
|
|
1529
2075
|
if (evt.target === cyInst) {
|
|
@@ -1678,7 +2224,7 @@ async function loadDepsAndRefs(name, detailEl) {
|
|
|
1678
2224
|
const items = refs.references || refs || [];
|
|
1679
2225
|
refsEl.innerHTML = items.length ? items.map((r) => {
|
|
1680
2226
|
const id = typeof r === "string" ? r : r.from || r.qualifiedName || r.name || "";
|
|
1681
|
-
const label = id.split(".").pop() || id;
|
|
2227
|
+
const label = typeof r === "string" ? id : r.name || id.split(".").pop() || id;
|
|
1682
2228
|
const kind = typeof r === "string" ? "" : r.kind || "";
|
|
1683
2229
|
return `<a class="detail-link" data-target="${escapeHtml(id)}">${escapeHtml(label)} <span style="opacity:0.5">${escapeHtml(kind)}</span></a>`;
|
|
1684
2230
|
}).join("") : '<span class="detail-empty">None</span>';
|
|
@@ -1744,74 +2290,13 @@ async function explainSymbol(name, detailEl) {
|
|
|
1744
2290
|
const section = detailEl.querySelector("#detail-explain");
|
|
1745
2291
|
const content = section.querySelector(".detail-explain-content");
|
|
1746
2292
|
section.classList.remove("hidden");
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
}
|
|
1755
|
-
function startStreaming() {
|
|
1756
|
-
if (streaming) return;
|
|
1757
|
-
streaming = true;
|
|
1758
|
-
rawText = "";
|
|
1759
|
-
content.textContent = "";
|
|
1760
|
-
}
|
|
1761
|
-
function finalize() {
|
|
1762
|
-
if (rawText) {
|
|
1763
|
-
renderMarkdownInto(content, rawText);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
try {
|
|
1767
|
-
const res = await fetch(`/api/symbols/${encodeURIComponent(name)}/explain`);
|
|
1768
|
-
if (!res.ok || !res.body) {
|
|
1769
|
-
content.textContent = "(explain unavailable)";
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
const reader = res.body.getReader();
|
|
1773
|
-
const decoder = new TextDecoder();
|
|
1774
|
-
let buffer = "";
|
|
1775
|
-
while (true) {
|
|
1776
|
-
const { done, value } = await reader.read();
|
|
1777
|
-
if (done) break;
|
|
1778
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1779
|
-
const lines = buffer.split("\n");
|
|
1780
|
-
buffer = lines.pop() || "";
|
|
1781
|
-
for (const line of lines) {
|
|
1782
|
-
if (!line.startsWith("data: ")) continue;
|
|
1783
|
-
const payload = line.slice(6);
|
|
1784
|
-
if (payload === "[DONE]") {
|
|
1785
|
-
finalize();
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
try {
|
|
1789
|
-
const event = JSON.parse(payload);
|
|
1790
|
-
if (event.type === "tool_call_start") {
|
|
1791
|
-
showToolStatus(event.name, event.input || {});
|
|
1792
|
-
} else if (event.type === "streaming_chunk" && event.content) {
|
|
1793
|
-
startStreaming();
|
|
1794
|
-
rawText += event.content;
|
|
1795
|
-
content.textContent = rawText;
|
|
1796
|
-
content.scrollTop = content.scrollHeight;
|
|
1797
|
-
} else if (event.type === "error") {
|
|
1798
|
-
startStreaming();
|
|
1799
|
-
rawText += `
|
|
1800
|
-
[Error: ${event.message}]`;
|
|
1801
|
-
content.textContent = rawText;
|
|
1802
|
-
}
|
|
1803
|
-
} catch {
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
finalize();
|
|
1808
|
-
} catch {
|
|
1809
|
-
if (!streaming) {
|
|
1810
|
-
content.textContent = "(explain failed)";
|
|
1811
|
-
} else {
|
|
1812
|
-
finalize();
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
2293
|
+
await streamExplanationInto(
|
|
2294
|
+
() => fetch(`/api/symbols/${encodeURIComponent(name)}/explain`),
|
|
2295
|
+
content,
|
|
2296
|
+
"Researching...",
|
|
2297
|
+
"(explain unavailable)",
|
|
2298
|
+
"(explain failed)"
|
|
2299
|
+
);
|
|
1815
2300
|
}
|
|
1816
2301
|
async function togglePin(name, node, btnEl) {
|
|
1817
2302
|
const wasPinned = pinnedNames.has(name);
|
|
@@ -1838,9 +2323,13 @@ async function togglePin(name, node, btnEl) {
|
|
|
1838
2323
|
}
|
|
1839
2324
|
function setupToolbar() {
|
|
1840
2325
|
const searchInput = document.getElementById("graph-search");
|
|
2326
|
+
const analyzeBtn = document.getElementById("graph-analyze");
|
|
1841
2327
|
const fitBtn = document.getElementById("graph-fit");
|
|
1842
2328
|
const relayoutBtn = document.getElementById("graph-relayout");
|
|
1843
2329
|
const clearBtn = document.getElementById("graph-clear");
|
|
2330
|
+
const analysisCloseBtn = document.getElementById("analysis-close");
|
|
2331
|
+
const analysisRerunBtn = document.getElementById("analysis-rerun");
|
|
2332
|
+
const analysisPanel = document.getElementById("analysis-panel");
|
|
1844
2333
|
let searchTimeout;
|
|
1845
2334
|
const dropdown = document.createElement("div");
|
|
1846
2335
|
dropdown.className = "search-dropdown hidden";
|
|
@@ -1875,17 +2364,15 @@ function setupToolbar() {
|
|
|
1875
2364
|
dropdown.querySelectorAll(".search-result").forEach((el) => {
|
|
1876
2365
|
el.addEventListener("click", () => {
|
|
1877
2366
|
const id = el.dataset.id || "";
|
|
1878
|
-
expandNodeAndLayout(id);
|
|
1879
2367
|
searchInput.value = "";
|
|
1880
2368
|
dropdown.classList.add("hidden");
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
}
|
|
2369
|
+
void focusSymbolInGraph(id, {
|
|
2370
|
+
maxDegrees: 1,
|
|
2371
|
+
animate: true,
|
|
2372
|
+
zoom: 1.2,
|
|
2373
|
+
flashDuration: 1500,
|
|
2374
|
+
openDetail: true
|
|
2375
|
+
});
|
|
1889
2376
|
});
|
|
1890
2377
|
});
|
|
1891
2378
|
}
|
|
@@ -1925,6 +2412,19 @@ function setupToolbar() {
|
|
|
1925
2412
|
cy.fit(40);
|
|
1926
2413
|
}
|
|
1927
2414
|
});
|
|
2415
|
+
analyzeBtn.addEventListener("click", () => {
|
|
2416
|
+
void runStructuralAnalysis();
|
|
2417
|
+
});
|
|
2418
|
+
analysisRerunBtn.addEventListener("click", () => {
|
|
2419
|
+
void runStructuralAnalysis();
|
|
2420
|
+
});
|
|
2421
|
+
analysisCloseBtn.addEventListener("click", () => {
|
|
2422
|
+
analysisPanel.classList.add("hidden");
|
|
2423
|
+
activeAnalysisFindingId = null;
|
|
2424
|
+
clearAnalysisStatus();
|
|
2425
|
+
clearAnalysisGraphClasses();
|
|
2426
|
+
setActiveFindingCard(null);
|
|
2427
|
+
});
|
|
1928
2428
|
relayoutBtn.addEventListener("click", () => {
|
|
1929
2429
|
runLayout();
|
|
1930
2430
|
});
|
|
@@ -1932,6 +2432,9 @@ function setupToolbar() {
|
|
|
1932
2432
|
if (!cy) return;
|
|
1933
2433
|
cy.elements().remove();
|
|
1934
2434
|
document.getElementById("symbol-detail").classList.add("hidden");
|
|
2435
|
+
const cyEl = document.getElementById("cy");
|
|
2436
|
+
if (cyEl) showOnboardingHint(cyEl);
|
|
2437
|
+
refreshAnalysisGraphState();
|
|
1935
2438
|
});
|
|
1936
2439
|
}
|
|
1937
2440
|
|
|
@@ -1943,15 +2446,33 @@ var sendBtn = document.getElementById("send-btn");
|
|
|
1943
2446
|
var cancelBtn = document.getElementById("cancel-btn");
|
|
1944
2447
|
var statusBadge = document.getElementById("status-badge");
|
|
1945
2448
|
var modelInfo = document.getElementById("model-info");
|
|
2449
|
+
var modelBtn = document.getElementById("model-btn");
|
|
2450
|
+
var modelDropdown = document.getElementById("model-dropdown");
|
|
2451
|
+
var modelList = document.getElementById("model-list");
|
|
1946
2452
|
var sessionBtn = document.getElementById("session-btn");
|
|
1947
2453
|
var sessionDropdown = document.getElementById("session-dropdown");
|
|
1948
2454
|
var sessionList = document.getElementById("session-list");
|
|
2455
|
+
var sessionUpdateRow = document.getElementById("session-update-row");
|
|
2456
|
+
var sessionUpdateBtn = document.getElementById("session-update-btn");
|
|
1949
2457
|
var saveBtn = document.getElementById("save-btn");
|
|
1950
2458
|
var saveLabelInput = document.getElementById("save-label");
|
|
2459
|
+
var contextFill = document.getElementById("context-fill");
|
|
2460
|
+
var contextLabel = document.getElementById("context-label");
|
|
2461
|
+
var settingsBtn = document.getElementById("settings-btn");
|
|
2462
|
+
var settingsModal = document.getElementById("settings-modal");
|
|
2463
|
+
var settingsBackdrop = document.getElementById("settings-backdrop");
|
|
2464
|
+
var settingsCloseBtn = document.getElementById("settings-close");
|
|
2465
|
+
var settingsPath = document.getElementById("settings-path");
|
|
2466
|
+
var settingsList = document.getElementById("settings-list");
|
|
2467
|
+
var settingsBanner = document.getElementById("settings-banner");
|
|
2468
|
+
var settingsSaveBtn = document.getElementById("settings-save");
|
|
2469
|
+
var settingsResetBtn = document.getElementById("settings-reset");
|
|
1951
2470
|
var ws;
|
|
1952
2471
|
var currentAssistantEl = null;
|
|
1953
2472
|
var assistantText = "";
|
|
1954
2473
|
var hadToolCalls = false;
|
|
2474
|
+
var settingsPayload = null;
|
|
2475
|
+
var activeSavedSession = null;
|
|
1955
2476
|
var TOOL_RESULT_MAX = 500;
|
|
1956
2477
|
function connect() {
|
|
1957
2478
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -1959,6 +2480,7 @@ function connect() {
|
|
|
1959
2480
|
ws.onopen = () => {
|
|
1960
2481
|
setStatus("ready");
|
|
1961
2482
|
fetchStatus();
|
|
2483
|
+
fetchContext();
|
|
1962
2484
|
};
|
|
1963
2485
|
ws.onclose = () => {
|
|
1964
2486
|
setStatus("error");
|
|
@@ -1983,11 +2505,29 @@ function setBusy(busy) {
|
|
|
1983
2505
|
setStatus("ready");
|
|
1984
2506
|
}
|
|
1985
2507
|
}
|
|
2508
|
+
var configOverlay = document.getElementById("config-overlay");
|
|
1986
2509
|
async function fetchStatus() {
|
|
1987
2510
|
try {
|
|
1988
2511
|
const res = await fetch("/api/status");
|
|
1989
2512
|
const data = await res.json();
|
|
1990
|
-
modelInfo.textContent =
|
|
2513
|
+
modelInfo.textContent = data.model || "Select model";
|
|
2514
|
+
modelInfo.classList.toggle("placeholder", !data.model);
|
|
2515
|
+
activeModel = data.model;
|
|
2516
|
+
if (data.needsSetup) {
|
|
2517
|
+
configOverlay.classList.remove("hidden");
|
|
2518
|
+
chatInput.disabled = true;
|
|
2519
|
+
sendBtn.disabled = true;
|
|
2520
|
+
const missingEl = document.getElementById("config-missing");
|
|
2521
|
+
if (missingEl && data.missing && data.missing.length > 0) {
|
|
2522
|
+
const isOnlyModelMissing = data.missing.length === 1 && data.missing[0].includes("MODEL");
|
|
2523
|
+
const hint = isOnlyModelMissing ? ` \u2014 select one from the <strong>model dropdown</strong> above, or set it in config` : "";
|
|
2524
|
+
missingEl.innerHTML = `<strong>Missing:</strong> ${data.missing.map(escapeHtml).join(", ")}${hint}`;
|
|
2525
|
+
missingEl.classList.remove("hidden");
|
|
2526
|
+
}
|
|
2527
|
+
} else {
|
|
2528
|
+
configOverlay.classList.add("hidden");
|
|
2529
|
+
chatInput.disabled = false;
|
|
2530
|
+
}
|
|
1991
2531
|
} catch {
|
|
1992
2532
|
}
|
|
1993
2533
|
}
|
|
@@ -2067,6 +2607,20 @@ function handleServerMessage(msg) {
|
|
|
2067
2607
|
case "busy":
|
|
2068
2608
|
addMessage("Agent is busy. Please wait for the current turn to finish.", "error");
|
|
2069
2609
|
break;
|
|
2610
|
+
case "context_status":
|
|
2611
|
+
updateContextIndicator(
|
|
2612
|
+
msg.contextTokens,
|
|
2613
|
+
msg.maxContextTokens
|
|
2614
|
+
);
|
|
2615
|
+
break;
|
|
2616
|
+
case "model_changed": {
|
|
2617
|
+
const changedModel = msg.model;
|
|
2618
|
+
modelInfo.textContent = changedModel || "Select model";
|
|
2619
|
+
modelInfo.classList.toggle("placeholder", !changedModel);
|
|
2620
|
+
activeModel = changedModel;
|
|
2621
|
+
void fetchStatus();
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2070
2624
|
}
|
|
2071
2625
|
}
|
|
2072
2626
|
function addMessage(text, type, markdown = false) {
|
|
@@ -2131,6 +2685,27 @@ function finalizeToolCall(name, result, elapsedMs) {
|
|
|
2131
2685
|
resultEl.textContent = truncated;
|
|
2132
2686
|
}
|
|
2133
2687
|
}
|
|
2688
|
+
function updateContextIndicator(contextTokens, maxContextTokens) {
|
|
2689
|
+
const pct = maxContextTokens > 0 ? Math.min(100, Math.round(contextTokens / maxContextTokens * 100)) : 0;
|
|
2690
|
+
contextFill.style.width = pct + "%";
|
|
2691
|
+
contextFill.classList.remove("warn", "critical");
|
|
2692
|
+
if (pct >= 80) {
|
|
2693
|
+
contextFill.classList.add("critical");
|
|
2694
|
+
} else if (pct >= 60) {
|
|
2695
|
+
contextFill.classList.add("warn");
|
|
2696
|
+
}
|
|
2697
|
+
contextLabel.textContent = pct + "%";
|
|
2698
|
+
const indicator = document.getElementById("context-indicator");
|
|
2699
|
+
indicator.title = `Context: ~${contextTokens.toLocaleString()} / ${maxContextTokens.toLocaleString()} tokens (${pct}%)`;
|
|
2700
|
+
}
|
|
2701
|
+
async function fetchContext() {
|
|
2702
|
+
try {
|
|
2703
|
+
const res = await fetch("/api/context");
|
|
2704
|
+
const data = await res.json();
|
|
2705
|
+
updateContextIndicator(data.contextTokens, data.maxContextTokens);
|
|
2706
|
+
} catch {
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2134
2709
|
function addUsageInfo(usage) {
|
|
2135
2710
|
const el = document.createElement("div");
|
|
2136
2711
|
el.className = "usage-info";
|
|
@@ -2140,6 +2715,227 @@ function addUsageInfo(usage) {
|
|
|
2140
2715
|
function scrollToBottom() {
|
|
2141
2716
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
2142
2717
|
}
|
|
2718
|
+
function closeHeaderMenus() {
|
|
2719
|
+
modelDropdown.classList.add("hidden");
|
|
2720
|
+
sessionDropdown.classList.add("hidden");
|
|
2721
|
+
}
|
|
2722
|
+
function isSettingsModalOpen() {
|
|
2723
|
+
return !settingsModal.classList.contains("hidden");
|
|
2724
|
+
}
|
|
2725
|
+
function formatSettingsValue(value) {
|
|
2726
|
+
return value === null ? "(unset)" : String(value);
|
|
2727
|
+
}
|
|
2728
|
+
function setSettingsBanner(message, tone) {
|
|
2729
|
+
settingsBanner.textContent = message;
|
|
2730
|
+
settingsBanner.className = `settings-banner ${tone}`;
|
|
2731
|
+
}
|
|
2732
|
+
function clearSettingsBanner() {
|
|
2733
|
+
settingsBanner.textContent = "";
|
|
2734
|
+
settingsBanner.className = "settings-banner hidden";
|
|
2735
|
+
}
|
|
2736
|
+
function createSettingsControl(entry, inputId) {
|
|
2737
|
+
const value = entry.persistedValue;
|
|
2738
|
+
if (entry.type === "boolean") {
|
|
2739
|
+
const select = document.createElement("select");
|
|
2740
|
+
select.id = inputId;
|
|
2741
|
+
select.className = "settings-select";
|
|
2742
|
+
select.dataset.settingKey = entry.key;
|
|
2743
|
+
select.innerHTML = `
|
|
2744
|
+
<option value="">Use default</option>
|
|
2745
|
+
<option value="true">True</option>
|
|
2746
|
+
<option value="false">False</option>
|
|
2747
|
+
`;
|
|
2748
|
+
select.value = value === null ? "" : String(value);
|
|
2749
|
+
select.disabled = entry.overriddenByEnv;
|
|
2750
|
+
return select;
|
|
2751
|
+
}
|
|
2752
|
+
if (entry.type === "enum") {
|
|
2753
|
+
const select = document.createElement("select");
|
|
2754
|
+
select.id = inputId;
|
|
2755
|
+
select.className = "settings-select";
|
|
2756
|
+
select.dataset.settingKey = entry.key;
|
|
2757
|
+
const unsetOption = document.createElement("option");
|
|
2758
|
+
unsetOption.value = "";
|
|
2759
|
+
unsetOption.textContent = "Use default";
|
|
2760
|
+
select.appendChild(unsetOption);
|
|
2761
|
+
for (const optionValue of entry.values ?? []) {
|
|
2762
|
+
const option = document.createElement("option");
|
|
2763
|
+
option.value = optionValue;
|
|
2764
|
+
option.textContent = optionValue;
|
|
2765
|
+
select.appendChild(option);
|
|
2766
|
+
}
|
|
2767
|
+
select.value = value === null ? "" : String(value);
|
|
2768
|
+
select.disabled = entry.overriddenByEnv;
|
|
2769
|
+
return select;
|
|
2770
|
+
}
|
|
2771
|
+
const input = document.createElement("input");
|
|
2772
|
+
input.id = inputId;
|
|
2773
|
+
input.className = "settings-control";
|
|
2774
|
+
input.dataset.settingKey = entry.key;
|
|
2775
|
+
input.type = entry.type === "number" ? "number" : "text";
|
|
2776
|
+
if (entry.type === "number") {
|
|
2777
|
+
input.step = "any";
|
|
2778
|
+
input.inputMode = "numeric";
|
|
2779
|
+
}
|
|
2780
|
+
input.placeholder = "Use default";
|
|
2781
|
+
input.value = value === null ? "" : String(value);
|
|
2782
|
+
input.disabled = entry.overriddenByEnv;
|
|
2783
|
+
return input;
|
|
2784
|
+
}
|
|
2785
|
+
function renderSettings() {
|
|
2786
|
+
if (!settingsPayload) {
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
settingsPath.textContent = settingsPayload.configPath;
|
|
2790
|
+
settingsList.innerHTML = "";
|
|
2791
|
+
for (const entry of settingsPayload.entries) {
|
|
2792
|
+
const item = document.createElement("section");
|
|
2793
|
+
item.className = "settings-item";
|
|
2794
|
+
if (entry.overriddenByEnv) {
|
|
2795
|
+
item.classList.add("is-disabled");
|
|
2796
|
+
}
|
|
2797
|
+
const header = document.createElement("div");
|
|
2798
|
+
header.className = "settings-item-header";
|
|
2799
|
+
const heading = document.createElement("div");
|
|
2800
|
+
const title = document.createElement("div");
|
|
2801
|
+
title.className = "settings-item-title";
|
|
2802
|
+
title.textContent = entry.key;
|
|
2803
|
+
const description = document.createElement("div");
|
|
2804
|
+
description.className = "settings-item-description";
|
|
2805
|
+
description.textContent = entry.description;
|
|
2806
|
+
heading.append(title, description);
|
|
2807
|
+
const badges = document.createElement("div");
|
|
2808
|
+
badges.className = "settings-item-badges";
|
|
2809
|
+
const envBadge = document.createElement("span");
|
|
2810
|
+
envBadge.className = "settings-badge env";
|
|
2811
|
+
envBadge.textContent = `Env: ${entry.envVar}`;
|
|
2812
|
+
badges.appendChild(envBadge);
|
|
2813
|
+
if (entry.overriddenByEnv) {
|
|
2814
|
+
const overrideBadge = document.createElement("span");
|
|
2815
|
+
overrideBadge.className = "settings-badge override";
|
|
2816
|
+
overrideBadge.textContent = "Env override active";
|
|
2817
|
+
badges.appendChild(overrideBadge);
|
|
2818
|
+
}
|
|
2819
|
+
header.append(heading, badges);
|
|
2820
|
+
const meta = document.createElement("div");
|
|
2821
|
+
meta.className = "settings-item-meta";
|
|
2822
|
+
meta.innerHTML = `
|
|
2823
|
+
<div class="settings-meta-block">
|
|
2824
|
+
<span class="settings-meta-label">Effective</span>
|
|
2825
|
+
<div class="settings-meta-value">${escapeHtml(formatSettingsValue(entry.effectiveValue))}</div>
|
|
2826
|
+
</div>
|
|
2827
|
+
<div class="settings-meta-block">
|
|
2828
|
+
<span class="settings-meta-label">Saved</span>
|
|
2829
|
+
<div class="settings-meta-value">${escapeHtml(formatSettingsValue(entry.persistedValue))}</div>
|
|
2830
|
+
</div>
|
|
2831
|
+
`;
|
|
2832
|
+
const controls = document.createElement("div");
|
|
2833
|
+
controls.className = "settings-item-controls";
|
|
2834
|
+
const inputId = `setting-${entry.key}`;
|
|
2835
|
+
const label = document.createElement("label");
|
|
2836
|
+
label.className = "settings-help";
|
|
2837
|
+
label.htmlFor = inputId;
|
|
2838
|
+
label.textContent = "Saved in your global minicode config";
|
|
2839
|
+
const control = createSettingsControl(entry, inputId);
|
|
2840
|
+
controls.append(label, control);
|
|
2841
|
+
if (entry.overriddenByEnv) {
|
|
2842
|
+
const overrideHelp = document.createElement("div");
|
|
2843
|
+
overrideHelp.className = "settings-help settings-help-warning";
|
|
2844
|
+
overrideHelp.textContent = entry.envSource === "home-dotenv" && entry.envSourcePath ? `Defined by ${entry.envVar} in ${entry.envSourcePath}. Update or remove that env var there to manage this setting here.` : `${entry.envVar} is currently defined by the running environment. Remove or update that env var to manage this setting here.`;
|
|
2845
|
+
controls.appendChild(overrideHelp);
|
|
2846
|
+
}
|
|
2847
|
+
item.append(header, meta, controls);
|
|
2848
|
+
settingsList.appendChild(item);
|
|
2849
|
+
}
|
|
2850
|
+
updateSettingsActions();
|
|
2851
|
+
}
|
|
2852
|
+
async function loadSettings() {
|
|
2853
|
+
settingsList.innerHTML = '<div class="dropdown-empty">Loading settings...</div>';
|
|
2854
|
+
settingsSaveBtn.disabled = true;
|
|
2855
|
+
settingsResetBtn.disabled = true;
|
|
2856
|
+
settingsSaveBtn.textContent = "Save settings";
|
|
2857
|
+
clearSettingsBanner();
|
|
2858
|
+
try {
|
|
2859
|
+
const res = await fetch("/api/config");
|
|
2860
|
+
if (!res.ok) {
|
|
2861
|
+
throw new Error(`Failed to load settings (${res.status})`);
|
|
2862
|
+
}
|
|
2863
|
+
const data = await res.json();
|
|
2864
|
+
settingsPayload = data.settings;
|
|
2865
|
+
renderSettings();
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
const message = error instanceof Error ? error.message : "Failed to load settings";
|
|
2868
|
+
settingsList.innerHTML = '<div class="dropdown-empty">Failed to load settings</div>';
|
|
2869
|
+
setSettingsBanner(message, "error");
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
function readSettingsControlValue(entry) {
|
|
2873
|
+
const control = settingsList.querySelector(`[data-setting-key="${entry.key}"]`);
|
|
2874
|
+
if (!control) {
|
|
2875
|
+
throw new Error(`Missing control for ${entry.key}`);
|
|
2876
|
+
}
|
|
2877
|
+
if (entry.type === "boolean" || entry.type === "enum") {
|
|
2878
|
+
const value = control.value.trim();
|
|
2879
|
+
return value === "" ? null : entry.type === "boolean" ? value === "true" : value;
|
|
2880
|
+
}
|
|
2881
|
+
const rawValue = control.value.trim();
|
|
2882
|
+
if (rawValue === "") {
|
|
2883
|
+
return null;
|
|
2884
|
+
}
|
|
2885
|
+
if (entry.type === "number") {
|
|
2886
|
+
const parsed = Number(rawValue);
|
|
2887
|
+
if (!Number.isFinite(parsed)) {
|
|
2888
|
+
throw new Error(`Expected a number for "${entry.key}".`);
|
|
2889
|
+
}
|
|
2890
|
+
return parsed;
|
|
2891
|
+
}
|
|
2892
|
+
return rawValue;
|
|
2893
|
+
}
|
|
2894
|
+
function collectSettingsUpdates() {
|
|
2895
|
+
if (!settingsPayload) {
|
|
2896
|
+
return {};
|
|
2897
|
+
}
|
|
2898
|
+
const updates = {};
|
|
2899
|
+
for (const entry of settingsPayload.entries) {
|
|
2900
|
+
const nextValue = readSettingsControlValue(entry);
|
|
2901
|
+
const baseline = entry.persistedValue;
|
|
2902
|
+
if (nextValue !== baseline) {
|
|
2903
|
+
updates[entry.key] = nextValue;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
return updates;
|
|
2907
|
+
}
|
|
2908
|
+
function updateSettingsActions() {
|
|
2909
|
+
if (!settingsPayload) {
|
|
2910
|
+
settingsSaveBtn.disabled = true;
|
|
2911
|
+
settingsResetBtn.disabled = true;
|
|
2912
|
+
settingsSaveBtn.textContent = "Save settings";
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
try {
|
|
2916
|
+
const changeCount = Object.keys(collectSettingsUpdates()).length;
|
|
2917
|
+
settingsSaveBtn.disabled = changeCount === 0;
|
|
2918
|
+
settingsResetBtn.disabled = changeCount === 0;
|
|
2919
|
+
settingsSaveBtn.textContent = changeCount === 0 ? "Save settings" : `Save ${changeCount} change${changeCount === 1 ? "" : "s"}`;
|
|
2920
|
+
} catch {
|
|
2921
|
+
settingsSaveBtn.disabled = true;
|
|
2922
|
+
settingsResetBtn.disabled = false;
|
|
2923
|
+
settingsSaveBtn.textContent = "Save settings";
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
function openSettings() {
|
|
2927
|
+
closeHeaderMenus();
|
|
2928
|
+
settingsModal.classList.remove("hidden");
|
|
2929
|
+
settingsModal.setAttribute("aria-hidden", "false");
|
|
2930
|
+
document.body.classList.add("modal-open");
|
|
2931
|
+
void loadSettings();
|
|
2932
|
+
}
|
|
2933
|
+
function closeSettings() {
|
|
2934
|
+
settingsModal.classList.add("hidden");
|
|
2935
|
+
settingsModal.setAttribute("aria-hidden", "true");
|
|
2936
|
+
document.body.classList.remove("modal-open");
|
|
2937
|
+
clearSettingsBanner();
|
|
2938
|
+
}
|
|
2143
2939
|
chatForm.addEventListener("submit", (e) => {
|
|
2144
2940
|
e.preventDefault();
|
|
2145
2941
|
const message = chatInput.value.trim();
|
|
@@ -2162,10 +2958,57 @@ chatInput.addEventListener("keydown", (e) => {
|
|
|
2162
2958
|
chatForm.dispatchEvent(new Event("submit"));
|
|
2163
2959
|
}
|
|
2164
2960
|
});
|
|
2961
|
+
var activeModel = "";
|
|
2962
|
+
modelBtn.addEventListener("click", (e) => {
|
|
2963
|
+
e.stopPropagation();
|
|
2964
|
+
const isOpen = !modelDropdown.classList.contains("hidden");
|
|
2965
|
+
modelDropdown.classList.toggle("hidden");
|
|
2966
|
+
sessionDropdown.classList.add("hidden");
|
|
2967
|
+
if (!isOpen) {
|
|
2968
|
+
refreshModelList();
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
document.addEventListener("click", (e) => {
|
|
2972
|
+
if (!modelDropdown.contains(e.target) && e.target !== modelBtn) {
|
|
2973
|
+
modelDropdown.classList.add("hidden");
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
async function refreshModelList() {
|
|
2977
|
+
try {
|
|
2978
|
+
const res = await fetch("/api/models");
|
|
2979
|
+
const data = await res.json();
|
|
2980
|
+
activeModel = data.activeModel;
|
|
2981
|
+
if (!data.models || data.models.length === 0) {
|
|
2982
|
+
modelList.innerHTML = '<div class="dropdown-empty">No models available</div>';
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
modelList.innerHTML = "";
|
|
2986
|
+
for (const m2 of data.models) {
|
|
2987
|
+
const el = document.createElement("div");
|
|
2988
|
+
el.className = "model-item" + (m2.id === activeModel ? " active" : "");
|
|
2989
|
+
el.textContent = m2.name ?? m2.id;
|
|
2990
|
+
el.title = m2.id;
|
|
2991
|
+
el.addEventListener("click", () => switchModel(m2.id));
|
|
2992
|
+
modelList.appendChild(el);
|
|
2993
|
+
}
|
|
2994
|
+
} catch {
|
|
2995
|
+
modelList.innerHTML = '<div class="dropdown-empty">Failed to load models</div>';
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
function switchModel(modelId) {
|
|
2999
|
+
ws.send(JSON.stringify({ type: "switch_model", model: modelId }));
|
|
3000
|
+
modelInfo.textContent = modelId || "Select model";
|
|
3001
|
+
modelInfo.classList.toggle("placeholder", !modelId);
|
|
3002
|
+
activeModel = modelId;
|
|
3003
|
+
modelDropdown.classList.add("hidden");
|
|
3004
|
+
addMessage(`Model switched to: ${modelId}`, "thinking");
|
|
3005
|
+
void fetchStatus();
|
|
3006
|
+
}
|
|
2165
3007
|
sessionBtn.addEventListener("click", (e) => {
|
|
2166
3008
|
e.stopPropagation();
|
|
2167
3009
|
const isOpen = !sessionDropdown.classList.contains("hidden");
|
|
2168
3010
|
sessionDropdown.classList.toggle("hidden");
|
|
3011
|
+
modelDropdown.classList.add("hidden");
|
|
2169
3012
|
if (!isOpen) {
|
|
2170
3013
|
refreshSessionList();
|
|
2171
3014
|
}
|
|
@@ -2176,7 +3019,9 @@ document.addEventListener("click", (e) => {
|
|
|
2176
3019
|
}
|
|
2177
3020
|
});
|
|
2178
3021
|
saveBtn.addEventListener("click", async () => {
|
|
2179
|
-
const
|
|
3022
|
+
const requestedLabel = saveLabelInput.value.trim();
|
|
3023
|
+
const label = requestedLabel || activeSavedSession?.label || void 0;
|
|
3024
|
+
const isUpdatingCurrentSession = !!activeSavedSession && (requestedLabel.length === 0 || requestedLabel === activeSavedSession.label);
|
|
2180
3025
|
try {
|
|
2181
3026
|
const res = await fetch("/api/sessions/save", {
|
|
2182
3027
|
method: "POST",
|
|
@@ -2186,8 +3031,11 @@ saveBtn.addEventListener("click", async () => {
|
|
|
2186
3031
|
if (res.ok) {
|
|
2187
3032
|
const data = await res.json();
|
|
2188
3033
|
saveLabelInput.value = "";
|
|
2189
|
-
addMessage(
|
|
2190
|
-
|
|
3034
|
+
addMessage(
|
|
3035
|
+
`${isUpdatingCurrentSession ? "Session updated" : "Session saved"}: "${data.label}"`,
|
|
3036
|
+
"thinking"
|
|
3037
|
+
);
|
|
3038
|
+
void refreshSessionList();
|
|
2191
3039
|
}
|
|
2192
3040
|
} catch {
|
|
2193
3041
|
}
|
|
@@ -2203,6 +3051,16 @@ async function refreshSessionList() {
|
|
|
2203
3051
|
const res = await fetch("/api/sessions");
|
|
2204
3052
|
const data = await res.json();
|
|
2205
3053
|
const sessions = data.sessions;
|
|
3054
|
+
activeSavedSession = sessions.find((session) => session.id === data.currentSessionId) ?? null;
|
|
3055
|
+
if (activeSavedSession) {
|
|
3056
|
+
sessionUpdateRow.classList.remove("hidden");
|
|
3057
|
+
sessionUpdateBtn.textContent = `Update "${activeSavedSession.label}"`;
|
|
3058
|
+
sessionUpdateBtn.title = `Save changes back to "${activeSavedSession.label}"`;
|
|
3059
|
+
} else {
|
|
3060
|
+
sessionUpdateRow.classList.add("hidden");
|
|
3061
|
+
sessionUpdateBtn.textContent = "Update current saved session";
|
|
3062
|
+
sessionUpdateBtn.title = "";
|
|
3063
|
+
}
|
|
2206
3064
|
if (!sessions || sessions.length === 0) {
|
|
2207
3065
|
sessionList.innerHTML = '<div class="dropdown-empty">No saved sessions</div>';
|
|
2208
3066
|
return;
|
|
@@ -2210,12 +3068,15 @@ async function refreshSessionList() {
|
|
|
2210
3068
|
sessionList.innerHTML = "";
|
|
2211
3069
|
for (const s of sessions) {
|
|
2212
3070
|
const el = document.createElement("div");
|
|
2213
|
-
|
|
2214
|
-
el.
|
|
3071
|
+
const isActive = activeSavedSession?.id === s.id;
|
|
3072
|
+
el.className = "session-item" + (isActive ? " active" : "");
|
|
3073
|
+
el.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>`;
|
|
2215
3074
|
el.addEventListener("click", () => loadSession(s.label));
|
|
2216
3075
|
sessionList.appendChild(el);
|
|
2217
3076
|
}
|
|
2218
3077
|
} catch {
|
|
3078
|
+
activeSavedSession = null;
|
|
3079
|
+
sessionUpdateRow.classList.add("hidden");
|
|
2219
3080
|
sessionList.innerHTML = '<div class="dropdown-empty">Failed to load sessions</div>';
|
|
2220
3081
|
}
|
|
2221
3082
|
}
|
|
@@ -2230,10 +3091,97 @@ async function loadSession(label) {
|
|
|
2230
3091
|
sessionDropdown.classList.add("hidden");
|
|
2231
3092
|
messagesEl.innerHTML = "";
|
|
2232
3093
|
addMessage(`Session "${label}" restored`, "thinking");
|
|
3094
|
+
void refreshSessionList();
|
|
2233
3095
|
}
|
|
2234
3096
|
} catch {
|
|
2235
3097
|
}
|
|
2236
3098
|
}
|
|
3099
|
+
sessionUpdateBtn.addEventListener("click", async () => {
|
|
3100
|
+
if (!activeSavedSession) {
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
try {
|
|
3104
|
+
sessionUpdateBtn.disabled = true;
|
|
3105
|
+
const res = await fetch("/api/sessions/save", {
|
|
3106
|
+
method: "POST",
|
|
3107
|
+
headers: { "Content-Type": "application/json" },
|
|
3108
|
+
body: JSON.stringify({ label: activeSavedSession.label })
|
|
3109
|
+
});
|
|
3110
|
+
if (res.ok) {
|
|
3111
|
+
const data = await res.json();
|
|
3112
|
+
addMessage(`Session updated: "${data.label}"`, "thinking");
|
|
3113
|
+
await refreshSessionList();
|
|
3114
|
+
}
|
|
3115
|
+
} catch {
|
|
3116
|
+
} finally {
|
|
3117
|
+
sessionUpdateBtn.disabled = false;
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
settingsBtn.addEventListener("click", () => {
|
|
3121
|
+
openSettings();
|
|
3122
|
+
});
|
|
3123
|
+
settingsCloseBtn.addEventListener("click", () => {
|
|
3124
|
+
closeSettings();
|
|
3125
|
+
});
|
|
3126
|
+
settingsBackdrop.addEventListener("click", () => {
|
|
3127
|
+
closeSettings();
|
|
3128
|
+
});
|
|
3129
|
+
settingsResetBtn.addEventListener("click", () => {
|
|
3130
|
+
clearSettingsBanner();
|
|
3131
|
+
renderSettings();
|
|
3132
|
+
});
|
|
3133
|
+
settingsSaveBtn.addEventListener("click", async () => {
|
|
3134
|
+
if (!settingsPayload) {
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
let updates;
|
|
3138
|
+
try {
|
|
3139
|
+
updates = collectSettingsUpdates();
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
const message = error instanceof Error ? error.message : "Failed to read settings";
|
|
3142
|
+
setSettingsBanner(message, "error");
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (Object.keys(updates).length === 0) {
|
|
3146
|
+
setSettingsBanner("No changes to save.", "info");
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
settingsSaveBtn.disabled = true;
|
|
3150
|
+
settingsSaveBtn.textContent = "Saving...";
|
|
3151
|
+
try {
|
|
3152
|
+
const res = await fetch("/api/config", {
|
|
3153
|
+
method: "POST",
|
|
3154
|
+
headers: { "Content-Type": "application/json" },
|
|
3155
|
+
body: JSON.stringify({
|
|
3156
|
+
updates
|
|
3157
|
+
})
|
|
3158
|
+
});
|
|
3159
|
+
const body = await res.json();
|
|
3160
|
+
if (!res.ok) {
|
|
3161
|
+
throw new Error("error" in body ? body.error : `Failed to save settings (${res.status})`);
|
|
3162
|
+
}
|
|
3163
|
+
settingsPayload = body.settings;
|
|
3164
|
+
renderSettings();
|
|
3165
|
+
setSettingsBanner(body.message, "success");
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
const message = error instanceof Error ? error.message : "Failed to save settings";
|
|
3168
|
+
setSettingsBanner(message, "error");
|
|
3169
|
+
updateSettingsActions();
|
|
3170
|
+
}
|
|
3171
|
+
});
|
|
3172
|
+
settingsList.addEventListener("input", () => {
|
|
3173
|
+
clearSettingsBanner();
|
|
3174
|
+
updateSettingsActions();
|
|
3175
|
+
});
|
|
3176
|
+
settingsList.addEventListener("change", () => {
|
|
3177
|
+
clearSettingsBanner();
|
|
3178
|
+
updateSettingsActions();
|
|
3179
|
+
});
|
|
3180
|
+
document.addEventListener("keydown", (event) => {
|
|
3181
|
+
if (event.key === "Escape" && isSettingsModalOpen()) {
|
|
3182
|
+
closeSettings();
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
2237
3185
|
var chatPane = document.getElementById("chat-pane");
|
|
2238
3186
|
var divider = document.getElementById("pane-divider");
|
|
2239
3187
|
divider.addEventListener("mousedown", (e) => {
|