import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid"; import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid"; import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid"; import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid"; import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid"; import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid"; import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid"; import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid"; /** @jsxImportSource @opentui/solid */ // @ts-nocheck // AFT sidebar slot. Header with "AFT" badge + version, then live status of search and semantic // indexes plus their on-disk size. Refreshes on mount/session change and on // server-pushed status invalidations with a small debounce, so the panel stays // current without polling. import { canonicalizeProjectRoot } from "@cortexkit/aft-bridge"; import { createEffect, createMemo, createSignal, on, onCleanup } from "opentui:runtime-module:solid-js"; import { AftRpcClient } from "../shared/rpc-client"; import { coerceAftStatus, formatSemanticIndexStatus, formatSemanticRefreshing, worktreeCacheRoleNote } from "../shared/status"; import { resolveCortexKitStorageRoot } from "../shared/storage-paths"; import { badgeTextColor } from "./badge-contrast"; import { createDebouncedStatusRefresh, refreshAftTuiSocketScope, subscribeStatusInvalidations } from "./notification-socket"; import { computeEffectiveOrder, DEFAULT_PREFS, DEFAULT_SLOT_ORDER, PLUGIN_KEY, persistCollapsedIfEnabled, readTuiPreferencesFile, resolveAftPrefs, seedCollapsedFromPrefs, watchTuiPreferences } from "./preferences"; const SINGLE_BORDER = { type: "single" }; const REFRESH_DEBOUNCE_MS = 200; function formatBytes(n) { if (!Number.isFinite(n) || n <= 0) return "—"; if (n >= 1_073_741_824) return `${(n / 1_073_741_824).toFixed(1)} GB`; if (n >= 1_048_576) return `${(n / 1_048_576).toFixed(1)} MB`; if (n >= 1_024) return `${Math.round(n / 1_024)} KB`; return `${n} B`; } function formatCount(n) { if (n == null || !Number.isFinite(n)) return "—"; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${Math.round(n / 1_000)}K`; return String(n); } /** Tagged rows for the Compression section. Each scope (Session / Project) * emits a "scope" header followed by two "stat" rows — Tokens Saved and * Compression Ratio — so the renderer can use the same StatRow layout as * Search Index / Semantic Index above. Pi's monospace overlay and the * OpenCode TUI dialog/sidebar all consume this same shape. */ function appendScope(rows, label, scope) { const savings = scope.savings_tokens; const pct = scope.original_tokens > 0 ? Math.round(savings / scope.original_tokens * 100) : 0; rows.push({ kind: "scope", label }); rows.push({ kind: "stat", label: "Tokens Saved", value: savings.toLocaleString("en-US") }); rows.push({ kind: "stat", label: "Compression Ratio", value: `${pct}%` }); } export function formatCompressionSidebarRows(compression) { if (!compression || compression.project.events <= 0) return []; const rows = []; if (compression.session.events > 0) { appendScope(rows, "Session", compression.session); } appendScope(rows, "Project", compression.project); return rows; } // Map index status → (label, theme color name). The label is what we want // the user to see; the color encodes severity so the eye lands on warnings. function statusDisplay(status) { switch (status) { case "ready": return { label: "ready", tone: "ok" }; case "loading": case "building": return { label: status, tone: "warn" }; case "failed": case "error": return { label: status, tone: "err" }; case "disabled": return { label: "disabled", tone: "muted" }; default: return { label: status || "unknown", tone: "muted" }; } } const StatRow = props => { const fg = createMemo(() => { switch (props.tone) { case "ok": return props.theme.success ?? props.theme.accent; case "warn": return props.theme.warning; case "err": return props.theme.error; case "muted": return props.theme.textMuted; case "accent": return props.theme.accent; default: return props.theme.text; } }); return (() => { var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("text"), _el$4 = _$createElement("b"); _$insertNode(_el$, _el$2); _$insertNode(_el$, _el$3); _$setProp(_el$, "width", "100%"); _$setProp(_el$, "flexDirection", "row"); _$setProp(_el$, "justifyContent", "space-between"); _$insert(_el$2, () => props.label); _$insertNode(_el$3, _el$4); _$insert(_el$4, () => props.value); _$effect(_p$ => { var _v$ = props.theme.textMuted, _v$2 = fg(); _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e)); _v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$2, _p$.t)); return _p$; }, { e: undefined, t: undefined }); return _el$; })(); }; const SectionHeader = props => (() => { var _el$5 = _$createElement("box"), _el$6 = _$createElement("text"), _el$7 = _$createElement("b"); _$insertNode(_el$5, _el$6); _$setProp(_el$5, "width", "100%"); _$insertNode(_el$6, _el$7); _$insert(_el$7, () => props.title); _$effect(_p$ => { var _v$3 = props.marginTop ?? 1, _v$4 = props.theme.text; _v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "marginTop", _v$3, _p$.e)); _v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$4, _p$.t)); return _p$; }, { e: undefined, t: undefined }); return _el$5; })(); // Map a status tone to a theme color — used for the collapsed-view status dots. function toneColor(theme, tone) { switch (tone) { case "ok": return theme.success ?? theme.accent; case "warn": return theme.warning; case "err": return theme.error; default: return theme.textMuted; } } // Collapsed-view row: label on the left, a status dot (or compact value) on the // right. Mirrors the expanded StatRow layout so the columns line up. const CollapsedRow = props => (() => { var _el$8 = _$createElement("box"), _el$9 = _$createElement("text"); _$insertNode(_el$8, _el$9); _$setProp(_el$8, "width", "100%"); _$setProp(_el$8, "flexDirection", "row"); _$setProp(_el$8, "justifyContent", "space-between"); _$insert(_el$9, () => props.label); _$insert(_el$8, () => props.children, null); _$effect(_$p => _$setProp(_el$9, "fg", props.theme.textMuted, _$p)); return _el$8; })(); // Compact "saved / ratio" string for the collapsed Compression row — e.g. // "7.6M / 64%". Uses the local `formatCount` (not the aft-bridge token // formatter) so the TUI bundle doesn't pull the bridge barrel, which exports // URL-fetch helpers unsuitable for Bun's TUI runtime. Returns null when no // compression has been recorded yet. export function collapsedCompressionValue(compression) { if (!compression || compression.project.events <= 0) return null; const { savings_tokens, original_tokens } = compression.project; const pct = original_tokens > 0 ? Math.round(savings_tokens / original_tokens * 100) : 0; return `${formatCount(savings_tokens)} / ${pct}%`; } // Degraded-mode reason → human-readable hint. Distinct strings per reason // because the UX direction is different: "home_root" tells the user to open a // real project subdirectory, "search_too_many_files" tells them the tree is too // big for full indexing, and "watcher_unavailable" is an honest soft // degradation (AFT continues without live external-change invalidation). export function degradedReasonLabel(reason) { if (reason === "home_root") { return "project root is your home directory"; } if (reason.startsWith("search_too_many_files:")) { const threshold = reason.split(":")[1] ?? "20000"; return `project exceeds ${threshold} files`; } if (reason === "watcher_unavailable") { return "file watcher unavailable; continuing without live external-change invalidation"; } return reason; // unknown reason — surface verbatim so users can grep logs } // Missing categories are intentionally muted: a green light requires an // explicit zero for every category that feeds that light. export function collapsedHealthLights(statusBar) { if (!statusBar) return null; const diagnostics = statusBar.errors !== undefined && statusBar.errors > 0 ? "err" : statusBar.warnings !== undefined && statusBar.warnings > 0 ? "warn" : statusBar.errors === 0 && statusBar.warnings === 0 ? "ok" : "muted"; const codeValues = [statusBar.dead_code, statusBar.unused_exports, statusBar.duplicates]; const code = codeValues.some(value => value !== undefined && value > 0) ? "warn" : codeValues.every(value => value === 0) ? "ok" : "muted"; const todos = statusBar.todos === undefined ? "muted" : statusBar.todos > 0 ? "warn" : "ok"; return { diagnostics, code, todos }; } // Keep the TUI on the bridge's shared resolver so its root matches the // configure payload used by the plugin and binary. export function resolveTuiStorageDir() { return resolveCortexKitStorageRoot(); } // One RPC client per project directory — same pattern as the /aft-status // dialog handler in tui/index.tsx. Sharing the map avoids opening a second // connection just for the sidebar. const sidebarClients = new Map(); function getClient(directory) { let client = sidebarClients.get(directory); if (client) return client; client = new AftRpcClient(resolveTuiStorageDir(), directory); sidebarClients.set(directory, client); return client; } export function scopedSidebarSnapshot(scoped, directory, sessionID) { if (!scoped) return null; if (scoped.directory !== directory || scoped.sessionID !== sessionID) return null; return scoped.snapshot; } /** * Stale-while-revalidate guard. A transient `not_initialized` snapshot (bridge * mid-respawn after a binary swap, or a momentary session-dir key miss) arrives * over RPC as `success: true`, so a naive `setStatus` would overwrite a good * snapshot and collapse the panel to the lazy-bridge placeholder — the blank * flicker that recovers on the next refresh. Suppress the downgrade only when we * already hold initialized data for the same context; never blocks the first * real snapshot, and a genuine context switch clears separately. */ export function shouldSuppressUninitializedDowngrade(incomingCacheRole, haveInitializedForContext) { return incomingCacheRole === "not_initialized" && haveInitializedForContext; } /** * Cross-project contamination belt. The RPC layer can hand back a snapshot * describing a DIFFERENT project than the one this sidebar asked about — a * multi-project host (Desktop / `opencode serve`) whose status handler * resolved another project's warm bridge, including long-lived processes * still running pre-fix plugin code. Rendering it shows another repo's * indexes/health in this window. * * A mismatched project_root is acceptable ONLY when the serving handler says * it resolved that directory DELIBERATELY: new servers attach * `served_directory` (their own cwd, or the SDK-verified `opencode -s` resume * directory) to every status response. That marker is handler-attached * provenance — it cannot be faked by snapshot contents. We explicitly do NOT * use `snapshot.session.id` here: Rust echoes the REQUESTED session id into * the snapshot, so it matches even when the data came from another project's * bridge (the hole that let contamination through this belt's first version). * * Rules: * - placeholder/synthetic snapshots (no project_root) → accept (not data) * - project_root (or canonical_root) matches the sidebar directory → accept * - mismatched root AND served_directory matches a snapshot root → accept * (deliberate, SDK-verified resume serve from a new server) * - otherwise → reject (stray; includes everything old servers cross-serve) */ export function isSnapshotForContext(snapshot, directory, servedDirectory) { // Canonicalize both sides through the SAME canonicalizer the bridge routes // by, so a symlinked / `/var`-vs-`/private/var` / trailing-slash spelling of // the sidebar directory still matches Rust's canonical_root. A raw stripSlash // compare (the old behavior) rejected legitimate snapshots whenever the TUI // directory and Rust's root were different spellings of the same location, // leaving the sidebar blank on aliased roots. const canon = p => canonicalizeProjectRoot(p); const roots = [snapshot.project_root, snapshot.canonical_root].filter(r => typeof r === "string" && r.length > 0); if (roots.length === 0) return true; // placeholder / synthetic const dir = canon(directory); if (roots.some(r => canon(r) === dir)) return true; if (typeof servedDirectory === "string" && servedDirectory.length > 0) { const served = canon(servedDirectory); return roots.some(r => canon(r) === served); } return false; } const SidebarContent = props => { const [status, setStatus] = createSignal(null); const [prefs, setPrefs] = createSignal(structuredClone(DEFAULT_PREFS)); const [collapsed, setCollapsed] = createSignal(seedCollapsedFromPrefs(DEFAULT_PREFS)); let inflight = null; let generation = 0; const currentDirectory = () => props.api.state.path.directory ?? ""; const requestRender = () => { try { props.api.renderer.requestRender(); } catch { // renderer may not be available during teardown; safe to ignore } }; const abortInflight = () => { if (!inflight) return; inflight.controller.abort(); inflight = null; }; const clearStatusForContext = (directory, sessionID) => { const current = status(); if (!current) return; if (current.directory === directory && current.sessionID === sessionID) return; setStatus(null); requestRender(); }; const refresh = async () => { const sid = props.sessionID(); const directory = currentDirectory(); if (!sid || !directory) { generation++; abortInflight(); if (status()) { setStatus(null); requestRender(); } return; } clearStatusForContext(directory, sid); if (inflight) { if (inflight.directory === directory && inflight.sessionID === sid) return; generation++; abortInflight(); } const requestGeneration = ++generation; const controller = new AbortController(); inflight = { controller, generation: requestGeneration, directory, sessionID: sid }; try { const client = getClient(directory); const response = await client.call("status", { sessionID: sid }, { signal: controller.signal, // With several RPC servers alive for this project hash, a stray // warm response (another project's bridge) must not beat the right // server or the placeholder — skip it at the port-scan level. accept: result => { const rec = result; if (rec?.success === false) return true; // errors handled below return isSnapshotForContext(coerceAftStatus(rec), directory, rec?.served_directory); } }); if (controller.signal.aborted || requestGeneration !== generation) return; if (currentDirectory() !== directory || props.sessionID() !== sid) return; if (response && response.success !== false) { const snapshot = coerceAftStatus(response); // Belt: never render a snapshot describing another project (see // isSnapshotForContext). Keep whatever we currently show instead. const servedDirectory = response.served_directory; if (!isSnapshotForContext(snapshot, directory, servedDirectory)) return; // Stale-while-revalidate: keep the last-good snapshot instead of // flickering to the lazy-bridge placeholder on a transient // not_initialized. See shouldSuppressUninitializedDowngrade. const current = status(); const haveGoodForContext = current !== null && current.directory === directory && current.sessionID === sid && current.snapshot.cache_role !== "not_initialized"; if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext)) return; // Equality gate: a pushed invalidation can still produce the same // snapshot (for example, a session-scoped status frame that does not // affect this sidebar's visible fields). Minting a new status object // would run SolidJS reactivity and schedule a host frame for no visible // change. Skip the update when the freshly-fetched snapshot is // byte-identical to what we already show for this exact context. // JSON.stringify is sound here because the snapshot is a plain object // coerced from the status RPC's JSON. if (current !== null && current.directory === directory && current.sessionID === sid && JSON.stringify(current.snapshot) === JSON.stringify(snapshot)) { return; } setStatus({ directory, sessionID: sid, snapshot }); requestRender(); } } catch { if (controller.signal.aborted || requestGeneration !== generation) return; // RPC server may not be ready yet, or the bridge may be respawning // after a binary swap. Keep the previous snapshot only when it belongs // to the current project/session; mismatched snapshots were cleared above. } finally { if (inflight?.generation === requestGeneration) inflight = null; } }; const statusDebouncer = createDebouncedStatusRefresh(refresh, REFRESH_DEBOUNCE_MS); const scheduleRefresh = () => statusDebouncer.schedule(); const reloadPrefs = async () => { const root = await readTuiPreferencesFile(); const next = resolveAftPrefs(root); setPrefs(next); setCollapsed(seedCollapsedFromPrefs(next)); requestRender(); }; void reloadPrefs(); const unwatchPrefs = watchTuiPreferences(() => { void reloadPrefs(); }); onCleanup(() => { unwatchPrefs(); generation++; abortInflight(); statusDebouncer.dispose(); }); // Refresh on session id change + initial load createEffect(on(props.sessionID, () => { refreshAftTuiSocketScope(); void refresh(); })); // Wire live updates: the server pushes a lightweight invalidation whenever // the bridge reports a status change. The sidebar coalesces bursts into one // trailing status fetch and stays completely idle when no backend state changes. createEffect(on(props.sessionID, sessionID => { if (!sessionID) return; const unsubscribe = subscribeStatusInvalidations(event => { if (event.sessionId && event.sessionId !== props.sessionID()) return; scheduleRefresh(); }); onCleanup(() => { unsubscribe(); generation++; abortInflight(); }); }, { defer: false })); const s = () => scopedSidebarSnapshot(status(), currentDirectory(), props.sessionID()); // Lazy-bridge: while AFT has no live bridge yet, the RPC server returns a // synthetic snapshot with `cache_role === "not_initialized"`. In that state // every metric is unknown by design — not "disabled" — so we hide the // version line and the entire Search Index / Semantic Index / Compression // grid until a first tool call warms the bridge. Users were reading the // pre-init `vunknown` + `Status: unknown` rows as broken state instead of // "AFT has not been used yet for this project". const notInitialized = () => s()?.cache_role === "not_initialized"; // Pre-compute display values so the JSX stays readable. createMemo for // each derived field would be overkill — these are cheap derivations. const searchStatus = () => statusDisplay(s()?.search_index?.status ?? "disabled"); const semanticStatus = () => { const rawStatus = s()?.semantic_index?.status ?? "disabled"; const display = statusDisplay(rawStatus); return { ...display, label: formatSemanticIndexStatus(rawStatus, s()?.semantic_index?.stage) }; }; const semanticRefreshing = () => formatSemanticRefreshing(s()?.semantic_index?.refreshing_count ?? 0); const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0; const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0; const compressionRows = () => formatCompressionSidebarRows(s()?.compression); const statusBar = () => s()?.status_bar; const degradedSummary = () => { const snap = s(); if (!snap?.degraded) return null; const reasons = snap.degraded_reasons ?? []; if (reasons.length === 0) return null; return reasons.map(degradedReasonLabel).join("; "); }; // Worktree borrow is a shared-index arrangement, not a degraded_reasons // entry. Keep this muted and separate from the DEGRADED badge above. const worktreeNote = () => worktreeCacheRoleNote(s()?.cache_role); return (() => { var _el$0 = _$createElement("box"), _el$1 = _$createElement("box"), _el$10 = _$createElement("box"), _el$11 = _$createElement("box"), _el$12 = _$createElement("text"), _el$13 = _$createElement("b"); _$insertNode(_el$0, _el$1); _$setProp(_el$0, "width", "100%"); _$setProp(_el$0, "flexDirection", "column"); _$setProp(_el$0, "border", SINGLE_BORDER); _$setProp(_el$0, "paddingTop", 1); _$setProp(_el$0, "paddingBottom", 1); _$setProp(_el$0, "paddingLeft", 1); _$setProp(_el$0, "paddingRight", 1); _$insertNode(_el$1, _el$10); _$setProp(_el$1, "flexDirection", "row"); _$setProp(_el$1, "justifyContent", "space-between"); _$setProp(_el$1, "alignItems", "center"); _$setProp(_el$1, "onMouseDown", () => { if (notInitialized()) return; setCollapsed(x => { const next = !x; persistCollapsedIfEnabled(prefs(), next); return next; }); }); _$insertNode(_el$10, _el$11); _$setProp(_el$10, "flexDirection", "row"); _$setProp(_el$10, "alignItems", "center"); _$insertNode(_el$11, _el$12); _$setProp(_el$11, "paddingLeft", 1); _$setProp(_el$11, "paddingRight", 1); _$insertNode(_el$12, _el$13); _$insert(_el$13, (() => { var _c$ = _$memo(() => !!notInitialized()); return () => _c$() ? "" : collapsed() ? "▶ " : "▼ "; })(), null); _$insert(_el$13, () => prefs().header.label, null); _$insert(_el$10, (() => { var _c$2 = _$memo(() => !!s()?.degraded); return () => _c$2() && (() => { var _el$14 = _$createElement("box"), _el$15 = _$createElement("text"), _el$16 = _$createElement("b"); _$insertNode(_el$14, _el$15); _$setProp(_el$14, "paddingLeft", 1); _$setProp(_el$14, "paddingRight", 1); _$setProp(_el$14, "marginLeft", 1); _$insertNode(_el$15, _el$16); _$insertNode(_el$16, _$createTextNode(`DEGRADED`)); _$effect(_p$ => { var _v$8 = props.theme.warning, _v$9 = badgeTextColor(props.theme.warning, props.theme.background); _v$8 !== _p$.e && (_p$.e = _$setProp(_el$14, "backgroundColor", _v$8, _p$.e)); _v$9 !== _p$.t && (_p$.t = _$setProp(_el$15, "fg", _v$9, _p$.t)); return _p$; }, { e: undefined, t: undefined }); return _el$14; })(); })(), null); _$insert(_el$1, (() => { var _c$3 = _$memo(() => !!(!notInitialized() && prefs().header.showVersion)); return () => _c$3() && (() => { var _el$18 = _$createElement("text"), _el$19 = _$createTextNode(`v`); _$insertNode(_el$18, _el$19); _$insert(_el$18, () => s()?.version ?? props.pluginVersion, null); _$effect(_$p => _$setProp(_el$18, "fg", props.theme.textMuted, _$p)); return _el$18; })(); })(), null); _$insert(_el$0, (() => { var _c$4 = _$memo(() => !!(s()?.degraded && degradedSummary())); return () => _c$4() && (() => { var _el$20 = _$createElement("box"), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(`⚠ `); _$insertNode(_el$20, _el$21); _$setProp(_el$20, "marginTop", 1); _$setProp(_el$20, "width", "100%"); _$insertNode(_el$21, _el$22); _$insert(_el$21, degradedSummary, null); _$effect(_$p => _$setProp(_el$21, "fg", props.theme.warning, _$p)); return _el$20; })(); })(), null); _$insert(_el$0, (() => { var _c$5 = _$memo(() => !!(!notInitialized() && worktreeNote())); return () => _c$5() && (() => { var _el$23 = _$createElement("box"), _el$24 = _$createElement("text"); _$insertNode(_el$23, _el$24); _$setProp(_el$23, "marginTop", 1); _$setProp(_el$23, "width", "100%"); _$insert(_el$24, worktreeNote); _$effect(_$p => _$setProp(_el$24, "fg", props.theme.textMuted, _$p)); return _el$23; })(); })(), null); _$insert(_el$0, (() => { var _c$6 = _$memo(() => !!notInitialized()); return () => _c$6() && (() => { var _el$25 = _$createElement("box"), _el$26 = _$createElement("text"); _$insertNode(_el$25, _el$26); _$setProp(_el$25, "marginTop", 1); _$setProp(_el$25, "width", "100%"); _$insert(_el$26, () => s().message || "AFT bridge is now spawned lazily, information here will be populated after first tool call."); _$effect(_$p => _$setProp(_el$26, "fg", props.theme.textMuted, _$p)); return _el$25; })(); })(), null); _$insert(_el$0, (() => { var _c$7 = _$memo(() => !!(!notInitialized() && collapsed())); return () => _c$7() && (() => { var _el$27 = _$createElement("box"); _$setProp(_el$27, "width", "100%"); _$setProp(_el$27, "flexDirection", "column"); _$insert(_el$27, (() => { var _c$9 = _$memo(() => !!prefs().sections.searchIndex); return () => _c$9() && _$createComponent(CollapsedRow, { get theme() { return props.theme; }, label: "Search Index", get children() { var _el$28 = _$createElement("text"); _$insertNode(_el$28, _$createTextNode(`●`)); _$effect(_$p => _$setProp(_el$28, "fg", toneColor(props.theme, searchStatus().tone), _$p)); return _el$28; } }); })(), null); _$insert(_el$27, (() => { var _c$0 = _$memo(() => !!prefs().sections.semanticIndex); return () => _c$0() && _$createComponent(CollapsedRow, { get theme() { return props.theme; }, label: "Semantic Index", get children() { var _el$30 = _$createElement("text"); _$insertNode(_el$30, _$createTextNode(`●`)); _$effect(_$p => _$setProp(_el$30, "fg", toneColor(props.theme, semanticStatus().tone), _$p)); return _el$30; } }); })(), null); _$insert(_el$27, (() => { var _c$1 = _$memo(() => !!(prefs().sections.codeHealth && collapsedHealthLights(statusBar()))); return () => _c$1() && _$createComponent(CollapsedRow, { get theme() { return props.theme; }, label: "Code Health", get children() { var _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$35 = _$createElement("text"), _el$37 = _$createElement("text"); _$insertNode(_el$32, _el$33); _$insertNode(_el$32, _el$35); _$insertNode(_el$32, _el$37); _$setProp(_el$32, "flexDirection", "row"); _$setProp(_el$32, "gap", 1); _$insertNode(_el$33, _$createTextNode(`●`)); _$insertNode(_el$35, _$createTextNode(`●`)); _$insertNode(_el$37, _$createTextNode(`●`)); _$effect(_p$ => { var _v$0 = toneColor(props.theme, collapsedHealthLights(statusBar()).diagnostics), _v$1 = toneColor(props.theme, collapsedHealthLights(statusBar()).code), _v$10 = toneColor(props.theme, collapsedHealthLights(statusBar()).todos); _v$0 !== _p$.e && (_p$.e = _$setProp(_el$33, "fg", _v$0, _p$.e)); _v$1 !== _p$.t && (_p$.t = _$setProp(_el$35, "fg", _v$1, _p$.t)); _v$10 !== _p$.a && (_p$.a = _$setProp(_el$37, "fg", _v$10, _p$.a)); return _p$; }, { e: undefined, t: undefined, a: undefined }); return _el$32; } }); })(), null); _$insert(_el$27, (() => { var _c$10 = _$memo(() => !!(prefs().sections.compression && collapsedCompressionValue(s()?.compression))); return () => _c$10() && _$createComponent(CollapsedRow, { get theme() { return props.theme; }, label: "Compression", get children() { var _el$39 = _$createElement("text"), _el$40 = _$createElement("b"); _$insertNode(_el$39, _el$40); _$insert(_el$40, () => collapsedCompressionValue(s()?.compression)); _$effect(_$p => _$setProp(_el$39, "fg", props.theme.textMuted, _$p)); return _el$39; } }); })(), null); return _el$27; })(); })(), null); _$insert(_el$0, (() => { var _c$8 = _$memo(() => !!(!notInitialized() && !collapsed())); return () => _c$8() && [_$memo(() => _$memo(() => !!prefs().sections.searchIndex)() && [_$createComponent(SectionHeader, { get theme() { return props.theme; }, title: "Search Index" }), _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Status", get value() { return searchStatus().label; }, get tone() { return searchStatus().tone; } }), _$memo(() => _$memo(() => (s()?.search_index?.files ?? null) != null)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Files", get value() { return formatCount(s().search_index.files); }, tone: "muted" })), _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Disk", get value() { return formatBytes(trigramBytes()); }, tone: "muted" })]), _$memo(() => _$memo(() => !!prefs().sections.semanticIndex)() && [_$createComponent(SectionHeader, { get theme() { return props.theme; }, title: "Semantic Index" }), _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Status", get value() { return semanticStatus().label; }, get tone() { return semanticStatus().tone; } }), _$memo(() => _$memo(() => !!semanticRefreshing())() && (() => { var _el$41 = _$createElement("box"), _el$42 = _$createElement("text"); _$insertNode(_el$41, _el$42); _$setProp(_el$41, "width", "100%"); _$insert(_el$42, semanticRefreshing); _$effect(_$p => _$setProp(_el$42, "fg", props.theme.textMuted, _$p)); return _el$41; })()), _$memo(() => _$memo(() => !!(s()?.semantic_index?.status === "loading" && s()?.semantic_index?.entries_total != null && s().semantic_index.entries_total > 0))() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Progress", get value() { return `${formatCount(s().semantic_index.entries_done)} / ${formatCount(s().semantic_index.entries_total)}`; }, tone: "warn" })), _$memo(() => _$memo(() => (s()?.semantic_index?.entries ?? null) != null)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Entries", get value() { return formatCount(s().semantic_index.entries); }, tone: "muted" })), _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Disk", get value() { return formatBytes(semanticBytes()); }, tone: "muted" })]), _$memo(() => _$memo(() => !!(prefs().sections.codeHealth && statusBar()))() && [_$createComponent(SectionHeader, { get theme() { return props.theme; }, get title() { return statusBar().tier2_stale ? "Code Health ~" : "Code Health"; } }), _$memo(() => _$memo(() => statusBar().errors !== undefined)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Errors", get value() { return formatCount(statusBar().errors); }, get tone() { return statusBar().errors > 0 ? "err" : "muted"; } })), _$memo(() => _$memo(() => statusBar().warnings !== undefined)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Warnings", get value() { return formatCount(statusBar().warnings); }, get tone() { return statusBar().warnings > 0 ? "warn" : "muted"; } })), _$memo(() => _$memo(() => statusBar().dead_code !== undefined)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Dead Code", get value() { return formatCount(statusBar().dead_code); }, tone: "muted" })), _$memo(() => _$memo(() => statusBar().unused_exports !== undefined)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Unused Exports", get value() { return formatCount(statusBar().unused_exports); }, tone: "muted" })), _$memo(() => _$memo(() => statusBar().duplicates !== undefined)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "Duplicates", get value() { return formatCount(statusBar().duplicates); }, tone: "muted" })), _$memo(() => _$memo(() => statusBar().todos !== undefined)() && _$createComponent(StatRow, { get theme() { return props.theme; }, label: "TODOs", get value() { return formatCount(statusBar().todos); }, tone: "muted" }))]), _$memo(() => _$memo(() => !!(prefs().sections.compression && compressionRows().length > 0))() && [_$createComponent(SectionHeader, { get theme() { return props.theme; }, title: "Compression" }), _$memo(() => compressionRows().map(row => row.kind === "scope" ? (() => { var _el$43 = _$createElement("box"), _el$44 = _$createElement("text"); _$insertNode(_el$43, _el$44); _$setProp(_el$43, "width", "100%"); _$insert(_el$44, () => row.label); _$effect(_$p => _$setProp(_el$44, "fg", props.theme.text, _$p)); return _el$43; })() : _$createComponent(StatRow, { get theme() { return props.theme; }, get label() { return row.label; }, get value() { return row.value; }, tone: "muted" })))]), _$memo(() => _$memo(() => !!(s()?.semantic_index?.status === "failed" && s()?.semantic_index?.error))() && (() => { var _el$45 = _$createElement("box"), _el$46 = _$createElement("text"), _el$47 = _$createTextNode(`⚠ `); _$insertNode(_el$45, _el$46); _$setProp(_el$45, "marginTop", 1); _$setProp(_el$45, "width", "100%"); _$insertNode(_el$46, _el$47); _$insert(_el$46, () => s().semantic_index.error, null); _$effect(_$p => _$setProp(_el$46, "fg", props.theme.error, _$p)); return _el$45; })())]; })(), null); _$effect(_p$ => { var _v$5 = props.theme.borderActive, _v$6 = props.theme.accent, _v$7 = badgeTextColor(props.theme.accent, props.theme.background); _v$5 !== _p$.e && (_p$.e = _$setProp(_el$0, "borderColor", _v$5, _p$.e)); _v$6 !== _p$.t && (_p$.t = _$setProp(_el$11, "backgroundColor", _v$6, _p$.t)); _v$7 !== _p$.a && (_p$.a = _$setProp(_el$12, "fg", _v$7, _p$.a)); return _p$; }, { e: undefined, t: undefined, a: undefined }); return _el$0; })(); }; export async function createAftSidebarSlot(api, pluginVersion) { const root = await readTuiPreferencesFile(); const order = computeEffectiveOrder(root, PLUGIN_KEY, DEFAULT_SLOT_ORDER); return { // DEFAULT_SLOT_ORDER (180) is AFT's coordinated default in the shared // tui-preferences ladder (anthropic-auth 160, AFT 180, magic-context 200). // Override via `order` or `forceToTop` in tui-preferences.jsonc. order, slots: { sidebar_content: (ctx, value) => { const theme = createMemo(() => ctx.theme.current); return _$createComponent(SidebarContent, { api: api, sessionID: () => value.session_id, get theme() { return theme(); }, pluginVersion: pluginVersion }); } } }; }