import type { ExtensionAPI, ExtensionCommandContext, KeybindingsManager, Theme, } from "@earendil-works/pi-coding-agent"; import type { Component, TUI } from "@earendil-works/pi-tui"; import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { discoverGpt56Profiles, findInitialProfileIndex, findProfile, getAvailableCapabilities, GPT56_THINKING_LEVELS, jumpCapabilityIndex, moveThinkingIndex, type Gpt56CapabilityId, type Gpt56Profile, wrapProfileIndex, } from "./profiles"; class Gpt56Slider implements Component { private selectedIndex: number; private closed = false; constructor( private readonly tui: TUI, private readonly theme: Theme, private readonly keybindings: KeybindingsManager, private readonly profiles: readonly Gpt56Profile[], initialIndex: number, private readonly currentProfile: Gpt56Profile | undefined, private readonly currentThinkingLevel: string, private readonly done: (profile: Gpt56Profile | null) => void, ) { this.selectedIndex = wrapProfileIndex(initialIndex, profiles.length); } private selected(): Gpt56Profile { return this.profiles[this.selectedIndex]; } private close(profile: Gpt56Profile | null): void { if (this.closed) return; this.closed = true; this.done(profile); } private select(index: number): void { this.selectedIndex = wrapProfileIndex(index, this.profiles.length); this.tui.requestRender(); } private jumpToCapability(capability: Gpt56CapabilityId): void { const target = this.profiles.find( (profile) => profile.capability === capability, ); if (!target) return; this.select( findInitialProfileIndex( this.profiles, target.modelId, this.selected().thinkingLevel, ), ); } handleInput(data: string): void { if (this.keybindings.matches(data, "tui.select.cancel")) { this.close(null); return; } if (this.keybindings.matches(data, "tui.select.confirm")) { this.close(this.selected()); return; } if (matchesKey(data, "left")) { this.select(moveThinkingIndex(this.profiles, this.selectedIndex, -1)); return; } if (matchesKey(data, "right")) { this.select(moveThinkingIndex(this.profiles, this.selectedIndex, 1)); return; } if (matchesKey(data, "up")) { this.select(jumpCapabilityIndex(this.profiles, this.selectedIndex, -1)); return; } if (matchesKey(data, "down")) { this.select(jumpCapabilityIndex(this.profiles, this.selectedIndex, 1)); return; } if (matchesKey(data, "home") || matchesKey(data, "end")) { const group = this.profiles.flatMap((profile, index) => profile.capability === this.selected().capability ? [index] : [], ); this.select(matchesKey(data, "home") ? group[0] : group[group.length - 1]); return; } if (data === "1") this.jumpToCapability("luna"); if (data === "2") this.jumpToCapability("terra"); if (data === "3") this.jumpToCapability("sol"); } private pad( content: string, width: number, alignment: "left" | "center" = "left", ): string { const clipped = truncateToWidth(content, Math.max(0, width)); const remaining = Math.max(0, width - visibleWidth(clipped)); if (alignment === "center") { const left = Math.floor(remaining / 2); return `${" ".repeat(left)}${clipped}${" ".repeat(remaining - left)}`; } return `${clipped}${" ".repeat(remaining)}`; } private frame( content: string, innerWidth: number, alignment: "left" | "center" = "left", ): string { return ( this.theme.fg("border", "│") + this.pad(content, innerWidth, alignment) + this.theme.fg("border", "│") ); } private renderNode(profile: Gpt56Profile): string { const active = profile === this.selected(); const node = `${active ? "●" : "○"} ${profile.thinkingLabel}`; return active ? this.theme.fg("accent", this.theme.bold(node)) : this.theme.fg("muted", node); } private renderGroup(capability: Gpt56CapabilityId): string { const group = this.profiles.filter( (profile) => profile.capability === capability, ); const active = this.selected().capability === capability; const label = group[0].capabilityLabel.padEnd(5); const marker = active ? this.theme.fg("accent", "❯") : " "; const styledLabel = active ? this.theme.fg("accent", this.theme.bold(label)) : this.theme.fg("text", label); const nodes = group.map((profile) => this.renderNode(profile)); return ` ${marker} ${styledLabel} ${nodes.join(this.theme.fg("border", " ─ "))}`; } render(width: number): string[] { const safeWidth = Math.max(4, width); const innerWidth = safeWidth - 2; const selected = this.selected(); const top = this.theme.fg("border", "╭") + this.theme.fg("border", "─".repeat(innerWidth)) + this.theme.fg("border", "╮"); const bottom = this.theme.fg("border", "╰") + this.theme.fg("border", "─".repeat(innerWidth)) + this.theme.fg("border", "╯"); const title = this.theme.fg( "accent", this.theme.bold(` GPT-5.6 · ${selected.providerId} `), ); const position = this.theme.fg( "dim", `${this.selectedIndex + 1}/${this.profiles.length}`, ); const titleGap = Math.max( 1, innerWidth - visibleWidth(title) - visibleWidth(position) - 2, ); const header = ` ${title}${" ".repeat(titleGap)}${position} `; const currentThinkingLabel = GPT56_THINKING_LEVELS.find( (thinking) => thinking.id === this.currentThinkingLevel, )?.label ?? this.currentThinkingLevel; const currentText = this.theme.fg( "muted", `${this.currentProfile?.capabilityLabel ?? "Other"} · ${currentThinkingLabel}`, ); const targetCapability = selected.capability === this.currentProfile?.capability ? this.theme.fg("accent", selected.capabilityLabel) : this.theme.fg("warning", this.theme.bold(selected.capabilityLabel)); const targetThinking = selected.thinkingLevel === this.currentThinkingLevel ? this.theme.fg("accent", selected.thinkingLabel) : this.theme.fg("warning", this.theme.bold(selected.thinkingLabel)); const targetText = `${targetCapability}${this.theme.fg("dim", " · ")}${targetThinking}`; const details = ` ${currentText}${this.theme.fg("dim", " → ")}${targetText}`; const hints = this.theme.fg( "dim", " ←/→ thinking · ↑/↓ capability · 1/2/3 direct · Enter apply · Esc cancel ", ); const capabilities = getAvailableCapabilities(this.profiles); return [ top, this.frame(header, innerWidth), this.frame("", innerWidth), ...capabilities.map((capability) => this.frame(this.renderGroup(capability), innerWidth), ), this.frame("", innerWidth), this.frame(details, innerWidth), this.frame(hints, innerWidth), bottom, ].map((line) => truncateToWidth(line, safeWidth, "")); } invalidate(): void {} } function discoverCurrentProviderProfiles( ctx: ExtensionCommandContext, ): Gpt56Profile[] | undefined { const providerId = ctx.model?.provider; if (!providerId) { ctx.ui.notify("No active Provider; select a model before using /g56", "warning"); return undefined; } const profiles = discoverGpt56Profiles( ctx.modelRegistry.getAvailable(), providerId, ); if (profiles.length === 0) { ctx.ui.notify( `Provider ${providerId} has no available GPT-5.6 Luna/Terra/Sol models`, "warning", ); return undefined; } return profiles; } async function chooseProfile( ctx: ExtensionCommandContext, profiles: readonly Gpt56Profile[], thinkingLevel: string, ): Promise { if (ctx.mode !== "tui") { ctx.ui.notify("/g56 without arguments is available only in Pi TUI mode", "warning"); return null; } const initialIndex = findInitialProfileIndex( profiles, ctx.model?.id, thinkingLevel, ); const currentProfile = profiles.find( (profile) => profile.modelId === ctx.model?.id && profile.thinkingLevel === thinkingLevel, ); return ctx.ui.custom( (tui, theme, keybindings, done) => new Gpt56Slider( tui, theme, keybindings, profiles, initialIndex, currentProfile, thinkingLevel, done, ), ); } async function applyProfile( pi: ExtensionAPI, ctx: ExtensionCommandContext, profile: Gpt56Profile, ): Promise { const model = ctx.modelRegistry.find(profile.providerId, profile.modelId); if (!model) { ctx.ui.notify( `Model disappeared from registry: ${profile.providerId}/${profile.modelId}`, "error", ); return; } try { const switched = await pi.setModel(model); if (!switched) { ctx.ui.notify( `Unable to switch to ${profile.providerId}/${profile.modelId}: Provider authentication is unavailable`, "error", ); return; } pi.setThinkingLevel(profile.thinkingLevel); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`GPT-5.6 switch failed: ${message}`, "error"); return; } const effectiveThinking = pi.getThinkingLevel(); const message = `${profile.providerId}/${profile.modelId} · thinking=${effectiveThinking}`; if (effectiveThinking !== profile.thinkingLevel) { ctx.ui.notify( `Switched with clamped thinking level: ${message}`, "warning", ); return; } ctx.ui.notify(`Switched: ${message}`, "info"); } export default function gpt56Switcher(pi: ExtensionAPI): void { let sliderOpen = false; pi.registerCommand("g56", { description: "Switch GPT-5.6 capability and thinking within the current Provider", handler: async (args, ctx) => { if (sliderOpen) { ctx.ui.notify("GPT-5.6 slider is already open", "warning"); return; } const profiles = discoverCurrentProviderProfiles(ctx); if (!profiles) return; const tokens = args.trim().split(/\s+/).filter(Boolean); if (tokens.length !== 0 && tokens.length !== 2) { ctx.ui.notify("Usage: /g56 [luna|terra|sol] [thinking-level]", "warning"); return; } let selected: Gpt56Profile | null; if (tokens.length === 2) { selected = findProfile(profiles, tokens[0], tokens[1]) ?? null; if (!selected) { ctx.ui.notify( `Profile is unavailable in Provider ${ctx.model?.provider}: ${tokens.join(" ")}`, "warning", ); return; } } else { sliderOpen = true; try { selected = await chooseProfile(ctx, profiles, pi.getThinkingLevel()); } finally { sliderOpen = false; } } if (selected) await applyProfile(pi, ctx, selected); }, }); }