From bab0d93dd2b5b55a3763dc66b6574bd4e44f2a50 Mon Sep 17 00:00:00 2001 From: Toni Ramiro Date: Sun, 18 Oct 2026 18:33:20 +0200 Subject: [PATCH] feat: selector de modelo con ventana desplazable y modelo por defecto del repo --- .pi/settings.json | 4 +++ CHANGELOG.md | 7 +++++ extensions/modelo-proyecto.ts | 53 ++++++++++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 .pi/settings.json diff --git a/.pi/settings.json b/.pi/settings.json new file mode 100644 index 0000000..9d6ccc7 --- /dev/null +++ b/.pi/settings.json @@ -0,0 +1,4 @@ +{ + "defaultProvider": "github-copilot", + "defaultModel": "claude-opus-5.5" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b722cf..7a22c20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ y el proyecto usa [Versionado Semántico](https://semver.org/lang/es/). ## [No publicado] +### Cambiado + +- El selector de modelo (`/modelo-proyecto` y `elegir_modelo_proyecto`) muestra + como máximo 15 modelos a la vez en una ventana que se desplaza con el cursor; + ↑/↓ dan la vuelta de un extremo a otro y el cursor parte del modelo actual + del proyecto. Fuera de la TUI se mantiene el diálogo estándar. + ### Pendiente - Fijar las reglas de modelo y de funcionamiento que la skill escribirá en el diff --git a/extensions/modelo-proyecto.ts b/extensions/modelo-proyecto.ts index 11dee1e..fb45fc8 100644 --- a/extensions/modelo-proyecto.ts +++ b/extensions/modelo-proyecto.ts @@ -16,9 +16,13 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from " import { join, resolve } from "node:path"; import type { Api, Model } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { DynamicBorder } from "@earendil-works/pi-coding-agent"; +import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; const SETTINGS_REL = join(".pi", "settings.json"); +/** Máximo de modelos visibles a la vez en el selector (la lista se desplaza). */ +const MAX_VISIBLES = 15; function clave(m: Model): string { return `${m.provider}/${m.id}`; @@ -88,10 +92,51 @@ async function elegir(ctx: ExtensionContext, proyecto: string): Promise etiqueta(m, actual)); - const elegido = await ctx.ui.select(`Modelo por defecto del proyecto (${proyecto})`, opciones); - if (!elegido) return undefined; - return lista[opciones.indexOf(elegido)]; + const titulo = `Modelo por defecto del proyecto (${proyecto})`; + + // Fuera de la TUI (p. ej. RPC) no hay componentes propios: diálogo estándar. + if (ctx.mode !== "tui") { + const opciones = lista.map((m) => etiqueta(m, actual)); + const elegido = await ctx.ui.select(titulo, opciones); + if (!elegido) return undefined; + return lista[opciones.indexOf(elegido)]; + } + + const items: SelectItem[] = lista.map((m) => ({ value: clave(m), label: etiqueta(m, actual) })); + const valor = await ctx.ui.custom((tui, theme, _kb, done) => { + const contenedor = new Container(); + contenedor.addChild(new DynamicBorder((s) => theme.fg("accent", s))); + contenedor.addChild(new Text(theme.fg("accent", theme.bold(titulo)))); + + // Ventana de MAX_VISIBLES elementos que se desplaza con el cursor; + // ↑ en el primero salta al último y ↓ en el último vuelve al primero. + const selector = new SelectList(items, Math.min(items.length, MAX_VISIBLES), { + selectedPrefix: (t) => theme.fg("accent", t), + selectedText: (t) => theme.fg("accent", t), + description: (t) => theme.fg("muted", t), + scrollInfo: (t) => theme.fg("dim", t), + noMatch: (t) => theme.fg("warning", t), + }); + const inicial = actual ? items.findIndex((i) => i.value === actual) : -1; + if (inicial >= 0) selector.setSelectedIndex(inicial); + selector.onSelect = (item) => done(item.value); + selector.onCancel = () => done(null); + contenedor.addChild(selector); + + contenedor.addChild(new Text(theme.fg("dim", "↑↓ moverse • enter elegir • esc cancelar"))); + contenedor.addChild(new DynamicBorder((s) => theme.fg("accent", s))); + + return { + render: (width: number) => contenedor.render(width), + invalidate: () => contenedor.invalidate(), + handleInput: (data: string) => { + selector.handleInput(data); + tui.requestRender(); + }, + }; + }); + if (!valor) return undefined; + return lista.find((m) => clave(m) === valor); } export default function (pi: ExtensionAPI) {