Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
11 kB · 336 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337import type { Model } from "@earendil-works/pi-ai";import { Container, type Focusable, fuzzyFilter, getKeybindings, Input, Key, matchesKey, Spacer, Text,} from "@earendil-works/pi-tui";import { theme } from "../theme/theme.js";import { DynamicBorder } from "./dynamic-border.js";import { keyText } from "./keybinding-hints.js";
type EnabledIds = string[] | null;
function isEnabled(enabledIds: EnabledIds, id: string): boolean { return enabledIds === null || enabledIds.includes(id);}
function toggle(enabledIds: EnabledIds, id: string): EnabledIds { if (enabledIds === null) return [id]; // First toggle: start with only this one const index = enabledIds.indexOf(id); if (index >= 0) return [...enabledIds.slice(0, index), ...enabledIds.slice(index + 1)]; return [...enabledIds, id];}
function enableAll(enabledIds: EnabledIds, allIds: string[], targetIds?: string[]): EnabledIds { if (enabledIds === null) return null; // Already all enabled const targets = targetIds ?? allIds; const result = [...enabledIds]; for (const id of targets) { if (!result.includes(id)) result.push(id); } return result.length === allIds.length ? null : result;}
function clearAll(enabledIds: EnabledIds, allIds: string[], targetIds?: string[]): EnabledIds { if (enabledIds === null) { return targetIds ? allIds.filter((id) => !targetIds.includes(id)) : []; } const targets = new Set(targetIds ?? enabledIds); return enabledIds.filter((id) => !targets.has(id));}
function move(enabledIds: EnabledIds, id: string, delta: number): EnabledIds { if (enabledIds === null) return null; const list = [...enabledIds]; const index = list.indexOf(id); if (index < 0) return list; const newIndex = index + delta; if (newIndex < 0 || newIndex >= list.length) return list; const result = [...list]; [result[index], result[newIndex]] = [result[newIndex], result[index]]; return result;}
function getSortedIds(enabledIds: EnabledIds, allIds: string[]): string[] { if (enabledIds === null) return allIds; const enabledSet = new Set(enabledIds); return [...enabledIds, ...allIds.filter((id) => !enabledSet.has(id))];}
interface ModelItem { fullId: string; model: Model<any>; enabled: boolean;}
export interface ModelsConfig { allModels: Model<any>[]; enabledModelIds: string[] | null;}
export interface ModelsCallbacks { /** Called whenever the enabled model set or order changes (session-only, no persist) */ onChange: (enabledModelIds: string[] | null) => void | Promise<void>; /** Called when user wants to persist current selection to settings */ onPersist: (enabledModelIds: string[] | null) => void | Promise<void>; onCancel: () => void;}
export class ScopedModelsSelectorComponent extends Container implements Focusable { private modelsById: Map<string, Model<any>> = new Map(); private allIds: string[] = []; private enabledIds: EnabledIds = null; private filteredItems: ModelItem[] = []; private selectedIndex = 0; private searchQuery = ""; private searchInput: Input;
private _focused = false; get focused(): boolean { return this._focused; } set focused(value: boolean) { this._focused = value; this.searchInput.focused = value; } private listContainer: Container; private footerText: Text; private callbacks: ModelsCallbacks; private maxVisible = 8; private isDirty = false;
constructor(config: ModelsConfig, callbacks: ModelsCallbacks) { super(); this.callbacks = callbacks;
for (const model of config.allModels) { const fullId = `${model.provider}/${model.id}`; this.modelsById.set(fullId, model); this.allIds.push(fullId); }
this.enabledIds = config.enabledModelIds === null ? null : [...config.enabledModelIds]; this.filteredItems = this.buildItems();
this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); this.addChild(new Text(theme.fg("accent", "Model Configuration"), 0, 0)); this.addChild( new Text(theme.fg("muted", `Session-only. ${keyText("app.models.save")} to save to settings.`), 0, 0), ); this.addChild(new Spacer(1));
this.searchInput = new Input(); this.addChild(this.searchInput); this.addChild(new Spacer(1));
this.listContainer = new Container(); this.addChild(this.listContainer);
this.addChild(new Spacer(1)); this.footerText = new Text(this.getFooterText(), 0, 0); this.addChild(this.footerText);
this.addChild(new DynamicBorder()); this.updateList(); }
private buildItems(): ModelItem[] { // Filter out IDs that no longer have a corresponding model (e.g., after logout) return getSortedIds(this.enabledIds, this.allIds) .filter((id) => this.modelsById.has(id)) .map((id) => ({ fullId: id, model: this.modelsById.get(id)!, enabled: isEnabled(this.enabledIds, id), })); }
private getFooterText(): string { const enabledCount = this.enabledIds?.length ?? this.allIds.length; const allEnabled = this.enabledIds === null; const countText = allEnabled ? "all enabled" : `${enabledCount}/${this.allIds.length} enabled`; const parts = [ `${keyText("tui.select.confirm")} toggle`, `${keyText("app.models.enableAll")} all`, `${keyText("app.models.clearAll")} clear`, `${keyText("app.models.toggleProvider")} provider`, `${keyText("app.models.reorderUp")}/${keyText("app.models.reorderDown")} reorder`, `${keyText("app.models.save")} save`, countText, ]; return this.isDirty ? theme.fg("dim", ` ${parts.join(" · ")} `) + theme.fg("warning", "(unsaved)") : theme.fg("dim", ` ${parts.join(" · ")}`); }
private refresh(): void { const query = this.searchInput.getValue(); const queryChanged = query !== this.searchQuery; this.searchQuery = query; const items = this.buildItems(); this.filteredItems = query ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) : items; this.selectedIndex = queryChanged ? 0 : Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1)); this.updateList(); this.footerText.setText(this.getFooterText()); }
private notifyChange(): void { this.callbacks.onChange(this.enabledIds === null ? null : [...this.enabledIds]); }
private updateList(): void { this.listContainer.clear();
if (this.filteredItems.length === 0) { this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0)); return; }
const startIndex = Math.max( 0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible), ); const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length); const allEnabled = this.enabledIds === null;
for (let i = startIndex; i < endIndex; i++) { const item = this.filteredItems[i]!; const isSelected = i === this.selectedIndex; const prefix = isSelected ? theme.fg("accent", "› ") : " "; const modelText = isSelected ? theme.fg("accent", item.model.id) : item.model.id; const providerBadge = theme.fg("muted", ` [${item.model.provider}]`); const status = allEnabled ? "" : item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗"); this.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0)); }
if (startIndex > 0 || endIndex < this.filteredItems.length) { this.listContainer.addChild( new Text(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredItems.length})`), 0, 0), ); }
if (this.filteredItems.length > 0) { const selected = this.filteredItems[this.selectedIndex]; this.listContainer.addChild(new Spacer(1)); this.listContainer.addChild(new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`), 0, 0)); } }
handleInput(data: string): void { const kb = getKeybindings();
if (kb.matches(data, "tui.select.up")) { if (this.filteredItems.length === 0) return; this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1; this.updateList(); return; } if (kb.matches(data, "tui.select.down")) { if (this.filteredItems.length === 0) return; this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1; this.updateList(); return; }
const reorderUp = kb.matches(data, "app.models.reorderUp"); const reorderDown = kb.matches(data, "app.models.reorderDown"); if (reorderUp || reorderDown) { if (this.enabledIds === null) return; const item = this.filteredItems[this.selectedIndex]; if (item && isEnabled(this.enabledIds, item.fullId)) { const delta = reorderUp ? -1 : 1; const currentIndex = this.enabledIds.indexOf(item.fullId); const newIndex = currentIndex + delta; if (newIndex >= 0 && newIndex < this.enabledIds.length) { this.enabledIds = move(this.enabledIds, item.fullId, delta); this.isDirty = true; this.selectedIndex += delta; this.refresh(); this.notifyChange(); } } return; }
if (kb.matches(data, "tui.select.confirm")) { const item = this.filteredItems[this.selectedIndex]; if (item) { this.enabledIds = toggle(this.enabledIds, item.fullId); this.isDirty = true; this.refresh(); this.notifyChange(); } return; }
if (kb.matches(data, "app.models.enableAll")) { const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined; this.enabledIds = enableAll(this.enabledIds, this.allIds, targetIds); this.isDirty = true; this.refresh(); this.notifyChange(); return; }
if (kb.matches(data, "app.models.clearAll")) { const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined; this.enabledIds = clearAll(this.enabledIds, this.allIds, targetIds); this.isDirty = true; this.refresh(); this.notifyChange(); return; }
if (kb.matches(data, "app.models.toggleProvider")) { const item = this.filteredItems[this.selectedIndex]; if (item) { const provider = item.model.provider; const providerIds = this.allIds.filter((id) => this.modelsById.get(id)!.provider === provider); const allEnabled = providerIds.every((id) => isEnabled(this.enabledIds, id)); this.enabledIds = allEnabled ? clearAll(this.enabledIds, this.allIds, providerIds) : enableAll(this.enabledIds, this.allIds, providerIds); this.isDirty = true; this.refresh(); this.notifyChange(); } return; }
if (kb.matches(data, "app.models.save")) { this.callbacks.onPersist(this.enabledIds === null ? null : [...this.enabledIds]); this.isDirty = false; this.footerText.setText(this.getFooterText()); return; }
if (matchesKey(data, Key.ctrl("c"))) { if (this.searchInput.getValue()) { this.searchInput.setValue(""); this.refresh(); } else { this.callbacks.onCancel(); } return; }
if (matchesKey(data, Key.escape)) { this.callbacks.onCancel(); return; }
this.searchInput.handleInput(data); this.refresh(); }
getSearchInput(): Input { return this.searchInput; }}