"use strict";
function el(tag, props, ...kids) {
const node = document.createElement(tag);
for (const [key, value] of Object.entries(props || {})) {
if (value === null || value === undefined || value === false) continue;
if (key === "class") node.className = value;
else if (key === "text") node.textContent = value;
else if (key.startsWith("on")) node.addEventListener(key.slice(2), value);
else node.setAttribute(key, value === true ? "" : String(value));
}
for (const kid of kids.flat()) {
if (kid === null || kid === undefined || kid === false) continue;
node.append(kid.nodeType ? kid : document.createTextNode(String(kid)));
}
return node;
}
function fill(node, ...kids) {
node.replaceChildren();
for (const kid of kids.flat()) {
if (kid === null || kid === undefined || kid === false) continue;
node.append(kid.nodeType ? kid : document.createTextNode(String(kid)));
}
return node;
}
const TOKEN_HEADER = "Semlith-Token";
const session = (() => {
const KEY = "semlith.token";
let token = "";
try {
const fromUrl = new URLSearchParams(location.search).get("token");
if (fromUrl) {
token = fromUrl;
sessionStorage.setItem(KEY, token);
const clean = location.pathname + location.hash;
history.replaceState(null, "", clean || "/");
} else {
token = sessionStorage.getItem(KEY) || "";
}
} catch (_) {
}
return {
get: () => token,
set(fresh) {
token = fresh;
try {
sessionStorage.setItem(KEY, fresh);
} catch (_) {
}
},
};
})();
function authed(extra) {
return { ...(extra || {}), [TOKEN_HEADER]: session.get() };
}
function imagePreview(path) {
const img = el("img", { class: "preview", alt: "" });
const failed = () =>
img.replaceWith(el("pre", { class: "muted", text: `${path} could not be read` }));
fetch(`/api/image?path=${encodeURIComponent(path)}`, {
credentials: "omit",
headers: authed(),
})
.then((response) => (response.ok ? response.blob() : Promise.reject(response.statusText)))
.then((blob) => {
const reader = new FileReader();
reader.onload = () => {
img.src = String(reader.result);
};
reader.onerror = failed;
reader.readAsDataURL(blob);
})
.catch(failed);
return img;
}
async function api(path, options) {
const options_ = options || {};
const response = await fetch(path, {
credentials: "omit",
...options_,
headers: authed(options_.headers),
});
if (!response.ok) {
let detail = response.statusText;
try {
const body = await response.json();
if (body && body.error) detail = body.error;
} catch (_) {
}
throw new Error(detail || "request failed");
}
return response.json();
}
function post(path, body) {
return api(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
const NUM = new Intl.NumberFormat();
const n = (value) => NUM.format(value || 0);
function bytes(value) {
const units = ["B", "KB", "MB", "GB", "TB"];
let size = Number(value) || 0;
let unit = 0;
while (size >= 1024 && unit < units.length - 1) {
size /= 1024;
unit += 1;
}
return `${unit === 0 ? size : size.toFixed(1)} ${units[unit]}`;
}
function zone() {
const minutes = -new Date().getTimezoneOffset();
const sign = minutes < 0 ? "-" : "+";
const off = Math.abs(minutes);
return `${sign}${String(Math.floor(off / 60)).padStart(2, "0")}:${String(off % 60).padStart(2, "0")}`;
}
function when(unix) {
if (!unix) return "never";
const seconds = Math.max(0, Math.floor(Date.now() / 1000) - unix);
if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
function clock(unix) {
return `${new Date(unix * 1000).toTimeString().slice(0, 8)} ${zone()}`;
}
const SVG_NS = "http://www.w3.org/2000/svg";
function icon(d, size, solid) {
const svg = document.createElementNS(SVG_NS, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", String(size || 16));
svg.setAttribute("height", String(size || 16));
svg.setAttribute("fill", solid ? "currentColor" : "none");
svg.setAttribute("stroke", solid ? "none" : "currentColor");
svg.setAttribute("stroke-width", "1.7");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
svg.setAttribute("aria-hidden", "true");
for (const segment of String(d).split("|")) {
const path = document.createElementNS(SVG_NS, "path");
path.setAttribute("d", segment);
svg.append(path);
}
return svg;
}
const ICONS = {
menu: "M4 7h16|M4 12h16|M4 17h16",
search: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14z|M20 20l-3.5-3.5",
plus: "M12 5v14|M5 12h14",
sun: "M12 3v2|M12 19v2|M3 12h2|M19 12h2|M5.6 5.6 7 7|M17 17l1.4 1.4|M18.4 5.6 17 7|M7 17l-1.4 1.4|M12 7.8a4.2 4.2 0 1 0 0 8.4 4.2 4.2 0 0 0 0-8.4z",
folder: "M3 7.5A1.5 1.5 0 0 1 4.5 6h4l2 2.5h7A1.5 1.5 0 0 1 19 10v7a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 3 17z",
file: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z|M14 3v5h5",
up: "M5 12h14|M11 6l-6 6 6 6",
alert: "M12 9v4|M12 17h.01|M12 4 3 19h18z",
monitor: "M4 5h16v10H4z|M9 19h6|M12 15v4",
moon: "M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5z",
copy: "M9 9h9a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z|M6 15H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v1",
more:
"M12 4.1a2 2 0 1 0 0 4 2 2 0 0 0 0-4z|M12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4z|"
+ "M12 15.9a2 2 0 1 0 0 4 2 2 0 0 0 0-4z",
sliders: "M4 7h10|M18 7h2|M4 12h4|M12 12h8|M4 17h11|M19 17h1|M15 5v4|M9 10v4|M16 15v4",
};
const NAV_ICONS = {
stores: "M12 4l8 4-8 4-8-4 8-4|M4 12l8 4 8-4|M4 16.5l8 4 8-4",
files: "M6 3h7l5 5v13H6z|M13 3v5h5",
index: "M4 6h16|M4 12h10|M4 18h13",
search: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14z|M16.2 16.2 20 20",
graph: "M5 6h4v4H5z|M15 14h4v4h-4z|M9 8h4v8h2",
ledger: "M5 4h11l3 3v13H5z|M9 9h6|M9 13h6|M9 17h4",
agents: "M9 3h6v5H9z|M12 8v3|M5 11h14v9H5z|M9 15h.01|M15 15h.01",
privacy: "M12 3l7 3v6c0 4.3-3 7.3-7 9-4-1.7-7-4.7-7-9V6z",
doctor: "M3 12h3l2-5 3 10 2.5-7 1.5 2h6",
about: "M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z|M12 11v5|M12 8h.01",
impact: "M16 12a3 3 0 1 0 6 0 3 3 0 0 0-6 0|M3 6l13 5|M3 12h13|M3 18l13-5",
reports: "M6 3h8l4 4v14H6z|M14 3v4h4|M9 17v-3|M12 17v-6|M15 17v-4",
cloud: "M7.5 18a4 4 0 0 1 .3-8A5 5 0 0 1 17 9.6 3.6 3.6 0 0 1 16.5 18z",
};
function copyButton(getText, label, word) {
const was = label || "Copy";
const face = () => (word ? was : icon(ICONS.copy, 14));
const button = el("button", {
class: "button secondary small copy",
type: "button",
"aria-label": was,
title: was,
onclick: async () => {
const text = typeof getText === "function" ? getText() : getText;
try {
await navigator.clipboard.writeText(text);
} catch (_) {
const area = el("textarea", { class: "offscreen" });
area.value = text;
document.body.append(area);
area.select();
try {
document.execCommand("copy");
} catch (__) {
}
area.remove();
}
button.classList.add("done");
fill(button, "Copied");
setTimeout(() => {
button.classList.remove("done");
fill(button, face());
}, 1400);
},
});
fill(button, face());
return button;
}
function copyField(command, word) {
return el(
"div",
{ class: word ? "copyfield worded" : "copyfield" },
el("code", { class: "text", text: command }),
copyButton(command, "Copy", word),
);
}
function codeBlock(text, label) {
return el(
"div",
{ class: "code-block" },
el("pre", { class: "code", text }),
copyButton(text, label || "Copy"),
);
}
function mono(text) {
return el("code", { class: "mono", text });
}
function says(...parts) {
return el("p", { class: "subtitle" }, parts);
}
function error(message) {
return el(
"div",
{ class: "error", role: "alert" },
icon(ICONS.alert, 17),
el("span", { class: "what", text: message }),
);
}
function empty(message) {
return el("div", { class: "empty" }, message);
}
const tip = {
node: null,
owner: null,
ensure() {
if (!this.node) {
this.node = el("div", { class: "tip", role: "tooltip", "aria-hidden": "true" });
document.body.append(this.node);
}
return this.node;
},
show(content, rect, owner) {
const node = this.ensure();
this.owner = owner || null;
fill(node, content);
node.setAttribute("aria-hidden", "false");
node.dataset.open = "true";
const box = node.getBoundingClientRect();
const margin = 8;
let left = rect.left;
if (left + box.width > window.innerWidth - margin) {
left = Math.max(margin, window.innerWidth - margin - box.width);
}
let top = rect.top - box.height - 6;
if (top < margin) top = rect.bottom + 6;
node.style.left = `${Math.max(margin, Math.round(left))}px`;
node.style.top = `${Math.round(top)}px`;
},
at(target, content) {
this.show(content || target.getAttribute("data-tip"), target.getBoundingClientRect(), target);
},
atPoint(x, y, content, owner) {
this.show(content, { left: x + 12, top: y - 8, bottom: y + 18 }, owner);
},
hide(owner) {
if (!this.node) return;
if (owner && this.owner !== owner) return;
this.node.dataset.open = "false";
this.node.setAttribute("aria-hidden", "true");
this.owner = null;
},
};
function wireTips() {
const find = (node) => (node && node.closest ? node.closest("[data-tip]") : null);
document.addEventListener("pointerover", (e) => {
const target = find(e.target);
if (target) tip.at(target);
else if (tip.owner && tip.owner.nodeType) tip.hide();
});
document.addEventListener("pointerout", (e) => {
const target = find(e.target);
if (target) tip.hide(target);
});
document.addEventListener("focusin", (e) => {
const target = find(e.target);
if (target) tip.at(target);
});
document.addEventListener("focusout", (e) => {
const target = find(e.target);
if (target) tip.hide(target);
});
window.addEventListener("scroll", () => tip.hide(), true);
window.addEventListener("resize", () => tip.hide());
}
const PER_PAGE = [5, 10, 25, 50];
function dataTable(spec) {
const columns = spec.columns;
const view = {
page: 1,
perPage: spec.perPage || 10,
sort: spec.sort || null,
dir: spec.dir || "asc",
};
let rows = spec.rows || [];
let total = spec.total === undefined ? rows.length : spec.total;
const headRow = el("tr", {});
const body = el("tbody", {});
const foot = el("div", { class: "table-foot" });
const table = el(
"table",
{ class: spec.className || null },
spec.caption ? el("caption", { class: "sr-only", text: spec.caption }) : null,
el("thead", {}, headRow),
body,
);
const node = el(
"div",
{ class: spec.grow ? "card table-card grow" : "card table-card" },
el("div", { class: "table-wrap" }, table),
foot,
);
function pages() {
return Math.max(1, Math.ceil(total / view.perPage));
}
function sorted() {
if (!view.sort) return rows;
const column = columns.find((c) => c.key === view.sort);
if (!column || !column.value) return rows;
const sign = view.dir === "desc" ? -1 : 1;
return [...rows].sort((a, b) => {
if (column.empty) {
const blank = Number(column.empty(a)) - Number(column.empty(b));
if (blank) return blank;
}
const x = column.value(a);
const y = column.value(b);
if (typeof x === "number" && typeof y === "number") return (x - y) * sign;
return String(x).localeCompare(String(y), undefined, { numeric: true }) * sign;
});
}
function shown() {
if (spec.server) return rows;
const from = (view.page - 1) * view.perPage;
return sorted().slice(from, from + view.perPage);
}
function changed() {
if (spec.server && spec.onChange) spec.onChange({ ...view });
else paint();
}
function sortBy(key) {
if (view.sort === key) view.dir = view.dir === "asc" ? "desc" : "asc";
else {
view.sort = key;
view.dir = "asc";
}
view.page = 1;
changed();
}
function paintHead() {
fill(
headRow,
columns.map((column) => {
const on = view.sort === column.key;
if (column.head) {
return el("th", { class: column.className || null }, column.head());
}
if (column.sortable === false) {
return el("th", { class: column.className || null, text: column.label });
}
return el(
"th",
{
class: column.className || null,
"aria-sort": on ? (view.dir === "asc" ? "ascending" : "descending") : "none",
},
el(
"button",
{ class: "sort", type: "button", onclick: () => sortBy(column.key) },
column.label,
el("span", { class: "arrow", text: on ? (view.dir === "asc" ? "↑" : "↓") : "" }),
),
);
}),
);
}
function paintBody() {
const page = shown();
fill(
body,
page.map((row, i) =>
el(
"tr",
{},
columns.map((column) =>
el(
"td",
{ class: column.className || null },
column.render ? column.render(row, i) : String(column.value ? column.value(row) : ""),
),
),
),
),
);
}
function paintFoot() {
const from = total === 0 ? 0 : (view.page - 1) * view.perPage + 1;
const to = Math.min(total, view.page * view.perPage);
fill(
foot,
el("span", { class: "meta", text: `${n(from)}–${n(to)} of ${n(total)}` }),
el("span", { class: "divider-v" }),
el("span", { class: "eyebrow", text: "per page" }),
el(
"span",
{ class: "chips" },
PER_PAGE.map((size) =>
el("button", {
class: "chip",
type: "button",
"aria-pressed": String(size === view.perPage),
text: String(size),
onclick: () => {
view.perPage = size;
view.page = 1;
changed();
},
}),
),
),
el("span", { class: "spacer" }),
el("button", {
class: "button ghost small",
type: "button",
text: "Prev",
disabled: view.page <= 1,
onclick: () => {
view.page -= 1;
changed();
},
}),
el("span", { class: "meta", text: `Page ${n(view.page)} / ${n(pages())}` }),
el("button", {
class: "button ghost small",
type: "button",
text: "Next",
disabled: view.page >= pages(),
onclick: () => {
view.page += 1;
changed();
},
}),
);
}
function paint() {
paintHead();
paintBody();
paintFoot();
}
paint();
return {
node,
update(next, count) {
rows = next || [];
total = count === undefined ? rows.length : count;
if (view.page > pages()) view.page = pages();
paint();
},
query: () => ({ ...view }),
};
}
function folderPicker(options) {
const { onChoose, choose, onError, multiple } = options || {};
const card = el("div", { class: "card picker", hidden: true });
const ticked = new Set();
function done() {
card.hidden = true;
if (onChoose) onChoose(multiple ? [...ticked] : undefined);
ticked.clear();
}
async function open(path) {
let data;
try {
data = await api(`/api/dirs?path=${encodeURIComponent(path || "")}`);
} catch (e) {
if (onError) onError(e.message);
return false;
}
card.hidden = false;
fill(
card,
el(
"div",
{ class: "crumbs" },
el(
"button",
{
class: "button secondary small",
type: "button",
disabled: !data.parent,
onclick: () => open(data.parent),
},
icon(ICONS.up, 15),
"Up",
),
el("span", { class: "where", text: data.path }),
el("span", { class: "spacer" }),
multiple
? el("button", {
class: "button small",
type: "button",
text: ticked.size
? `Use ${ticked.size} folder${ticked.size === 1 ? "" : "s"}`
: "Use this folder",
onclick: () => {
if (!ticked.size) ticked.add(data.path);
done();
},
})
: el("button", {
class: "button small",
type: "button",
text: choose || "Use this folder",
onclick: () => {
card.hidden = true;
if (onChoose) onChoose(data.path);
},
}),
el("button", {
class: "button ghost small",
type: "button",
text: "Close",
onclick: () => {
card.hidden = true;
},
}),
),
el(
"div",
{ class: "entries" },
data.entries.length
? data.entries.map((entry) => {
const inert = multiple && !entry.dir;
const row = el(
"button",
{
class: inert ? "entry inert" : "entry",
type: "button",
disabled: inert,
title: entry.path,
onclick: () => {
if (entry.dir) open(entry.path);
else {
card.hidden = true;
if (onChoose) onChoose(multiple ? [entry.path] : entry.path);
}
},
},
icon(entry.dir ? ICONS.folder : ICONS.file),
el("span", { class: "name", text: entry.name }),
entry.adoptable ? pill("a store", "good") : null,
);
if (!multiple || !entry.dir) return row;
const box = el("input", {
type: "checkbox",
class: "tick",
"aria-label": `Index ${entry.name}`,
checked: ticked.has(entry.path),
onchange: () => {
if (box.checked) ticked.add(entry.path);
else ticked.delete(entry.path);
open(data.path);
},
});
return el("div", { class: "entry-row" }, box, row);
})
: el("div", { class: "card pad" }, empty("Nothing here that Semlith can index.")),
),
);
return true;
}
return {
node: card,
open,
close: () => {
card.hidden = true;
},
isOpen: () => !card.hidden,
};
}
function pathCell(value, className) {
return el(
"span",
{
class: className ? `one-line tail ${className}` : "one-line tail",
"data-tip": value,
title: value,
},
el("bdi", { text: value }),
);
}
function lineCell(value, className) {
return el("span", {
class: className ? `one-line ${className}` : "one-line",
"data-tip": value,
title: value,
text: value,
});
}
function labelled(id, text, input) {
input.id = id;
return [el("label", { class: "sr-only", for: id, text }), input];
}
function pageHead(title, subtitle, extra) {
const { pill, actions } = extra || {};
return el(
"div",
{ class: "page-head" },
el(
"div",
{ class: "line" },
el("h1", { text: title }),
pill || null,
actions ? el("span", { class: "spacer" }) : null,
actions ? el("div", { class: "actions" }, actions) : null,
),
subtitle ? el("p", { class: "subtitle", text: subtitle }) : null,
);
}
const menu = {
node: null,
owner: null,
ensure() {
if (this.node) return this.node;
this.node = el("div", { class: "menu", role: "menu", hidden: true });
document.body.append(this.node);
document.addEventListener("pointerdown", (e) => {
if (this.node.hidden) return;
if (this.node.contains(e.target) || (this.owner && this.owner.contains(e.target))) return;
this.close();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") this.close();
});
window.addEventListener("resize", () => this.close());
window.addEventListener("scroll", () => this.close(), true);
return this.node;
},
open(at, items) {
const node = this.ensure();
if (this.owner === at && !node.hidden) return this.close();
this.owner = at;
at.setAttribute("aria-expanded", "true");
fill(
node,
items.map((item) =>
el("button", {
class: item.tone ? `menu-item ${item.tone}` : "menu-item",
type: "button",
role: "menuitem",
text: item.label,
onclick: () => {
this.close();
item.onclick();
},
}),
),
);
node.hidden = false;
const box = node.getBoundingClientRect();
const rect = at.getBoundingClientRect();
const margin = 8;
let left = rect.right - box.width;
left = Math.min(Math.max(margin, left), window.innerWidth - margin - box.width);
let top = rect.bottom + 6;
if (top + box.height > window.innerHeight - margin) top = rect.top - box.height - 6;
node.style.left = `${Math.round(left)}px`;
node.style.top = `${Math.round(Math.max(margin, top))}px`;
node.querySelector(".menu-item")?.focus();
},
close() {
if (!this.node) return;
this.node.hidden = true;
if (this.owner) this.owner.setAttribute("aria-expanded", "false");
this.owner = null;
},
};
function rowMenu(items) {
const button = el("button", {
class: "button secondary small icon",
type: "button",
"aria-haspopup": "menu",
"aria-expanded": "false",
"aria-label": "Actions",
title: "Actions",
onclick: () => menu.open(button, items()),
});
fill(button, icon(ICONS.more, 18, true));
return button;
}
function pill(text, tone, props) {
return el(
"span",
{ class: tone ? `pill ${tone}` : "pill", ...(props || {}) },
tone ? el("i", {}) : null,
text,
);
}
const NARROW = "(max-width: 899px)";
const state = {
stores: [],
theme: "light",
navOpen: !window.matchMedia(NARROW).matches,
pendingPath: "",
pendingSymbol: "",
impactSymbol: "",
pendingQuery: "",
search: { query: "", stores: [] },
agents: null,
runs: null,
onRuns: null,
};
const VIEWS = [
{ group: "Workspace", id: "stores", label: "Stores", title: "Stores" },
{ group: "Workspace", id: "files", label: "Files", title: "Files" },
{ group: "Workspace", id: "search", label: "Search", title: "Search" },
{ group: "Workspace", id: "graph", label: "Graph", title: "Graph" },
{ group: "Workspace", id: "impact", label: "Impact", title: "Impact" },
{ group: "Workspace", id: "index", label: "Inside the index", title: "Inside the index" },
{ group: "Operate", id: "ledger", label: "Retrieval ledger", title: "Retrieval ledger" },
{ group: "Operate", id: "reports", label: "Reports", title: "Reports" },
{ group: "Operate", id: "agents", label: "Agents", title: "Agents" },
{ group: "Operate", id: "cloud", label: "Cloud", title: "Cloud" },
{ group: "Operate", id: "privacy", label: "Privacy", title: "Privacy" },
{ group: "Operate", id: "doctor", label: "Doctor", title: "Doctor" },
{ group: "Operate", id: "about", label: "About", title: "About" },
];
function paintStoreCount() {
const node = document.getElementById("daemon-stores");
if (!node) return;
const many = state.stores.length;
const parts = [`${many} store${many === 1 ? "" : "s"}`];
if (state.agents !== null) {
parts.push(`${state.agents} agent${state.agents === 1 ? "" : "s"}`);
}
node.textContent = parts.join(" · ");
}
function noteAgents(data) {
state.agents = (data.connections || []).length;
paintStoreCount();
}
const live = {
seen: {},
watchers: [],
timer: null,
};
function watchLive(domains, run) {
live.watchers.push({ domains, run });
}
function resetLive() {
live.watchers = [];
}
async function pollChanges() {
if (document.visibilityState === "hidden") return;
let counters;
try {
counters = await api("/api/changes");
} catch (_) {
return;
}
const moved = [];
for (const [domain, value] of Object.entries(counters)) {
if (live.seen[domain] === undefined) {
live.seen[domain] = value;
continue;
}
if (live.seen[domain] !== value) {
live.seen[domain] = value;
moved.push(domain);
}
}
if (!moved.length) return;
for (const watcher of live.watchers) {
if (watcher.domains.some((domain) => moved.includes(domain))) {
try {
watcher.run(moved);
} catch (_) {
}
}
}
}
function startLive() {
if (live.timer) return;
live.timer = setInterval(pollChanges, 1000);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") pollChanges();
});
}
async function refreshRuns() {
let data;
try {
data = await api("/api/index/runs");
} catch (_) {
return state.runs;
}
state.runs = data;
paintRunCount();
if (state.onRuns) state.onRuns(data);
return data;
}
function paintRunCount() {
const on = (state.runs?.runs || []).filter((run) => TICKING.has(run.status)).length;
const waiting = (state.runs?.queue || []).length;
for (const node of document.querySelectorAll(".run-count")) {
node.textContent = on ? `${on} indexing${waiting ? ` · ${waiting} queued` : ""}` : "";
node.hidden = !on;
}
}
const TICKING = new Set(["queued", "running", "paused", "stopping"]);
async function repaintView() {
if (!shell.main) return;
const at = shell.main.querySelector(".scroller")?.scrollTop ?? 0;
await render();
const scroller = shell.main?.querySelector(".scroller");
if (scroller) scroller.scrollTop = at;
}
function graphInk() {
const style = getComputedStyle(document.documentElement);
const read = (name, fallback) => (style.getPropertyValue(name) || fallback).trim();
return {
edge: read("--line", "#dce3e8"),
hot: read("--blue", "#4c7088"),
node: read("--panel", "#ffffff"),
nodeLine: read("--line", "#dce3e8"),
text: read("--ink-2", "#3a4d5e"),
nearFill: read("--blue-soft", "#e3ecf2"),
nearLine: read("--blue", "#4c7088"),
nearText: read("--ink", "#1e2a35"),
warn: read("--amber-ink", "#7a4e0a"),
sel: read("--accent", "#f0a43c"),
selLine: read("--accent-edge", "#c97f14"),
selText: read("--accent-ink", "#1e2a35"),
muted: read("--muted", "#64778a"),
};
}
function shortPath(path) {
const parts = String(path).split("/").filter(Boolean);
return parts.slice(-2).join("/") || path;
}
function stillness() {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function graphCanvas(options) {
const { onPick, onHover } = options || {};
const canvas = el("canvas", { class: "graph-canvas" });
const wrap = el("div", { class: "graph-stage" }, canvas);
let nodes = [];
let edges = [];
let total = 0;
let drawn = [];
let selected = null;
let near = new Set();
let hovered = null;
let dragging = null;
let down = null;
let frame = null;
let running = true;
let settled = false;
let attached = false;
let alpha = 1;
let load = [];
function size() {
const ratio = window.devicePixelRatio || 1;
const box = wrap.getBoundingClientRect();
const w = Math.max(box.width, 240);
const h = Math.max(box.height, 240);
canvas.width = Math.round(w * ratio);
canvas.height = Math.round(h * ratio);
const ctx = canvas.getContext("2d");
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
return { w, h, ctx };
}
const MAX_STEP = 0.012;
const ALPHA_FLOOR = 0;
const DRIFT = 0.00003;
function step(w, h) {
const clock = performance.now() * 0.00028;
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j];
let dx = (b.x - a.x) * w;
let dy = (b.y - a.y) * h;
let d2 = dx * dx + dy * dy;
if (d2 < 1) {
d2 = 1;
dx = 0.6;
dy = 0.4;
}
const d = Math.sqrt(d2);
const force = (5200 / d2) * alpha;
a.vx -= ((dx / d) * force) / w;
a.vy -= ((dy / d) * force) / h;
b.vx += ((dx / d) * force) / w;
b.vy += ((dy / d) * force) / h;
}
}
for (const edge of drawn) {
const a = nodes[edge.from];
const b = nodes[edge.to];
if (!a || !b) continue;
const dx = (b.x - a.x) * w;
const dy = (b.y - a.y) * h;
const d = Math.max(1, Math.sqrt(dx * dx + dy * dy));
const pull = (d - 138) * 0.0025 * alpha;
const ax = Math.sqrt(load[edge.from] || 1);
const bx = Math.sqrt(load[edge.to] || 1);
a.vx += ((((dx / d) * pull * d) / w) * 0.02) / ax;
a.vy += ((((dy / d) * pull * d) / h) * 0.02) / ax;
b.vx -= ((((dx / d) * pull * d) / w) * 0.02) / bx;
b.vy -= ((((dy / d) * pull * d) / h) * 0.02) / bx;
}
let moved = 0;
for (const node of nodes) {
node.vx += (0.5 - node.x) * 0.006 * alpha;
node.vy += (0.5 - node.y) * 0.008 * alpha;
node.vx += Math.cos(clock + node.phase) * DRIFT;
node.vy += Math.sin(clock * 0.9 + node.phase * 1.7) * DRIFT;
if (node === dragging) {
node.vx = 0;
node.vy = 0;
continue;
}
node.vx *= 0.86;
node.vy *= 0.86;
node.vx = Math.max(-MAX_STEP, Math.min(MAX_STEP, node.vx));
node.vy = Math.max(-MAX_STEP, Math.min(MAX_STEP, node.vy));
moved += Math.abs(node.vx) + Math.abs(node.vy);
node.x = Math.min(0.94, Math.max(0.06, node.x + node.vx));
node.y = Math.min(0.92, Math.max(0.08, node.y + node.vy));
}
alpha = Math.max(ALPHA_FLOOR, alpha * 0.985);
return moved;
}
function reheat(to) {
alpha = Math.max(alpha, to === undefined ? 0.35 : to);
}
function separate() {
const box = wrap.getBoundingClientRect();
const w = Math.max(box.width, 240);
const h = Math.max(box.height, 240);
for (let pass = 0; pass < 4; pass++) {
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const a = nodes[i];
const b = nodes[j];
if (!a.w || !b.w) return;
const dx = (b.x - a.x) * w;
const dy = (b.y - a.y) * h;
const wantX = (a.w + b.w) / 2 + 8;
const wantY = (a.h + b.h) / 2 + 6;
const overX = wantX - Math.abs(dx);
const overY = wantY - Math.abs(dy);
if (overX <= 0 || overY <= 0) continue;
if (overX / w < overY / h) {
const push = ((dx >= 0 ? 1 : -1) * overX) / w / 2;
a.x = Math.min(0.94, Math.max(0.06, a.x - push));
b.x = Math.min(0.94, Math.max(0.06, b.x + push));
} else {
const push = ((dy >= 0 ? 1 : -1) * overY) / h / 2;
a.y = Math.min(0.92, Math.max(0.08, a.y - push));
b.y = Math.min(0.92, Math.max(0.08, b.y + push));
}
}
}
}
}
function box(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function paint() {
const { w, h, ctx } = size();
const ink = graphInk();
ctx.clearRect(0, 0, w, h);
ctx.font = '500 11px "IBM Plex Mono", ui-monospace, monospace';
for (const node of nodes) {
const ease = stillness() ? (node === hovered ? 1 : 0) : node.hover || 0;
node.hover = ease;
const text = ctx.measureText(node.label).width;
node.w = (text + 22) * (1 + 0.05 * ease);
node.h = 28 * (1 + 0.05 * ease);
}
for (const edge of drawn) {
const a = nodes[edge.from];
const b = nodes[edge.to];
if (!a || !b) continue;
const ax = a.x * w;
const ay = a.y * h;
const bx = b.x * w;
const by = b.y * h;
const hot = edge.from === selected || edge.to === selected;
const angle = Math.atan2(by - ay, bx - ax);
const inset = Math.min(b.w / 2 + 6, Math.abs(b.h / 2 / Math.sin(angle) || b.w));
const ex = bx - Math.cos(angle) * Math.min(inset, b.w / 2 + 6);
const ey = by - Math.sin(angle) * (b.h / 2 + 3);
ctx.save();
ctx.strokeStyle = hot ? ink.hot : ink.edge;
ctx.fillStyle = hot ? ink.hot : ink.edge;
ctx.lineWidth = hot ? 1.7 : 1.1;
const settled = edge.confidence === "extracted" || edge.confidence === "resolved";
if (edge.confidence === "ambiguous") {
ctx.strokeStyle = hot ? ink.hot : ink.warn;
ctx.fillStyle = hot ? ink.hot : ink.warn;
ctx.setLineDash([6, 4]);
} else {
ctx.setLineDash(settled ? [] : [2, 3]);
}
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(ex, ey);
ctx.stroke();
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex - Math.cos(angle - 0.4) * 7, ey - Math.sin(angle - 0.4) * 7);
ctx.lineTo(ex - Math.cos(angle + 0.4) * 7, ey - Math.sin(angle + 0.4) * 7);
ctx.closePath();
ctx.fill();
ctx.restore();
}
nodes.forEach((node, i) => {
const on = i === selected;
const beside = near.has(i);
const x = node.x * w - node.w / 2;
const y = node.y * h - node.h / 2;
box(ctx, x, y, node.w, node.h, 8);
ctx.fillStyle = on ? ink.sel : beside ? ink.nearFill : ink.node;
ctx.save();
if (node.hover) {
ctx.shadowColor = ink.nearLine;
ctx.shadowBlur = 14 * node.hover;
}
ctx.fill();
ctx.restore();
ctx.lineWidth = (on ? 1.6 : 1) + 0.8 * node.hover;
ctx.strokeStyle = on ? ink.selLine : beside ? ink.nearLine : ink.nodeLine;
ctx.stroke();
ctx.fillStyle = on ? ink.selText : beside ? ink.nearText : ink.text;
ctx.fillText(node.label, x + 11, y + node.h / 2 + 4);
});
}
function tick() {
if (wrap.isConnected) attached = true;
else if (attached) {
frame = null;
running = false;
observer.disconnect();
return;
}
const box = wrap.getBoundingClientRect();
if (running || dragging) step(Math.max(box.width, 240), Math.max(box.height, 240));
for (const node of nodes) {
node.hover = (node.hover || 0) + ((node === hovered ? 1 : 0) - (node.hover || 0)) * 0.16;
if (node.hover < 0.002) node.hover = 0;
}
paint();
frame = requestAnimationFrame(tick);
}
function at(event) {
const box = canvas.getBoundingClientRect();
return { x: event.clientX - box.left, y: event.clientY - box.top, w: box.width, h: box.height };
}
function hit(point) {
for (let i = nodes.length - 1; i >= 0; i--) {
const node = nodes[i];
const cx = node.x * point.w;
const cy = node.y * point.h;
if (
Math.abs(point.x - cx) < (node.w || 80) / 2 &&
Math.abs(point.y - cy) < (node.h || 28) / 2
) {
return i;
}
}
return null;
}
function selectAt(index) {
selected = index;
near = new Set();
for (const edge of drawn) {
if (edge.from === index) near.add(edge.to);
if (edge.to === index) near.add(edge.from);
}
}
canvas.addEventListener("pointerdown", (event) => {
const index = hit(at(event));
if (index === null) return;
canvas.setPointerCapture(event.pointerId);
dragging = nodes[index];
reheat();
down = { x: event.clientX, y: event.clientY, index };
canvas.classList.add("dragging");
event.preventDefault();
});
canvas.addEventListener("pointermove", (event) => {
const point = at(event);
if (dragging) {
dragging.x = Math.min(0.94, Math.max(0.06, point.x / point.w));
dragging.y = Math.min(0.92, Math.max(0.08, point.y / point.h));
if (!frame) paint();
return;
}
const index = hit(point);
const node = index === null ? null : nodes[index];
canvas.classList.toggle("over-node", Boolean(node));
if (node !== hovered) {
hovered = node;
if (!frame) paint();
}
if (onHover) onHover(node, event.clientX, event.clientY);
});
const release = (event) => {
if (down) {
const moved = Math.abs(event.clientX - down.x) + Math.abs(event.clientY - down.y);
if (moved < 4) {
selectAt(down.index);
if (onPick) onPick(nodes[down.index]);
}
}
dragging = null;
down = null;
canvas.classList.remove("dragging");
if (!frame) paint();
};
canvas.addEventListener("pointerup", release);
canvas.addEventListener("pointercancel", release);
canvas.addEventListener("pointerleave", () => {
hovered = null;
dragging = null;
down = null;
canvas.classList.remove("dragging", "over-node");
if (onHover) onHover(null);
if (!frame) paint();
});
const observer = new ResizeObserver(() => {
if (!frame) paint();
});
observer.observe(wrap);
return {
node: wrap,
draw(data, kinds) {
nodes = (data.nodes || []).map((row, i) => {
const angle = (i / Math.max((data.nodes || []).length, 1)) * Math.PI * 2;
return {
...row,
label: row.name.length > 26 ? `${row.name.slice(0, 25)}…` : row.name,
x: 0.5 + Math.cos(angle) * 0.28,
y: 0.5 + Math.sin(angle) * 0.28,
vx: 0,
vy: 0,
phase: Math.random() * Math.PI * 2,
hover: 0,
callers: 0,
callees: 0,
};
});
edges = data.edges || [];
total = data.total || 0;
selected = null;
near = new Set();
hovered = null;
this.filter(kinds);
alpha = 1;
const box = wrap.getBoundingClientRect();
for (let i = 0; i < 220; i++) {
step(Math.max(box.width, 240), Math.max(box.height, 240));
}
settled = true;
paint();
separate();
paint();
},
filter(kinds) {
drawn = kinds ? edges.filter((e) => kinds.has(e.kind)) : edges;
load = nodes.map(() => 1);
for (const node of nodes) {
node.callers = 0;
node.callees = 0;
}
for (const edge of drawn) {
if (nodes[edge.from]) nodes[edge.from].callees += 1;
if (nodes[edge.to]) nodes[edge.to].callers += 1;
if (load[edge.from] !== undefined) load[edge.from] += 1;
if (load[edge.to] !== undefined) load[edge.to] += 1;
}
if (selected !== null) selectAt(selected);
reheat(0.25);
if (settled) paint();
return drawn.length;
},
pick(name) {
const index = nodes.findIndex((node) => node.name === name);
if (index < 0) return false;
selectAt(index);
paint();
if (onPick) onPick(nodes[index]);
return true;
},
counts: () => ({ nodes: nodes.length, edges: drawn.length, total }),
best() {
const score = new Map();
for (const edge of drawn) {
if (edge.confidence !== "extracted" && edge.confidence !== "resolved") continue;
for (const end of [edge.from, edge.to]) {
score.set(end, (score.get(end) || 0) + 1);
}
}
let pick = null;
let most = 0;
for (const [index, count] of score) {
if (count <= most || !nodes[index]) continue;
most = count;
pick = nodes[index].name;
}
return pick;
},
fit() {
if (!nodes.length) return;
const xs = nodes.map((node) => node.x);
const ys = nodes.map((node) => node.y);
const spread = (values, low, high) => {
const min = Math.min(...values);
const max = Math.max(...values);
const span = max - min;
if (span < 0.001) return () => (low + high) / 2;
return (value) => low + ((value - min) / span) * (high - low);
};
const toX = spread(xs, 0.08, 0.92);
const toY = spread(ys, 0.1, 0.9);
for (const node of nodes) {
node.x = toX(node.x);
node.y = toY(node.y);
node.vx = 0;
node.vy = 0;
}
separate();
paint();
},
running: () => running,
toggle() {
running = !running;
if (running) reheat(0.2);
return running;
},
start() {
if (stillness()) {
running = false;
paint();
return;
}
if (!frame) tick();
},
stop() {
running = false;
if (frame) cancelAnimationFrame(frame);
frame = null;
observer.disconnect();
},
repaint: paint,
};
}
const EDGE_KINDS = ["defines", "calls", "imports", "references", "contains", "aliases"];
async function graphView() {
await refreshStores();
const kinds = new Set(EDGE_KINDS);
const chosen = new Set();
const meta = el("span", { class: "graph-count" });
const rail = el("div", { class: "graph-rail" });
const canvas = graphCanvas({
onPick: (node) => select(node),
onHover: (node, x, y) => {
if (!node) return tip.hide("graph");
tip.atPoint(
x,
y,
[
el(
"div",
{ class: "tip-head" },
el("i", {}),
node.name,
),
row("kind", node.kind),
row("file", `${shortPath(node.path)}:${node.start_line}-${node.end_line}`),
row("store", node.store || (state.stores[0] && state.stores[0].name) || "—"),
row("calls", `${node.callers} in · ${node.callees} out`),
],
"graph",
);
},
});
function row(key, value) {
return el(
"div",
{ class: "tip-row" },
el("span", { class: "k", text: key }),
el("span", { class: "v", text: String(value) }),
);
}
const pause = el("button", {
class: "button secondary small",
type: "button",
text: "Pause",
onclick: (e) => {
const on = canvas.toggle();
e.currentTarget.textContent = on ? "Pause" : "Resume";
},
});
const fit = el("button", {
class: "button secondary small",
type: "button",
text: "Fit",
onclick: () => canvas.fit(),
});
const reset = el("button", {
class: "button ghost small",
type: "button",
text: "Reset",
onclick: () => load({}),
});
function paintPause() {
pause.textContent = canvas.running() ? "Pause" : "Settled";
}
function counts() {
const { nodes, edges, total } = canvas.counts();
const shown = total && total > nodes ? `${n(nodes)} of ${n(total)} symbols` : `${n(nodes)} symbols`;
meta.textContent = `${shown} · ${n(edges)} edges`;
}
function blank(message) {
return fill(
rail,
el("div", { class: "graph-selected" }, el("div", { class: "rail-hint", text: message })),
);
}
function ends(title, list, absent, extra) {
return el(
"div",
{ class: "rail-group" },
el("h3", { text: title }),
list.length
? el(
"ul",
{ class: "rail-list" },
list.map((end) => endRow(end)),
)
: el("div", { class: "rail-hint", text: absent }),
extra || null,
);
}
function endRow(end) {
if (end.confidence === "ambiguous") {
const li = el("li", { class: "ambiguous-row" });
let open = false;
const toggle = el("button", {
class: "link-button",
type: "button",
text: `${end.name} · ${end.definitions} definitions`,
"aria-expanded": "false",
onclick: async () => {
open = !open;
toggle.setAttribute("aria-expanded", String(open));
if (!open) return fill(nested);
fill(nested, el("div", { class: "rail-hint", text: "Loading…" }));
let all;
try {
all = await api(
scoped(`/api/neighbors?name=${encodeURIComponent(state.graphSelected)}&all=1`),
);
} catch (e) {
return fill(nested, error(e.message));
}
const each = all.callees.filter((c) => c.name === end.name);
fill(
nested,
el(
"ul",
{ class: "rail-list nested" },
each.map((one) =>
el(
"li",
{},
el("a", {
href: `#graph?name=${encodeURIComponent(one.name)}`,
text: `${shortPath(one.path)}:${one.start_line}`,
onclick: (e) => {
e.preventDefault();
focus(one.name);
},
}),
),
),
),
);
},
});
const nested = el("div", { class: "nested-wrap" });
return fill(li, toggle, confidenceBadge(end.confidence), el("span", { class: "via", text: end.kind }), nested);
}
return el(
"li",
{},
el("a", {
href: `#graph?name=${encodeURIComponent(end.name)}`,
text: end.name,
onclick: (e) => {
e.preventDefault();
focus(end.name);
},
}),
confidenceBadge(end.confidence),
el("span", { class: "via", text: end.kind }),
);
}
function confidenceLegend() {
return el(
"div",
{ class: "conf-legend" },
Object.entries(CONFIDENCE).map(([value, [label, why]]) =>
el(
"span",
{ class: "legend-chip" },
el("span", { class: `conf ${value}`, text: label }),
el("span", { class: "why", text: why }),
),
),
);
}
async function select(node) {
fill(rail, el("div", { class: "rail-hint", text: "Loading…" }));
let data;
try {
data = await api(scoped(`/api/neighbors?name=${encodeURIComponent(node.name)}`));
} catch (e) {
return fill(rail, error(e.message));
}
state.graphSelected = node.name;
const edge = (e) => ({
name: e.name,
kind: e.kind,
confidence: e.confidence,
definitions: e.definitions,
path: e.path,
start_line: e.start_line,
});
fill(
rail,
el(
"div",
{ class: "graph-selected" },
el("span", { class: "eyebrow", text: "Selected symbol" }),
el("h2", { class: "sym", text: node.name }),
el("div", {
class: "loc",
"data-tip": node.path,
text: `${shortPath(node.path)}:${node.start_line}-${node.end_line}`,
}),
el(
"div",
{ class: "chips" },
el("span", { class: "chip static blue", text: node.kind }),
node.store ? el("span", { class: "chip static", text: node.store }) : null,
el("span", {
class: "chip static",
text: `${Math.max(node.end_line - node.start_line + 1, 1)} lines`,
}),
),
el("button", {
class: "link-button quiet",
type: "button",
text: "Blast radius",
onclick: () => {
state.impactSymbol = node.name;
go("impact");
},
}),
),
ends("Incoming", data.callers.map(edge), "Nothing in the graph points at this."),
ends(
"Outgoing",
data.callees.map(edge),
"A leaf, as far as the extracted edges go.",
data.hidden
? el("button", {
class: "link-button quiet",
type: "button",
text: `Show ${data.hidden} unresolved`,
onclick: async (e) => {
const button = e.currentTarget;
button.textContent = "Loading…";
let all;
try {
all = await api(scoped(`/api/neighbors?name=${encodeURIComponent(node.name)}&all=1`));
} catch (err) {
button.replaceWith(error(err.message));
return;
}
button.replaceWith(
el(
"ul",
{ class: "rail-list nested" },
all.unresolved.map((one) =>
el(
"li",
{},
el("span", { text: one.name }),
el("span", { class: "via", text: one.kind }),
),
),
),
);
},
})
: null,
),
confidenceLegend(),
el(
"div",
{ class: "rail-actions" },
el("button", {
class: "button secondary small",
type: "button",
text: "Ask the index a question",
onclick: () => {
state.pendingQuery = node.name;
go("search");
},
}),
),
);
}
function scoped(path) {
const query = new URLSearchParams();
for (const store of chosen) query.append("store", store);
const tail = query.toString();
return tail ? `${path}${path.includes("?") ? "&" : "?"}${tail}` : path;
}
async function load(params) {
meta.textContent = "loading…";
const query = new URLSearchParams(params || {});
for (const store of chosen) query.append("store", store);
let data;
try {
data = await api(`/api/graph?${query}`);
} catch (e) {
meta.textContent = "";
return fill(rail, error(e.message));
}
if (!data.nodes.length) {
meta.textContent = "0 symbols";
canvas.draw({ nodes: [], edges: [] }, kinds);
return blank(
data.total === 0
? "This store has no symbols yet. The graph is built as files are indexed — run Index once, or leave the daemon watching."
: "Nothing in this scope. Clear the filters, or pick a store.",
);
}
canvas.draw(data, kinds);
counts();
if (!data.edges.length) {
return blank(
"Nothing in view is connected. Either this scope holds both ends of no " +
"edge — widen it, or clear it — or the language here is one semlith " +
"parses for definitions but not yet for calls. The Index page's " +
"per-language table says which.",
);
}
const centre = (params && params.name) || canvas.best();
if (centre && canvas.pick(centre)) return;
blank("Pick a node to see what calls it and what it calls.");
}
function focus(name) {
load({ name, limit: "45" });
}
function applyScope() {
const value = scopeInput.value.trim();
if (!value) return load({});
load(value.includes("/") || value.includes(".") ? { path: value } : { name: value });
}
const scopeInput = el("input", {
type: "search",
placeholder: "Scope to a path, or find a symbol",
"aria-describedby": "graph-scope-hint",
onkeydown: (e) => {
if (e.key !== "Enter") return;
applyScope();
},
});
const scopeButton = el("button", {
class: "button secondary small",
type: "button",
text: "Scope",
onclick: () => applyScope(),
});
const kindChips = EDGE_KINDS.map((kind) =>
el("button", {
class: "chip",
type: "button",
"aria-pressed": "true",
text: kind,
onclick: (e) => {
const on = e.currentTarget.getAttribute("aria-pressed") !== "true";
e.currentTarget.setAttribute("aria-pressed", String(on));
if (on) kinds.add(kind);
else kinds.delete(kind);
canvas.filter(kinds);
canvas.fit();
counts();
},
}),
);
const storeChips = liveStores().map((store) =>
el("button", {
class: "chip",
type: "button",
"aria-pressed": "false",
text: store.name,
onclick: (e) => {
const on = e.currentTarget.getAttribute("aria-pressed") !== "true";
e.currentTarget.setAttribute("aria-pressed", String(on));
if (on) chosen.add(store.name);
else chosen.delete(store.name);
load({});
},
}),
);
const page = el(
"div",
{ class: "graph-page" },
el(
"div",
{ class: "graph-band" },
pageHead(
"Graph",
"Edges are re-extracted on the same pass that re-embeds a file. Never a stale build artifact.",
{ actions: [el("div", { class: "filters" }, kindChips), fit, reset, pause] },
),
el(
"div",
{ class: "graph-controls" },
el(
"div",
{ class: "graph-scope" },
icon(ICONS.search, 16),
labelled("graph-scope", "Scope the graph", scopeInput),
scopeButton,
el("span", {
class: "meta",
id: "graph-scope-hint",
text: "Enter applies it",
}),
),
storeChips.length > 1 ? el("div", { class: "filters" }, storeChips) : null,
),
),
el(
"div",
{ class: "graph-body" },
el(
"div",
{ class: "graph-frame" },
canvas.node,
el(
"div",
{ class: "graph-legend" },
el("span", { class: "key extracted" }, el("i", {}), "extracted"),
el("span", { class: "key resolved" }, el("i", {}), "resolved"),
el("span", { class: "key inferred" }, el("i", {}), "inferred"),
el("span", { class: "key ambiguous" }, el("i", {}), "ambiguous"),
el("span", { class: "key selected" }, el("i", {}), "selected"),
),
meta,
),
el("div", { class: "graph-side" }, rail, mapPanel()),
),
);
blank("Pick a node to see what calls it and what it calls.");
setTimeout(async () => {
canvas.start();
paintPause();
const pending = state.pendingSymbol;
state.pendingSymbol = "";
if (pending) return load({ name: pending, limit: "45" });
try {
const overview = await api("/api/graph?limit=1");
const busiest = (overview.nodes || [])[0];
if (busiest) return focus(busiest.name);
} catch (_) {
}
load({});
}, 0);
return page;
}
function ringed(node) {
node.classList.add("ringed");
return node;
}
function stat(label, value, note) {
return el(
"div",
{ class: "stat" },
el("span", { class: "eyebrow", text: label }),
el("span", { class: "value", text: value }),
note ? el("span", { class: "sub", text: note }) : null,
);
}
async function ledgerView() {
let data;
try {
data = await api("/api/ledger");
} catch (e) {
return el("div", { class: "view" }, pageHead("Retrieval ledger"), error(e.message));
}
const on = data.recording;
watchLive(["ledger"], repaintView);
return el(
"div",
{ class: "view" },
pageHead(
"Retrieval ledger",
"Every query an agent ran, recorded locally. The honest token number, a debugging trail, and an audit record that never left the machine.",
{
pill: on
? pill("recording", "on", {
title:
"On by default. Rows live in the store beside the chunks and never leave this machine.",
})
: pill("not recording", null),
},
),
el(
"div",
{ class: "strip" },
stat(
"Queries recorded",
n(data.queries),
`${n(data.clients)} client${data.clients === 1 ? "" : "s"}`,
),
stat("Excerpt tokens", n(data.excerpt_tokens), "what agents were actually sent"),
stat(
"Whole-file tokens",
n(data.whole_file_tokens),
"what reading those files whole would have cost, counted with the store's tokenizer",
),
ringed(
stat(
"Measured ratio",
data.ratio ? `${data.ratio.toFixed(1)}×` : "—",
data.ratio ? `coverage ${data.coverage}% · ${data.tier}` : "needs a recorded query",
),
),
stat(
"Coverage",
`${data.coverage || 0}%`,
`${n(data.credited || 0)} of ${n(data.queries)} retrievals credited`,
),
stat(
"Net tokens",
n(data.net_tokens || 0),
"whole-file less excerpt, over rows that found something",
),
),
el(
"div",
{ class: "strip" },
stat(
"Zero-hit",
`${n(data.zero_hit || 0)} of ${n(data.queries)}`,
"queries the corpus could not answer — recorded, and credited nothing",
),
stat(
"Refunds",
`${n(data.refunds || 0)}${data.refunds_measured ? "" : "+"}`,
data.refunds_measured
? "files read whole after all, seen by the steering hook — measured"
: "a floor: no steering hook reports here, so reads semlith never served are uncounted",
),
stat("Tier", data.tier || "modelled", "modelled · measured — measured when the store's own tokenizer counted it"),
),
clientBreakdown(data.by_client),
ledgerSessions(data),
sessionReplay(),
ledgerRows(data),
el(
"div",
{ class: "scroller" },
el(
"div",
{ class: "grid three" },
el(
"div",
{ class: "card pad dense" },
copyField("semlith ledger --last 20"),
el("p", {
class: "subtitle",
text: "Prints the ledger on the command line. The same rows this page shows.",
}),
),
el(
"div",
{ class: "card pad dense" },
copyField("semlith ledger --verify"),
el("p", {
class: "subtitle",
text: "Re-walks the hash chain and names the first row that does not verify.",
}),
el("p", {
class: "subtitle",
text: data.intact
? "The chain is intact."
: "The chain does not verify. Some rows have been edited or removed.",
}),
),
el(
"div",
{ class: "card pad dense" },
copyField("semlith start --no-ledger"),
el("p", {
class: "subtitle",
text: "Run the daemon without recording, for this session only. SEMLITH_LEDGER=0 does the same for a machine.",
}),
),
),
on
? null
: el(
"div",
{ class: "notice" },
el("div", { class: "what", text: "Recording is off" }),
el(
"div",
{},
"The daemon was started with ",
mono("--no-ledger"),
". Start it without that flag to record what your agents retrieve. Nothing is sent anywhere; the rows live in the store beside the chunks.",
),
),
says(
"Stored in ",
mono("~/.semlith/stores/<name>/store.db"),
", table ",
mono("retrievals"),
". On by default; ",
mono("--no-ledger"),
" or ",
mono("SEMLITH_LEDGER=0"),
" turns it off, and deleting the rows is one ",
mono("DELETE"),
".",
),
),
);
}
function sessionReplayToggle() {
const state_ = { enabled: false, from: "", client: "" };
const note = el("p", { class: "note" });
const button = el("button", {
class: "button secondary small",
type: "button",
text: "Loading…",
disabled: true,
});
function paint() {
button.disabled = false;
button.textContent = state_.enabled ? "Turn off" : "Turn on";
button.classList.toggle("on", state_.enabled);
note.textContent = state_.enabled
? `On. The Retrieval ledger's Session replay tab reads ${state_.client} transcripts under ${state_.from} to mark what happened after each answer. Nothing is sent anywhere.`
: `Off. Nothing reads ${state_.from || "this machine's agent transcripts"}. Turning this on lets the ledger's Session replay tab mark each answer as a refund, a miss or an edit — locally, and still sent nowhere.`;
}
button.addEventListener("click", async () => {
button.disabled = true;
try {
const answer = await api("/api/ledger/replay", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ on: !state_.enabled }),
});
state_.enabled = !!answer.enabled;
} catch (e) {
note.textContent = e.message;
button.disabled = false;
return;
}
paint();
});
(async () => {
try {
const data = await api("/api/ledger/replay");
state_.enabled = !!data.enabled;
state_.from = data.from || "";
state_.client = data.client || "";
} catch (_) {
}
paint();
})();
return el(
"div",
{ class: "card pad" },
el(
"div",
{ class: "card-head" },
el("h2", { text: "Session replay" }),
el("span", { class: "spacer" }),
button,
),
note,
);
}
function sessionReplay() {
const body = el("div", { class: "replay-body" });
function counts(session) {
const rows = [
["read the whole file", session.refund, "refund"],
["grepped anyway", session.miss, "miss"],
["edited", session.sufficed, "sufficed"],
["nothing recorded", session.unknown, "unknown"],
];
return el(
"span",
{ class: "replay-counts" },
rows.map(([label, count, kind]) =>
count
? el("span", { class: `replay-count ${kind}`, text: `${count} ${label}` })
: null,
),
);
}
async function load() {
let data;
try {
data = await api("/api/ledger/replay");
} catch (e) {
fill(body, error(e.message));
return;
}
if (!data.enabled) {
fill(
body,
el("p", {
class: "subtitle",
text: "Off. Turn it on under Privacy and this reads this machine's agent transcripts to mark what happened after each answer. Nothing is sent anywhere.",
}),
);
return;
}
if (!data.sessions.length) {
fill(
body,
el("p", {
class: "subtitle",
text: `On, and no transcript under ${data.from || "this machine"} holds a semlith call yet.`,
}),
);
return;
}
fill(
body,
el("p", {
class: "note",
text: `Read from ${data.client} transcripts under ${data.from}${data.skipped ? `, ${data.skipped} older transcript${data.skipped === 1 ? "" : "s"} not read` : ""}.`,
}),
data.sessions.map((session) =>
el(
"div",
{ class: "replay-row" },
el(
"span",
{ class: "line one" },
el("span", { class: "id", text: session.id.slice(0, 12) }),
el("span", { class: "muted", text: session.project }),
el("span", { class: "spacer" }),
el("span", {
class: "muted",
text: `${session.answers} answer${session.answers === 1 ? "" : "s"}`,
}),
),
counts(session),
),
),
);
}
load();
return el(
"div",
{ class: "card pad" },
el("span", { class: "card-title", text: "Session replay" }),
el("p", {
class: "subtitle",
text: "Read from this machine's agent session logs, never sent anywhere. Turn on under Privacy.",
}),
body,
);
}
const MODEL_PRICES = [
["Sonnet 5", 3, "sonnet_5"],
["Opus 5", 15, "opus_5"],
["Haiku 4.5", 1, "haiku_4_5"],
];
function offerDownload(name, text, type) {
const blob = new Blob([text], { type: `${type};charset=utf-8` });
const url = URL.createObjectURL(blob);
const link = el("a", { href: url, download: name });
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
}
function exportRows(columns, rows, format, name) {
const cells = (row) => columns.map(([, read]) => String(read(row) ?? ""));
const heads = columns.map(([head]) => head);
if (format === "json") {
const out = rows.map((row) => {
const one = {};
columns.forEach(([head, read]) => {
one[head] = read(row);
});
return one;
});
offerDownload(`${name}.json`, JSON.stringify(out, null, 2), "application/json");
return;
}
if (format === "csv") {
const quote = (v) =>
v.includes('"') || v.includes(",") || v.includes("\n")
? `"${v.split('"').join('""')}"`
: v;
const lines = [heads.map(quote).join(","), ...rows.map((row) => cells(row).map(quote).join(","))];
offerDownload(`${name}.csv`, `${lines.join("\n")}\n`, "text/csv");
return;
}
const lines = [
`| ${heads.join(" | ")} |`,
`| ${heads.map(() => "---").join(" | ")} |`,
...rows.map((row) => `| ${cells(row).join(" | ")} |`),
];
offerDownload(`${name}.md`, `${lines.join("\n")}\n`, "text/markdown");
}
function ledgerSessions(data) {
const all = data.sessions || [];
let client = "";
let tier = "";
let price = MODEL_PRICES[0];
const table = dataTable({
className: "w-sessions",
sort: "last",
dir: "desc",
perPage: 12,
rows: [],
caption: "Every agent session this machine recorded, newest first.",
columns: [
{
key: "last",
label: "Last seen",
className: "meta",
value: (r) => r.last,
render: (r) => el("span", { class: "one-line", title: r.when, text: r.when }),
},
{
key: "session",
label: "Session",
className: "meta",
render: (r) =>
el("span", {
class: "one-line",
title: r.session || "recorded before sessions were",
text: r.session ? r.session.slice(0, 12) : "—",
}),
},
{ key: "client", label: "Agent", className: "meta", render: (r) => r.client },
{ key: "store", label: "Store", className: "meta narrow-drop", render: (r) => r.store },
{ key: "retrievals", label: "Reads", className: "num", value: (r) => r.retrievals, render: (r) => n(r.retrievals) },
{
key: "net_tokens",
label: "Net tokens",
className: "num",
value: (r) => r.net_tokens,
render: (r) => n(r.net_tokens),
},
{
key: "cost",
label: "Cost",
className: "num",
value: (r) => r.net_tokens,
render: (r) => `$${((r.net_tokens / 1e6) * price[1]).toFixed(2)}`,
},
{ key: "tier", label: "Tier", className: "meta", render: (r) => r.tier },
],
});
function shown() {
return all.filter(
(row) => (!client || row.client === client) && (!tier || row.tier === tier),
);
}
function repaint() {
const rows = shown();
table.update(rows, rows.length);
count.textContent = `${rows.length} of ${all.length} session${all.length === 1 ? "" : "s"}`;
}
const count = el("span", { class: "muted" });
const clients = [...new Set(all.map((row) => row.client))].sort();
const clientPick = el(
"select",
{
class: "chip",
"aria-label": "Filter by client",
onchange: (e) => {
client = e.currentTarget.value;
repaint();
},
},
el("option", { value: "", text: "every client" }),
clients.map((one) => el("option", { value: one, text: one })),
);
const tierPick = el(
"select",
{
class: "chip",
"aria-label": "Filter by tier",
onchange: (e) => {
tier = e.currentTarget.value;
repaint();
},
},
el("option", { value: "", text: "every tier" }),
el("option", { value: "measured", text: "measured" }),
el("option", { value: "modelled", text: "modelled" }),
);
const modelPick = el(
"select",
{
class: "chip",
"aria-label": "Cost at",
onchange: (e) => {
price = MODEL_PRICES[Number(e.currentTarget.value)] || MODEL_PRICES[0];
repaint();
},
},
MODEL_PRICES.map(([name], i) =>
el("option", { value: String(i), text: `cost at ${name}` }),
),
);
const columns = () => [
["when", (r) => r.when],
["session", (r) => r.session],
["agent", (r) => r.client],
["store", (r) => r.store],
["reads", (r) => r.retrievals],
["zero_hit", (r) => r.zero_hit],
["net_tokens", (r) => r.net_tokens],
[`cost_usd_at_${price[2]}`, (r) => ((r.net_tokens / 1e6) * price[1]).toFixed(2)],
["tier", (r) => r.tier],
];
const exports = ["Markdown", "CSV", "JSON"].map((label) =>
el("button", {
class: "button secondary small",
type: "button",
text: label,
onclick: () =>
exportRows(
columns(),
shown(),
label === "Markdown" ? "md" : label.toLowerCase(),
"semlith-sessions",
),
}),
);
repaint();
return el(
"div",
{ class: "card pad" },
el("span", { class: "card-title", text: "Sessions" }),
el("div", { class: "filters" }, clientPick, tierPick, modelPick, count, el("span", { class: "spacer" }), exports),
all.length
? table.node
: empty("No session has recorded a retrieval yet."),
el("p", {
class: "subtitle",
text: "Net tokens are whole-file less excerpt, over the reads that found something. A session counted by the four-character fallback is modelled, not measured, and says so in its own row.",
}),
);
}
function ledgerRows(data) {
const rows = data.rows || [];
const table = dataTable({
className: "w-ledger",
sort: "at",
dir: "desc",
perPage: 15,
rows,
caption: "Every retrieval recorded on this machine, newest first.",
columns: [
{
key: "at",
label: "When",
className: "meta",
value: (r) => r.at,
render: (r) => el("span", { class: "one-line", title: r.when, text: r.when }),
},
{ key: "client", label: "Client", className: "meta", render: (r) => r.client },
{ key: "store", label: "Store", className: "meta narrow-drop", render: (r) => r.store },
{
key: "query",
label: "Query",
className: "path",
render: (r) => el("span", { class: "one-line", title: r.query, text: r.query }),
},
{ key: "hits", label: "Hits", className: "num", render: (r) => n(r.hits) },
{
key: "ms",
label: "ms",
className: "num narrow-drop",
render: (r) => n(r.ms),
},
],
});
table.update(rows, rows.length);
return el(
"div",
{ class: "card pad" },
el("span", { class: "card-title", text: "The rows" }),
rows.length
? table.node
: empty("Nothing recorded yet. A search from any client writes a row here."),
data.legacy_rows
? el("p", {
class: "subtitle",
text: "Some rows here are older than the query id, and were written once per open store — figures that include them may count one search several times. They are left as they are: the chain is never rewritten.",
})
: null,
);
}
function clientBreakdown(byClient) {
const entries = Object.entries(byClient || {}).sort((a, b) => b[1] - a[1]);
if (!entries.length) return null;
return el(
"div",
{ class: "client-breakdown" },
el("span", { class: "eyebrow", text: "by client" }),
entries.map(([client, count], i) =>
el(
"span",
{ class: "client" },
i ? el("span", { class: "sep", text: "·" }) : null,
el("span", { class: "who", text: client }),
el("span", { class: "count", text: n(count) }),
),
),
el("span", {
class: "client-note",
text: "— MCP, HTTP and CLI, under the client's own name",
}),
);
}
function liveStores() {
return state.stores.filter((store) => {
if (store.missing || store.unopened) return false;
const roots = store.roots || [];
return !roots.length || roots.some((root) => root.present);
});
}
async function refreshStores() {
try {
const data = await api("/api/stores");
state.stores = data.stores || [];
paintStoreCount();
} catch (_) {
}
return state.stores;
}
function agentsCard() {
const card = el("div", { class: "card pad dense" });
const body = el("div", { class: "rows" });
fill(
card,
el("span", { class: "card-title", text: "Agents connected" }),
body,
el("span", { class: "spacer" }),
el("button", {
class: "button ghost small",
type: "button",
text: "Copy config for another client",
onclick: () => go("agents"),
}),
);
fill(body, el("div", { class: "rail-hint", text: "Asking the daemon…" }));
api("/api/agents")
.then((data) => {
noteAgents(data);
watchLive(["clients"], repaintView);
const live = data.connections || [];
if (!live.length) {
fill(
body,
el("div", {
class: "rail-hint",
text: "No client is talking to this daemon right now.",
}),
);
return;
}
fill(
body,
live.map((client) =>
el(
"div",
{ class: "kv" },
el("span", { text: client.name }),
el("span", { class: "spacer" }),
el("span", {
class: "meta",
text: `${client.transport} · ${n(client.queries)} quer${
client.queries === 1 ? "y" : "ies"
}`,
}),
),
),
);
})
.catch((e) => fill(body, error(e.message)));
return card;
}
function ask({ title, body, confirm, tone, run }) {
const dialog = el("dialog", { class: "modal" });
const problem = el("div", { class: "note" });
const go = el("button", {
class: tone === "bad" ? "button danger" : "button",
type: "button",
text: confirm,
onclick: async () => {
go.disabled = true;
const was = go.textContent;
go.textContent = "Working…";
problem.className = "note";
problem.textContent = "";
try {
await run();
dialog.close();
} catch (e) {
problem.className = "note bad";
problem.textContent = e.message;
go.disabled = false;
go.textContent = was;
}
},
});
fill(
dialog,
el("h2", { class: "card-title", text: title }),
el("p", { class: "subtitle", text: body }),
problem,
el(
"div",
{ class: "actions" },
el("span", { class: "spacer" }),
el("button", {
class: "button secondary",
type: "button",
text: "Cancel",
onclick: () => dialog.close(),
}),
go,
),
);
dialog.addEventListener("close", () => dialog.remove());
dialog.addEventListener("click", (e) => {
if (e.target === dialog) dialog.close();
});
document.body.append(dialog);
dialog.showModal();
return dialog;
}
function confirmDelete(name) {
ask({
title: `Delete ${name}?`,
body: "Its vectors, chunks, graph and ledger are deleted, and the registry stops listing it. The files it indexed are untouched.",
confirm: `Delete ${name}`,
tone: "bad",
run: async () => {
const done = await post("/api/store/delete", { store: name });
await refreshStores();
await render();
note(done.message);
},
});
}
function note(text, bad) {
const holder = document.querySelector(".view > .note.page-note");
if (!holder) return;
holder.className = bad ? "note page-note bad" : "note page-note";
holder.textContent = text;
}
async function storesView() {
const stores = await refreshStores();
watchLive(["stores", "events"], repaintView);
const rootNote = el("div", { class: "note" });
const rootPicker = folderPicker({
choose: "Point the store here",
onError: (message) => {
rootNote.className = "note bad";
rootNote.textContent = message;
},
onChoose: async (path) => {
rootNote.className = "note";
rootNote.textContent = "Re-pointing…";
try {
const done = await post("/api/root", { store: rootPicker.store, root: path });
rootNote.textContent = done.message;
await refreshStores();
} catch (e) {
rootNote.className = "note bad";
rootNote.textContent = e.message;
}
},
});
function repoint(store, missing) {
rootPicker.store = store;
rootNote.className = "note";
rootNote.textContent = `Choose where ${missing.split("/").pop() || missing} lives now.`;
rootPicker.open("");
}
const adoptNote = el("div", { class: "note" });
const picker = folderPicker({
choose: "Adopt this directory",
onError: (message) => {
adoptNote.className = "note bad";
adoptNote.textContent = message;
},
onChoose: async (path) => {
adoptNote.className = "note";
adoptNote.textContent = "Adopting…";
try {
const done = await post("/api/adopt", { path });
adoptNote.textContent =
done.message || `${path} is registered. It joins on the next daemon start.`;
await refreshStores();
} catch (e) {
adoptNote.className = "note bad";
adoptNote.textContent = e.message;
picker.open(path);
}
},
});
const head = pageHead("Stores", "Everything indexed on this machine. Nothing leaves it.", {
actions: [
el(
"button",
{ class: "button secondary", type: "button", onclick: () => picker.open("") },
icon(ICONS.folder),
"Adopt existing .semlith",
),
el(
"button",
{ class: "button", type: "button", onclick: () => go("index") },
icon(ICONS.plus),
"Index a folder",
),
],
});
if (!stores.length) {
return el(
"div",
{ class: "view" },
head,
picker.node,
adoptNote,
empty("No store is open. Index a folder and it appears here."),
);
}
const totals = stores.reduce(
(sum, s) => ({
files: sum.files + s.files,
chunks: sum.chunks + s.chunks,
bytes: sum.bytes + s.bytes,
lines: sum.lines + (s.lines || 0),
watching: sum.watching + (s.watching ? 1 : 0),
formats: Math.max(sum.formats, s.formats || 0),
readers: Math.max(sum.readers, s.readers || 0),
}),
{ files: 0, chunks: 0, bytes: 0, lines: 0, watching: 0, formats: 0, readers: 0 },
);
const dim = stores.find((s) => s.dim)?.dim;
const table = dataTable({
className: "w-stores",
caption: "Every store on this machine: where it is, what it holds, and when it was last written to.",
sort: "name",
perPage: 10,
rows: stores,
columns: [
{
key: "name",
label: "Store",
value: (s) => s.name,
render: (s) =>
el(
"div",
{ class: "rows tight" },
lineCell(s.name, "name"),
pathCell(s.dir, "meta"),
s.missing
? el(
"div",
{ class: "chips" },
pill("missing", "bad", { title: `nothing at ${s.dir}` }),
el("span", {
class: "meta",
text: `nothing at ${s.dir} — delete the entry, or put the store back`,
}),
)
: null,
!s.missing && (s.roots || []).length && !(s.roots || []).some((r) => r.present)
? el(
"div",
{ class: "chips" },
pill("root missing", "bad", {
title: (s.roots || []).map((r) => r.path).join(", "),
}),
el("span", {
class: "meta",
text: `nothing at ${(s.roots || []).map((r) => r.path).join(", ")} — re-point it, or delete the store`,
}),
)
: null,
s.pruned
? el(
"div",
{ class: "chips" },
pill(`${n(s.pruned)} reconciled`, "warn"),
el("span", {
class: "meta",
text: "files it held from outside its roots, dropped when this daemon opened it. The files on disk are untouched.",
}),
)
: null,
el("span", {
class: "meta only-narrow",
text: (s.roots || []).map((r) => r.path).join(", ") || "no roots",
}),
lineCell(`${s.model} · ${s.dim} dims`, "meta"),
s.trusted === false
? el(
"div",
{ class: "chips" },
pill("not trusted", "warn"),
el("button", {
class: "button secondary small",
type: "button",
text: "Trust this store",
onclick: async (e) => {
const button = e.currentTarget;
button.disabled = true;
adoptNote.className = "note";
adoptNote.textContent = "Trusting…";
try {
await post("/api/trust", { path: s.dir });
adoptNote.textContent = `${s.dir} is trusted. semlith opens it from its own directory now, without --store.`;
await refreshStores();
} catch (err) {
adoptNote.className = "note bad";
adoptNote.textContent = err.message;
button.disabled = false;
}
},
}),
)
: null,
s.loose_mode
? el(
"div",
{ class: "chips" },
pill(`mode ${s.loose_mode}`, "warn"),
el("span", {
class: "meta",
text: "other users on this machine can read what this store indexed; the next open narrows it to 700",
}),
)
: null,
),
},
{
key: "roots",
label: "Roots",
className: "meta narrow-drop",
sortable: false,
render: (s) =>
(s.roots || []).length
? s.roots.map((r) =>
r.present
? pathCell(r.path)
: el(
"div",
{ class: "gone-row" },
pathCell(r.path, "gone"),
el("button", {
class: "button danger small",
type: "button",
text: "Re-point",
onclick: () => repoint(s.name, r.path),
}),
),
)
: "—",
},
{ key: "files", label: "Files", className: "num", value: (s) => s.files, render: (s) => n(s.files) },
{
key: "chunks",
label: "Chunks",
className: "num narrow-drop",
value: (s) => s.chunks,
render: (s) => n(s.chunks),
},
{
key: "saved",
label: "Saved",
className: "num narrow-drop",
value: (s) => (s.savings ? s.savings.net_tokens : -1),
render: (s) => {
if (!s.savings || !s.savings.total) {
return el("span", { class: "meta", text: "—" });
}
return el(
"span",
{
class: "meta",
title: `${n(s.savings.net_tokens)} tokens saved over ${n(s.savings.credited)} of ${n(s.savings.total)} retrievals, counted ${s.savings.tier}`,
},
`${n(s.savings.net_tokens)} · ${s.savings.coverage}% · ${s.savings.tier}`,
);
},
},
{
key: "last_write",
label: "Last write",
value: (s) => s.last_write || 0,
render: (s) => {
return s.last_write
? pill(when(s.last_write), "good")
: el("span", { class: "meta", text: "—" });
},
},
{
key: "open",
label: "",
className: "row-end",
sortable: false,
render: (s) =>
rowMenu(() => [
{
label: "Open in Files",
onclick: () => {
state.pendingStore = s.name;
go("files");
},
},
{
label: "Delete store…",
tone: "bad",
onclick: () => confirmDelete(s.name),
},
]),
},
],
});
const feed = stores
.flatMap((s) => (s.events || []).map((e) => ({ at: e.at, text: e.text })))
.sort((a, b) => b.at - a.at)
.slice(0, 40);
return el(
"div",
{ class: "view" },
head,
picker.node,
adoptNote,
rootPicker.node,
rootNote,
el("div", { class: "note page-note" }),
el(
"div",
{ class: "strip" },
stat("Stores", n(stores.length), `${totals.watching} being watched`),
stat("Files", n(totals.files), `${n(totals.formats)} formats`),
stat("Chunks", n(totals.chunks), dim ? `${dim}-dimension vectors` : "embedded and searchable"),
stat(
"Lines",
n(totals.lines),
`across ${n(totals.files)} files, read by ${n(totals.readers)} readers`,
),
stat("On disk", bytes(totals.bytes), "int8 quantised"),
),
el(
"div",
{ class: "scroller" },
table.node,
el(
"div",
{ class: "grid fill" },
el(
"div",
{ class: "card pad dense" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "Watcher" }),
el("span", { class: "meta", text: "live" }),
),
feed.length
? el(
"div",
{ class: "feed" },
feed.map((e) =>
el(
"div",
{ class: "row" },
el("span", {
class: "at",
text: clock(e.at),
title: clock(e.at),
}),
lineCell(e.text, "what"),
),
),
)
: empty("Nothing has changed on disk since the daemon started."),
),
agentsCard(),
),
),
);
}
const FILES_PER_REQUEST = 500;
const FILES_SELECTABLE = 10_000;
async function filesView() {
const chosenExt = new Set();
const summary = el("span", { class: "pill" });
const holder = el("div", {});
const announcer = el("div", { class: "sr-only", role: "status", "aria-live": "polite" });
let justForgot = false;
const storeFilter = storePicker(state.pendingStore || "", () => load(true));
state.pendingStore = "";
const footnote = el("p", {
class: "subtitle",
text: "Forget drops the file's chunks and vectors. The file on disk is untouched.",
});
const pathInput = el("input", {
type: "text",
placeholder: "path glob, e.g. src/**",
oninput: () => {
clearTimeout(load.timer);
load.timer = setTimeout(() => load(true), 200);
},
});
let generation = 0;
async function forget(path, row, store) {
try {
await post("/api/forget", { path, store });
row.remove();
picked.delete(path);
paintBulk();
justForgot = true;
announcer.textContent = `${path} forgotten. Its chunks and vectors are gone from ${store}; the file on disk is untouched.`;
load();
} catch (e) {
bulkNote.className = "note bad";
bulkNote.textContent = e.message;
throw e;
}
}
const picked = new Map();
const bulkNote = el("div", { class: "note", role: "status", "aria-live": "polite" });
const bulkBar = el("div", { class: "bulk", hidden: true });
function paintBulk() {
bulkBar.hidden = picked.size === 0;
if (!picked.size) return;
const many = picked.size;
fill(
bulkBar,
el("span", {
class: "meta",
text: `${n(many)} file${many === 1 ? "" : "s"} selected`,
}),
matches > many
? matches <= FILES_SELECTABLE
? el("button", {
class: "button ghost small",
type: "button",
text: `Select all ${n(matches)} matches`,
onclick: () => selectEveryMatch(),
})
: el("span", {
class: "meta",
text: `${n(matches)} files match — narrow the filter to ${n(
FILES_SELECTABLE,
)} or fewer to select them all`,
})
: null,
el("span", { class: "spacer" }),
el("button", {
class: "button ghost small",
type: "button",
text: "Clear",
onclick: () => {
picked.clear();
for (const box of table.node.querySelectorAll("input.pick")) box.checked = false;
paintBulk();
},
}),
el("button", {
class: "button danger small",
type: "button",
text: `Forget ${n(many)} file${many === 1 ? "" : "s"}`,
onclick: (e) => {
const button = e.currentTarget;
const stores = [...new Set(picked.values())];
ask({
title: `Forget ${n(many)} file${many === 1 ? "" : "s"}?`,
body: `Their chunks and vectors are dropped from ${stores.join(
" and ",
)}. The files on disk are untouched, and indexing the folder again brings them back.`,
confirm: `Forget ${n(many)} file${many === 1 ? "" : "s"}`,
tone: "bad",
run: () => forgetPicked(button),
});
},
}),
);
}
async function forgetPicked(button) {
button.disabled = true;
button.textContent = "Forgetting…";
bulkNote.className = "note";
bulkNote.textContent = "";
const byStore = new Map();
for (const [path, store] of picked) {
if (!byStore.has(store)) byStore.set(store, []);
byStore.get(store).push(path);
}
try {
let files = 0;
let forgot = 0;
let absent = 0;
for (const [store, paths] of byStore) {
const done = await post("/api/forget", { paths, store });
files += done.files === undefined ? 1 : done.files;
forgot += done.forgot || 0;
absent += (done.not_indexed || []).length;
}
picked.clear();
justForgot = true;
paintBulk();
bulkNote.textContent = absent
? `${n(files)} file${files === 1 ? "" : "s"} forgotten, ${n(forgot)} chunk${
forgot === 1 ? "" : "s"
} removed. ${n(absent)} were not indexed and were left alone.`
: `${n(files)} file${files === 1 ? "" : "s"} forgotten, ${n(forgot)} chunk${
forgot === 1 ? "" : "s"
} removed.`;
load();
} catch (e) {
bulkNote.className = "note bad";
bulkNote.textContent = e.message;
button.disabled = false;
paintBulk();
throw e;
}
}
const table = dataTable({
className: "w-files",
caption: "Every indexed file, the reader that parsed it, and what it contributed to the store.",
server: true,
sort: "path",
columns: [
{
key: "pick",
label: "",
className: "pick",
sortable: false,
head: () => {
const all = el("input", {
type: "checkbox",
class: "pick all",
"aria-label": "Select every file on this page",
onchange: () => {
for (const box of table.node.querySelectorAll("tbody input.pick")) {
if (box.checked !== all.checked) box.click();
}
},
});
return all;
},
render: (f) => {
const box = el("input", {
type: "checkbox",
class: "pick",
"aria-label": `Select ${f.path}`,
onchange: () => {
if (box.checked) picked.set(f.path, f.store);
else picked.delete(f.path);
paintBulk();
},
});
box.checked = picked.has(f.path);
return box;
},
},
{
key: "path",
label: "Path",
className: "path",
render: (f) => pathCell(f.path),
},
{
key: "store",
label: "Store",
className: "meta narrow-drop",
render: (f) => f.store,
},
{
key: "reader",
label: "Read as",
className: "narrow-drop",
render: (f) => el("span", { class: "tag", text: f.reader }),
},
{
key: "lang",
label: "Language",
className: "meta narrow-drop",
render: (f) => f.lang || "—",
},
{
key: "lines",
label: "Lines",
className: "num",
render: (f) => (f.reader === "image" ? "—" : n(f.lines)),
},
{ key: "chunks", label: "Chunks", className: "num narrow-drop", render: (f) => n(f.chunks) },
{
key: "indexed",
label: "Indexed",
className: "meta",
render: (f) => when(f.indexed_at),
},
{
key: "forget",
label: "",
sortable: false,
render: (f) => {
const button = el("button", {
class: "forget",
type: "button",
text: "Forget",
onclick: () => {
const row = button.closest("tr");
ask({
title: "Forget this file?",
body: `${f.path} — its chunks and vectors are dropped from ${f.store}. The file on disk is untouched, and indexing the folder again brings it back.`,
confirm: "Forget it",
tone: "bad",
run: async () => {
button.disabled = true;
button.textContent = "Forgetting…";
await forget(f.path, row, f.store);
},
});
},
});
return button;
},
},
],
rows: [],
total: 0,
onChange: () => load(),
});
async function load(reset) {
const mine = ++generation;
const view = table.query();
if (reset) view.page = 1;
const params = new URLSearchParams();
if (pathInput.value.trim()) params.set("path", pathInput.value.trim());
for (const ext of chosenExt) params.append("ext", ext);
for (const store of storeFilter.stores()) params.append("store", store);
params.set("sort", view.sort || "path");
params.set("dir", view.dir);
params.set("limit", String(view.perPage));
params.set("offset", String((view.page - 1) * view.perPage));
let data;
try {
data = await api(`/api/files?${params}`);
} catch (e) {
if (mine !== generation) return;
fill(holder, el("div", { class: "card pad" }, error(e.message)));
summary.textContent = "";
return;
}
if (mine !== generation) return;
summary.textContent = `${n(data.total)} files · ${n(data.formats)} format${
data.formats === 1 ? "" : "s"
} · ${n(data.stores)} store${data.stores === 1 ? "" : "s"}`;
if (!data.total) {
const because = justForgot
? "Nothing left under this filter — the file that matched it has been forgotten."
: "Nothing indexed matches that. The filter, not the corpus — clear it and look again.";
fill(holder, el("div", { class: "card pad" }, empty(because)));
footnote.hidden = true;
justForgot = false;
return;
}
footnote.hidden = false;
justForgot = false;
table.update(data.files, data.total);
if (!holder.contains(table.node)) fill(holder, table.node);
matches = data.total;
}
let matches = 0;
async function selectEveryMatch() {
bulkNote.className = "note";
bulkNote.textContent = `Selecting ${n(matches)} files…`;
for (let offset = 0; offset < matches && offset < FILES_SELECTABLE; offset += FILES_PER_REQUEST) {
const params = new URLSearchParams();
if (pathInput.value.trim()) params.set("path", pathInput.value.trim());
for (const ext of chosenExt) params.append("ext", ext);
for (const store of storeFilter.stores()) params.append("store", store);
params.set("limit", String(FILES_PER_REQUEST));
params.set("offset", String(offset));
let data;
try {
data = await api(`/api/files?${params}`);
} catch (e) {
bulkNote.className = "note bad";
bulkNote.textContent = e.message;
return;
}
for (const file of data.files || []) picked.set(file.path, file.store);
if (!(data.files || []).length) break;
}
bulkNote.textContent = "";
for (const box of table.node.querySelectorAll("tbody input.pick")) box.checked = true;
paintBulk();
}
const EXTENSIONS = ["rs", "md", "py", "ts", "js", "go", "pdf", "docx", "epub", "png", "jpg"];
const extChips = EXTENSIONS.map((ext) =>
el("button", {
class: "chip",
type: "button",
"aria-pressed": "false",
text: `.${ext}`,
onclick: (e) => {
const on = e.currentTarget.getAttribute("aria-pressed") !== "true";
e.currentTarget.setAttribute("aria-pressed", String(on));
if (on) chosenExt.add(ext);
else chosenExt.delete(ext);
clearTimeout(load.timer);
load(true);
},
}),
);
load();
return el(
"div",
{ class: "view" },
pageHead(
"Files",
"What is indexed, and which reader parsed it — so “not indexed” and “not discussed” stop looking the same.",
{ pill: summary },
),
el(
"div",
{ class: "filters" },
el(
"div",
{ class: "field wide" },
icon(ICONS.search, 15),
labelled("files-filter", "Filter files by path glob", pathInput),
),
extChips,
),
storeFilter.node,
bulkBar,
bulkNote,
holder,
footnote,
announcer,
);
}
function storePicker(initial, onChange) {
let picked = initial || "";
const row = el("div", { class: "store-chips" });
const paint = () => {
for (const chip of row.children) {
chip.setAttribute("aria-pressed", String(chip.getAttribute("data-store") === picked));
}
};
const add = (name, label) =>
row.append(
el("button", {
class: "chip",
type: "button",
"data-store": name,
text: label,
onclick: () => {
picked = name;
paint();
onChange();
},
}),
);
add("", "All stores");
for (const store of liveStores()) add(store.name, store.name);
paint();
return { node: row, stores: () => (picked ? [picked] : []) };
}
function freshMark(fresh) {
const ok = fresh !== false;
return el(
"span",
{
class: "fresh",
title: ok ? "indexed from the bytes on disk" : "file changed since it was indexed",
},
el("span", { class: `fresh-dot ${ok ? "is-fresh" : "is-stale"}` }),
el("span", { class: "word", text: ok ? "fresh" : "stale" }),
);
}
function codeGutter(text, startLine) {
const from = Number(startLine) || 1;
return el(
"div",
{ class: "gutter-code" },
String(text == null ? "" : text)
.split("\n")
.map((line, i) =>
el(
"div",
{ class: "code-line" },
el("span", { class: "ln", text: String(from + i) }),
el("span", { class: "lt", text: line }),
),
),
);
}
function spanCard(span) {
const named = span.symbol ? `${span.symbol_kind || ""} ${span.symbol}`.trim() : "";
return el(
"div",
{ class: "span-card" },
el(
"div",
{ class: "span-head" },
el("span", { class: "path", text: span.path }),
el("span", { class: "lines", text: `${span.start_line}-${span.end_line}` }),
span.store ? el("span", { class: "from", text: span.store }) : null,
el("span", { class: "spacer" }),
freshMark(span.fresh),
),
named ? el("div", { class: "sig", text: named }) : null,
codeGutter(span.text, span.start_line),
);
}
const LIST_LABELS = {
definition: "definition — this chunk defines the name that was typed",
vector: "vector — the embedding matched",
keyword: "full text — the terms matched",
graph: "graph — reached from a neighbouring symbol",
image: "image — the picture matched the words",
};
function fusionBadges(lists) {
if (!lists || !lists.length) return null;
return el(
"span",
{ class: "badges" },
lists.map((list) =>
el("span", { class: `badge ${list}`, title: LIST_LABELS[list] || list, text: list }),
),
);
}
const CONFIDENCE = {
extracted: ["extracted", "the import names the target"],
resolved: ["resolved", "name and module hint agree on one definition"],
inferred: ["inferred", "matched by bare name"],
ambiguous: ["ambiguous", "several definitions, none chosen"],
};
function confidenceBadge(value) {
if (!value) return null;
const [label, why] = CONFIDENCE[value] || [value, ""];
return el("span", { class: `conf ${value}`, title: why, text: label });
}
const PROVENANCE_NOTE = {
extracted: "Found through the graph: the source names the target, so nothing had to be ranked.",
resolved: "Found through the graph: the name and its module hint agree on one definition.",
inferred: "Found through the graph by bare name. Corroborate before you rely on it.",
ambiguous:
"Found through the graph, but several definitions carry this name and none was chosen.",
};
function bestLine(text, query) {
const terms = (query || "")
.split(/[^A-Za-z0-9_]+/)
.filter((t) => t.length > 2)
.map((t) => t.toLowerCase());
let best = null;
let bestScore = -1;
for (const raw of (text || "").split("\n")) {
const line = raw.trim();
if (!line) continue;
const lowered = line.toLowerCase();
const score = terms.filter((t) => lowered.includes(t)).length;
if (score > bestScore) {
bestScore = score;
best = line;
}
}
if (!best) return "";
return best.length > 120 ? `${best.slice(0, 120)}…` : best;
}
function rowCost(hit, query) {
const named = hit.symbol ? `${hit.symbol_kind || ""} ${hit.symbol}`.trim() : "";
const marks = (hit.lists || []).join("+") + (hit.fresh === false ? " stale" : "");
const row = ` ${hit.start_line}-${hit.end_line} ${named} ${marks}\n ${bestLine(
hit.text,
query,
)}\n`;
return Math.ceil(row.length / 4);
}
function symbolIn(hit) {
const match = hit.text.match(
/(?:fn|function|def|func|class|struct|interface|type)\s+([A-Za-z_][A-Za-z0-9_]*)/,
);
return match ? match[1] : "";
}
async function searchView() {
await refreshStores();
const results = el("div", { class: "results locate-list" });
const bodyCard = el("div", { class: "span-card" });
const aroundName = el("span", { class: "sym" });
const footer = el("div", { class: "locate-footer" });
const meta = el("span", { class: "search-meta" });
const shapeHint = el("div", { class: "shape-hint", hidden: true });
const ego = graphCanvas({
onHover: (node, x, y) => {
if (!node) return tip.hide("ego");
tip.atPoint(
x,
y,
[
el("div", { class: "tip-head" }, el("i", {}), node.name),
node.kind ? egoRow("kind", node.kind) : null,
node.path
? egoRow("file", `${shortPath(node.path)}:${node.start_line}-${node.end_line}`)
: null,
].filter(Boolean),
"ego",
);
},
onPick: (node) => {
aroundName.textContent = node ? node.name : "—";
},
});
const stage = el(
"div",
{ class: "ego-panel", hidden: true },
ego.node,
el("span", { class: "ego-hint", text: "drag a node · click to select" }),
);
const aroundHead = el(
"div",
{ class: "around-head", hidden: true },
el("span", { class: "title" }, "Around ", aroundName),
el("span", { class: "meta", text: "graph expansion" }),
);
const showRing = (on) => {
stage.hidden = !on;
aroundHead.hidden = !on;
};
let k = 8;
const kValue = el("span", { class: "dial-value", text: "k = 8" });
const bump = (delta) => {
const next = Math.min(50, Math.max(1, k + delta));
if (next === k) return;
k = next;
kValue.textContent = `k = ${k}`;
run();
};
const kDial = el(
"div",
{ class: "dial" },
kValue,
el("button", { class: "step", type: "button", "aria-label": "fewer hits", title: "fewer hits", text: "–", onclick: () => bump(-1) }),
el("button", { class: "step", type: "button", "aria-label": "more hits", title: "more hits", text: "+", onclick: () => bump(1) }),
);
let prefer = "any";
const preferPills = ["code", "docs", "any"].map((name) =>
el("button", {
class: "seg",
type: "button",
"aria-pressed": String(name === prefer),
text: name,
onclick: () => {
if (prefer === name) return;
prefer = name;
for (const pill of preferPills) {
pill.setAttribute("aria-pressed", String(pill.textContent === prefer));
}
run();
},
}),
);
const preferDial = el(
"div",
{ class: "dial" },
el("span", { class: "dial-label", text: "Prefer" }),
el("span", { class: "segs" }, preferPills),
);
let view = "results";
const viewPills = ["results", "brief"].map((name) =>
el("button", {
class: "seg",
type: "button",
"aria-pressed": String(name === view),
text: name,
onclick: () => {
if (view === name) return;
view = name;
for (const pill of viewPills) {
pill.setAttribute("aria-pressed", String(pill.textContent === view));
}
run();
},
}),
);
const viewDial = el(
"div",
{ class: "dial" },
el("span", { class: "dial-label", text: "View" }),
el("span", { class: "segs" }, viewPills),
);
let filterTimer = 0;
const rerun = () => {
clearTimeout(filterTimer);
filterTimer = setTimeout(() => run(), 250);
};
const langField = el("input", {
class: "bare",
size: "8",
placeholder: "any",
oninput: rerun,
onchange: () => run(),
});
const pathField = el("input", {
class: "bare",
size: "8",
placeholder: "any",
oninput: rerun,
onchange: () => run(),
});
const langDial = el(
"div",
{ class: "dial" },
el("span", { class: "dial-prefix", text: "lang:" }),
labelled("search-lang", "Only this language", langField),
);
const pathDial = el(
"div",
{ class: "dial" },
el("span", { class: "dial-prefix", text: "path:" }),
labelled("search-path", "Only paths matching this glob", pathField),
);
const budgetField = el("input", {
class: "bare",
size: "5",
value: "1500",
inputmode: "numeric",
oninput: rerun,
onchange: () => run(),
});
const budgetDial = el(
"div",
{ class: "dial" },
el("span", { class: "dial-label", text: "Budget" }),
labelled("search-budget", "Budget in tokens", budgetField),
el("span", { class: "unit", text: "tok" }),
);
const budget = () => Math.max(200, Number(String(budgetField.value).replace(/[^0-9]/g, "")) || 1500);
const picker = storePicker(state.search.stores[0] || "", () => run());
let generation = 0;
const input = el("input", {
type: "search",
placeholder: "Ask the index a question",
oninput: () => {
if (!input.value.trim()) shapeHint.hidden = true;
},
onkeydown: (e) => {
if (e.key === "Enter") run();
},
});
const bodyCol = el("div", { class: "body-col" }, bodyCard, aroundHead, stage);
const twoStage = el(
"div",
{ class: "two-stage" },
el("div", { class: "locate-col" }, results, footer),
bodyCol,
);
const nothing = () =>
empty("Type a phrase or an identifier. Both halves of the search run either way.");
async function run() {
const query = input.value.trim();
state.search = { query, stores: picker.stores() };
if (!query) {
shapeHint.hidden = true;
fill(results, nothing());
showBody(null, "");
fill(footer);
meta.textContent = "";
return;
}
twoStage.classList.remove("one-stage");
if (view === "brief") {
twoStage.classList.add("one-stage");
return runBrief(query);
}
const mine = ++generation;
const params = new URLSearchParams({ query, k: String(k), prefer });
for (const store of picker.stores()) params.append("store", store);
const lang = langField.value.trim();
const path = pathField.value.trim();
if (lang) params.append("lang", lang);
if (path) params.append("path", path);
meta.textContent = "searching…";
const slow = setTimeout(() => {
if (mine === generation) {
meta.textContent = "loading the embedding model — the first search after the daemon starts pays for it once";
}
}, 1200);
let data;
try {
data = await api(`/api/search?${params}`);
} catch (e) {
clearTimeout(slow);
if (mine !== generation) return;
fill(results, error(e.message));
showBody(null, "");
fill(footer);
meta.textContent = "";
return;
}
clearTimeout(slow);
if (mine !== generation) return;
if (data.shape_label) {
fill(
shapeHint,
el("span", { class: "dot" }),
el("span", { text: `${data.shape_label} · ${data.weighting}` }),
);
shapeHint.hidden = false;
}
meta.textContent = data.hits.length
? `${data.hits.length} hit${data.hits.length === 1 ? "" : "s"} · ${(data.micros / 1000).toFixed(1)} ms`
: "";
if (!data.hits.length) {
showBody(null, "");
fill(footer);
fill(
results,
empty(
"No chunk in the selected stores matches that. If a store chip is on it is the filter, not the corpus — clear it and ask again.",
),
);
return;
}
const groups = [];
for (const hit of data.hits) {
const found = groups.find((g) => g.path === hit.path && g.store === hit.store);
if (found) found.hits.push(hit);
else groups.push({ path: hit.path, store: hit.store, hits: [hit] });
}
const cap = budget();
let spent = 0;
const shown = [];
for (const group of groups) {
const cost = group.hits.reduce((total, hit) => total + rowCost(hit, query), 4);
if (shown.length && spent + cost > cap) break;
spent += cost;
shown.push(group);
}
const kept = shown.reduce((total, group) => total + group.hits.length, 0);
fill(
results,
shown.map((group) => {
const spans = group.hits.map((hit) => `${hit.start_line}-${hit.end_line}`);
return el(
"div",
{ class: "locate-group" },
el(
"div",
{ class: "locate-file" },
el("span", { class: "file", "data-tip": group.path, text: shortPath(group.path) }),
group.store ? el("span", { class: "from", text: group.store }) : null,
el("span", { class: "spacer" }),
el("span", {
class: "span-summary",
text: spans.length > 1 ? `${spans.length} spans · ${spans.join(", ")}` : spans[0],
}),
),
group.hits.map((hit) => locateRow(hit, query)),
);
}),
);
fill(
footer,
el("span", {
text: `${kept} of ${data.hits.length} shown · ${n(spent)} tokens · budget ${n(cap)}`,
}),
kept < data.hits.length
? el("span", {
class: "truncated",
text: `truncated: ${kept} of ${data.hits.length}`,
})
: null,
);
const first = results.querySelector(".locate-row");
if (first) first.setAttribute("aria-pressed", "true");
showBody(shown[0].hits[0], query);
}
function locateRow(hit, query) {
const line = bestLine(hit.text, query);
const row = el(
"button",
{
class: "locate-row",
type: "button",
"aria-pressed": "false",
onclick: () => {
for (const other of results.querySelectorAll(".locate-row")) {
other.setAttribute("aria-pressed", "false");
}
row.setAttribute("aria-pressed", "true");
showBody(hit, query);
},
},
el(
"span",
{ class: "row-top" },
el("span", {
class: "lines",
text: hit.image
? `${hit.image.width}×${hit.image.height} px`
: `${hit.start_line}-${hit.end_line}`,
}),
hit.image
? null
: el("span", {
class: "sym",
text: hit.symbol ? `${hit.symbol_kind || ""} ${hit.symbol}`.trim() : "prose",
}),
fusionBadges(hit.lists),
el("span", { class: "spacer" }),
freshMark(hit.fresh),
confidenceBadge(hit.provenance),
),
line ? el("span", { class: "locate-excerpt", text: line }) : null,
);
return row;
}
let openHit = null;
let openQuery = "";
let wholeSpan = null;
let signature = "";
let whole = false;
let definitions = 0;
let bodyGeneration = 0;
async function runBrief(question) {
const mine = ++generation;
const params = new URLSearchParams({ question, budget: String(budget()), prefer });
for (const store of picker.stores()) params.append("store", store);
const lang = langField.value.trim();
const path = pathField.value.trim();
if (lang) params.append("lang", lang);
if (path) params.append("path", path);
showBody(null, "");
shapeHint.hidden = true;
meta.textContent = "assembling…";
const slow = setTimeout(() => {
if (mine === generation) {
meta.textContent =
"loading the embedding model — the first search after the daemon starts pays for it once";
}
}, 1200);
let data;
try {
data = await api(`/api/brief?${params}`);
} catch (e) {
clearTimeout(slow);
if (mine !== generation) return;
fill(results, error(e.message));
fill(footer);
meta.textContent = "";
return;
}
clearTimeout(slow);
if (mine !== generation) return;
const brief = data.brief || {};
const spans = brief.spans || [];
meta.textContent = spans.length ? `one call · ${(data.micros / 1000).toFixed(1)} ms` : "";
if (!spans.length) {
fill(footer);
fill(
results,
empty(
"No chunk in the selected stores matches that. If a store chip is on it is the filter, not the corpus — clear it and ask again.",
),
);
return;
}
const rows = [];
for (const span of spans) {
rows.push(
el(
"div",
{ class: "brief-span" },
el(
"div",
{ class: "brief-head" },
el("span", { class: "path", text: `${shortPath(span.path)}:${span.start_line}-${span.end_line}` }),
span.symbol ? el("span", { class: "sym", text: span.symbol }) : null,
el(
"span",
{ class: "brief-lists" },
...(span.lists || []).map((list) => el("span", { class: "tag", text: list })),
),
),
span.text
? el("pre", { class: "brief-text", text: span.text })
: el("div", { class: "brief-dropped", text: "text left out for the budget" }),
),
);
}
for (const symbol of brief.symbols || []) {
const edges = [];
for (const end of symbol.callers || []) {
edges.push(
el(
"div",
{ class: "brief-edge" },
el("span", { class: "k", text: "called by" }),
el("span", { class: "v", text: `${end.name} · ${shortPath(end.path)}:${end.start_line}` }),
),
);
}
for (const end of symbol.callees || []) {
edges.push(
el(
"div",
{ class: "brief-edge" },
el("span", { class: "k", text: "calls" }),
el("span", { class: "v", text: `${end.name} · ${shortPath(end.path)}:${end.start_line}` }),
),
);
}
if (symbol.hidden) {
edges.push(el("div", { class: "brief-dropped", text: `and ${symbol.hidden} more edges` }));
}
rows.push(
el(
"div",
{ class: "brief-symbol" },
el(
"div",
{ class: "brief-head" },
el("span", { class: "sym", text: symbol.name }),
el("span", { class: "brief-lists" }, el("span", { class: "tag", text: symbol.found_by })),
),
...edges,
),
);
}
fill(results, el("div", { class: "brief-view" }, ...rows));
const cut = brief.cut || {};
const dropped = [];
if (cut.spans) dropped.push(`${cut.spans} spans not located`);
if (cut.span_text) dropped.push(`${cut.span_text} left without text`);
if (cut.symbols) dropped.push(`${cut.symbols} symbols' edges`);
fill(
footer,
el("span", {
text:
`${brief.tokens} of ${brief.budget} tokens · counted with ${brief.counted_with}` +
(dropped.length ? ` · dropped ${dropped.join(", ")}` : ""),
}),
);
}
function showBody(hit, query) {
openHit = hit;
openQuery = query;
wholeSpan = null;
signature = "";
whole = false;
definitions = 0;
const mine = ++bodyGeneration;
paintBody();
if (!hit) {
ego.draw({ nodes: [], edges: [] }, null);
aroundName.textContent = "—";
return;
}
const target = hit.symbol || `${hit.path}:${hit.start_line}`;
const params = new URLSearchParams({ target });
if (hit.store) params.append("store", hit.store);
api(`/api/read?${params}`)
.then((data) => {
if (mine !== bodyGeneration) return;
if (!data.span) {
definitions = (data.definitions || []).length;
if (definitions) paintBody();
return;
}
wholeSpan = data.span;
if (hit.symbol) {
signature = (data.span.text || "")
.split("\n")
.map((l) => l.trim())
.find((l) => l.length > 0) || "";
}
paintBody();
})
.catch(() => {
});
drawAround(hit, mine);
}
function paintBody() {
const hit = openHit;
if (!hit) {
fill(
bodyCard,
empty("Pick a row. The span opens here, with what it is called and how it was found."),
);
showRing(false);
return;
}
const showing = whole && wholeSpan ? wholeSpan : hit;
const lines = `${showing.start_line}-${showing.end_line}`;
const wider =
wholeSpan &&
(wholeSpan.start_line < hit.start_line || wholeSpan.end_line > hit.end_line);
fill(
bodyCard,
el(
"div",
{ class: "span-head" },
el("span", { class: "path", "data-tip": hit.path, text: shortPath(hit.path) }),
el("span", { class: "lines", text: lines }),
el("span", { class: "spacer" }),
freshMark(hit.fresh),
confidenceBadge(hit.provenance),
),
hit.provenance && PROVENANCE_NOTE[hit.provenance]
? el("p", { class: "prov-note", text: PROVENANCE_NOTE[hit.provenance] })
: null,
hit.fresh === false
? el("p", {
class: "note",
text: "This file has changed since it was indexed. The lines below are what was read then.",
})
: null,
signature ? el("div", { class: "sig", text: signature }) : null,
hit.image ? imagePreview(hit.path) : codeGutter(showing.text, showing.start_line),
el(
"div",
{ class: "span-foot" },
el("button", {
class: "button secondary small",
type: "button",
disabled: !wider,
title: wider
? null
: definitions
? `${definitions} definitions carry this name, and which one this is was not settled. The Read page lists them.`
: "this span is already the whole of it",
text: whole
? "Collapse to the matched span"
: hit.symbol
? "Read whole symbol"
: "Read the whole section",
onclick: () => {
whole = !whole;
paintBody();
},
}),
el("span", {
class: "body-meta",
"data-tip": hit.path,
text: `semlith_read · ${shortPath(hit.path)}:${lines} · second stage`,
}),
el("span", { class: "spacer" }),
el("a", {
href: "#graph",
class: "quiet",
text: "Open in graph",
onclick: (e) => {
e.preventDefault();
state.pendingSymbol = hit.symbol || symbolIn(hit);
go("graph");
},
}),
),
);
}
async function drawAround(hit, mine) {
const name = hit.symbol || symbolIn(hit);
aroundName.textContent = name || "—";
if (!name) {
showRing(false);
return ego.draw({ nodes: [], edges: [] }, null);
}
showRing(true);
const params = new URLSearchParams({ name });
if (hit.store) params.append("store", hit.store);
let data;
try {
data = await api(`/api/symbol?${params}`);
} catch (_) {
return;
}
if (mine !== bodyGeneration) return;
ego.draw(egoGraph(name, data), null);
ego.pick(name);
}
fill(results, nothing());
paintBody();
setTimeout(() => {
ego.start();
if (state.pendingQuery) {
input.value = state.pendingQuery;
state.pendingQuery = "";
run();
} else if (state.search.query) {
input.value = state.search.query;
run();
}
input.focus();
}, 0);
return el(
"div",
{ class: "search-page" },
el(
"div",
{ class: "search-band" },
el("h1", { class: "sr-only", text: "Search" }),
el(
"div",
{ class: "search-field" },
icon(ICONS.search, 18),
labelled("search-query", "Search the index", input),
meta,
),
shapeHint,
el(
"div",
{ class: "filters" },
picker.node,
el("span", { class: "rule" }),
kDial,
preferDial,
viewDial,
langDial,
pathDial,
budgetDial,
el("span", { class: "dial-note", text: "what one answer may cost an agent" }),
),
),
twoStage,
);
}
function egoRow(key, value) {
return el(
"div",
{ class: "tip-row" },
el("span", { class: "k", text: key }),
el("span", { class: "v", text: String(value) }),
);
}
function egoGraph(name, data) {
const centre = (data.symbols || [])[0] || {};
const nodes = [
{
name,
kind: centre.kind || "symbol",
path: centre.path,
start_line: centre.start_line,
end_line: centre.end_line,
},
];
const index = new Map([[name, 0]]);
const edges = [];
const add = (label, kind, where) => {
if (!index.has(label)) {
index.set(label, nodes.length);
nodes.push({
name: label,
kind,
path: where && where.path,
start_line: where && where.start_line,
end_line: where && where.end_line,
});
}
return index.get(label);
};
const link = (from, to, kind, confidence) => {
if (from !== to) edges.push({ from, to, kind, confidence });
};
for (const end of data.callers || []) {
link(add(end.name, end.kind, end), 0, end.kind, end.confidence);
}
for (const end of data.callees || []) {
link(0, add(end.name, end.kind, end), end.kind, end.confidence);
}
for (const hop of data.ego || []) {
const via = index.get(hop.via);
if (via === undefined) continue;
const it = add(hop.name, hop.kind);
if (hop.direction === "caller") link(it, via, hop.kind, hop.confidence);
else link(via, it, hop.kind, hop.confidence);
}
return { nodes, edges };
}
function sized(property, share, attrs) {
const node = el("span", attrs || {});
node.style[property] = `${Math.max(0, Math.min(100, share * 100))}%`;
return node;
}
function healthPanel() {
const node = el(
"div",
{ class: "health" },
el("section", { class: "health-card" }, el("div", { class: "rail-hint", text: "Reading the graph…" })),
);
let rows = [];
async function refresh() {
try {
const data = await api("/api/stores?coverage=1");
rows = data.stores || [];
} catch {
rows = [];
}
paint();
}
function bar(counts) {
const total = counts.reduce((sum, [, n]) => sum + n, 0);
if (!total) return el("div", { class: "rail-hint", text: "No call edges yet." });
return el(
"div",
{ class: "support-bar", role: "img", "aria-label": counts.map(([k, n]) => `${k} ${n}`).join(", ") },
counts.map(([kind, n]) =>
n ? sized("width", n / total, { class: `seg ${kind}`, title: `${kind} ${n}` }) : null,
),
);
}
function paint() {
const counts = { extracted: 0, resolved: 0, ambiguous: 0, unresolved: 0 };
const languages = [];
let unresolvedNames = 0;
let several = 0;
const top = [];
const months = new Map();
for (const store of rows) {
for (const row of store.coverage || []) {
if (!row.definitions && !row.files) continue;
counts.extracted += row.extracted || 0;
counts.resolved += row.resolved || 0;
counts.ambiguous += row.ambiguous || 0;
counts.unresolved += row.unresolved || 0;
languages.push({ store: store.name, ...row });
}
const health = store.health;
if (!health) continue;
unresolvedNames += health.unresolved_names || 0;
several += health.several_definitions || 0;
for (const one of health.unresolved_top || []) top.push(one);
for (const one of health.months || []) {
months.set(one.month, (months.get(one.month) || 0) + one.chunks);
}
}
top.sort((a, b) => b.edges - a.edges);
const carrying = languages.filter((row) => row.extracted || row.resolved || row.ambiguous);
const byFiles = [...languages].sort((a, b) => b.files - a.files).slice(0, 8);
const filesTotal = languages.reduce((sum, row) => sum + row.files, 0) || 1;
const monthRows = [...months.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-12);
const peak = Math.max(1, ...monthRows.map(([, n]) => n));
fill(
node,
el(
"section",
{ class: "health-card" },
el("span", { class: "eyebrow", text: "Language mix" }),
byFiles.length
? el(
"div",
{ class: "mix" },
byFiles.map((row) =>
el(
"div",
{ class: "mix-row" },
el("span", { class: "k", text: row.language }),
el("span", { class: "meter" }, sized("width", row.files / filesTotal)),
el("span", { class: "v", text: `${n(row.files)} file${row.files === 1 ? "" : "s"}` }),
),
),
)
: el("div", { class: "rail-hint", text: "Nothing indexed yet." }),
),
el(
"section",
{ class: "health-card" },
el("span", { class: "eyebrow", text: "Chunks by month indexed" }),
monthRows.length
? el(
"div",
{ class: "months" },
monthRows.map(([month, count]) =>
el(
"div",
{ class: "month" },
el("span", { class: "col" }, sized("height", count / peak)),
el("span", { class: "m", text: month.slice(2) }),
el("span", { class: "c", text: n(count) }),
),
),
)
: el("div", { class: "rail-hint", text: "No files indexed yet." }),
el("p", { class: "note", text: "When semlith read the file, not when it was written." }),
),
el(
"section",
{ class: "health-card" },
el("span", { class: "eyebrow", text: "Graph health" }),
bar([
["extracted", counts.extracted],
["resolved", counts.resolved],
["ambiguous", counts.ambiguous],
["unresolved", counts.unresolved],
]),
el(
"div",
{ class: "health-facts" },
el("div", { class: "fact" }, el("span", { class: "v", text: n(unresolvedNames) }), el("span", { class: "k", text: "call targets with no definition here" })),
el("div", { class: "fact" }, el("span", { class: "v", text: n(several) }), el("span", { class: "k", text: "names with several definitions" })),
el("div", { class: "fact" }, el("span", { class: "v", text: `${carrying.length} of ${languages.length}` }), el("span", { class: "k", text: "languages carrying edges" })),
),
top.length
? el(
"div",
{ class: "mix" },
top.slice(0, 5).map((one) =>
el(
"div",
{ class: "mix-row" },
el("span", { class: "k", text: one.name }),
el("span", { class: "v", text: `${n(one.edges)} edge${one.edges === 1 ? "" : "s"}` }),
),
),
)
: null,
),
);
}
refresh();
return { node, paint: refresh };
}
function coveragePanel() {
const node = el("div", { class: "rows" });
let rows = [];
async function refresh() {
try {
const data = await api("/api/stores?coverage=1");
rows = data.stores || [];
} catch {
rows = [];
}
paint();
}
function paint() {
const coverage = [];
for (const store of rows) {
for (const row of store.coverage || []) {
if (!row.definitions && !row.files) continue;
coverage.push({ store: store.name, ...row });
}
}
fill(
node,
coverage.length
? dataTable({
className: "w-coverage",
caption:
"What the graph covers, per language: files indexed, files the parser gave up on, definitions, and call edges by how firmly each one landed.",
sort: "files",
dir: "desc",
perPage: 10,
rows: coverage,
columns: [
{ key: "store", label: "Store", className: "meta narrow-drop", value: (r) => r.store, render: (r) => r.store },
{ key: "language", label: "Language", value: (r) => r.language, render: (r) => r.language },
{ key: "files", label: "Files", className: "num", value: (r) => r.files, render: (r) => String(r.files) },
{
key: "parser_failed",
label: "Unparsed",
className: "num",
value: (r) => r.parser_failed,
render: (r) =>
el("span", {
class: r.parser_failed ? "bad" : "meta",
title: "Files whose parse expired, so the store holds their text and none of their structure.",
text: String(r.parser_failed),
}),
},
{ key: "definitions", label: "Definitions", className: "num", value: (r) => r.definitions, render: (r) => String(r.definitions) },
{ key: "extracted", label: "Extracted", className: "num", value: (r) => r.extracted, render: (r) => String(r.extracted) },
{ key: "resolved", label: "Resolved", className: "num", value: (r) => r.resolved, render: (r) => String(r.resolved) },
{ key: "ambiguous", label: "Ambiguous", className: "num", value: (r) => r.ambiguous, render: (r) => String(r.ambiguous) },
{
key: "unresolved",
label: "Unresolved",
className: "num",
value: (r) => r.unresolved,
title: "Call targets no definition in this store satisfies.",
render: (r) => String(r.unresolved),
},
{
key: "settled",
label: "Settled",
className: "num",
value: (r) => r.settled,
render: (r) => `${r.settled} %`,
},
],
}).node
: empty("No language holds a definition yet. Index a folder of code and the graph fills in."),
);
}
paint();
refresh();
return { node, paint: refresh };
}
function spell(ms) {
const all = Math.max(0, Math.round(ms / 1000));
const seconds = String(all % 60).padStart(2, "0");
const hours = Math.floor(all / 3600);
const minutes = String(Math.floor(all / 60) % 60).padStart(2, "0");
return hours ? `${hours}:${minutes}:${seconds}` : `${minutes}:${seconds}`;
}
const RUN_TONE = {
queued: "warn",
running: "good",
paused: "warn",
stopping: "warn",
done: "good",
stopped: "bad",
failed: "bad",
};
const RUN_WORD = {
queued: "queued",
running: "indexing",
paused: "paused",
stopping: "stopping",
done: "done",
stopped: "stopped",
failed: "failed",
};
function runCard(run, controls) {
const bar = el("span", {});
const pct = el("span", { class: "pct" });
const status = el("span", { class: "meta" });
const elapsed = el("span", { class: "meta" });
const log = el("div", { class: "log", "aria-live": "polite" });
const badge = pill("", "good");
const where = el("span", { class: "meta" });
const pause = el("button", { class: "button secondary small", type: "button" });
const stop = el("button", { class: "button ghost small", type: "button" });
const remove = el("button", {
class: "button ghost small",
type: "button",
text: "Remove",
title: "Dismiss this card. The store is not touched.",
});
const expand = el("button", {
class: "button ghost small",
type: "button",
"aria-expanded": "false",
});
let open = true;
let shown = 0;
let readAt = 0;
let ticking = false;
function paintClock() {
const ms = ticking ? shown + (Date.now() - readAt) : shown;
elapsed.textContent = ms < 1000 ? `${(ms / 1000).toFixed(1)}s` : spell(ms);
}
function absorb(next) {
shown = next.elapsed_ms || 0;
readAt = Date.now();
ticking = !!next.ticking;
paintClock();
const share = next.total ? Math.min(100, (next.scanned / next.total) * 100) : 0;
const finished = next.status === "done";
const width = finished ? 100 : next.status === "stopped" ? 0 : share;
bar.style.width = `${width.toFixed(1)}%`;
pct.textContent = `${Math.round(width)}%`;
fill(
badge,
el("i", {}),
next.status === "queued" && next.position
? `queued · ${next.position} in line`
: RUN_WORD[next.status] || next.status,
);
badge.className = `pill ${RUN_TONE[next.status] || "warn"}`;
const perSecond = next.elapsed_ms ? (next.chunks / next.elapsed_ms) * 1000 : 0;
const rate = perSecond >= 10 ? n(Math.round(perSecond)) : perSecond.toFixed(1);
status.textContent = next.phase
? `${n(next.scanned)}/${n(next.total)} files · ${next.phase} ·`
: next.total
? `${n(next.scanned)}/${n(next.total)} files · ${n(next.chunks)} chunks · ${rate} chunks/s ·`
: RUN_WORD[next.status] || next.status;
where.textContent = (next.paths || []).join(", ");
const held = next.status === "paused";
const live = TICKING.has(next.status);
pause.textContent = held ? "Resume" : "Pause";
pause.hidden = !live || next.status === "queued";
stop.textContent = next.status === "queued" ? "Take out of the queue" : "Stop";
stop.hidden = !live;
stop.disabled = next.status === "stopping";
remove.hidden = live;
if (touched === false) {
open = live;
touched = null;
}
paintFold();
}
function paintFold() {
log.hidden = !open;
where.hidden = !open;
expand.textContent = open ? "Hide the log" : "Show the log";
expand.setAttribute("aria-expanded", String(open));
}
let touched = false;
pause.addEventListener("click", () => controls.hold(pause.textContent === "Pause"));
stop.addEventListener("click", () => controls.stop(stop.textContent !== "Stop"));
remove.addEventListener("click", () => controls.remove());
expand.addEventListener("click", () => {
open = !open;
touched = null;
paintFold();
if (open && controls.reveal) controls.reveal();
});
const node = el(
"div",
{ class: "card pad run-card" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: run.store }),
badge,
el("span", { class: "spacer" }),
pct,
),
el("div", { class: "bar" }, bar),
el(
"div",
{ class: "filters" },
status,
elapsed,
el("span", { class: "spacer" }),
expand,
pause,
stop,
remove,
),
where,
log,
);
absorb(run);
const clock = setInterval(() => {
if (!node.isConnected) return clearInterval(clock);
if (ticking) paintClock();
}, 1000);
return { node, absorb, log, isOpen: () => open };
}
function logLine(log, event) {
const outcome = event.outcome || null;
const key =
event.event === "file" ? `${event.scanned}/${event.total}` : event.event || "";
const text =
event.event === "file"
? event.why
? `${event.path} — ${event.why}`
: event.path
: logText(event);
log.append(
el(
"div",
{ class: outcome ? `line ${outcome}` : "line" },
key ? el("span", { class: "key", text: key }) : null,
outcome ? el("span", { class: "outcome", text: outcome }) : null,
el("span", { class: "what", text }),
),
);
const atEnd = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
if (atEnd) log.scrollTop = log.scrollHeight;
while (log.childElementCount > 500) log.firstElementChild.remove();
}
function logText(event) {
switch (event.event) {
case "submitted":
return event.ahead
? `waiting — ${n(event.ahead)} run${event.ahead === 1 ? "" : "s"} ahead of this one`
: "submitted";
case "queued":
return event.ahead
? `waiting for ${event.store}'s writer — ${n(event.ahead)} job${
event.ahead === 1 ? "" : "s"
} ahead of this one`
: `waiting for ${event.store}'s writer; it finishes what the watcher is doing first`;
case "started":
return "walking the tree and hashing what it finds";
case "slice":
return `${n(event.remaining)} paths left; the writer is giving the watcher a turn and will carry on`;
case "paused":
return "held between files — the writer is still this run's";
case "resumed":
return "carrying on";
case "error":
return event.error || "failed";
case "done":
if (event.dequeued) return "removed from the queue before it started; nothing was indexed";
if (event.stopped) {
return "stopped — everything this run embedded was undone, so the store is as it was before it started";
}
return `${n(event.indexed)} indexed, ${n(event.unchanged)} unchanged, ${n(
event.skipped,
)} skipped, ${n(event.removed)} removed, ${n(event.chunks)} chunks · ${spell(
event.elapsed_ms || 0,
)}`;
default:
return event.event || "";
}
}
function settingField(key, label, limit, onSave) {
const fixed = limit.source === "environment";
const input = el("input", {
type: "number",
min: "1",
value: String(limit.value),
disabled: fixed,
"aria-label": label,
});
const why = el("div", { class: "note" });
function explain() {
const asked = Number(input.value);
if (!fixed && asked > limit.derived) {
why.className = "note bad";
const held = limit.source === "saved" ? `${limit.value} saved. ` : "";
why.textContent = `${held}Above the ${limit.derived} this machine derived — ${limit.reason}. It will be used as you set it.`;
return;
}
why.className = "note";
const from =
limit.source === "saved"
? `${limit.value} saved; ${limit.derived} is what this machine derives`
: `${limit.derived} derived`;
why.textContent = fixed
? `Set by the environment, so the page cannot change it. Derived here: ${limit.derived} — ${limit.reason}`
: `${from} — ${limit.reason}`;
}
input.addEventListener("input", explain);
input.addEventListener("change", () => {
const asked = Math.max(1, Number(input.value) || 1);
input.value = String(asked);
onSave(key, asked);
});
explain();
return el(
"div",
{ class: "setting" },
el("div", { class: "field" }, el("span", { class: "prefix", text: label }), input),
why,
);
}
async function indexView() {
await refreshStores();
const first = await refreshRuns();
const note = el("div", { class: "note" });
const cards = el("div", { class: "cards" });
const queueCard = el("div", { class: "card pad", hidden: true });
const settingsCard = el("div", { class: "card pad", hidden: true });
const urlCard = el("div", { class: "card pad", hidden: true });
const urlNote = el("div", { class: "note" });
const urlField = el("input", { type: "text", placeholder: "link to a page, a PDF or a file" });
const urlTarget = el("select", { class: "select" });
const clearDone = el("button", {
class: "button ghost small",
type: "button",
text: "Remove all finished",
hidden: true,
});
let transient = false;
const drawn = new Map();
const cursors = new Map();
function complain(message, focus) {
note.className = "note bad";
note.textContent = message;
if (focus) focus.focus();
}
function say(message) {
note.className = "note";
note.textContent = message;
}
async function control(store, action, run) {
try {
await post("/api/index/control", { store, action, run });
} catch (e) {
complain(e.message);
}
await refreshRuns();
}
async function catchUpLog(run, card) {
const after = cursors.get(run.id);
const from = after === undefined ? run.log_from : after;
let data;
try {
data = await api(
`/api/index/log?store=${encodeURIComponent(run.store)}&run=${run.id}${
from === undefined || from === null ? "" : `&after=${from}`
}`,
);
} catch (_) {
return;
}
for (const line of data.lines || []) logLine(card.log, line);
if (data.cursor !== null && data.cursor !== undefined) cursors.set(run.id, data.cursor);
}
function paint(data) {
const runs = data?.runs || [];
const seen = new Set();
for (const run of runs) {
seen.add(run.id);
let card = drawn.get(run.id);
if (!card) {
card = runCard(run, {
hold: (wantPause) => control(run.store, wantPause ? "pause" : "resume", run.id),
stop: (queued) => {
if (queued) return control(run.store, "dequeue", run.id);
ask({
title: `Stop ${run.store}'s index run?`,
body: "Everything it has embedded so far is undone, so the store is left exactly as it was before the run started. The other runs are untouched.",
confirm: "Stop and undo",
tone: "bad",
run: () => control(run.store, "stop", run.id),
});
},
remove: () => control(run.store, "remove", run.id),
reveal: () => {
const card = drawn.get(run.id);
if (card) catchUpLog(run, card);
},
});
drawn.set(run.id, card);
cards.append(card.node);
} else {
card.absorb(run);
}
if (card.isOpen()) catchUpLog(run, card);
}
for (const [id, card] of drawn) {
if (seen.has(id)) continue;
card.node.remove();
drawn.delete(id);
cursors.delete(id);
}
const order = runs
.slice()
.sort((a, b) => {
const live = Number(TICKING.has(b.status)) - Number(TICKING.has(a.status));
return live || (b.id || 0) - (a.id || 0);
})
.map((run) => drawn.get(run.id)?.node)
.filter(Boolean);
for (const node of order) cards.append(node);
if (transient && !runs.some((run) => TICKING.has(run.status)) && !(data?.queue || []).length) {
transient = false;
say("");
}
clearDone.hidden = !runs.some((run) => !TICKING.has(run.status));
const queue = data?.queue || [];
queueCard.hidden = !queue.length;
if (queue.length) {
fill(
queueCard,
el("span", { class: "eyebrow", text: "Waiting" }),
el("p", {
class: "subtitle",
text: "In the order they will start. Each one begins by itself the moment a run finishes, whether or not this page is open.",
}),
el(
"div",
{ class: "queue" },
queue.map((row) =>
el(
"div",
{ class: "queue-row" },
el("span", { class: "key", text: `${row.position}` }),
el("span", { class: "name", text: row.store }),
pathCell((row.paths || []).join(", ")),
el("span", { class: "spacer" }),
el("button", {
class: "button ghost small",
type: "button",
text: "Take out of the queue",
onclick: () => control(row.store, "dequeue", row.run),
}),
),
),
),
);
}
paintSettings(data?.limits);
}
async function saveSetting(key, value) {
try {
const answer = await post("/api/index/settings", { [key]: value });
say("Saved. It applies to the next run queued.");
paintSettings(answer.limits);
} catch (e) {
complain(e.message);
}
}
let limitsDrawn = null;
function paintSettings(limits) {
if (!limits) return;
const signature = JSON.stringify([
limits.runs_at_once,
limits.embed_threads,
limits.index_memory_mb,
]);
if (signature === limitsDrawn) return;
limitsDrawn = signature;
const machine = limits.machine || {};
fill(
settingsCard,
el("span", { class: "eyebrow", text: "How hard this machine may work" }),
el("p", {
class: "subtitle",
text: `${machine.logical_cores} logical core(s), ${n(
machine.total_memory_mb,
)} MiB of memory, ${n(
machine.available_memory_mb,
)} MiB free now. Each value below is derived from that, and each is yours to change.`,
}),
el(
"div",
{ class: "settings" },
settingField("runs_at_once", "runs at once", limits.runs_at_once, saveSetting),
settingField("embed_threads", "threads each", limits.embed_threads, saveSetting),
settingField("index_memory_mb", "MiB per store", limits.index_memory_mb, saveSetting),
),
);
}
const field = el("textarea", {
rows: "2",
placeholder: "~/work/api\n~/work/cli",
value: state.pendingPath || "",
});
state.pendingPath = "";
const target = el(
"select",
{ class: "field", "aria-label": "Where to index into" },
el("option", { value: "each", text: "each folder becomes its own store" }),
liveStores().map((store) =>
el("option", { value: store.name, text: `add to ${store.name}` }),
),
);
function paintTargets() {
const paths = field.value
.split("\n")
.map((path) => path.trim())
.filter(Boolean);
const covers = (store) =>
!paths.length ||
paths.every((path) =>
(store.roots || []).some((root) => root.present && path.startsWith(root.path)),
);
const chosen = target.value;
fill(
target,
el("option", { value: "each", text: "each folder becomes its own store" }),
liveStores().map((store) =>
el("option", {
value: store.name,
text: covers(store)
? `add to ${store.name}`
: `add to ${store.name} — outside its roots`,
disabled: !covers(store),
}),
),
);
target.value = [...target.options].some((o) => o.value === chosen && !o.disabled)
? chosen
: "each";
}
field.addEventListener("input", paintTargets);
paintTargets();
fill(
urlTarget,
liveStores().map((store) => el("option", { value: store.name, text: store.name })),
);
const only = liveStores();
urlTarget.value = only.length === 1 ? only[0].name : "";
if (only.length !== 1) {
urlTarget.prepend(el("option", { value: "", text: "choose a store…" }));
urlTarget.value = "";
}
const picker = folderPicker({
multiple: true,
onChoose: (paths) => {
if (!paths || !paths.length) return;
const already = field.value.split("\n").map((p) => p.trim()).filter(Boolean);
field.value = [...new Set([...already, ...paths])].join("\n");
field.rows = Math.min(8, Math.max(2, field.value.split("\n").length));
},
onError: (message) => complain(message),
});
const projects = projectsChecklist({
onChoose: (paths) => {
field.value = paths.join("\n");
field.rows = Math.min(8, Math.max(2, paths.length));
target.value = "each";
},
onError: (message) => complain(message),
});
const folderButton = el(
"button",
{
class: "button secondary",
type: "button",
"aria-pressed": "false",
onclick: () => reveal("picker"),
},
icon(ICONS.folder),
"Choose folders…",
);
const projectsButton = el(
"button",
{
class: "button secondary",
type: "button",
"aria-pressed": "false",
onclick: () => reveal("projects"),
},
icon(ICONS.folder),
"Projects under a folder…",
);
const urlButton = el(
"button",
{
class: "button secondary",
type: "button",
"aria-pressed": "false",
onclick: () => reveal("url"),
},
icon(ICONS.file),
"Add from a URL",
);
const settingsButton = el(
"button",
{
class: "button secondary",
type: "button",
"aria-pressed": "false",
onclick: () => reveal("settings"),
},
icon(ICONS.sliders),
"Machine limits",
);
function reveal(which) {
const wantPicker = which === "picker" && !picker.isOpen();
const wantProjects = which === "projects" && !projects.isOpen();
const wantUrl = which === "url" && urlCard.hidden;
const wantSettings = which === "settings" && settingsCard.hidden;
if (!wantPicker) picker.close();
if (!wantProjects) projects.close();
urlCard.hidden = !wantUrl;
settingsCard.hidden = !wantSettings;
if (wantPicker) picker.open("");
if (wantProjects) projects.open("");
if (wantUrl) urlField.focus();
if (wantProjects) target.value = "each";
target.disabled = wantProjects;
target.title = wantProjects
? "Each project under the folder becomes its own store."
: "";
folderButton.setAttribute("aria-pressed", String(wantPicker));
projectsButton.setAttribute("aria-pressed", String(wantProjects));
urlButton.setAttribute("aria-pressed", String(wantUrl));
settingsButton.setAttribute("aria-pressed", String(wantSettings));
}
const start = el("button", { class: "button", type: "button", text: "Start indexing" });
const addButton = el("button", {
class: "button secondary",
type: "button",
text: "Fetch and index",
});
async function begin(route, body) {
start.disabled = true;
try {
const answer = await post(route, body);
const started = (answer.runs || []).filter((run) => run.run !== undefined);
const refused = (answer.runs || []).filter((run) => run.error);
transient = true;
say(
`${started.length} run${started.length === 1 ? "" : "s"} queued${
refused.length ? `; ${refused.length} refused` : ""
}.`,
);
if (refused.length) {
complain(refused.map((run) => `${run.path}: ${run.error}`).join("; "));
}
await refreshStores();
await refreshRuns();
return null;
} catch (e) {
complain(e.message);
return e.message;
} finally {
start.disabled = false;
}
}
start.addEventListener("click", () => {
const paths = field.value
.split("\n")
.map((path) => path.trim())
.filter(Boolean);
if (!paths.length) {
complain("Give a path to index, or choose a folder.", field);
return;
}
begin("/api/index", { path: paths, store: target.value });
});
addButton.addEventListener("click", async () => {
const url = urlField.value.trim();
if (!url) {
urlNote.className = "note bad";
urlNote.textContent = "Give an https URL to fetch.";
urlField.focus();
return;
}
urlNote.className = "note";
urlNote.textContent = "";
if (!urlTarget.value) {
urlNote.className = "note bad";
urlNote.textContent =
"Pick the store to fetch into, in the selector beside this field.";
urlTarget.focus();
return;
}
const failed = await begin("/api/add", { url, store: urlTarget.value });
if (failed) {
urlNote.className = "note bad";
urlNote.textContent = failed;
urlCard.hidden = false;
}
});
clearDone.addEventListener("click", async () => {
const stores = new Set(
(state.runs?.runs || [])
.filter((run) => !TICKING.has(run.status))
.map((run) => run.store),
);
for (const store of stores) await control(store, "clear");
});
paint(first);
const coverage = coveragePanel();
const health = healthPanel();
state.onRuns = (data) => {
paint(data);
coverage.paint();
health.paint();
};
watchLive(["runs", "stores"], refreshRuns);
return el(
"div",
{ class: "view" },
pageHead(
"Inside the index",
"What this machine has read, what the graph made of it, and what is being kept current.",
),
says(
"Each folder becomes its own store, indexed by its own writer. A run lives in the daemon, not in this page — leaving, refreshing or closing the tab changes nothing, and a run ends only on its Stop or when ",
mono("semlith start"),
" does.",
),
el(
"div",
{ class: "field tall full area" },
el("span", { class: "prefix", text: "paths" }),
labelled("index-path", "Paths to index, one per line", field),
),
el(
"div",
{ class: "filters" },
folderButton,
projectsButton,
urlButton,
settingsButton,
target,
start,
),
note,
el(
"div",
{ class: "scroller" },
picker.node,
projects.node,
fill(
urlCard,
el("span", { class: "eyebrow", text: "Add from a URL" }),
el("p", {
class: "subtitle",
text: "One https request, for exactly this URL — a page, a PDF, or a file on GitHub. Nothing is crawled and no credential is ever sent. The file is saved inside this store's downloads folder, never in your working tree.",
}),
el(
"div",
{ class: "field tall full" },
el("span", { class: "prefix", text: "url" }),
labelled("index-url", "URL to fetch and index", urlField),
),
el(
"div",
{ class: "filters" },
labelled("index-url-store", "Fetch into", urlTarget),
el("span", { class: "spacer" }),
addButton,
),
urlNote,
),
el("div", { class: "filters" }, el("span", { class: "spacer" }), clearDone),
health.node,
el("span", { class: "eyebrow", text: "What the graph covers" }),
coverage.node,
cards,
queueCard,
settingsCard,
),
);
}
function projectsChecklist(options) {
const { onChoose, onError } = options || {};
const card = el("div", { class: "card picker", hidden: true });
const ticked = new Set();
async function open(path) {
let data;
try {
data = await api(`/api/projects?path=${encodeURIComponent(path || "")}`);
} catch (e) {
if (onError) onError(e.message);
return false;
}
card.hidden = false;
const rows = data.projects || [];
for (const row of rows) {
if (data.repositories && !row.indexed && !ticked.size) ticked.add(row.path);
}
const paint = () => open(data.path);
fill(
card,
el(
"div",
{ class: "crumbs" },
el("button", {
class: "button ghost small",
type: "button",
text: "Up",
disabled: !data.parent,
onclick: () => open(data.parent),
}),
el("span", { class: "where", text: data.path }),
el("span", { class: "spacer" }),
el("button", {
class: "button secondary small",
type: "button",
text: "All",
onclick: () => {
for (const row of rows) ticked.add(row.path);
paint();
},
}),
el("button", {
class: "button secondary small",
type: "button",
text: "None",
onclick: () => {
ticked.clear();
paint();
},
}),
el("button", {
class: "button small",
type: "button",
disabled: !ticked.size,
text: ticked.size ? `Use ${ticked.size}` : "Use",
onclick: () => {
card.hidden = true;
if (onChoose) onChoose([...ticked]);
ticked.clear();
},
}),
el("button", {
class: "button ghost small",
type: "button",
text: "Close",
onclick: () => {
card.hidden = true;
},
}),
),
el("p", {
class: "subtitle",
text: data.repositories
? "The repositories directly under this folder. One level only: a monorepo is one store, and its nested repositories are its own business."
: "Nothing under this folder is a repository, so these are its plain subfolders.",
}),
el(
"div",
{ class: "entries" },
rows.length
? rows.map((row) => {
const box = el("input", {
type: "checkbox",
class: "tick",
"aria-label": `Index ${row.name}`,
checked: ticked.has(row.path),
onchange: () => {
if (box.checked) ticked.add(row.path);
else ticked.delete(row.path);
paint();
},
});
return el(
"div",
{ class: "entry-row" },
box,
el(
"button",
{
class: "entry",
type: "button",
title: `Open ${row.path}`,
onclick: () => open(row.path),
},
icon(ICONS.folder),
el("span", { class: "name", text: row.name }),
row.indexed ? pill(`in ${row.indexed}`, "warn") : null,
),
);
})
: empty("Nothing here that Semlith can index."),
),
(data.folders || []).filter((f) => !rows.some((row) => row.path === f.path)).length
? el(
"div",
{ class: "entries" },
el("p", { class: "subtitle", text: "Or open one of these:" }),
(data.folders || [])
.filter((f) => !rows.some((row) => row.path === f.path))
.map((folder) =>
el(
"div",
{ class: "entry-row" },
el(
"button",
{
class: "entry",
type: "button",
title: `Open ${folder.path}`,
onclick: () => open(folder.path),
},
icon(ICONS.folder),
el("span", { class: "name", text: folder.name }),
),
),
),
)
: null,
);
return true;
}
return {
node: card,
open,
close: () => {
card.hidden = true;
},
isOpen: () => !card.hidden,
};
}
function installPanel() {
const card = el("div", { class: "card pad install-panel" });
const title = () => el("span", { class: "card-title", text: "This machine" });
fill(card, title(), el("p", { class: "subtitle", text: "Checking…" }));
const stateWord = {
done: "done",
"already-done": "already done",
skipped: "not done",
failed: "failed",
};
const tone = (state) =>
state === "done" || state === "already-done" ? "good" : state === "failed" ? "bad" : "warn";
function paint(setup) {
const note = el("div", { class: "note" });
const fixPath = el("button", {
class: "button small",
type: "button",
text: "Add it to PATH",
onclick: async () => {
fixPath.disabled = true;
note.className = "note";
note.textContent = "Editing your shell profile…";
try {
const done = await post("/api/setup", { step: "path" });
note.textContent = `${done.step.detail} — open a new terminal for it to take effect.`;
paint(done.status);
} catch (e) {
note.className = "note bad";
note.textContent = e.message;
fixPath.disabled = false;
}
},
});
const install = el("button", {
class: "button small",
type: "button",
text: "Install it",
disabled: true,
onclick: async () => {
install.disabled = true;
check.disabled = true;
note.className = "note";
note.textContent = "Downloading and verifying…";
try {
const done = await post("/api/upgrade", { action: "apply" });
note.textContent = done.restart;
} catch (e) {
note.className = "note bad";
note.textContent = e.message;
} finally {
check.disabled = false;
}
},
});
const check = el("button", {
class: "button secondary small",
type: "button",
text: "Check for updates",
onclick: async () => {
check.disabled = true;
note.className = "note";
note.textContent = "Asking GitHub…";
try {
const found = await post("/api/upgrade", { action: "check" });
note.textContent = found.available
? `${found.latest} is available; you are on ${found.installed}.`
: `Already on the newest build, ${found.installed}.`;
if (found.blocked) note.textContent += ` ${found.blocked}`;
install.disabled = !found.available || Boolean(found.blocked);
} catch (e) {
note.className = "note bad";
note.textContent = e.message;
} finally {
check.disabled = false;
}
},
});
fill(
card,
title(),
el(
"div",
{ class: "rows" },
(setup.steps || []).map((step) =>
el(
"div",
{ class: "kv step" },
pill(stateWord[step.state] || step.state, tone(step.state)),
el("span", { class: "card-title", text: step.name }),
lineCell(step.detail, "meta"),
step.name === "path" && step.state !== "already-done" && step.state !== "done"
? el("span", { class: "spacer" })
: null,
step.name === "path" && step.state !== "already-done" && step.state !== "done"
? fixPath
: null,
),
),
),
note,
el("hr", { class: "rule" }),
el("span", { class: "eyebrow", text: `Installed version ${setup.version}` }),
el("div", { class: "actions" }, check, install),
);
}
api("/api/setup")
.then(paint)
.catch((e) => fill(card, title(), error(e.message)));
return card;
}
async function doctorView() {
let data;
try {
data = await api("/api/doctor");
} catch (e) {
return el("div", { class: "view" }, pageHead("Doctor"), error(e.message));
}
const state = (c) => {
if (c.note) return { text: "cannot register", kind: null };
if (c.registered) return { text: `registered (${c.scope || "user"})`, kind: "good" };
if (!c.command) return { text: "not registered", kind: "bad" };
if (!c.present) return { text: "not installed", kind: null };
if (c.scope === "project")
return { text: "one project only", kind: "bad" };
return { text: "installed, not registered", kind: "bad" };
};
const steering = (r) => {
const parts = [];
if (r.skill && r.skill !== "paste") parts.push(`skill ${r.skill === "present" ? "linked" : r.skill}`);
if (r.hook && r.hook !== "paste") parts.push(`hook ${r.hook}`);
if (r.rules && r.rules !== "paste") parts.push(`rule ${r.rules}`);
return parts;
};
const clients = dataTable({
caption:
"Every documented client, whether it is on this machine, whether it is registered, whether the skill and the steering hook are installed, and what would fix it.",
rows: data.clients || [],
perPage: 25,
columns: [
{ key: "name", label: "Client", sortable: true, value: (r) => r.name },
{
key: "state",
label: "semlith",
sortable: true,
value: (r) => state(r).text,
render: (r) => {
const s = state(r);
return pill(s.text, s.kind);
},
},
{
key: "steering",
label: "Skill & hook",
sortable: true,
value: (r) => steering(r).join(", "),
render: (r) => {
const parts = steering(r);
if (!parts.length) return el("span", { class: "sub", text: "—" });
return el(
"span",
{ class: "pills" },
...parts.map((part) =>
pill(part, part.endsWith("present") || part.endsWith("linked") ? "good" : "warn"),
),
);
},
},
{
key: "repair",
label: "To fix",
render: (r) =>
r.repair
? copyField(r.repair)
: el("span", { class: "sub", text: r.note ? "nothing to run" : "—" }),
},
],
});
const rulesBox = el("div", { class: "strip" });
const note = el("div", { class: "note" });
const paintRules = (rules) => {
rulesBox.textContent = "";
for (const rule of rules) {
const row = el(
"div",
{ class: "stat fact" },
el("span", { class: "eyebrow", text: rule.id }),
el("span", {
class:
rule.applicable === false
? "fact-value"
: rule.ok
? "fact-value"
: "fact-value bad",
text: rule.applicable === false ? "not applicable" : rule.ok ? "holds" : "fails",
}),
el("span", { class: "sub", text: rule.check }),
);
if (!rule.ok && rule.manual) row.appendChild(copyField(rule.manual));
if (!rule.ok && rule.repair) {
const fix = el("button", {
class: "button secondary small",
type: "button",
text: "Fix",
onclick: async () => {
fix.disabled = true;
note.className = "note";
note.textContent = "Applying…";
try {
const out = await post("/api/privacy/fix", { rule: rule.id });
const applied = (out.applied || [])[0];
note.textContent = applied
? `${applied.path}: ${applied.now}, was ${applied.was}` +
(applied.rechecked_ok ? "" : " — the rule still does not hold")
: "Nothing to apply.";
paintRules(out.rules || []);
} catch (e) {
note.className = "note bad";
note.textContent = e.message;
fix.disabled = false;
}
},
});
row.appendChild(fix);
}
rulesBox.appendChild(row);
}
};
paintRules(data.rules || []);
const rows = data.clients || [];
const registered = rows.filter((c) => c.registered).length;
const fixable = rows.filter((c) => !c.registered && c.repair).length;
return el(
"div",
{ class: "view" },
pageHead(
"Doctor",
"Whether each agent client on this machine can reach semlith, and what to run for the ones that cannot.",
),
el(
"div",
{ class: "card pad" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "Clients" }),
el("span", { class: "spacer" }),
el("span", {
class: "meta",
text: `${n(registered)} registered · ${n(fixable)} to fix · ${n(rows.length)} known`,
}),
),
clients.node,
),
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "Rules" }),
el("span", {
class: "meta",
text: "what the daemon found when it looked, not what the documentation says",
}),
),
rulesBox,
note,
);
}
async function agentsView() {
let data;
try {
data = await api("/api/agents");
} catch (e) {
return el("div", { class: "view" }, pageHead("Agents"), error(e.message));
}
noteAgents(data);
const clients = data.clients || [];
const tools = data.tools || [];
const live = data.connections || [];
const endpoint = data.endpoint || { url: "", open: false };
const address = el("code", { class: "text", text: endpoint.url });
const endpointNote = el("div", { class: "note" });
const toggle = el("button", {
class: "button secondary small",
type: "button",
text: endpoint.open ? "Stop" : "Start",
onclick: () => {
if (!endpoint.open) return apply();
ask({
title: "Close the MCP endpoint?",
body: "Every connected agent loses semlith until it is started again. The daemon, the watcher and the portal are unaffected, and Start puts it back.",
confirm: "Close the endpoint",
tone: "bad",
run: apply,
});
},
});
async function apply() {
toggle.disabled = true;
endpointNote.className = "note";
endpointNote.textContent = endpoint.open ? "Closing…" : "Opening…";
try {
const done = await post("/api/endpoint", { open: !endpoint.open });
endpoint.open = done.open;
toggle.textContent = done.open ? "Stop" : "Start";
endpointNote.textContent = done.open
? "The endpoint is answering. A configured client reconnects on its next call."
: "The endpoint is closed. The daemon, the watcher and the portal are unaffected.";
fill(statePill, el("i", {}), done.open ? "answering" : "closed");
statePill.className = done.open ? "pill good" : "pill warn";
} catch (e) {
endpointNote.className = "note bad";
endpointNote.textContent = e.message;
} finally {
toggle.disabled = false;
}
}
const statePill = pill(endpoint.open ? "answering" : "closed", endpoint.open ? "good" : "warn");
const keyNote = el("div", { class: "note" });
const rotate = el("button", {
class: "button secondary small",
type: "button",
text: "Rotate key",
onclick: async () => {
rotate.disabled = true;
keyNote.className = "note";
keyNote.textContent = "Rotating…";
try {
const done = await post("/api/key", {});
if (stanzas.revealed) stanzas.key = done.key;
showClient(chosen);
const carried = done.updated || [];
const rewrote = carried.length
? `Carried the new key into ${carried.length} configuration file${
carried.length === 1 ? "" : "s"
}: ${carried.join(", ")}.`
: "No configuration file on this machine carried the old key.";
keyNote.textContent = done.previous_valid
? `New key. ${rewrote} The previous one keeps working for fifteen minutes, so a session already open finishes — any client configured elsewhere needs the new stanza before then.`
: `New key. ${rewrote} The previous one is refused now; any client configured elsewhere needs the new stanza.`;
} catch (e) {
keyNote.className = "note bad";
keyNote.textContent = e.message;
} finally {
rotate.disabled = false;
}
},
});
const MASKED = data.key_set ? "sml_" + "•".repeat(24) : "no key yet";
const keyBox = el("code", { class: "text", text: MASKED });
const reveal = el("button", {
class: "button secondary small",
type: "button",
text: "Reveal",
onclick: async () => {
if (stanzas.revealed) {
stanzas.revealed = false;
stanzas.key = "${" + KEY_ENV + "}";
keyBox.textContent = MASKED;
reveal.textContent = "Reveal";
showClient(chosen);
return;
}
reveal.disabled = true;
try {
const shown = await post("/api/agents/reveal", {});
stanzas.revealed = true;
stanzas.key = String(shown.key);
keyBox.textContent = stanzas.key;
reveal.textContent = "Hide";
showClient(chosen);
} catch (e) {
keyNote.className = "note bad";
keyNote.textContent = e.message;
} finally {
reveal.disabled = false;
}
},
});
const connected = dataTable({
className: "w-agents",
caption: "Every documented client, whether it is on this machine, and whether it is registered.",
sort: "name",
grow: false,
perPage: 10,
rows: live,
columns: [
{
key: "name",
label: "Connected",
value: (c) => c.name,
render: (c) => el("div", { class: "who" }, el("span", { class: "dot good" }), c.name),
},
{ key: "transport", label: "Transport", className: "meta", value: (c) => c.transport },
{ key: "revision", label: "Revision", className: "meta narrow-drop", value: (c) => c.revision },
{
key: "queries",
label: "Queries",
className: "num",
value: (c) => c.queries,
render: (c) => n(c.queries),
},
],
});
const GROUPS = [
["terminal", "Terminal"],
["editor", "Editors"],
["desktop", "Desktop"],
];
const KEY_ENV = data.key_env || "SEMLITH_AGENT_KEY";
const stanzas = { key: "${" + KEY_ENV + "}", revealed: false };
let group = GROUPS.find(([id]) => clients.some((c) => c.group === id))[0];
let chosen = clients.findIndex((c) => c.group === group);
const tabs = el("div", { class: "tabs" });
const chips = el("div", { class: "filters" });
const body = el("div", { class: "stanza" });
const KEY_SLOT = "${SEMLITH_AGENT_KEY}";
const overHttp = (text) => text.includes("/mcp") || text.includes("--transport http");
function httpStanza() {
return `{\n "mcpServers": {\n "semlith": {\n "url": "${endpoint.url}",\n "headers": { "Authorization": "Bearer ${stanzas.key}" }\n }\n }\n}`;
}
function showGroup(id) {
group = id;
const first = clients.findIndex((c) => c.group === id);
showClient(first < 0 ? chosen : first);
}
function showClient(index) {
chosen = index;
const client = clients[index];
fill(
tabs,
GROUPS.map(([id, label]) => {
const count = clients.filter((c) => c.group === id).length;
return el(
"button",
{
class: "tab",
type: "button",
"aria-pressed": String(id === group),
onclick: () => showGroup(id),
},
label,
el("span", { class: "count", text: String(count) }),
);
}),
);
fill(
chips,
clients.map((c, i) =>
c.group === group
? el("button", {
class: "chip",
type: "button",
"aria-pressed": String(i === chosen),
text: c.name,
onclick: () => showClient(i),
})
: null,
),
);
if (!client) {
fill(body, empty("No client stanza is compiled into this build."));
return;
}
const own = (client.stanzas || []).map((s) => ({
format: s.format,
text: s.text.trimEnd().replaceAll(KEY_SLOT, stanzas.key),
}));
const shells = own.filter((s) => s.format === "sh");
const wired = shells.filter((s) => overHttp(s.text));
const spawned = shells.filter((s) => !overHttp(s.text));
const blocks = own.filter((s) => s.format !== "sh");
const http = blocks.filter((s) => overHttp(s.text));
const stdio = blocks.filter((s) => !overHttp(s.text));
const config = http.length ? http : stdio;
fill(
body,
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: client.name }),
el("span", { class: "meta", text: client.note }),
),
el("span", {
class: "eyebrow",
text: config.length && config === http
? "Over HTTP — one endpoint, one key, no subprocess"
: "As a subprocess — no key needed",
}),
(config.length ? config.map((s) => s.text) : [httpStanza()]).map((text) =>
codeBlock(text, "Copy stanza"),
),
wired.length ? el("span", { class: "eyebrow", text: "Or from a terminal" }) : null,
wired.map((s) => copyField(s.text, true)),
spawned.length
? el("span", { class: "eyebrow", text: "Or add the subprocess from a terminal" })
: null,
spawned.map((s) => copyField(s.text, true)),
);
}
showClient(chosen);
const planBox = el("div", { class: "rules" });
const planNote = el("div", { class: "note" });
let planned = null;
const write = el("button", {
class: "button secondary small",
type: "button",
text: "Write these files",
hidden: true,
onclick: async () => {
write.disabled = true;
planNote.className = "note";
planNote.textContent = "Writing…";
try {
const out = await post("/api/agents/register", { confirm: true });
const written = out.written || [];
planNote.textContent = written.length
? `Wrote ${written.length} file${written.length === 1 ? "" : "s"}. Each has a .semlith-backup beside it.`
: "Nothing needed writing.";
planned = out.plan || [];
paintPlan();
} catch (e) {
planNote.className = "note bad";
planNote.textContent = e.message;
write.disabled = false;
}
},
});
const paintPlan = () => {
planBox.textContent = "";
for (const item of planned || []) {
const what =
item.action === "create"
? "will be created"
: item.action === "merge"
? "will be merged into"
: item.action === "already-done"
? "already has semlith"
: `will be left alone — ${item.reason}`;
planBox.appendChild(
el(
"div",
{ class: "rule-row" },
el(
"div",
{ class: "rule-head" },
el("span", { class: "rule-id", text: item.client }),
),
el("code", { class: "rule-check", text: item.path }),
el("span", { class: "sub", text: what }),
),
);
}
const pending = (planned || []).some(
(p) => p.action === "create" || p.action === "merge",
);
write.hidden = !pending;
write.disabled = !pending;
};
const preview = el("button", {
class: "button small",
type: "button",
text: "Show what would be written",
onclick: async () => {
preview.disabled = true;
planNote.className = "note";
planNote.textContent = "Reading…";
try {
const out = await post("/api/agents/register", {});
planned = out.plan || [];
planNote.textContent = planned.length
? "Nothing has been written yet."
: "There is no client on this machine whose file semlith would write.";
paintPlan();
} catch (e) {
planNote.className = "note bad";
planNote.textContent = e.message;
} finally {
preview.disabled = false;
}
},
});
const registerAll = el(
"div",
{ class: "card pad" },
el("span", { class: "card-title", text: "Register the clients that have no command" }),
says(
"Ten of the twenty-seven cannot be asked to register themselves, so semlith would write their configuration file. Every path is listed before anything is written, each file is backed up beside itself, and one that does not parse is left alone. This is the terminal's ",
mono("semlith setup --register-all"),
".",
),
el("div", { class: "head" }, preview, write),
planBox,
planNote,
);
const MECHANISMS = {
launchd: "a launchd login agent",
systemd: "a systemd user service",
schtasks: "a Windows logon task",
};
const labelled = (label, value) =>
el(
"div",
{ class: "rows tight" },
el("span", { class: "eyebrow", text: label }),
copyField(value),
);
const serviceCard = () => {
if (!data.service) return null;
const status = data.service.status || {};
const mechanism = MECHANISMS[status.mechanism] || "a login service";
const started = data.service.last_started
? ` Last started ${when(data.service.last_started)}.`
: "";
const bits = [];
if (status.installed) {
bits.push(
says(
`The daemon is installed as ${mechanism}, so the endpoint is answering again after a reboot without anybody opening a terminal for it.${started}`,
),
);
} else {
bits.push(
says(
`The daemon is not installed as a login service, so the endpoint lives exactly as long as whatever started it: close that terminal, or reboot, and every client configured below loses semlith until somebody starts it again.${started}`,
),
labelled("To install", "semlith start --service"),
);
}
if (status.definition) bits.push(labelled("Definition", status.definition));
if (status.log) bits.push(labelled("Log", status.log));
if (status.installed && status.restarts === false) {
bits.push(
el("div", {
class: "note",
text: "A logon task is restarted only when it failed. One that exited cleanly stays stopped until the next logon, so a daemon stopped on purpose is a daemon started again on purpose.",
}),
);
}
return el(
"div",
{ class: "card pad dense" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "Login service" }),
el("span", { class: "spacer" }),
pill(
status.installed ? "installed" : "not installed",
status.installed ? "good" : "warn",
),
),
bits,
);
};
const inUse = (data.doctor || []).filter((c) => c && c.in_use);
const clientState = (c) => {
if (c.note) return { text: "cannot register", kind: null };
if (c.registered) return { text: `registered (${c.scope || "user"})`, kind: "good" };
if (!c.command) return { text: "not registered", kind: "bad" };
if (!c.present) return { text: "not installed", kind: null };
if (c.scope === "project") return { text: "one project only", kind: "bad" };
return { text: "installed, not registered", kind: "bad" };
};
const clientWhy = (c) => {
const lines = [];
if (c.note) lines.push(el("span", { class: "sub", text: c.note }));
if (c.explain && !/this directory/i.test(c.explain))
lines.push(el("span", { class: "sub", text: c.explain }));
const off = (c.disabled_in || []).filter(Boolean);
if (off.length)
lines.push(el("span", { class: "meta", text: `switched off for: ${off.join(", ")}` }));
if (c.repair) lines.push(copyField(c.repair));
if (!lines.length && !c.present && c.fault)
lines.push(
el("span", {
class: "sub",
text: c.command
? `Nothing named ${c.command} is on this machine.`
: "None of this client's configuration files are on this machine.",
}),
);
return lines.length
? el("div", { class: "rows tight" }, lines)
: el("span", { class: "sub", text: "—" });
};
const inUseTable = inUse.length
? dataTable({
caption:
"Every agent client installed on this machine, whether it is registered with semlith, and what to run for the ones that are not.",
sort: "name",
grow: false,
perPage: 10,
rows: inUse,
columns: [
{ key: "name", label: "Client", sortable: true, value: (c) => c.name },
{
key: "state",
label: "semlith",
sortable: true,
value: (c) => clientState(c).text,
render: (c) => {
const s = clientState(c);
return pill(s.text, c.fault ? "bad" : s.kind);
},
},
{ key: "why", label: "Why", className: "why", render: clientWhy },
],
})
: null;
const inUseCard = data.doctor
? el(
"div",
{ class: "card pad" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "Clients in use" }),
el("span", { class: "spacer" }),
el("span", {
class: "meta",
text: `${n(inUse.length)} of ${n((data.doctor || []).length)} known`,
}),
),
inUseTable
? inUseTable.node
: empty("No documented client is installed on this machine."),
says("All twenty-seven, including the ones nobody here has, are on the Doctor page."),
)
: null;
return el(
"div",
{ class: "view" },
pageHead("Agents", "One endpoint, every client, no per-client process.", {
pill: statePill,
actions: [
el("div", { class: "copyfield worded" }, address, copyButton(endpoint.url, "Copy", true)),
toggle,
rotate,
],
}),
endpointNote,
serviceCard(),
el(
"div",
{ class: "card pad dense" },
el("span", { class: "card-title", text: "Agent key" }),
el("div", { class: "copyfield" }, keyBox, el("div", { class: "actions" }, reveal)),
says(
"Shown truncated, and fetched in full only when you press Reveal — the page does not receive it just for being open. No registration semlith writes carries it: a registered client launches ",
mono("semlith mcp"),
", which reads the key from ",
mono(data.key_path || "~/.semlith/agent.key"),
" itself, so a rotation reconfigures nothing. The key is for the HTTP stanzas below, which a daemon on another machine still needs — paste one of those and you export ",
mono("${" + KEY_ENV + "}"),
" yourself.",
),
),
keyNote,
registerAll,
inUseCard,
el(
"div",
{ class: "grid scroller" },
el(
"div",
{ class: "rows" },
live.length
? connected.node
: el(
"div",
{ class: "card pad dense" },
el("span", { class: "card-title", text: "Connected" }),
empty("No client is talking to this daemon right now."),
),
el(
"div",
{ class: "card pad dense tools" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "Tools exposed" }),
el("span", {
class: "meta",
text: `${tools.length} tool${tools.length === 1 ? "" : "s"}`,
}),
),
el(
"div",
{ class: "cost" },
el("span", { class: "card-title", text: "What the tool list costs" }),
el("span", {
class: "cost-line",
text: `${tools.length} tools · ${n(data.tool_list_bytes || 0)} bytes · ${n(
data.tool_list_tokens || Math.ceil((data.tool_list_bytes || 0) / 4),
)} tokens per session`,
}),
el("span", {
class: "cost-note",
text: `read once, before the agent asks anything — counted ${
data.tool_list_tier || "chars4"
}`,
}),
),
el(
"div",
{ class: "tool-grid" },
tools.flatMap((tool) => [
el("span", { class: "tool-name", text: tool.name }),
el("span", { class: "tool-about", text: tool.about }),
]),
),
),
installPanel(),
),
el("div", { class: "card pad stanzas" }, tabs, chips, body),
),
);
}
async function privacyView() {
let data;
try {
data = await api("/api/privacy");
} catch (e) {
return el("div", { class: "view" }, pageHead("Privacy"), error(e.message));
}
watchLive(["privacy"], repaintView);
const tokenBox = el("code", { class: "text", text: data.token_preview || "—" });
const rotateNote = el("div", { class: "note" });
const rotate = el("button", {
class: "button secondary small",
type: "button",
text: "Rotate",
onclick: async () => {
rotate.disabled = true;
rotateNote.className = "note";
rotateNote.textContent = "Rotating…";
try {
const fresh = await post("/api/rotate", {});
session.set(String(fresh.token));
tokenBox.textContent = `${String(fresh.token).slice(0, 16)}…`;
rotateNote.textContent =
"Rotated. The old token stopped working immediately. This is the portal's own session token, not the agent key: no client stanza carries it, so nothing needs reconfiguring. The agent key is rotated on the Agents page.";
} catch (e) {
rotateNote.className = "note bad";
rotateNote.textContent = e.message;
} finally {
rotate.disabled = false;
}
},
});
const fact = (key, value, why) =>
el(
"div",
{ class: "stat fact" },
el("span", { class: "eyebrow", text: key }),
el("span", { class: "fact-value", text: value }),
el("span", { class: "sub", text: why }),
);
const rulesBox = el("div", { class: "rules" });
const fixNote = el("div", { class: "note" });
const paintRules = (rules) => {
rulesBox.textContent = "";
for (const rule of rules) {
const unmeasured = rule.applicable === false;
const row = el(
"div",
{ class: rule.ok || unmeasured ? "rule-row" : "rule-row bad" },
el(
"div",
{ class: "rule-head" },
el("span", { class: "dot " + (unmeasured ? "" : rule.ok ? "good" : "warn") }),
el("span", { class: "rule-id", text: rule.id }),
),
el("p", { class: "rule-text", text: rule.rule }),
el(
"div",
{ class: "rule-found" },
el("span", { class: "eyebrow", text: "found" }),
el("code", { class: "rule-check", text: rule.check }),
),
);
if (!rule.ok && rule.manual) {
row.appendChild(
el(
"div",
{ class: "rule-found" },
el("span", { class: "eyebrow", text: "to fix" }),
copyField(rule.manual),
),
);
}
if (!rule.ok && rule.repair) {
const fix = el("button", {
class: "button secondary small",
type: "button",
text: "Fix",
onclick: async () => {
fix.disabled = true;
fixNote.className = "note";
fixNote.textContent = "Applying…";
try {
const out = await post("/api/privacy/fix", { rule: rule.id });
const applied = (out.applied || [])[0];
fixNote.textContent = applied
? `${applied.path}: ${applied.now}, was ${applied.was}` +
(applied.rechecked_ok ? "" : " — the rule still does not hold")
: "Nothing to apply.";
paintRules(out.rules || []);
} catch (e) {
fixNote.className = "note bad";
fixNote.textContent = e.message;
fix.disabled = false;
}
},
});
row.appendChild(fix);
}
rulesBox.appendChild(row);
}
};
paintRules(data.rules || []);
const scanNote = el("div", { class: "note" });
const scanBox = el("div", { class: "rows tight" });
const scanTable = dataTable({
caption: "Every file a store is still holding that semlith would refuse to index today.",
sort: "path",
columns: [
{ key: "store", label: "Store", className: "meta", sortable: true, value: (f) => f.store },
{
key: "path",
label: "Path",
sortable: true,
value: (f) => f.path,
render: (f) => pathCell(f.path),
},
{
key: "why",
label: "Why it would be refused",
sortable: false,
render: (f) => lineCell(f.why),
},
{
key: "forget",
label: "",
sortable: false,
render: (f) =>
el("button", {
class: "forget",
type: "button",
text: "Forget",
onclick: () =>
ask({
title: "Forget this file?",
body: `${f.path} — its chunks and vectors are dropped from ${f.store}. The file on disk is untouched.`,
confirm: "Forget it",
tone: "bad",
run: () => forgetFound([f]),
}),
}),
},
],
rows: [],
});
async function forgetFound(findings) {
scanNote.className = "note";
scanNote.textContent = "";
const byStore = new Map();
for (const f of findings) {
if (!byStore.has(f.store)) byStore.set(f.store, []);
byStore.get(f.store).push(f.key || f.path);
}
try {
for (const [store, paths] of byStore) await post("/api/forget", { paths, store });
} catch (e) {
scanNote.className = "note bad";
scanNote.textContent = e.message;
throw e;
}
await runScan();
}
const scanButton = el("button", {
class: "button secondary small",
type: "button",
text: "Scan",
onclick: () => runScan(),
});
async function runScan() {
scanButton.disabled = true;
scanButton.textContent = "Scanning…";
scanNote.className = "note";
scanNote.textContent = "";
try {
const found = (await api("/api/privacy/scan")).findings || [];
scanTable.update(found);
const stores = [...new Set(found.map((f) => f.store))];
fill(
scanBox,
found.length
? [
el(
"div",
{ class: "bulk" },
el("span", {
class: "meta",
text: `${n(found.length)} file${found.length === 1 ? "" : "s"} would be refused today`,
}),
el("span", { class: "spacer" }),
el("button", {
class: "button danger small",
type: "button",
text: "Forget all",
onclick: () =>
ask({
title: `Forget ${n(found.length)} file${found.length === 1 ? "" : "s"}?`,
body: `Their chunks and vectors are dropped from ${stores.join(
" and ",
)}. The files on disk are untouched.`,
confirm: `Forget ${n(found.length)} file${found.length === 1 ? "" : "s"}`,
tone: "bad",
run: () => forgetFound(found),
}),
}),
),
scanTable.node,
]
: empty("Nothing this store holds would be refused today."),
);
} catch (e) {
scanNote.className = "note bad";
scanNote.textContent = e.message;
} finally {
scanButton.disabled = false;
scanButton.textContent = "Scan again";
}
}
const step = (number, what, command) =>
el(
"div",
{ class: "step-row" },
el("span", { class: "n", text: number }),
el(
"div",
{ class: "what" },
el("div", { class: "say", text: what }),
copyField(command),
),
);
return el(
"div",
{ class: "view" },
pageHead(
"Privacy",
"The claim is “nothing leaves this machine”. This page is how you check it yourself, in about a minute.",
{ pill: pill(data.airgap ? "airgap mode armed" : "airgap off", data.airgap ? "good" : null) },
),
el(
"div",
{ class: "strip" },
fact("Bind address", data.bind, "loopback only, with no flag to change it"),
fact("CORS", "none", "no origin may read this server's responses"),
fact("Host check", "localhost only", "a foreign Host header gets 400 before anything runs"),
fact("Assets", "include_bytes!", "the page you are reading is inside the binary"),
fact("Telemetry", "none", "no analytics, and no update check Semlith makes on its own"),
fact(
"Retrieval ledger",
"records locally",
"the daemon prints this on every start; --no-ledger stops it for a session, SEMLITH_LEDGER=0 for a machine, and the rows never leave the store",
),
fact("Model cache", data.model_cached ? "cached" : "not downloaded", data.model_cache),
fact(
"Cloud",
"not connected",
"this binary has no cloud command and opens no connection to any host; the Cloud page describes an optional service and contacts nothing",
),
),
sessionReplayToggle(),
el(
"div",
{ class: "grid scroller" },
el(
"div",
{ class: "card pad" },
el("span", { class: "card-title", text: "Verify it yourself" }),
el(
"div",
{ class: "steps-list" },
step(
"1",
"Ask the operating system what this process has open. Only loopback should appear.",
"lsof -nP -p $(pgrep -f 'semlith start') -i",
),
step(
"2",
"Watch every interface but loopback while you search. Nothing should appear.",
"sudo tcpdump -i any -n 'not host 127.0.0.1 and not host ::1'",
),
step(
"3",
"Arm the refusal. Anything that would reach the network exits instead, naming what it refused.",
"semlith start --airgap",
),
step("4", "Or pull the cable: the portal loads and searches with no network at all.", "ifconfig en0 down"),
),
),
el(
"div",
{ class: "rows" },
el(
"div",
{ class: "card pad" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "Rules" }),
el("span", { class: "spacer" }),
pill(
(data.rules || []).every((r) => r.ok)
? "holding for new writes"
: "check the rows",
(data.rules || []).every((r) => r.ok) ? "good" : "warn",
),
),
el("p", {
class: "subtitle",
text: "What semlith refuses, and what this daemon found when it checked. Every row is a rule the binary enforces and a test that fails if it stops.",
}),
rulesBox,
fixNote,
),
el(
"div",
{ class: "card pad" },
el(
"div",
{ class: "head" },
el("span", { class: "card-title", text: "What is already stored" }),
el("span", { class: "spacer" }),
scanButton,
),
el("p", {
class: "subtitle",
text: "The rules above decide what semlith will take in from now on. This checks what the stores are already holding: every file that semlith would refuse today — indexed before a rule widened, or before the credential content scan existed.",
}),
scanBox,
scanNote,
),
el(
"div",
{ class: "card pad" },
el("span", { class: "card-title", text: "The one outbound connection that exists" }),
says(
"The embedding model is downloaded once, on first index, and cached. ",
mono("semlith upgrade"),
" and ",
mono("semlith add"),
" reach the network only in the second you ask them to. ",
mono("--airgap"),
" refuses all three and exits naming what it refused.",
),
copyField("SEMLITH_MODEL_CACHE=/media/usb/models semlith index ."),
el(
"div",
{ class: "chips" },
pill(data.model_cached ? "model cached" : "model not downloaded", data.model_cached ? "good" : "warn"),
el("span", {
class: "meta",
text: "granite-embedding-small-english-r2 · int8 · Apache-2.0",
}),
),
),
el(
"div",
{ class: "card pad" },
el("span", { class: "card-title", text: "Session token" }),
el("div", { class: "copyfield" }, tokenBox, el("div", { class: "actions" }, rotate)),
rotateNote,
says(
"Generated at start, handed to this page once by the printed URL, and sent back as a ",
mono(data.token_header),
" header on every ",
mono("/api"),
" route. It is in no cookie: every port on localhost is the same site, so a cookie would travel to a page served by anything else on this machine, and a header will not. Shown truncated — no response carries it in full except the one that rotates it.",
),
el("hr", { class: "rule" }),
el("span", { class: "card-title", text: "Content-Security-Policy" }),
codeBlock(data.csp),
el("p", {
class: "subtitle",
text: `Host headers answered: ${(data.host_allowed || []).join(", ")}. Everything else gets 400.`,
}),
),
),
),
);
}
function langCard(languages, withEdges) {
const edges = new Set(withEdges);
return el(
"div",
{ class: "card pad lang-card" },
el(
"div",
{ class: "rows tight" },
el("span", { class: "card-title", text: `${languages.length} languages` }),
el("span", {
class: "subtitle",
text:
edges.size === languages.length
? `Search filters and the code graph read the same table, so the two cannot disagree. Every one carries graph edges.`
: `Search filters and the code graph read the same table, so the two cannot disagree. ${edges.size} of ${languages.length} carry graph edges.`,
}),
),
el(
"div",
{ class: "lang-grid" },
languages.map((lang) => {
const has = edges.has(lang.name);
const names = (lang.extensions || [])
.map((e) => `.${e}`)
.concat(lang.filenames || []);
return el(
"div",
{ class: "lang-row" },
el("span", {
class: "tick on",
title: "indexed, and --lang selects it",
text: "✓",
}),
el("span", { class: "name", text: lang.name }),
has ? el("span", { class: "graph-mark", text: "graph" }) : null,
el("span", { class: "spacer" }),
el("span", { class: "exts", text: names.join(" ") }),
);
}),
),
says(
`All ${languages.length} are indexed and selectable with `,
mono("--lang"),
edges.size === languages.length
? ", and every one marked "
: `. The ${edges.size} marked `,
mono("graph"),
edges.size === languages.length
? " carries symbols and edges as well, from a tree-sitter grammar. "
: " carry symbols and edges as well, from a tree-sitter grammar; the rest are searched as text. ",
mono("semlith languages"),
" prints the same table. Extension and filename decide the language — file contents are never read to guess it, because a store is searched far more often than it is built. Those grammars are most of what the binary weighs, and no language was dropped to hit a size.",
),
);
}
async function aboutView() {
let about;
let models;
let languages;
try {
[about, models, languages] = await Promise.all([
api("/api/about"),
api("/api/models"),
api("/api/languages"),
]);
} catch (e) {
return el("div", { class: "view" }, pageHead("About"), error(e.message));
}
const row = (key, value) =>
el(
"div",
{ class: "kv pair" },
el("span", { class: "k", text: key }),
el("span", { class: "v", text: value }),
);
const list = (models && models.models) || [];
const table = dataTable({
className: "w-models",
caption: "Every embedding model a store can be built with, and which of them this machine has fetched.",
sort: "name",
perPage: 10,
rows: list,
columns: [
{
key: "name",
label: "Model",
className: "path",
value: (m) => m.name,
render: (m) =>
el(
"div",
{ class: "rows tight" },
el("span", { class: "one-line", "data-tip": m.name, text: m.name }),
m.code
? el("span", { class: "meta one-line", "data-tip": m.code, text: m.code })
: null,
),
},
{ key: "dim", label: "Dims", className: "num", value: (m) => m.dim, render: (m) => String(m.dim) },
{
key: "bytes",
label: "Size",
className: "num",
value: (m) => m.bytes || 0,
empty: (m) => !m.bytes,
render: (m) => (m.bytes ? bytes(m.bytes) : "—"),
},
{ key: "description", label: "Note", className: "meta", value: (m) => m.description },
],
});
return el(
"div",
{ class: "view" },
pageHead("About", "One Rust binary. The portal you are reading is compiled into it."),
el(
"div",
{ class: "grid two" },
el(
"div",
{ class: "rows" },
el(
"div",
{ class: "card pad" },
row("Version", `${about.version} · store format ${about.format_version}`),
row("Binary", `${about.binary} · ${bytes(about.binary_bytes)} · ${about.target}`),
row("Bound to", about.bind),
row("Store home", about.store_home),
row("Model cache", about.model_cache),
row("Uptime", `${Math.floor(about.uptime / 60)}m · pid ${about.pid}`),
),
langCard(languages.languages || [], about.graph_languages || []),
),
list.length ? table.node : el("div", { class: "card pad" }, empty("No model is listed.")),
),
el("p", {
class: "subtitle",
text: "The model is fixed when a store is created — vectors from two models are not comparable. To switch, delete the store and index again.",
}),
);
}
function welcomeView() {
const note = el("div", { class: "note" });
const field = el("input", { type: "text", placeholder: "~/Documents/work" });
const step = (num, title, what) =>
el(
"div",
{ class: "step" },
el("span", { class: "eyebrow", text: num }),
el("span", { class: "card-title", text: title }),
el("span", { class: "what", text: what }),
);
return el(
"div",
{ class: "welcome" },
el("div", { class: "lockup" }, logoImage(38), el("span", { class: "name", text: "Semlith" })),
el(
"div",
{ class: "hero" },
el(
"div",
{ class: "titles" },
el("h1", { class: "hero-title", text: "No stores yet" }),
el("p", {
class: "subtitle",
text: "The daemon is running and holds nothing. Point it at a folder and it indexes everything a person could read in there — code, Markdown, PDFs, Office files, notebooks, books and mail.",
}),
),
el(
"div",
{ class: "field tall" },
el("span", { class: "prefix", text: "path" }),
labelled("welcome-path", "Folder to index", field),
),
note,
el(
"div",
{ class: "actions" },
el("button", {
class: "button",
type: "button",
text: "Index this folder",
onclick: () => {
const path = field.value.trim();
if (!path) {
note.className = "note bad";
note.textContent = "Give a path first, or open the picker instead.";
field.focus();
return;
}
state.pendingPath = path;
go("index");
},
}),
el("button", {
class: "button secondary",
type: "button",
text: "Open the folder picker",
onclick: () => go("index"),
}),
el("button", {
class: "button ghost",
type: "button",
text: "Skip for now",
onclick: () => go("about"),
}),
),
el("hr", { class: "rule" }),
el("span", { class: "eyebrow", text: "Or from a terminal" }),
copyField("semlith index ~/Documents/work"),
says(
"No ",
mono("--store"),
" flag. It lands in ",
mono("~/.semlith/stores/work"),
" and is registered against that root.",
),
),
el(
"div",
{ class: "steps" },
step("01", "Index", "One pass over the folder. Only what changed is re-embedded on the next."),
step("02", "Stays current", "The daemon watches the roots and re-embeds on save."),
step("03", "Ask", "From here, the CLI, or any agent over MCP — the same fused search."),
),
el("div", { class: "foot", text: "127.0.0.1 · loopback only · no external asset" }),
);
}
function mapPanel() {
const body = el("div", { class: "map-body" }, el("div", { class: "rail-hint", text: "Grouping…" }));
const shown = el("div", { class: "map-shown" });
(async () => {
let data;
try {
data = await api("/api/map");
} catch (e) {
fill(body, error(e.message));
return;
}
const list = data.communities || [];
if (!list.length) {
fill(body, el("div", { class: "rail-hint", text: "No call or import edges to group yet." }));
return;
}
fill(
body,
list.map((community) =>
el(
"button",
{
class: "map-row",
type: "button",
onclick: () => {
state.pendingQuery = community.label;
go("search");
},
},
el(
"span",
{ class: "line one" },
el("span", { class: "label", text: community.label }),
el("span", { class: "size", text: `${community.size} symbol${community.size === 1 ? "" : "s"}` }),
),
el("span", {
class: "line hubs",
text: `hubs: ${community.hubs
.map((hub) => (hub.path ? `${hub.name} · ${shortPath(hub.path)}:${hub.line}` : hub.name))
.join(" · ")}`,
}),
el("span", {
class: "line cross",
text: community.cross
? `${community.label} → ${community.cross[0]} · ${community.cross[1]} settled edge${community.cross[1] === 1 ? "" : "s"}`
: "nothing leaves this group",
}),
),
),
);
fill(
shown,
el("span", {
text: `Shown ${data.shown} of ${data.total} · communities over settled calls and imports edges`,
}),
);
})();
return el(
"section",
{ class: "map-panel" },
el("div", { class: "card-head" }, el("h2", { text: "Map" }), el("span", { class: "mono-chip", text: "subsystems" })),
body,
shown,
);
}
function capLabel(text) {
return el("span", { class: "eyebrow", text });
}
function statCell(label, value) {
return el("div", { class: "impact-stat" }, capLabel(label), el("span", { class: "n", text: String(value) }));
}
async function impactView() {
await refreshStores();
const results = el("div", { class: "impact-results" });
const subject = el("span", { class: "impact-subject", text: "—" });
const depthPill = el("span", { class: "pill warn", text: "depth 3 · reverse" });
const nameInput = el("input", {
type: "search",
placeholder: "A symbol's name, matched exactly",
"aria-label": "Symbol",
onkeydown: (e) => {
if (e.key === "Enter") run();
},
});
if (state.impactSymbol) nameInput.value = state.impactSymbol;
const depthInput = el("input", {
type: "number",
min: "1",
max: "10",
value: "3",
"aria-label": "Hops",
class: "hops",
});
let allEdges = false;
const verified = chipToggle("Prefer verified edges", true, (on) => {
allEdges = !on;
if (nameInput.value.trim()) run();
});
const go = el("button", { class: "button small", type: "button", text: "Reach", onclick: () => run() });
function reachedRow(row) {
return el(
"div",
{ class: "impact-row" },
el("span", { class: "sym", text: row.name }),
el("span", { class: "where", text: `${shortPath(row.path)}:${row.line}` }),
el(
"span",
{ class: "via" },
el("span", { class: "one-line", text: `${row.via} · ${row.kind}` }),
confidenceBadge(row.confidence),
),
el("span", { class: "hops-n", text: String(row.hop) }),
);
}
async function run() {
const name = nameInput.value.trim();
state.impactSymbol = name;
if (!name) {
subject.textContent = "—";
fill(results, el("p", { class: "subtitle", text: "Name a symbol to read the graph backwards from it." }));
return;
}
const depth = Math.min(10, Math.max(1, Number(depthInput.value) || 3));
depthInput.value = String(depth);
subject.textContent = name;
depthPill.textContent = `depth ${depth} · reverse`;
fill(results, el("p", { class: "subtitle", text: "Reading…" }));
let data;
try {
const query = new URLSearchParams({ name, depth: String(depth) });
if (allEdges) query.set("all_edges", "1");
data = await api(`/api/impact?${query}`);
} catch (e) {
fill(results, error(e.message));
return;
}
const impact = data.impact;
if (!impact || !impact.reached.length) {
fill(
results,
el("p", { class: "headline", text: data.headline || "nothing reaches that name" }),
impact && impact.definitions.length
? el("p", { class: "note", text: "The symbol is indexed; nothing in an open store calls it." })
: el("p", {
class: "note",
text: "No definition of that name is in an open store. Check the spelling, or index the repository that holds it.",
}),
);
return;
}
const inferred = impact.reached.filter((r) => r.confidence === "inferred" || r.confidence === "ambiguous").length;
const byHop = new Map();
for (const row of impact.reached) {
if (!byHop.has(row.hop)) byHop.set(row.hop, []);
byHop.get(row.hop).push(row);
}
fill(
results,
el("p", { class: "headline", text: data.headline }),
el(
"div",
{ class: "impact-stats" },
statCell("Reached", impact.reached.length),
statCell("Files", impact.files.length),
statCell("Unsettled", inferred),
),
allEdges
? el("p", {
class: "note hypothesis",
text: "Walked names with several definitions. Rows badged ambiguous are a hypothesis, not a finding.",
})
: null,
el(
"div",
{ class: "impact-head-row" },
capLabel("Reached symbol"),
capLabel("Where"),
capLabel("Via"),
capLabel("Hops"),
),
[...byHop.entries()].map(([hop, rows]) =>
el(
"section",
{ class: "impact-block" },
el(
"div",
{ class: "impact-hop" },
el("h2", { text: `${hop} hop${hop === 1 ? "" : "s"}` }),
el("span", { class: "muted", text: `${rows.length} definition${rows.length === 1 ? "" : "s"}` }),
),
rows.map(reachedRow),
),
),
el(
"section",
{ class: "impact-block impact-files" },
el("div", { class: "impact-hop" }, el("h2", { text: "Files" })),
impact.files.map((file) =>
el(
"div",
{ class: "impact-row" },
el("span", { class: "sym", text: shortPath(file.path) }),
el("span", { class: "where", text: `${file.symbols} definition${file.symbols === 1 ? "" : "s"}` }),
el("span", { class: "via", text: `nearest ${file.nearest} hop${file.nearest === 1 ? "" : "s"}` }),
),
),
),
impact.hidden
? el("p", { class: "note", text: `${impact.hidden} more left out at the limit of ${data.limit}.` })
: null,
);
}
fill(results, el("p", { class: "subtitle", text: "Name a symbol to read the graph backwards from it." }));
if (state.impactSymbol) run();
return el(
"div",
{ class: "view" },
pageHead("Impact", "Reverse reachability. What breaks if this changes — before the edit, not after the test run.", {
pill: el("span", { class: "mono-chip", text: "semlith_impact" }),
}),
el(
"div",
{ class: "impact-band" },
nameInput,
el("label", { class: "hops-label" }, el("span", { text: "hops" }), depthInput),
verified,
go,
),
el("div", { class: "impact-subject-row" }, capLabel("Changing"), subject, el("span", { class: "spacer" }), depthPill),
results,
pathFinderCard(),
traceCard(),
);
}
function pathFinderCard() {
const from = el("input", { type: "text", placeholder: "from", "aria-label": "Path from" });
const to = el("input", { type: "text", placeholder: "to", "aria-label": "Path to" });
const body = el("div", { class: "path-body" });
const query = el("span", { class: "mono-chip", text: "max 6 hops" });
let preferVerified = true;
let strict = false;
const verifiedChip = chipToggle("Prefer verified edges", true, (on) => {
preferVerified = on;
if (!on) strict = false;
strictChip.classList.toggle("on", strict);
strictChip.setAttribute("aria-pressed", String(strict));
run();
});
const strictChip = chipToggle("Strict", false, (on) => {
strict = on;
if (on) {
preferVerified = true;
verifiedChip.classList.add("on");
verifiedChip.setAttribute("aria-pressed", "true");
}
run();
});
async function run() {
const a = from.value.trim();
const b = to.value.trim();
if (!a || !b) {
fill(body, el("p", { class: "subtitle", text: "Name both ends of the chain." }));
return;
}
const allEdges = !preferVerified && !strict;
query.textContent = `${a} → ${b} · max 6 hops`;
fill(body, el("p", { class: "subtitle", text: "Walking…" }));
let data;
try {
const q = new URLSearchParams({ from: a, to: b, depth: "6" });
if (allEdges) q.set("all_edges", "1");
data = await api(`/api/path?${q}`);
} catch (e) {
fill(body, error(e.message));
return;
}
if (!data.path) {
fill(
body,
el(
"div",
{ class: "refusal" },
el("p", {
class: "mono",
text: allEdges
? "Not connected within 6 hops by any edge in the store"
: "Not connected within 6 hops by resolved edges",
}),
allEdges
? null
: el("button", {
class: "link",
type: "button",
text: "Show inferred chain",
onclick: () => {
preferVerified = false;
strict = false;
verifiedChip.classList.remove("on");
verifiedChip.setAttribute("aria-pressed", "false");
strictChip.classList.remove("on");
strictChip.setAttribute("aria-pressed", "false");
run();
},
}),
),
);
return;
}
fill(body, chainBlock(data.path, data.summary));
}
fill(body, el("p", { class: "subtitle", text: "Name both ends of the chain." }));
return el(
"section",
{ class: "card pad path-card" },
el("div", { class: "card-head" }, el("h2", { text: "Path finder" }), query),
el("div", { class: "path-band" }, from, to, verifiedChip, strictChip, el("button", { class: "button small", type: "button", text: "Walk", onclick: () => run() })),
body,
);
}
function chipToggle(label, on, onchange) {
const node = el("button", {
class: `chip-toggle${on ? " on" : ""}`,
type: "button",
text: label,
"aria-pressed": String(on),
onclick: () => {
const next = !node.classList.contains("on");
node.classList.toggle("on", next);
node.setAttribute("aria-pressed", String(next));
onchange(next);
},
});
return node;
}
function chainBlock(steps, summary) {
const rows = [];
steps.forEach((step, i) => {
rows.push(
el(
"div",
{ class: "hop" },
el("span", { class: "n", text: String(i + 1) }),
el("span", { class: "end", text: `${step.from} @ ${shortPath(step.from_path)}:${step.from_line}` }),
el("span", { class: "arrow", "aria-hidden": "true", text: "→" }),
el("span", { class: "end", text: `${step.to} @ ${shortPath(step.to_path)}:${step.to_line}` }),
el("span", { class: "kind", text: step.kind }),
confidenceBadge(step.confidence),
),
);
const next = steps[i + 1];
if (next && (next.from_path !== step.to_path || next.from_line !== step.to_line)) {
rows.push(
el("div", { class: "seam" }, `seam · ${step.to}: ${Math.max(2, step.definitions)} definitions · continues from ${next.from} @ ${shortPath(next.from_path)}:${next.from_line}`),
);
}
});
const s = summary || {};
let trailer = `${s.hops} hop${s.hops === 1 ? "" : "s"} · ${s.extracted} extracted · ${s.resolved} resolved · ${s.inferred} inferred · ${s.ambiguous} ambiguous`;
if (s.seams) {
const names = (s.ambiguous_names || []).map(([name, count]) => `${name}: ${count} definitions`).join(", ");
trailer += ` · ${s.seams} through ambiguous names (${names})`;
}
return [
...rows,
el("p", { class: "trailer mono", text: trailer }),
s.hypothesis
? el("p", {
class: "note hypothesis",
text: "A hypothesis, not a finding. Read the seam before you rely on it.",
})
: null,
];
}
function traceCard() {
const from = el("input", { type: "text", placeholder: "from", "aria-label": "Trace from" });
const to = el("input", { type: "text", placeholder: "to", "aria-label": "Trace to" });
const body = el("div", { class: "trace-body" });
let evidenceText = "";
const copy = copyButton(() => evidenceText, "Copy as evidence", true);
async function run() {
const a = from.value.trim();
const b = to.value.trim();
if (!a || !b) {
fill(body, el("p", { class: "subtitle", text: "Name both ends of the chain." }));
return;
}
fill(body, el("p", { class: "subtitle", text: "Reading…" }));
let data;
try {
data = await api(`/api/trace?${new URLSearchParams({ from: a, to: b, depth: "6" })}`);
} catch (e) {
fill(body, error(e.message));
return;
}
const trace = data.trace;
evidenceText = data.evidence || "";
const parts = [capLabel("Answer"), el("p", { class: "answer", text: trace.answer })];
if (trace.chain && trace.chain.steps.length) {
parts.push(capLabel("Chain"));
parts.push(...chainBlock(trace.chain.steps, trace.chain.summary));
parts.push(capLabel("Supporting lines"));
for (const line of trace.lines) {
parts.push(
el(
"div",
{ class: "support" },
el("code", { text: line.at ? `${line.at} ${line.code}` : "no call line recorded" }),
el(
"div",
{ class: "support-foot" },
el("span", { class: "hop-of", text: line.hop }),
el("span", {
class: `mark ${line.mark.startsWith("candidate") ? "candidate" : "fact"}`,
text: line.mark,
}),
),
),
);
}
}
fill(body, ...parts);
}
fill(body, el("p", { class: "subtitle", text: "Name both ends of the chain." }));
return el(
"section",
{ class: "card pad trace-card" },
el(
"div",
{ class: "card-head" },
el("h2", { text: "Trace" }),
el("span", { class: "mono-chip", text: "evidence view" }),
el("span", { class: "spacer" }),
copy,
),
el("div", { class: "path-band" }, from, to, el("button", { class: "button small", type: "button", text: "Trace", onclick: () => run() })),
body,
);
}
const REPORTS = [
[
"savings",
"Retrieval savings",
"What was not read, what that would have cost, and how much of the ledger the figure covers.",
],
["access", "AI access audit", "Which agents read this corpus, how often, and what they missed."],
["change", "Change brief", "What this machine re-read in the last seven days."],
["health", "Index health", "What the index holds and what the graph made of it."],
["gaps", "Knowledge gaps", "Asked and not answered, called and not found, and the names that mislead."],
];
const REPORT_FORMATS = [
["markdown", "Markdown", "md", "text/markdown"],
["csv", "CSV", "csv", "text/csv"],
["json", "JSON", "json", "application/json"],
["html", "HTML", "html", "text/html"],
];
async function reportsView() {
await refreshStores();
let model = MODEL_PRICES[0];
const preview = el("div", { class: "report-preview" });
const previewHead = el("div", { class: "card-head" });
async function show(kind, title) {
fill(previewHead, el("h2", { text: title }), el("span", { class: "mono-chip", text: "generated here" }));
fill(preview, el("p", { class: "subtitle", text: "Generating…" }));
let data;
try {
data = await api(
`/api/report?${new URLSearchParams({ kind, format: "markdown", model: model[0] })}`,
);
} catch (e) {
fill(preview, error(e.message));
return;
}
fill(preview, el("pre", { class: "report-text", text: data.text }));
}
async function save(kind, format) {
const [name, , extension, type] = REPORT_FORMATS.find(([id]) => id === format);
let data;
try {
data = await api(`/api/report?${new URLSearchParams({ kind, format, model: model[0] })}`);
} catch (e) {
fill(preview, error(e.message));
return;
}
offerDownload(`semlith-${kind}.${extension}`, data.text, type);
void name;
}
const modelPick = el(
"select",
{
class: "chip",
"aria-label": "Price tokens at",
onchange: (e) => {
model = MODEL_PRICES[Number(e.currentTarget.value)] || MODEL_PRICES[0];
},
},
MODEL_PRICES.map(([name], i) =>
el("option", { value: String(i), text: `prices at ${name}` }),
),
);
return el(
"div",
{ class: "view" },
pageHead(
"Reports",
"Generated from this machine's ledger, index and graph. Nothing leaves the machine.",
{ pill: el("span", { class: "mono-chip", text: "semlith_report" }) },
),
el("div", { class: "filters" }, modelPick, el("span", {
class: "muted",
text: "PDF is your browser's print of the HTML — no PDF writer ships in the binary.",
})),
el(
"div",
{ class: "grid two" },
REPORTS.map(([kind, title, what]) =>
el(
"section",
{ class: "card pad report-card" },
el("span", { class: "card-title", text: title }),
el("p", { class: "subtitle", text: what }),
el(
"div",
{ class: "filters" },
el("button", {
class: "button small",
type: "button",
text: "Generate",
onclick: () => show(kind, title),
}),
REPORT_FORMATS.map(([id, label]) =>
el("button", {
class: "button secondary small",
type: "button",
text: label,
onclick: () => save(kind, id),
}),
),
),
copyField(`semlith report ${kind} --format markdown`),
),
),
),
el("section", { class: "card pad" }, previewHead, preview),
);
}
const CLOUD_ROWS = [
[
"One URL for cloud agents.",
"Claude Code cloud sessions, routines and CI cannot reach a laptop; they can reach an org store.",
],
[
"The whole organisation in one index.",
"Cross-repository paths, and documents beside code.",
],
[
"A pull-request check that states what the graph proves,",
"with no model and no guess.",
],
];
async function cloudView() {
return el(
"div",
{ class: "view" },
pageHead("Cloud", null, { pill: pill("not connected", null) }),
el("p", {
class: "subtitle",
text: "Semlith Cloud is one hosted store for a whole organisation: every connected repository a root of it, indexed on push, served over MCP to any agent, with a pull-request impact check and a team ledger. This binary works without it. Nothing here contacts a server.",
}),
el(
"div",
{ class: "card pad" },
el(
"div",
{ class: "cloud-rows" },
CLOUD_ROWS.map(([lead, rest]) =>
el(
"p",
{ class: "cloud-row" },
el("b", { text: lead }),
el("span", { text: ` ${rest}` }),
),
),
),
el("div", { class: "cloud-block" }, copyField("semlith cloud login"), el("p", {
class: "subtitle",
text: "Not in this release. When it arrives it will store an org token under ~/.semlith/ and send it to that host and no other.",
})),
el("div", { class: "cloud-block" }, copyField("semlith cloud connect acme"), el("p", {
class: "subtitle",
text: "Will add the org's store to this machine's registry as a remote store, listed beside the local ones with a remote badge.",
})),
el("p", {
class: "note",
text: "This build has no cloud command and opens no connection to any host. The Privacy page's own reading is where to check that rather than take it from here.",
}),
),
);
}
const RENDER = {
stores: storesView,
files: filesView,
index: indexView,
search: searchView,
agents: agentsView,
privacy: privacyView,
doctor: doctorView,
about: aboutView,
graph: graphView,
ledger: ledgerView,
impact: impactView,
reports: reportsView,
cloud: cloudView,
};
function go(id) {
if (shell.main && window.matchMedia(NARROW).matches && state.navOpen) {
setNav(false);
}
if ((location.hash || "").slice(1) === id) render();
else location.hash = `#${id}`;
}
function isDark() {
const chosen = document.documentElement.getAttribute("data-theme");
if (chosen) return chosen === "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
function logoImage(size) {
return el("img", {
class: "logo",
src: isDark() ? "logo-dark.svg" : "logo.svg",
alt: "",
width: size,
height: size,
});
}
const THEMES = ["system", "light", "dark"];
function theme(next) {
if (next === "system") {
document.documentElement.removeAttribute("data-theme");
} else {
document.documentElement.setAttribute("data-theme", next);
}
state.theme = next;
try {
if (next === "system") localStorage.removeItem("semlith-theme");
else localStorage.setItem("semlith-theme", next);
} catch (_) {
}
for (const img of document.querySelectorAll("img.logo")) {
img.src = isDark() ? "logo-dark.svg" : "logo.svg";
}
paintThemeButton();
}
function paintThemeButton() {
const button = shell.themeButton;
if (!button) return;
const next = THEMES[(THEMES.indexOf(state.theme || "system") + 1) % THEMES.length];
const label = {
system: "Follow the system theme",
light: "Switch to light",
dark: "Switch to dark",
}[next];
const glyph = { system: ICONS.monitor, light: ICONS.sun, dark: ICONS.moon }[next];
fill(button, icon(glyph));
button.setAttribute("aria-label", label);
button.setAttribute("title", label);
}
const shell = {};
function buildShell() {
const hamburger = el(
"button",
{
class: "hamburger",
type: "button",
"aria-label": "Toggle navigation",
"aria-expanded": String(state.navOpen),
onclick: () => setNav(!state.navOpen, true),
},
icon(ICONS.menu, 17),
);
const pageTitle = el("span", { class: "page-title" });
const themeButton = el("button", {
class: "icon-button",
type: "button",
onclick: () =>
theme(THEMES[(THEMES.indexOf(state.theme || "system") + 1) % THEMES.length]),
});
shell.themeButton = themeButton;
paintThemeButton();
const topbar = el(
"div",
{ class: "topbar" },
hamburger,
el(
"button",
{
class: "brand",
type: "button",
"aria-label": "Semlith — go to Stores",
onclick: () => go("stores"),
},
logoImage(26),
el("span", { class: "wordmark", text: "Semlith" }),
),
el("span", { class: "divider-v" }),
pageTitle,
el("button", {
class: "pill good run-count",
type: "button",
hidden: true,
title: "Go to Index",
onclick: () => go("index"),
}),
el("span", { class: "spacer" }),
el(
"button",
{
class: "search-launcher",
type: "button",
"aria-label": "Search the index",
onclick: () => go("search"),
},
icon(ICONS.search, 15),
el("span", { class: "what", text: "Ask the index a question" }),
el("span", { class: "key-hint", text: "/" }),
),
themeButton,
);
const groups = [];
for (const view of VIEWS) {
const last = groups[groups.length - 1];
if (last && last.label === view.group) last.items.push(view);
else groups.push({ label: view.group, items: [view] });
}
const rail = el(
"nav",
{ class: "rail", "aria-label": "Sections" },
groups.map((group, i) =>
el(
"div",
{ class: "rail-group" },
group.items.map((view) =>
el(
"button",
{
class: "rail-item",
type: "button",
"data-view": view.id,
"aria-label": view.label,
title: view.label,
onclick: () => go(view.id),
},
icon(NAV_ICONS[view.id], 17),
),
),
i < groups.length - 1 ? el("div", { class: "rail-rule" }) : null,
),
),
el("span", { class: "spacer" }),
el("span", { class: "live-dot", title: "semlith start" }),
);
const sidebar = el(
"nav",
{ class: "sidebar", "aria-label": "Sections" },
groups.map((group) =>
el(
"div",
{ class: "nav-group" },
el("div", { class: "nav-group-label", text: group.label }),
group.items.map((view) =>
el(
"button",
{
class: "nav-item",
type: "button",
"data-view": view.id,
onclick: () => go(view.id),
},
icon(NAV_ICONS[view.id], 16),
el("span", { text: view.label }),
),
),
),
),
el("span", { class: "spacer" }),
el(
"div",
{ class: "daemon-card" },
el("div", { class: "who" }, el("span", { class: "live-dot" }), mono("semlith start")),
el("div", { class: "fact", text: `${location.host} · sole writer` }),
el("div", { class: "fact", id: "daemon-stores", text: "" }),
),
);
const scrim = el("button", {
class: "scrim",
type: "button",
"aria-label": "Close navigation",
hidden: true,
onclick: () => setNav(false, true),
});
const main = el("main", {});
const body = el("div", { class: "body" }, rail, sidebar, scrim, main);
Object.assign(shell, { topbar, pageTitle, rail, sidebar, scrim, main, hamburger });
return el("div", { id: "app" }, topbar, body);
}
function setNav(open, fromUser) {
const was = state.navOpen;
state.navOpen = open;
const narrow = window.matchMedia(NARROW).matches;
shell.hamburger.setAttribute("aria-expanded", String(open));
shell.sidebar.hidden = !open;
shell.rail.hidden = open || narrow;
shell.scrim.hidden = !(open && narrow);
if (!fromUser || !narrow || open === was) return;
if (open) {
const first = shell.sidebar.querySelector(".nav-item");
if (first) first.focus();
} else {
shell.hamburger.focus();
}
}
function markCurrent(current) {
for (const node of document.querySelectorAll("[data-view]")) {
if (node.getAttribute("data-view") === current) node.setAttribute("aria-current", "page");
else node.removeAttribute("aria-current");
}
}
let renderGeneration = 0;
async function render() {
const current = (location.hash || "#stores").slice(1);
const root = document.getElementById("root");
if (!state.stores.length && current !== "index" && current !== "about") {
fill(root, welcomeView());
shell.main = null;
return;
}
if (!shell.main || !root.contains(shell.main)) {
fill(root, buildShell());
setNav(state.navOpen);
}
resetLive();
state.onRuns = null;
const view = VIEWS.find((v) => v.id === current) || VIEWS[0];
shell.pageTitle.textContent = view.title;
document.title = `Semlith · ${view.title}`;
markCurrent(view.id);
paintStoreCount();
paintRunCount();
const mine = ++renderGeneration;
fill(shell.main, el("div", { class: "view" }, el("p", { class: "subtitle", text: "Loading…" })));
try {
const node = await (RENDER[view.id] || RENDER.stores)();
if (mine !== renderGeneration) return;
fill(shell.main, node);
} catch (e) {
if (mine !== renderGeneration) return;
fill(shell.main, el("div", { class: "view" }, error(e.message)));
}
}
async function boot() {
let saved = null;
try {
saved = localStorage.getItem("semlith-theme");
} catch (_) {
}
if (saved === "dark" || saved === "light") {
theme(saved);
} else {
state.theme = "system";
}
wireTips();
await refreshStores();
api("/api/agents").then(noteAgents).catch(() => {});
refreshRuns().catch(() => {});
await render();
startLive();
window.addEventListener("hashchange", render);
window.matchMedia(NARROW).addEventListener("change", (e) => {
if (shell.main) setNav(!e.matches);
});
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && state.navOpen && window.matchMedia(NARROW).matches && shell.main) {
setNav(false, true);
return;
}
if (e.key !== "/" || e.target.matches("input, textarea, select")) return;
e.preventDefault();
if ((location.hash || "").slice(1) === "search") {
const box = document.querySelector(".search-field input");
if (box) box.focus();
return;
}
go("search");
});
}
boot();