repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/strata/widget.ts
Rawimport 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}`;
}