Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/widget.ts

Raw
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<Theme, "bold" | "fg">;

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 | undefined>
): 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}`;
}