"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 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);
}
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",
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",
};
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 },
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) => {
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 row = el(
"button",
{
class: "entry",
type: "button",
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 }),
);
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 },
el("bdi", { text: value }),
);
}
function lineCell(value, className) {
return el("span", {
class: className ? `one-line ${className}` : "one-line",
"data-tip": 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: "",
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: "index", label: "Index", title: "Index" },
{ group: "Explore", id: "search", label: "Search", title: "Search" },
{ group: "Explore", id: "graph", label: "Graph", title: "Graph" },
{ group: "Operate", id: "agents", label: "Agents", title: "Agents" },
{ group: "Operate", id: "ledger", label: "Ledger", title: "Retrieval ledger" },
{ group: "Operate", id: "privacy", label: "Privacy", title: "Privacy" },
{ group: "Operate", id: "doctor", label: "Doctor", title: "Doctor" },
{ group: "About", 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 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 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 || [];
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();
},
filter(kinds) {
drawn = !kinds || !kinds.size ? edges : edges.filter((e) => kinds.has(e.kind));
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 }),
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";
},
});
function paintPause() {
pause.textContent = canvas.running() ? "Pause" : "Settled";
}
function counts() {
const { nodes, edges } = canvas.counts();
meta.textContent = `${n(nodes)} symbols · ${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(
`/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(`/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`,
}),
),
),
ends("Callers", data.callers.map(edge), "Nothing in the graph calls this."),
ends(
"Callees",
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(`/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("a", {
class: "button secondary small",
href: "#search",
text: "Chunks it lives in",
onclick: (e) => {
e.preventDefault();
state.pendingQuery = node.name;
go("search");
},
}),
),
);
}
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();
const centre = params && params.name;
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" });
}
const scopeInput = el("input", {
type: "search",
placeholder: "Scope to a path, or find a symbol",
onkeydown: (e) => {
if (e.key !== "Enter") return;
const value = e.currentTarget.value.trim();
if (!value) return load({});
load(value.includes("/") || value.includes(".") ? { path: value } : { name: value });
},
});
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);
counts();
},
}),
);
const storeChips = state.stores.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), pause] },
),
el(
"div",
{ class: "graph-controls" },
el(
"div",
{ class: "graph-scope" },
icon(ICONS.search, 16),
labelled("graph-scope", "Scope the graph", scopeInput),
),
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,
),
rail,
),
);
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",
`${data.queries ? Math.round(((data.queries - (data.credited || 0)) * 100) / data.queries) : 0}%`,
"queries the corpus could not answer — recorded, and credited nothing",
),
stat("Tier", data.tier || "modelled", "modelled · measured — measured when the store's own tokenizer counted it"),
),
clientBreakdown(data.by_client),
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. Nothing here needs a key.",
}),
),
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 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",
}),
);
}
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;
}
},
});
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",
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"),
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: "last_write",
label: "Last write",
value: (s) => s.last_write || 0,
render: (s) => {
if (s.unopened) return pill("not opened", "warn", { title: s.unopened });
if (!s.watching) return pill("not watching", "warn");
return s.last_write ? pill(when(s.last_write), "good") : pill("never");
},
},
{
key: "open",
label: "",
className: "row-end",
sortable: false,
render: (s) =>
rowMenu(() => [
{ label: "Open in Files", onclick: () => 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), `${n(totals.readers)} readers in use`),
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) }),
lineCell(e.text, "what"),
),
),
)
: empty("Nothing has changed on disk since the daemon started."),
),
agentsCard(),
),
),
);
}
async function filesView() {
const chosenExt = new Set();
const summary = el("span", { class: "pill" });
const holder = el("div", {});
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();
load();
} catch (e) {
bulkNote.className = "note bad";
bulkNote.textContent = e.message;
throw e;
}
}
const picked = new Map();
const bulkNote = el("div", { class: "note" });
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`,
}),
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();
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",
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",
sortable: false,
render: (f) => f.store,
},
{
key: "reader",
label: "Read as",
className: "narrow-drop",
sortable: false,
render: (f) => el("span", { class: "tag", text: f.reader }),
},
{
key: "lang",
label: "Language",
className: "meta narrow-drop",
sortable: false,
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);
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) {
fill(
holder,
el(
"div",
{ class: "card pad" },
empty("Nothing indexed matches that. The filter, not the corpus — clear it and look again."),
),
);
return;
}
table.update(data.files, data.total);
if (!holder.contains(table.node)) fill(holder, table.node);
}
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,
),
bulkBar,
bulkNote,
holder,
el("p", {
class: "subtitle",
text: "Forget drops the file's chunks and vectors. The file on disk is untouched.",
}),
);
}
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 state.stores) 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 = {
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),
);
const langField = el("input", { class: "bare", size: "8", placeholder: "any", onchange: () => run() });
const pathField = el("input", { class: "bare", size: "8", placeholder: "any", 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", 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 it something",
oninput: () => {
if (!input.value.trim()) shapeHint.hidden = true;
},
onkeydown: (e) => {
if (e.key === "Enter") run();
},
});
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;
}
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…";
let data;
try {
data = await api(`/api/search?${params}`);
} catch (e) {
if (mine !== generation) return;
fill(results, error(e.message));
showBody(null, "");
fill(footer);
meta.textContent = "";
return;
}
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;
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,
langDial,
pathDial,
budgetDial,
el("span", { class: "dial-note", text: "what one answer may cost an agent" }),
),
),
el(
"div",
{ class: "two-stage" },
el("div", { class: "locate-col" }, results, footer),
el(
"div",
{ class: "body-col" },
bodyCard,
aroundHead,
stage,
),
),
);
}
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 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" });
let shown = 0;
let readAt = 0;
let ticking = false;
function paintClock() {
const ms = ticking ? shown + (Date.now() - readAt) : shown;
elapsed.textContent = 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 rate = next.elapsed_ms ? Math.round((next.chunks / next.elapsed_ms) * 1000) : 0;
status.textContent = next.total
? `${n(next.scanned)}/${n(next.total)} files · ${n(next.chunks)} chunks${
rate ? ` · ${n(rate)} chunks/s` : ""
} ·`
: RUN_WORD[next.status] || next.status;
where.textContent = (next.paths || []).join(", ");
const held = next.status === "paused";
pause.textContent = held ? "Resume" : "Pause";
pause.hidden = !TICKING.has(next.status) || next.status === "queued";
stop.textContent = next.status === "queued" ? "Remove" : "Stop";
stop.hidden = !TICKING.has(next.status);
stop.disabled = next.status === "stopping";
}
pause.addEventListener("click", () => controls.hold(pause.textContent === "Pause"));
stop.addEventListener("click", () => controls.stop(stop.textContent === "Remove"));
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" }),
pause,
stop,
),
where,
log,
);
absorb(run);
const clock = setInterval(() => {
if (!node.isConnected) return clearInterval(clock);
if (ticking) paintClock();
}, 1000);
return { node, absorb, log };
}
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";
why.textContent = `Above the ${limit.derived} this machine derived — ${limit.reason}. It will be used as you set it.`;
return;
}
why.className = "note";
why.textContent = fixed
? `Set by the environment, so the page cannot change it. Derived here: ${limit.derived} — ${limit.reason}`
: `${limit.derived} derived — ${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 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) {
try {
await post("/api/index/control", { store, action });
} catch (e) {
complain(e.message);
}
await refreshRuns();
}
async function catchUpLog(run, card) {
const after = cursors.get(run.store);
const from = after === undefined ? run.log_from : after;
let data;
try {
data = await api(
`/api/index/log?store=${encodeURIComponent(run.store)}${
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.store, data.cursor);
}
function paint(data) {
const runs = data?.runs || [];
const seen = new Set();
for (const run of runs) {
seen.add(run.store);
let card = drawn.get(run.store);
if (!card) {
card = runCard(run, {
hold: (wantPause) => control(run.store, wantPause ? "pause" : "resume"),
stop: (queued) => {
if (queued) return control(run.store, "dequeue");
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"),
});
},
});
drawn.set(run.store, card);
cards.append(card.node);
} else {
card.absorb(run);
}
catchUpLog(run, card);
}
for (const [store, card] of drawn) {
if (seen.has(store)) continue;
card.node.remove();
drawn.delete(store);
cursors.delete(store);
}
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: "Remove",
onclick: () => control(row.store, "dequeue"),
}),
),
),
),
);
}
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" }),
state.stores.map((store) =>
el("option", { value: store.name, text: `add to ${store.name}` }),
),
);
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();
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);
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 = "";
const failed = await begin("/api/add", {
url,
store: target.value === "each" ? undefined : target.value,
});
if (failed) {
urlNote.className = "note bad";
urlNote.textContent = failed;
urlCard.hidden = false;
}
});
paint(first);
state.onRuns = paint;
watchLive(["runs", "stores"], refreshRuns);
return el(
"div",
{ class: "view" },
pageHead("Index"),
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: "actions" }, addButton),
urlNote,
),
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("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(
"span",
{ class: "entry" },
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."),
),
);
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 clients = dataTable({
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: "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: async () => {
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",
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 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 secondary 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" }),
el("p", {
class: "subtitle",
text: "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 `semlith setup --register-all`.",
}),
el("div", { class: "head" }, preview, write),
planBox,
planNote,
);
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,
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,
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 · about ${n(
Math.ceil((data.tool_list_bytes || 0) / 4),
)} tokens per session`,
}),
el("span", {
class: "cost-note",
text: "read once, before the agent asks anything",
}),
),
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({
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),
),
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) ? "all holding" : "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: "Scan" }),
el("span", { class: "spacer" }),
scanButton,
),
el("p", {
class: "subtitle",
text: "Every file a store is still holding 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",
sort: "name",
perPage: 10,
rows: list,
columns: [
{
key: "name",
label: "Model",
className: "path",
value: (m) => m.name,
render: (m) => el("span", { class: "one-line", "data-tip": m.name, text: m.name }),
},
{ 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,
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" }),
);
}
const RENDER = {
stores: storesView,
files: filesView,
index: indexView,
search: searchView,
agents: agentsView,
privacy: privacyView,
doctor: doctorView,
about: aboutView,
graph: graphView,
ledger: ledgerView,
};
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,
});
}
function theme(next) {
document.documentElement.setAttribute("data-theme", next);
state.theme = next;
try {
localStorage.setItem("semlith-theme", next);
} catch (_) {
}
for (const img of document.querySelectorAll("img.logo")) {
img.src = next === "dark" ? "logo-dark.svg" : "logo.svg";
}
paintThemeButton();
}
function paintThemeButton() {
const button = shell.themeButton;
if (!button) return;
const toDark = !isDark();
fill(button, icon(toDark ? ICONS.moon : ICONS.sun));
button.setAttribute("aria-label", toDark ? "Switch to dark" : "Switch to light");
button.setAttribute("title", toDark ? "Switch to dark" : "Switch to light");
}
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(isDark() ? "light" : "dark"),
});
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 = isDark() ? "dark" : "light";
}
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();