Download packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts from SaylorTwift/pi: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts
- Command line
-
hf download hf://SaylorTwift/pi/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts
-
curl -L -o scoped-models-selector.ts https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts
13.6 kB
| import type { Model } from "@earendil-works/pi-ai"; | |
| import { | |
| Container, | |
| type Focusable, | |
| fuzzyFilter, | |
| getKeybindings, | |
| Input, | |
| Key, | |
| matchesKey, | |
| Spacer, | |
| Text, | |
| } from "@earendil-works/pi-tui"; | |
| import { getModelSearchText } from "../model-search.ts"; | |
| import { theme } from "../theme/theme.ts"; | |
| import { DynamicBorder } from "./dynamic-border.ts"; | |
| import { keyDisplayText } from "./keybinding-hints.ts"; | |
| // EnabledIds: null = all enabled (no filter), string[] = explicit ordered list | |
| type EnabledIds = string[] | null; | |
| function isEnabled(enabledIds: EnabledIds, id: string): boolean { | |
| return enabledIds === null || enabledIds.includes(id); | |
| } | |
| /** Collapse an explicit list back to null (= all enabled) when it covers every available model. */ | |
| function normalizeEnabled(result: string[], allIds: string[]): EnabledIds { | |
| return result.length === allIds.length && result.every((id) => allIds.includes(id)) ? null : result; | |
| } | |
| function toggle(enabledIds: EnabledIds, allIds: string[], id: string): EnabledIds { | |
| if (enabledIds === null) return allIds.filter((modelId) => modelId !== id); | |
| const index = enabledIds.indexOf(id); | |
| if (index >= 0) return [...enabledIds.slice(0, index), ...enabledIds.slice(index + 1)]; | |
| return normalizeEnabled([...enabledIds, id], allIds); | |
| } | |
| 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 normalizeEnabled(result, allIds); | |
| } | |
| 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> | undefined; | |
| enabled: boolean; | |
| } | |
| export interface ModelsConfig { | |
| allModels: Model<any>[]; | |
| enabledModelIds: string[] | null; | |
| refreshStatus?: string; | |
| } | |
| 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; | |
| } | |
| /** | |
| * Component for enabling/disabling models for Ctrl+P cycling. | |
| * Changes are session-only until explicitly persisted with Ctrl+S. | |
| */ | |
| 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 searchInput: Input; | |
| // Focusable implementation - propagate to searchInput for IME cursor positioning | |
| 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; | |
| private refreshStatusText?: Text; | |
| 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(); | |
| // Header | |
| this.addChild(new DynamicBorder()); | |
| this.addChild(new Spacer(1)); | |
| this.addChild(new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0)); | |
| this.addChild( | |
| new Text(theme.fg("muted", `Session-only. ${keyDisplayText("app.models.save")} to save to settings.`), 0, 0), | |
| ); | |
| this.addChild(new Spacer(1)); | |
| // Search input | |
| this.searchInput = new Input(); | |
| this.addChild(this.searchInput); | |
| this.addChild(new Spacer(1)); | |
| // List container | |
| this.listContainer = new Container(); | |
| this.addChild(this.listContainer); | |
| // Footer hint | |
| this.addChild(new Spacer(1)); | |
| if (config.refreshStatus) { | |
| this.refreshStatusText = new Text(theme.fg("muted", ` ${config.refreshStatus}`), 0, 0); | |
| this.addChild(this.refreshStatusText); | |
| } | |
| this.footerText = new Text(this.getFooterText(), 0, 0); | |
| this.addChild(this.footerText); | |
| this.addChild(new DynamicBorder()); | |
| this.updateList(); | |
| } | |
| updateModels(models: readonly Model<any>[], enabledModelIds?: string[] | null): void { | |
| const selectedId = this.filteredItems[this.selectedIndex]?.fullId; | |
| if (enabledModelIds !== undefined) this.enabledIds = enabledModelIds === null ? null : [...enabledModelIds]; | |
| this.modelsById.clear(); | |
| this.allIds = []; | |
| for (const model of models) { | |
| const fullId = `${model.provider}/${model.id}`; | |
| this.modelsById.set(fullId, model); | |
| this.allIds.push(fullId); | |
| } | |
| this.refresh(); | |
| const refreshedIndex = selectedId ? this.filteredItems.findIndex((item) => item.fullId === selectedId) : -1; | |
| if (refreshedIndex >= 0) { | |
| this.selectedIndex = refreshedIndex; | |
| this.updateList(); | |
| } | |
| } | |
| setRefreshStatus(message: string, kind: "muted" | "success" | "warning"): void { | |
| this.refreshStatusText?.setText(theme.fg(kind, ` ${message}`)); | |
| } | |
| private buildItems(): ModelItem[] { | |
| return getSortedIds(this.enabledIds, this.allIds).map((id) => ({ | |
| fullId: id, | |
| model: this.modelsById.get(id), | |
| enabled: isEnabled(this.enabledIds, id), | |
| })); | |
| } | |
| private getFooterText(): string { | |
| const enabledCount = this.enabledIds?.filter((id) => this.modelsById.has(id)).length ?? this.allIds.length; | |
| const unavailableCount = this.enabledIds?.filter((id) => !this.modelsById.has(id)).length ?? 0; | |
| const allEnabled = this.enabledIds === null; | |
| const countText = allEnabled | |
| ? "all enabled" | |
| : `${enabledCount}/${this.allIds.length} enabled${unavailableCount ? ` · ${unavailableCount} unavailable` : ""}`; | |
| const parts = [ | |
| `${keyDisplayText("tui.select.confirm")} toggle`, | |
| `${keyDisplayText("app.models.enableAll")} all`, | |
| `${keyDisplayText("app.models.clearAll")} clear`, | |
| `${keyDisplayText("app.models.toggleProvider")} provider`, | |
| `${keyDisplayText("app.models.reorderUp")}/${keyDisplayText("app.models.reorderDown")} reorder`, | |
| `${keyDisplayText("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 items = this.buildItems(); | |
| this.filteredItems = query | |
| ? fuzzyFilter(items, query, (item) => | |
| item.model | |
| ? getModelSearchText({ id: item.model.id, provider: item.model.provider, name: item.model.name }) | |
| : item.fullId, | |
| ) | |
| : items; | |
| this.selectedIndex = 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); | |
| for (let i = startIndex; i < endIndex; i++) { | |
| const item = this.filteredItems[i]!; | |
| const isSelected = i === this.selectedIndex; | |
| const prefix = isSelected ? theme.fg("accent", "→ ") : " "; | |
| const id = item.model?.id ?? item.fullId; | |
| const styledId = item.model ? id : theme.strikethrough(id); | |
| const modelText = isSelected ? theme.fg("accent", styledId) : styledId; | |
| const providerBadge = theme.fg("muted", item.model ? ` [${item.model.provider}]` : " [unavailable]"); | |
| const status = item.model && item.enabled ? theme.fg("accent", "✓ ") : " "; | |
| this.listContainer.addChild(new Text(`${prefix}${status}${modelText}${providerBadge}`, 0, 0)); | |
| } | |
| // Add scroll indicator if needed | |
| 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", ` ${selected.model ? `Model Name: ${selected.model.name}` : "Model unavailable"}`), | |
| 0, | |
| 0, | |
| ), | |
| ); | |
| } | |
| } | |
| handleInput(data: string): void { | |
| const kb = getKeybindings(); | |
| // Navigation | |
| 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; | |
| } | |
| // Reorder enabled models | |
| 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; | |
| // Only move if within bounds | |
| 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; | |
| } | |
| // Toggle on Enter | |
| if (kb.matches(data, "tui.select.confirm")) { | |
| const item = this.filteredItems[this.selectedIndex]; | |
| if (item) { | |
| this.enabledIds = toggle(this.enabledIds, this.allIds, item.fullId); | |
| this.isDirty = true; | |
| this.refresh(); | |
| this.notifyChange(); | |
| } | |
| return; | |
| } | |
| // Enable all (filtered if search active, otherwise all) | |
| 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; | |
| } | |
| // Clear all (filtered if search active, otherwise all) | |
| 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; | |
| } | |
| // Toggle provider of current item | |
| if (kb.matches(data, "app.models.toggleProvider")) { | |
| const item = this.filteredItems[this.selectedIndex]; | |
| if (item?.model) { | |
| 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; | |
| } | |
| // Save/persist to settings | |
| 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; | |
| } | |
| // Ctrl+C - clear search or cancel if empty | |
| if (matchesKey(data, Key.ctrl("c"))) { | |
| if (this.searchInput.getValue()) { | |
| this.searchInput.setValue(""); | |
| this.refresh(); | |
| } else { | |
| this.callbacks.onCancel(); | |
| } | |
| return; | |
| } | |
| // Escape - cancel | |
| if (matchesKey(data, Key.escape)) { | |
| this.callbacks.onCancel(); | |
| return; | |
| } | |
| // Pass everything else to search input | |
| this.searchInput.handleInput(data); | |
| this.refresh(); | |
| } | |
| getSearchInput(): Input { | |
| return this.searchInput; | |
| } | |
| } | |