kaizen-cli 0.1.45

Distributable agent observability: real-time-tailable sessions, agile-style retros, and repo-level improvement (Cursor, Claude Code, Codex). SQLite, redact before any sync you enable.
Documentation
import { clock, count, dateTime, duration, label, money, shortId, statusExplanation, statusLabel } from "./kaizen-format.js";

const $ = selector => document.querySelector(selector);
const MAX_ITEMS = 40;

export function renderDetail(report) {
  const detail = report?.selected;
  const summary = report?.sessions?.find(row => row.id === detail?.session?.id);
  if (!detail) return renderEmpty();
  $("#selected-session").textContent = shortId(detail.session.id);
  $("#detail-facts").replaceChildren(...facts(detail.session, summary));
  renderPrompt(detail.prompt);
  renderEvents(detail.events || []);
  renderSpans(detail.spans || []);
  renderSimple("#detail-files", detail.files || [], "No files recorded.");
  renderTools(summary?.top_tools || []);
}

function renderEmpty() {
  $("#selected-session").textContent = "No session selected";
  $("#detail-facts").replaceChildren(...fact("Status", "Waiting for data"));
  renderPrompt(null);
  renderSimple("#detail-events", [], "No events available.");
  renderSimple("#detail-spans", [], "No spans available.");
  renderSimple("#detail-files", [], "No files recorded.");
  renderSimple("#detail-tools", [], "No tools recorded.");
}

function facts(session, summary) {
  const status = summary?.status || String(session.status).toLowerCase();
  return [
    ...fact("Agent", label(session.agent)),
    ...fact("Model", session.model || "Unknown"),
    ...fact("Started", dateTime(session.started_at_ms)),
    ...fact("Duration", duration(session.started_at_ms, session.ended_at_ms)),
    ...fact("Status", statusLabel(status)),
    ...(statusExplanation(status) ? fact("Status note", statusExplanation(status)) : []),
    ...fact("Cost", money(summary?.cost_usd_e6)),
    ...fact("Errors", count(summary?.error_count)),
  ];
}

function renderPrompt(prompt) {
  $("#detail-prompt").textContent = prompt || "Prompt unavailable for this session.";
}

function fact(name, value) {
  return [node("dt", name), node("dd", value)];
}

function renderEvents(events) {
  const recent = events.slice(-MAX_ITEMS).reverse();
  renderList("#detail-events", recent, eventRow, "No events available.");
}

function eventRow(event) {
  const row = node("li");
  row.append(node("time", clock(event.ts_ms)), node("strong", label(event.kind)));
  row.append(node("span", event.tool || label(event.source)));
  if (event.payload?.summary) row.append(node("code", event.payload.summary));
  return row;
}

function renderSpans(spans) {
  const rows = flatten(spans).slice(0, MAX_ITEMS);
  renderList("#detail-spans", rows, spanRow, "No spans available.");
}

function spanRow(entry) {
  const span = entry.node.span || {};
  const row = node("li");
  row.className = "span-row";
  row.style.setProperty("--depth", entry.depth);
  row.append(node("strong", span.tool || "Unknown tool"));
  const status = String(span.status || "").toLowerCase() === "orphaned" ? "No result event" : label(span.status);
  row.append(node("span", `${status} | ${span.lead_time_ms || 0} ms`));
  return row;
}

function flatten(nodes, depth = 0) {
  return nodes.flatMap(item => [
    { node: item, depth },
    ...flatten(item.children || [], depth + 1),
  ]);
}

function renderTools(tools) {
  const rows = tools.map(([tool, calls]) => `${tool}: ${count(calls)} calls`);
  renderSimple("#detail-tools", rows, "No tools recorded.");
}

function renderSimple(selector, values, emptyText) {
  renderList(selector, values.slice(0, MAX_ITEMS), value => node("li", value), emptyText);
}

function renderList(selector, values, render, emptyText) {
  const target = $(selector);
  const rows = values.length ? values.map(render) : [empty(emptyText)];
  target.replaceChildren(...rows);
}

function empty(text) {
  const row = node("li", text);
  row.className = "empty-note";
  return row;
}

function node(tag, text = "") {
  return Object.assign(document.createElement(tag), { textContent: text });
}