@kage-core/kage-graph-mcp 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +157 -137
- package/dist/daemon.js +78 -57
- package/dist/index.js +24 -182
- package/dist/kernel.js +1097 -274
- package/dist/structural-worker.js +17 -13
- package/package.json +4 -2
- package/viewer/console.js +164 -19
- package/viewer/index.html +133 -78
|
@@ -14,17 +14,21 @@ function notifyDone(shared) {
|
|
|
14
14
|
Atomics.notify(done, 0);
|
|
15
15
|
}
|
|
16
16
|
const data = node_worker_threads_1.workerData;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
async function run() {
|
|
18
|
+
try {
|
|
19
|
+
await (0, kernel_js_1.ensureTreeSitterLanguages)((0, kernel_js_1.treeSitterLanguagesForPaths)(data.files));
|
|
20
|
+
const results = data.files.map((file) => (0, kernel_js_1.buildStructuralFileForWorker)(data.projectDir, file, data.knownFiles, data.prior));
|
|
21
|
+
writeResult(data.outputPath, { ok: true, results });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
writeResult(data.outputPath, {
|
|
25
|
+
ok: false,
|
|
26
|
+
results: [],
|
|
27
|
+
error: error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
notifyDone(data.shared);
|
|
32
|
+
}
|
|
30
33
|
}
|
|
34
|
+
void run();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
40
40
|
"three": "^0.184.0",
|
|
41
|
-
"
|
|
41
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
42
|
+
"typescript": "^5.0.0",
|
|
43
|
+
"web-tree-sitter": "^0.24.7"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@types/node": "^22.0.0"
|
package/viewer/console.js
CHANGED
|
@@ -9,9 +9,36 @@
|
|
|
9
9
|
suppressed: src("suppressed", root + "suppressed.json"),
|
|
10
10
|
metrics: src("metrics", "./data/kage/metrics.json"),
|
|
11
11
|
activity: src("activity", root + "activity.json"),
|
|
12
|
+
value: src("value", root + "value.json"),
|
|
12
13
|
};
|
|
13
14
|
var state = { items: [], filter: "all", q: "", metrics: null, graphReady: false, showAll: false };
|
|
14
15
|
|
|
16
|
+
// Theme-aware colors: the canvas graph and donut can't use CSS variables directly,
|
|
17
|
+
// so resolve them once (and again when the OS light/dark preference flips).
|
|
18
|
+
var THEME = {};
|
|
19
|
+
function cssVar(name, fallback) {
|
|
20
|
+
var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
21
|
+
return v || fallback;
|
|
22
|
+
}
|
|
23
|
+
function refreshTheme() {
|
|
24
|
+
THEME = {
|
|
25
|
+
gain: cssVar("--gain", "#0c7a4d"),
|
|
26
|
+
warn: cssVar("--warn", "#9a6b08"),
|
|
27
|
+
code: cssVar("--code", "#155e9c"),
|
|
28
|
+
memory: cssVar("--memory", "#6d49b8"),
|
|
29
|
+
ink: cssVar("--ink", "#1c1e1a"),
|
|
30
|
+
muted: cssVar("--muted", "#5d635b"),
|
|
31
|
+
faint: cssVar("--faint", "#8b9088"),
|
|
32
|
+
paper: cssVar("--paper", "#fffdf9"),
|
|
33
|
+
line: cssVar("--line-strong", "#c9c4b4"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
refreshTheme();
|
|
37
|
+
if (window.matchMedia) {
|
|
38
|
+
var scheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
39
|
+
if (scheme.addEventListener) scheme.addEventListener("change", function () { refreshTheme(); renderInsights(state.metrics, state.items); });
|
|
40
|
+
}
|
|
41
|
+
|
|
15
42
|
function getJSON(p) { return fetch(p).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); }
|
|
16
43
|
function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }
|
|
17
44
|
function fmt(n) {
|
|
@@ -39,7 +66,8 @@
|
|
|
39
66
|
|
|
40
67
|
// ---- nav ----
|
|
41
68
|
var META = {
|
|
42
|
-
|
|
69
|
+
gains: ["kage://gains", "What Kage saved you", "Tokens, dollars, and bad memories caught — receipts, not vibes."],
|
|
70
|
+
overview: ["kage://trust", "Memory trust", "Whether this repo's agent memory can be trusted — at a glance."],
|
|
43
71
|
graph: ["kage://memory-map", "Memory ↔ code map", "Each packet anchored to the files it's grounded in. Hover a node to inspect."],
|
|
44
72
|
memory: ["kage://memory", "Memory", "Every packet Kage has stored, with health and grounding."],
|
|
45
73
|
activity: ["kage://activity", "Activity", "What agents actually recalled and captured here, over time."],
|
|
@@ -62,15 +90,16 @@
|
|
|
62
90
|
});
|
|
63
91
|
|
|
64
92
|
// ---- load ----
|
|
65
|
-
Promise.all([getJSON(paths.trust), getJSON(paths.suppressed), getJSON(paths.lifecycle), getJSON(paths.metrics), getJSON(paths.activity)])
|
|
66
|
-
.then(function (r) { render(r[0], r[1], r[2], r[3], r[4]); })
|
|
67
|
-
.catch(function () { render(null, null, null, null, null); });
|
|
93
|
+
Promise.all([getJSON(paths.trust), getJSON(paths.suppressed), getJSON(paths.lifecycle), getJSON(paths.metrics), getJSON(paths.activity), getJSON(paths.value)])
|
|
94
|
+
.then(function (r) { render(r[0], r[1], r[2], r[3], r[4], r[5]); })
|
|
95
|
+
.catch(function () { render(null, null, null, null, null, null); });
|
|
68
96
|
|
|
69
|
-
function render(trust, suppressed, lifecycle, metrics, activity) {
|
|
97
|
+
function render(trust, suppressed, lifecycle, metrics, activity, value) {
|
|
70
98
|
state.items = (lifecycle && lifecycle.items) || [];
|
|
71
99
|
state.metrics = metrics || {};
|
|
72
100
|
state.activity = activity || {};
|
|
73
101
|
document.getElementById("repo").textContent = resolveRepoName(metrics, lifecycle);
|
|
102
|
+
renderGains(value);
|
|
74
103
|
renderHero(trust);
|
|
75
104
|
renderTiles(metrics, state.items);
|
|
76
105
|
renderAttention(state.items, suppressed);
|
|
@@ -78,7 +107,115 @@
|
|
|
78
107
|
renderInsights(metrics, state.items);
|
|
79
108
|
renderActivity(activity);
|
|
80
109
|
var start = (location.hash || "").replace("#", "");
|
|
81
|
-
show(META[start] ? start : "
|
|
110
|
+
show(META[start] ? start : "gains");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- gains (value ledger receipts) ----
|
|
114
|
+
// value.json is the raw ledger written by recall: { totals, events[] }. Windows are
|
|
115
|
+
// recomputed here with the same rules as `kage gains` (today = local midnight,
|
|
116
|
+
// 7d = rolling, all-time = totals so trimmed events never lose history).
|
|
117
|
+
var DOLLARS_PER_MILLION_TOKENS = 15;
|
|
118
|
+
function dollars(tokens) { return (tokens / 1e6) * DOLLARS_PER_MILLION_TOKENS; }
|
|
119
|
+
function fmtDollars(tokens) {
|
|
120
|
+
var d = dollars(tokens);
|
|
121
|
+
return "$" + (d >= 100 ? Math.round(d) : d.toFixed(2));
|
|
122
|
+
}
|
|
123
|
+
function summarizeWindow(events, cutoff) {
|
|
124
|
+
var w = { tokens_saved: 0, stale_withheld: 0, recalls: 0, caller_answers: 0 };
|
|
125
|
+
events.forEach(function (e) {
|
|
126
|
+
var at = Date.parse(e.at);
|
|
127
|
+
if (!at || at < cutoff) return;
|
|
128
|
+
if (e.kind === "recall_served") { w.recalls += 1; w.tokens_saved += Math.max(0, e.tokens_saved || 0); }
|
|
129
|
+
else if (e.kind === "stale_withheld") w.stale_withheld += 1;
|
|
130
|
+
else if (e.kind === "caller_answered") w.caller_answers += 1;
|
|
131
|
+
});
|
|
132
|
+
return w;
|
|
133
|
+
}
|
|
134
|
+
function renderGains(value) {
|
|
135
|
+
var hero = document.getElementById("gainsHero");
|
|
136
|
+
var tiles = document.getElementById("gainsTiles");
|
|
137
|
+
var timeline = document.getElementById("gainsTimeline");
|
|
138
|
+
if (!hero) return;
|
|
139
|
+
var events = (value && value.events) || [];
|
|
140
|
+
var totals = (value && value.totals) || null;
|
|
141
|
+
var midnight = new Date(); midnight.setHours(0, 0, 0, 0);
|
|
142
|
+
var today = summarizeWindow(events, midnight.getTime());
|
|
143
|
+
var week = summarizeWindow(events, Date.now() - 7 * 86400000);
|
|
144
|
+
var all = totals || summarizeWindow(events, 0);
|
|
145
|
+
|
|
146
|
+
hero.textContent = "";
|
|
147
|
+
var head = el("div", "r-head");
|
|
148
|
+
head.appendChild(el("span", null, "Kage · savings receipt"));
|
|
149
|
+
head.appendChild(el("span", null, new Date().toISOString().slice(0, 10)));
|
|
150
|
+
hero.appendChild(head);
|
|
151
|
+
var hh = el("div", "r-hero");
|
|
152
|
+
var big = el("div", "big");
|
|
153
|
+
big.appendChild(el("b", null, all.tokens_saved ? "0" : "—"));
|
|
154
|
+
big.appendChild(el("span", "unit", "tokens saved, all time"));
|
|
155
|
+
if (all.tokens_saved) big.appendChild(el("span", "dollars", "≈ " + fmtDollars(all.tokens_saved)));
|
|
156
|
+
hh.appendChild(big);
|
|
157
|
+
hh.appendChild(el("p", "sub", all.tokens_saved
|
|
158
|
+
? "Context your agents did not have to re-read from source, because Kage served grounded memory instead."
|
|
159
|
+
: "No savings recorded yet. Run kage recall or kage_context against this repo and the receipt fills in."));
|
|
160
|
+
hero.appendChild(hh);
|
|
161
|
+
var wins = el("div", "r-windows");
|
|
162
|
+
[["Today", today], ["Last 7 days", week], ["All time", all]].forEach(function (p) {
|
|
163
|
+
var w = el("div", "r-win");
|
|
164
|
+
w.appendChild(el("div", "k", p[0]));
|
|
165
|
+
w.appendChild(el("div", "v", fmt(p[1].tokens_saved) + " tok"));
|
|
166
|
+
w.appendChild(el("div", "d", "≈ " + fmtDollars(p[1].tokens_saved)));
|
|
167
|
+
wins.appendChild(w);
|
|
168
|
+
});
|
|
169
|
+
hero.appendChild(wins);
|
|
170
|
+
var lines = el("div", "r-lines");
|
|
171
|
+
[
|
|
172
|
+
["Recalls served from memory", fmt(all.recalls), "gain"],
|
|
173
|
+
["Stale memories caught & withheld", fmt(all.stale_withheld), all.stale_withheld ? "warn" : ""],
|
|
174
|
+
["Caller questions answered from the graph", fmt(all.caller_answers), ""],
|
|
175
|
+
].forEach(function (li) {
|
|
176
|
+
var row = el("div", "r-line");
|
|
177
|
+
row.appendChild(el("span", null, li[0]));
|
|
178
|
+
row.appendChild(el("span", "dots"));
|
|
179
|
+
row.appendChild(el("b", li[2], li[1]));
|
|
180
|
+
lines.appendChild(row);
|
|
181
|
+
});
|
|
182
|
+
hero.appendChild(lines);
|
|
183
|
+
hero.appendChild(el("div", "r-foot", "estimated at $" + DOLLARS_PER_MILLION_TOKENS + " per 1M input tokens · ledger: .agent_memory/reports/value.json · verify: kage gains"));
|
|
184
|
+
if (all.tokens_saved) countUp(big.querySelector("b"), all.tokens_saved, 900);
|
|
185
|
+
|
|
186
|
+
if (tiles) {
|
|
187
|
+
tiles.textContent = "";
|
|
188
|
+
[
|
|
189
|
+
{ k: "Saved (7 days)", v: fmt(week.tokens_saved), s: "≈ " + fmtDollars(week.tokens_saved) + " of context not re-read", cls: "green" },
|
|
190
|
+
{ k: "Recalls served (7d)", v: fmt(week.recalls), s: fmt(all.recalls) + " all-time", cls: "green" },
|
|
191
|
+
{ k: "Stale caught", v: fmt(all.stale_withheld), s: all.stale_withheld ? "withheld before they misled an agent" : "nothing withheld yet", cls: all.stale_withheld ? "warn" : "" },
|
|
192
|
+
{ k: "Graph answers", v: fmt(all.caller_answers), s: "caller questions answered from the code graph", cls: "code" },
|
|
193
|
+
].forEach(function (d) { var x = el("div", "tile"); x.appendChild(el("div", "k", d.k)); x.appendChild(el("div", "v " + (d.cls || ""), d.v)); x.appendChild(el("div", "s", d.s)); tiles.appendChild(x); });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (timeline) {
|
|
197
|
+
timeline.textContent = ""; timeline.className = "vfeed";
|
|
198
|
+
if (!events.length) { timeline.appendChild(el("div", "empty", "No value events yet. Each recall, withheld stale memory, and graph answer lands here with a timestamp.")); return; }
|
|
199
|
+
var V_ICON = { recall_served: "✓", stale_withheld: "⊘", caller_answered: "◆" };
|
|
200
|
+
events.slice(-30).reverse().forEach(function (e) {
|
|
201
|
+
var row = el("div", "vev " + (e.kind || ""));
|
|
202
|
+
row.appendChild(el("span", "vi", V_ICON[e.kind] || "•"));
|
|
203
|
+
var mid = el("div", "vt");
|
|
204
|
+
if (e.kind === "recall_served") {
|
|
205
|
+
mid.appendChild(document.createTextNode("Recall served — saved "));
|
|
206
|
+
mid.appendChild(el("b", null, "~" + fmt(Math.max(0, e.tokens_saved || 0)) + " tokens"));
|
|
207
|
+
mid.appendChild(document.createTextNode(" vs reading source"));
|
|
208
|
+
} else if (e.kind === "stale_withheld") {
|
|
209
|
+
mid.appendChild(document.createTextNode("Stale memory withheld"));
|
|
210
|
+
if (e.packet_title) { mid.appendChild(document.createTextNode(" — ")); mid.appendChild(el("b", null, e.packet_title)); }
|
|
211
|
+
} else {
|
|
212
|
+
mid.appendChild(document.createTextNode("Caller question answered from the code graph"));
|
|
213
|
+
}
|
|
214
|
+
row.appendChild(mid);
|
|
215
|
+
row.appendChild(el("span", "when", relTime(e.at)));
|
|
216
|
+
timeline.appendChild(row);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
82
219
|
}
|
|
83
220
|
|
|
84
221
|
// ---- activity feed (real recorded recalls + captures) ----
|
|
@@ -341,15 +478,15 @@
|
|
|
341
478
|
var generated = c.generated || 0;
|
|
342
479
|
var groundedCurrent = items.length - needsReview - generated;
|
|
343
480
|
var seg = [
|
|
344
|
-
{ k: "Grounded & current", v: groundedCurrent, col:
|
|
345
|
-
{ k: "Needs review", v: needsReview, col:
|
|
346
|
-
{ k: "Generated", v: generated, col:
|
|
481
|
+
{ k: "Grounded & current", v: groundedCurrent, col: THEME.gain },
|
|
482
|
+
{ k: "Needs review", v: needsReview, col: THEME.warn },
|
|
483
|
+
{ k: "Generated", v: generated, col: THEME.code },
|
|
347
484
|
].filter(function (s) { return s.v > 0; });
|
|
348
485
|
var total = items.length || 1;
|
|
349
486
|
var stops = [], acc = 0;
|
|
350
487
|
seg.forEach(function (s) { var from = acc / total * 360, to = (acc + s.v) / total * 360; stops.push(s.col + " " + from + "deg " + to + "deg"); acc += s.v; });
|
|
351
488
|
var donut = document.getElementById("donut");
|
|
352
|
-
donut.style.background = "conic-gradient(" + (stops.join(", ") || "
|
|
489
|
+
donut.style.background = "conic-gradient(" + (stops.join(", ") || THEME.gain + " 0deg 360deg") + ")";
|
|
353
490
|
var pct = Math.round(groundedCurrent / total * 100);
|
|
354
491
|
// The big % is the health readout, so color it by how healthy it is — a high
|
|
355
492
|
// grounded share is green, not a warning. (Amber/red only when it's actually low.)
|
|
@@ -417,6 +554,14 @@
|
|
|
417
554
|
|
|
418
555
|
// ---- memory <-> code graph (interactive canvas) ----
|
|
419
556
|
var G = { nodes: [], edges: [], hover: -1, focus: -1, raf: 0, alpha: 1, filter: "all", view: { s: 1, tx: 0, ty: 0 }, drag: null, pan: null, tween: null };
|
|
557
|
+
// Canvas needs concrete colors; turn a resolved theme hex into rgba at a given alpha.
|
|
558
|
+
function rgba(hex, a) {
|
|
559
|
+
var h = String(hex).replace("#", "");
|
|
560
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
561
|
+
var n = parseInt(h, 16);
|
|
562
|
+
if (isNaN(n)) return hex;
|
|
563
|
+
return "rgba(" + ((n >> 16) & 255) + "," + ((n >> 8) & 255) + "," + (n & 255) + "," + a + ")";
|
|
564
|
+
}
|
|
420
565
|
function seeded(n) { var x = Math.sin(n * 999.137) * 43758.5453; return x - Math.floor(x); }
|
|
421
566
|
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
|
|
422
567
|
function nodeR(nd) { return Math.min(13, 4.5 + nd.deg * 1.1); }
|
|
@@ -470,8 +615,8 @@
|
|
|
470
615
|
G.edges.forEach(function (e) { if (seed[e[0]]) set[e[1]] = 1; if (seed[e[1]]) set[e[0]] = 1; });
|
|
471
616
|
return set;
|
|
472
617
|
}
|
|
473
|
-
function color(nd) { return nd.kind === "file" ?
|
|
474
|
-
function bodyColor(nd) { return
|
|
618
|
+
function color(nd) { return nd.kind === "file" ? THEME.code : (nd.review ? THEME.warn : THEME.memory); }
|
|
619
|
+
function bodyColor(nd) { return rgba(THEME.paper, 0.94); }
|
|
475
620
|
var DIAMOND = { decision: 1, bug_fix: 1, test: 1, gotcha: 1 };
|
|
476
621
|
function shapePath(x, y, r, nd) {
|
|
477
622
|
if (nd.kind === "file") { roundRect(x - r * 1.3, y - r * 0.78, r * 2.6, r * 1.56, 3); return; }
|
|
@@ -518,7 +663,7 @@
|
|
|
518
663
|
ctx.lineWidth = 1 / v.s;
|
|
519
664
|
G.edges.forEach(function (e) {
|
|
520
665
|
var on = active && emph[e[0]] && emph[e[1]];
|
|
521
|
-
ctx.strokeStyle = on ?
|
|
666
|
+
ctx.strokeStyle = on ? rgba(THEME.code, 0.7) : (active ? rgba(THEME.faint, 0.08) : rgba(THEME.faint, 0.25));
|
|
522
667
|
ctx.beginPath(); ctx.moveTo(n[e[0]].x, n[e[0]].y); ctx.lineTo(n[e[1]].x, n[e[1]].y); ctx.stroke();
|
|
523
668
|
});
|
|
524
669
|
n.forEach(function (nd, i) {
|
|
@@ -534,7 +679,7 @@
|
|
|
534
679
|
if (nd.kind === "memory") { ctx.globalAlpha = (dim ? 0.14 : 1) * 0.9; ctx.fillStyle = col; ctx.beginPath(); ctx.arc(nd.x, nd.y, Math.max(2.2, r * 0.2), 0, 6.2832); ctx.fill(); }
|
|
535
680
|
ctx.restore();
|
|
536
681
|
// halo on hover/focus
|
|
537
|
-
if (!dim && strong) { ctx.save(); shapePath(nd.x, nd.y, r + 5, nd); ctx.strokeStyle = i === G.focus ?
|
|
682
|
+
if (!dim && strong) { ctx.save(); shapePath(nd.x, nd.y, r + 5, nd); ctx.strokeStyle = i === G.focus ? THEME.ink : col; ctx.lineWidth = 1.8 / v.s; ctx.shadowColor = col; ctx.shadowBlur = 9; ctx.stroke(); ctx.restore(); }
|
|
538
683
|
});
|
|
539
684
|
// labels in screen space (mono pill, centered below) so they stay crisp at any zoom
|
|
540
685
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -548,9 +693,9 @@
|
|
|
548
693
|
if (sx < -60 || sx > W + 60 || sy < -20 || sy > H + 20) return;
|
|
549
694
|
ctx.font = (strong ? "700 " : "600 ") + "11px ui-monospace, Menlo, monospace";
|
|
550
695
|
var w = ctx.measureText(nd.label).width, pw = w + 14, ph = 18, lx = sx - pw / 2, ly = sy + r + 7;
|
|
551
|
-
ctx.globalAlpha = 0.
|
|
552
|
-
ctx.lineWidth = 1; ctx.strokeStyle = strong ?
|
|
553
|
-
ctx.globalAlpha = 1; ctx.fillStyle = strong ?
|
|
696
|
+
ctx.globalAlpha = 0.94; ctx.fillStyle = rgba(THEME.paper, 0.94); roundRect(lx, ly, pw, ph, 4); ctx.fill();
|
|
697
|
+
ctx.lineWidth = 1; ctx.strokeStyle = strong ? rgba(THEME.gain, 0.6) : rgba(THEME.line, 0.8); ctx.stroke();
|
|
698
|
+
ctx.globalAlpha = 1; ctx.fillStyle = strong ? THEME.ink : THEME.muted; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
554
699
|
ctx.fillText(nd.label, sx, ly + ph / 2);
|
|
555
700
|
});
|
|
556
701
|
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic"; ctx.globalAlpha = 1;
|
|
@@ -581,10 +726,10 @@
|
|
|
581
726
|
if (G.focus < 0) { detail.style.display = "none"; return; }
|
|
582
727
|
var nd = G.nodes[G.focus], html;
|
|
583
728
|
if (nd.kind === "file") {
|
|
584
|
-
html = "<div class='gd-k
|
|
729
|
+
html = "<div class='gd-k k-file'>code file</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
|
|
585
730
|
"<div class='gd-row'>cited by <b>" + nd.deg + "</b> memor" + (nd.deg === 1 ? "y" : "ies") + "</div>";
|
|
586
731
|
} else {
|
|
587
|
-
html = "<div class='gd-k
|
|
732
|
+
html = "<div class='gd-k k-memory'>" + escapeHtml(nd.sub) + "</div><b class='gd-t'>" + escapeHtml(nd.tip) + "</b>" +
|
|
588
733
|
"<div class='gd-row'>health <b class='" + (nd.health || "") + "'>" + (nd.health || "—") + "</b>" + (nd.uses ? " · used " + nd.uses + "×" : "") + " · " + nd.deg + " file" + (nd.deg === 1 ? "" : "s") + "</div>" +
|
|
589
734
|
(nd.files && nd.files.length ? "<div class='gd-files'>" + nd.files.map(escapeHtml).join(" · ") + "</div>" : "");
|
|
590
735
|
}
|