import type { Theme } from "@earendil-works/pi-coding-agent"; import { type Component, hyperlink, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui"; import type { Source } from "./types.js"; export const STRATA_WIDGET_KEY = "strata"; export const STRATA_ICON = String.fromCodePoint(0xf0f58); export type StrataPhase = | "Collecting" | "Planning" | "Ready" | "Answering" | "Refreshing" | "Stale" | "Feedback sent" | "Error" | "Closed"; export type StrataWidgetState = { phase: StrataPhase; source?: Source; url?: string; reviewed?: number; totalHunks?: number; layers?: number; comments?: number; error?: string; }; type WidgetTheme = Pick; const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu; export function sanitizeTerminalText(value: string, maximum = 160): string { const clean = value .replace(CONTROL_CHARACTERS, " ") .replace(/\s+/gu, " ") .trim(); return Array.from(clean).slice(0, maximum).join(""); } export function createStrataWidget( state: StrataWidgetState, theme: WidgetTheme, ): Component { const snapshot = { ...state }; return { invalidate() {}, render: (width) => renderStrataWidget(snapshot, theme, width), }; } export function renderStrataWidget( input: StrataWidgetState, theme: WidgetTheme, width: number, ): string[] { const state = sanitizeState(input); const safeWidth = Math.max(0, width); if (safeWidth === 0) return ["", ""]; return [ renderIdentity(state, theme, safeWidth), renderAction(state, theme, safeWidth), ]; } function renderIdentity( state: StrataWidgetState, theme: WidgetTheme, width: number, ): string { const cue = theme.fg("borderAccent", "▏"); const identity = `${theme.fg("accent", STRATA_ICON)} ${theme.fg("accent", theme.bold("STRATA"))}`; const phase = theme.fg(phaseColor(state.phase), `● ${state.phase}`); const source = state.source ? theme.fg("muted", sourceLabel(state.source)) : undefined; const reviewed = state.reviewed !== undefined && state.totalHunks !== undefined ? theme.fg("muted", `${state.reviewed}/${state.totalHunks} reviewed`) : undefined; const layers = countLabel(state.layers, "layer", theme); const comments = countLabel(state.comments, "comment", theme); const lead = `${cue} ${identity} ${phase}`; const candidates = [ joinDetails(lead, source, reviewed, layers, comments), joinDetails(lead, source, reviewed, layers), joinDetails(lead, source, reviewed), joinDetails(lead, reviewed), lead, `${cue} ${phase}`, phase, ]; return chooseCandidate(candidates, width); } function renderAction( state: StrataWidgetState, theme: WidgetTheme, width: number, ): string { const cue = theme.fg("borderAccent", "▏"); const link = state.url ? theme.fg("mdLink", hyperlink(shortUrl(state.url), state.url)) : undefined; const hints = actionHints(state).map((hint) => theme.fg("dim", hint)); const lead = `${cue} `; const candidates = link ? [...hints.map((hint) => `${lead}${link} ${hint}`), `${lead}${link}`] : [...hints.map((hint) => `${lead}${hint}`), ...hints]; return chooseCandidate(candidates, width); } function sanitizeState(state: StrataWidgetState): StrataWidgetState { return { ...state, source: state.source?.ref ? { ...state.source, ref: sanitizeTerminalText(state.source.ref) } : state.source, error: state.error ? sanitizeTerminalText(state.error) : undefined, }; } function sourceLabel(source: Source): string { if (source.kind === "working") return "working tree"; if (source.kind === "staged") return "staged"; return `base ${source.ref ?? "HEAD"}`; } function actionHints(state: StrataWidgetState): string[] { switch (state.phase) { case "Collecting": return [ "Capturing Git snapshot · /strata cancel", "Capturing Git snapshot", ]; case "Planning": return [ "Organizing review layers · /strata cancel", "Organizing review layers", ]; case "Ready": return ["/strata cancel"]; case "Answering": return ["Answering browser question"]; case "Refreshing": return ["Refreshing snapshot"]; case "Stale": return ["Refresh in browser"]; case "Feedback sent": return ["Feedback queued for Pi"]; case "Error": return [ state.error ? `${state.error} · ${state.url ? "Retry in browser" : "Run /strata to retry"}` : state.url ? "Retry in browser" : "Run /strata to retry", ]; case "Closed": return ["Run /strata to reopen"]; } } function phaseColor( phase: StrataPhase, ): "accent" | "success" | "warning" | "error" | "muted" { switch (phase) { case "Ready": case "Feedback sent": return "success"; case "Stale": return "warning"; case "Error": return "error"; case "Closed": return "muted"; default: return "accent"; } } function countLabel( count: number | undefined, name: string, theme: WidgetTheme, ): string | undefined { return count === undefined ? undefined : theme.fg("muted", `${count} ${name}${count === 1 ? "" : "s"}`); } function joinDetails( lead: string, ...details: Array ): string { const present = details.filter( (value): value is string => value !== undefined, ); return present.length === 0 ? lead : `${lead}${present.map((value) => ` · ${value}`).join("")}`; } function chooseCandidate(candidates: string[], width: number): string { const fitting = candidates.find( (candidate) => visibleWidth(candidate) <= width, ); return fitting ?? truncateToWidth(candidates.at(-1) ?? "", width, "…"); } function shortUrl(url: string): string { const parsed = new URL(url); return `${parsed.protocol}//${parsed.host}`; }