import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { DEFAULT_PREVIEW_WIDTH, type PreviewResult } from "./menu.ts"; import { ActivityMeter, METER_UPPER_BOUND_TPS_MAX, METER_UPPER_BOUND_TPS_MIN, rateToLevel } from "./activity-meter.ts"; import { DEFAULT_WORKING_WORD, dimAttribute, ELAPSED_INTERVAL_MS, formatTokenRate, getThinkingLevelColorizer, METER_INTERVAL_MS, SHIMMER_INTERVAL_MS, SPINNER_FRAME_MS } from "./format.ts"; import { buildLoaderMessage } from "./loader-message.ts"; import { getResponseModelColorizer } from "./nvidia-green.ts"; import { isOutputStyleId, OUTPUT_STYLES } from "./output-style.ts"; import { buildCompletionMarkerContent, buildCompletionMarkerLine, buildPromptBoxLines } from "./prompt-decorator.ts"; import { formatServiceTierSummary } from "./service-tier.ts"; import { DEFAULT_SETTINGS, fromCycleDirection, fromCycleMeterUpperBound, fromCycleSpeed, isBorderStyle, isDoneMarkerBorderColor, isDoneMarkerBorderStyle, isDoneMarkerStyle, isPromptBorderColor, isSpinnerColor, isThinkingLevelColor, LOADER_ORDER_ID, MENU_ENTRIES, OUTPUT_STYLE_ID, parseLoaderOrder } from "./settings.ts"; import { isWordPackEnabled, selectWorkingTextSelection, WORD_PACK_MENU_PREFIX, wordPacksPath, type WordPack } from "./word-packs.ts"; // Simulated load for the menu preview: a 2.4s cosine wave peaking at 46 tps for the meter, // flat 28 tps for the token readouts. const METER_PERIOD_MS = 2400, METER_PEAK_RATE = 46, TOKEN_RATE_PER_SEC = 28; const PROMPT_IDS = new Set(MENU_ENTRIES.filter(entry => entry.section === "User Prompt").map(entry => entry.id)); const MARKER_IDS = new Set(MENU_ENTRIES.filter(entry => entry.section === "Completion Marker").map(entry => entry.id)); function meterRate(elapsedMs: number): number { return ((1 - Math.cos((2 * Math.PI * elapsedMs) / METER_PERIOD_MS)) / 2) * METER_PEAK_RATE; } /** Stateful, per-menu preview renderer that follows the active settings section. */ export class PreviewRenderer { // Preserve constructor-time randomness so rendering never changes the preview phrase. readonly #poolFraction = Math.random(); readonly #packs: readonly WordPack[]; readonly #meter = new ActivityMeter(); #lastMeterUpdate = 0; #cachedPackWord = ""; #cachedPackSignature = ""; readonly #ctx: ExtensionContext; constructor(ctx: ExtensionContext, packs: readonly WordPack[] = []) { this.#ctx = ctx; this.#packs = packs; } render(values: Record, elapsedMs: number, activeItemId?: string, width: number = DEFAULT_PREVIEW_WIDTH): PreviewResult { if (PROMPT_IDS.has(activeItemId ?? "")) return this.promptPreview(values, width); if (MARKER_IDS.has(activeItemId ?? "")) return this.markerPreview(values, width); if (activeItemId === "useNerdFont") return { lines: ["", `Icon preview: ${values.useNerdFont ? "" : "π"}`, ""] }; if (activeItemId === OUTPUT_STYLE_ID) { const id = isOutputStyleId(values[OUTPUT_STYLE_ID]) ? values[OUTPUT_STYLE_ID] : DEFAULT_SETTINGS.outputStyle; return { lines: ["", OUTPUT_STYLES[id].description, this.#ctx.ui.theme.fg("dim", "Applies from your next message")] }; } if (activeItemId === "showResponseModelFooter") { if (values.showResponseModelFooter !== true) return { lines: ["", "Response model footer is disabled.", ""] }; const color = isThinkingLevelColor(values.responseModelColor) ? values.responseModelColor : DEFAULT_SETTINGS.decorations.responseModelColor; const colored = getResponseModelColorizer(this.#ctx.ui.theme, color, this.#ctx.thinkingLevel, this.#ctx.model?.provider)("test-model"); return { lines: ["", values.responseModelDimmed === true ? dimAttribute(colored) : colored, ""] }; } if (activeItemId?.startsWith(WORD_PACK_MENU_PREFIX)) return this.packPreview(activeItemId.slice(WORD_PACK_MENU_PREFIX.length), values); let nextRefreshInMs: number | undefined; if (values.shimmer !== false) { nextRefreshInMs = SHIMMER_INTERVAL_MS; } else if (values.animatedSpinner !== false) { nextRefreshInMs = SPINNER_FRAME_MS; } else if (values.tokenActivityMonitor !== false) { nextRefreshInMs = METER_INTERVAL_MS; } else if (values.elapsedTime !== false || values.outputTokens !== false) { nextRefreshInMs = ELAPSED_INTERVAL_MS; } const help = activeItemId === "meterUpperBoundTps" ? this.#ctx.ui.theme.fg("dim", `⣿ at ${fromCycleMeterUpperBound(values.meterUpperBoundTps)} tps or more · ←→ presets · ␣ type ${METER_UPPER_BOUND_TPS_MIN}–${METER_UPPER_BOUND_TPS_MAX}`) : ""; return { lines: ["", this.loaderPreview(values, elapsedMs), help], nextRefreshInMs }; } private loaderPreview(values: Record, elapsedMs: number): string { this.#meter.setDirection(fromCycleDirection(values.meterDirection)); if (elapsedMs - this.#lastMeterUpdate >= METER_INTERVAL_MS) { this.#meter.push(rateToLevel(meterRate(elapsedMs), fromCycleMeterUpperBound(values.meterUpperBoundTps))); this.#lastMeterUpdate = elapsedMs; } const features = { substituteDefaultMessage: values.substituteDefaultMessage !== false, elapsedTime: values.elapsedTime !== false, outputTokens: values.outputTokens !== false, tokenRate: values.showTokenRate !== false, responseModel: values.showResponseModel !== false }; const decorations = { shimmer: values.shimmer !== false, shimmerInverted: values.shimmerInverted === true, shimmerDirection: fromCycleDirection(values.shimmerDirection), shimmerSpeed: fromCycleSpeed(values.shimmerSpeed), tokenActivityMonitor: values.tokenActivityMonitor !== false, meterColor: isThinkingLevelColor(values.meterColor) ? values.meterColor : DEFAULT_SETTINGS.decorations.meterColor, meterDimmed: values.meterDimmed === true, tokenRateColor: isThinkingLevelColor(values.tokenRateColor) ? values.tokenRateColor : DEFAULT_SETTINGS.decorations.tokenRateColor, tokenRateDimmed: values.tokenRateDimmed === true, responseModelColor: isThinkingLevelColor(values.responseModelColor) ? values.responseModelColor : DEFAULT_SETTINGS.decorations.responseModelColor, responseModelDimmed: values.responseModelDimmed === true, spinnerColor: isSpinnerColor(values.spinnerColor) ? values.spinnerColor : DEFAULT_SETTINGS.decorations.spinnerColor, }; let word = DEFAULT_WORKING_WORD; if (features.substituteDefaultMessage) { const packValues = this.packValues(values); const includeDefaultWorkingText = values.includeDefaultWorkingText !== false; const signature = JSON.stringify([packValues, includeDefaultWorkingText]); if (signature !== this.#cachedPackSignature) { this.#cachedPackWord = selectWorkingTextSelection(packValues, this.#packs, this.#poolFraction, includeDefaultWorkingText).text; this.#cachedPackSignature = signature; } word = this.#cachedPackWord; } const theme = this.#ctx.ui.theme; return buildLoaderMessage(theme, this.#ctx.thinkingLevel, this.#ctx.model?.provider, features, decorations, parseLoaderOrder(values[LOADER_ORDER_ID]), { spinnerAtMs: values.animatedSpinner !== false ? elapsedMs : undefined, word, shimmerAtMs: elapsedMs, renderMeter: (colorizer, dimmed) => this.#meter.render((level, char) => ActivityMeter.colorizeCell(level, char, theme, colorizer, dimmed)), elapsedMs, outputTokens: Math.max(0, Math.floor(elapsedMs / 1000 * TOKEN_RATE_PER_SEC)), tokenRateText: formatTokenRate(TOKEN_RATE_PER_SEC), responseModel: features.responseModel ? "test-model" : undefined, }); } private packValues(values: Record): Record { const enabled: Record = {}; for (const pack of this.#packs) { const value = values[`${WORD_PACK_MENU_PREFIX}${pack.id}`]; if (typeof value === "boolean") enabled[pack.id] = value; } return enabled; } private packPreview(id: string, values: Record): PreviewResult { const pack = this.#packs.find((candidate) => candidate.id === id); if (!pack) return { lines: ["This word pack is unavailable."] }; const enabled = isWordPackEnabled(id, this.packValues(values)); const source = pack.bundled ? "Shipped example" : `Custom: ${wordPacksPath()}`; return { lines: [`${pack.name}: ${enabled ? "enabled" : "disabled"}`, `${pack.words.length} phrases · ${source}`, ...(pack.description ? [pack.description] : [])] }; } private promptPreview(values: Record, width: number): PreviewResult { if (values.decorateUserPrompt !== true) return { lines: ["User prompt decoration is disabled."] }; const borderColor = values.borderColor; const borderStyle = values.borderStyle; const timestamp = values.promptTimestamp === true ? Date.now() : undefined; const lines = buildPromptBoxLines("ping", timestamp, width, this.#ctx.ui.theme, { showIcon: values.promptIcon === true, showTimestamp: values.promptTimestamp === true, showProvider: values.promptProvider === true, showModel: values.promptModel === true, icon: values.useNerdFont ? "" : "π", provider: this.#ctx.model?.provider, model: this.#ctx.model?.id, borderColor: isPromptBorderColor(borderColor) ? borderColor : DEFAULT_SETTINGS.decorations.borderColor, borderStyle: isBorderStyle(borderStyle) ? borderStyle : "double", thinkingLevel: this.#ctx.thinkingLevel, }); return timestamp === undefined ? { lines } : { lines, nextRefreshInMs: 1_000 - (timestamp % 1_000) }; } private markerPreview(values: Record, width: number): PreviewResult { if (values.doneMarker !== true) return { lines: ["Completion marker is disabled."] }; const theme = this.#ctx.ui.theme; const icon = values.doneMarkerIcon === true ? theme.fg("text", values.useNerdFont === true ? "" : "π") : ""; const word = values.randomizeDoneMarker === true ? "Concocted" : "Worked"; const details: string[] = []; if (values.doneMarkerTokens === true) details.push("↓ 949 tokens"); if (values.doneMarkerInputs === true) details.push("2 mid-turn inputs"); if (values.doneMarkerServiceTier === true) details.push(formatServiceTierSummary([{ tier: "flex", calls: 3 }])); let model = ""; if (values.doneMarkerModel === true) { const color = isThinkingLevelColor(values.doneMarkerModelColor) ? values.doneMarkerModelColor : DEFAULT_SETTINGS.decorations.doneMarkerModelColor; const modelColored = getThinkingLevelColorizer(theme, color, this.#ctx.thinkingLevel)("test-model"); model = values.doneMarkerModelDimmed === true ? dimAttribute(modelColored) : modelColored; } const content = buildCompletionMarkerContent(theme, icon, word, "2m 52s", details, model); const borderStyle = isDoneMarkerBorderStyle(values.doneMarkerBorderStyle) ? values.doneMarkerBorderStyle : "none"; const borderColor = isDoneMarkerBorderColor(values.doneMarkerBorderColor) ? values.doneMarkerBorderColor : DEFAULT_SETTINGS.decorations.doneMarkerBorderColor; const markerStyle = isDoneMarkerStyle(values.doneMarkerStyle) ? values.doneMarkerStyle : DEFAULT_SETTINGS.decorations.doneMarkerStyle; const marker = buildCompletionMarkerLine(content, width, theme, borderStyle, borderColor, markerStyle, this.#ctx.thinkingLevel); return { lines: ["", marker, ""] }; } }