zugzbot 1.0.40 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/agents/sdd-coder.md +1 -1
- package/.opencode/agents/sdd-deployer.md +1 -1
- package/.opencode/agents/sdd-orchestrator.md +1 -1
- package/.opencode/agents/sdd-reviewer.md +1 -1
- package/.opencode/agents/sdd-spec-writer.md +1 -1
- package/.opencode/agents/sdd-tester.md +1 -1
- package/.opencode/rules/sdd-global.md +5 -2
- package/.opencode/skills/shadcn-templates/SKILL.md +24 -8
- package/.utils/export_opencode_session.py +1 -1
- package/.utils/toggle_model.py +145 -168
- package/opencode.json +3 -3
- package/package.json +1 -1
|
@@ -22,11 +22,14 @@ Cuando el estado del sistema tiene `loopMode === true` (activado mediante el com
|
|
|
22
22
|
- **Specs Incrementales**: Si la petición es grande, el orquestador debe dividirla secuencialmente en contratos incrementales. Completa un spec por completo (F0->F4) y arranca el siguiente de inmediato de forma autónoma hasta finalizar todo el plan.
|
|
23
23
|
- **Rollback de Emergencia**: Si una misma fase falla de forma continua (más de 2 rollbacks), desactiva `loopMode` usando `sdd_set_phase` y solicita asistencia humana en el chat.
|
|
24
24
|
|
|
25
|
-
## 3. Uso Mandatorio de Bloques Prehechos de Shadcn UI
|
|
25
|
+
## 3. Uso Mandatorio de Bloques Prehechos de Shadcn UI - Estrategia "Block-First" (MANDATORIO)
|
|
26
26
|
Para optimizar el tiempo de desarrollo, garantizar una experiencia visual premium y evitar la invención de layouts o formularios comunes desde cero:
|
|
27
27
|
- **Prioridad de Bloques**: Al diseñar cualquier interfaz de usuario (Layouts, Dashboards, Paneles de Control, Vistas de Configuración, Páginas de Autenticación, Tablas de Datos, Gráficos), los agentes **deben** buscar y utilizar de forma prioritaria los bloques prehechos del registro oficial `@shadcn` (como `dashboard-01`, `sidebar-01` al `16`, `login-01` al `05`, `signup-01` al `05`).
|
|
28
28
|
- **Búsqueda Proactiva**: El Spec-Writer **debe** consultar el registro utilizando las herramientas del MCP `shadcn` (`shadcn_search_items_in_registries`, `shadcn_list_items_in_registries`) al redactar el contrato (F1) para identificar qué bloques se ajustan a la necesidad, y declararlos en `frontend.components` y `files_affected` de `contract.json`. Además, el Spec-Writer **debe** invocar `shadcn_view_items_in_registries` sobre los bloques complejos seleccionados para inspeccionar por adelantado sus archivos, rutas y dependencias inyectadas por defecto, anticipando cualquier colisión de ruteo y documentándolo con precisión en el contrato.
|
|
29
|
-
- **
|
|
29
|
+
- **Inyección Zero-Touch y Preservación del Bloque en Fase A (MANDATORIO)**: El Coder (F2) **debe** instalar el bloque usando `npx shadcn@latest add <block-name>`. La CLI inteligente de Shadcn colocará automáticamente el archivo de página (ej. `src/app/dashboard/page.tsx` o `src/app/page.tsx`) y los componentes y datos locales asociados (ej. `src/components/` o `src/app/dashboard/data.json`) en las carpetas de producción correctas listas para renderizar.
|
|
30
|
+
- **No realizar migraciones físicas o reescrituras de imports artificiales**: Si la CLI ya colocó los archivos en sus rutas funcionales de producción de manera nativa, queda **estrictamente prohibido** mover los archivos a carpetas artificiales (como `src/components/blocks/...`) o reescribir sus imports usando alias complejos. Esto rompe la integración de Shadcn y destruye la sincronización de estilos y gráficos en modo oscuro. ¡Conserva el esqueleto tal como lo instala la CLI!
|
|
31
|
+
- **Preservación de Datos Dummy**: El archivo de datos del bloque (por ejemplo, `data.json` o constantes locales) debe mantenerse intacto en su ruta inyectada por la CLI en esta Fase A para garantizar que la vista se muestre de inmediato en el navegador exactamente idéntica al demo real.
|
|
32
|
+
- **Sustitución Atómica de Datos en Fase B**: Una vez que el bloque compila e interactúa en el navegador de manera idéntica al demo con los datos de ejemplo, el Coder procederá de forma exclusiva a sustituir los datos mock por lógica y fetchings de datos dinámicos del backend, manteniendo intactos los contenedores estructurales del bloque (grids de diseño, paddings y contenedores flex) sin alteración alguna para evitar aberraciones visuales.
|
|
30
33
|
- **Resolución Activa de Conflictos de Ruteo por Inyección de Bloques**: Si la CLI de Shadcn inyecta automáticamente una estructura de rutas (ej: `src/app/dashboard/page.tsx`) que compite con las rutas del contrato (como un route group `(dashboard)` o una ruta raíz), el Coder **debe** borrar o refactorizar de inmediato el código inyectado por defecto para evitar bucles de redirección infinitos o colisiones en el router.
|
|
31
34
|
|
|
32
35
|
## 4. Soporte para Subdirectorios, Monorepos (targetDir) y Estándar Corporativo `src/`
|
|
@@ -72,14 +72,30 @@ npx shadcn@latest add <component-name>
|
|
|
72
72
|
|
|
73
73
|
Al diseñar o codificar páginas con Shadcn UI, sigue estas reglas estrictas:
|
|
74
74
|
|
|
75
|
-
### 3.1 Componer, no reinventar (MANDATORIO)
|
|
76
|
-
Utiliza las plantillas y bloques existentes de Shadcn como el estándar absoluto de tu interfaz. Queda **estrictamente prohibido** escribir layouts, páginas de autenticación o cuadros de mando desde cero si ya existe un bloque oficial que resuelva el caso de uso.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
### 3.1 Componer, no reinventar - Estrategia "Block-First" con Inyección Zero-Touch (MANDATORIO)
|
|
76
|
+
Utiliza las plantillas y bloques existentes de Shadcn como el estándar absoluto de tu interfaz. Queda **estrictamente prohibido** escribir layouts, páginas de autenticación o cuadros de mando desde cero si ya existe un bloque oficial que resuelva el caso de uso.
|
|
77
|
+
|
|
78
|
+
Para garantizar interfaces de usuario premium y visualmente idénticas a los demos oficiales de Shadcn, se debe seguir de manera mandatoria la **Metodología de Arquitectura Block-First**:
|
|
79
|
+
|
|
80
|
+
1. **El Bloque es el Esqueleto Absoluto (Fase A - Inyección Pura y Preservación de Estructura)**:
|
|
81
|
+
- El Coder (F2) **debe** instalar el bloque de Shadcn correspondiente mediante la CLI (`npx shadcn@latest add <block-name>`).
|
|
82
|
+
- La CLI de Shadcn colocará automáticamente la página del bloque (ej: `src/app/dashboard/page.tsx`) y los componentes y datos asociados en las ubicaciones de producción correctas de tu proyecto.
|
|
83
|
+
- **Acción Obligatoria / Prohibición de Migración**: Queda **estrictamente prohibido** mover los archivos del bloque a carpetas artificiales (como `src/components/blocks/...`) o reescribir manualmente sus imports si la CLI ya los ha ubicado en sus posiciones funcionales definitivas. Esto evita errores sutiles de compilación, rotura de imports y degradación de los estilos de modo oscuro en gráficos de Recharts.
|
|
84
|
+
- **Preservación de Datos Mock/Dummy**: El archivo de datos del bloque (por ejemplo, `data.json` o constantes locales) **debe ser preservado e importado de manera intacta en su ruta nativa** durante esta Fase A para garantizar que la vista renderice con datos idénticos al demo original en el navegador.
|
|
85
|
+
- **Meta**: Al terminar la Fase A, la aplicación debe renderizarse de manera *pixel-perfect* (idéntica) al demo oficial de Shadcn en local, sin un solo error de compilación o de imports rotos.
|
|
86
|
+
|
|
87
|
+
2. **Sustitución de Datos Atómica (Fase B - Conexión de Lógica)**:
|
|
88
|
+
- Una vez comprobado que el esqueleto visual se muestra perfecto y sin aberraciones de flexbox o alineación, el Coder procederá a sustituir de manera quirúrgica y atómica los datos dummy o locales por la lógica de negocio real (estados de React, fetchings de API del backend).
|
|
89
|
+
- El Coder **no debe modificar las clases contenedoras estructurales** (`SidebarProvider`, `SidebarInset`, grids de diseño principales, anchos o paddings del bloque) para evitar corrupciones visuales en pantallas grandes o pequeñas.
|
|
90
|
+
|
|
91
|
+
3. **Prevención de Parpadeos (Flicker) de Sidebar y Providers**:
|
|
92
|
+
- Si el bloque utiliza providers globales como `<SidebarProvider>`, el Coder debe extraer la barra lateral (`AppSidebar`) y sus providers principales hacia `src/app/layout.tsx` (o un layout del Route Group respectivo) en lugar de dejarlos redundantes en el `page.tsx` de cada ruta, permitiendo un ruteo libre de parpadeos desagradables y garantizando consistencia.
|
|
93
|
+
|
|
94
|
+
4. **Catálogo de Bloques a Explorar**:
|
|
95
|
+
- **Dashboards**: Utiliza `dashboard-01` o cualquier variante de panel de control interactivo oficial.
|
|
96
|
+
- **Sidebars / Barras de Navegación**: Utiliza `sidebar-01` al `16` para paneles laterales funcionales, colapsables, dinámicos o anidados. No programes componentes de navegación complejos a mano.
|
|
97
|
+
- **Login / Registro**: Utiliza `login-01` al `05` y `signup-01` al `05` para flujos de login y registro de usuarios.
|
|
98
|
+
- **Gráficos y Métricas**: Busca y añade bloques de gráficos oficiales de Recharts (`chart-area-*`, `chart-bar-*`, `chart-pie-*`, `chart-radar-*`) para dar vida a la visualización de datos.
|
|
83
99
|
|
|
84
100
|
**REGLA CRÍTICA DE RUTAS DUPLICADAS POR SHADCN CLI**:
|
|
85
101
|
Al agregar un bloque interactivo complejo (como `dashboard-01`), el CLI de Shadcn puede inyectar automáticamente archivos en directorios fijos (como `src/app/dashboard/page.tsx`). El Coder **debe** verificar inmediatamente si existe una colisión con la estructura de rutas establecida por el contrato (`contract.json`) —por ejemplo, si el contrato establece un Route Group `src/app/(dashboard)/page.tsx` que sirve en la raíz `/`—. Si ocurre esto, el Coder tiene la obligación de **borrar, mover o refactorizar de inmediato el archivo inyectado automáticamente** para eliminar la duplicidad de rutas, evitando así bucles infinitos de redirección o errores fatales de compilación del router de Next.js.
|
|
@@ -9,7 +9,7 @@ DB_PATH = "/Users/wavesbyte/.local/share/opencode/opencode.db"
|
|
|
9
9
|
|
|
10
10
|
# Escribe aquí el ID de la sesión que deseas exportar (ejemplo: "ses_1234...")
|
|
11
11
|
# Si se deja vacío, el script requerirá el ID como argumento al ejecutarlo
|
|
12
|
-
TARGET_SESSION_ID = "
|
|
12
|
+
TARGET_SESSION_ID = "ses_119a"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def format_timestamp(ts):
|
package/.utils/toggle_model.py
CHANGED
|
@@ -4,54 +4,11 @@ import os
|
|
|
4
4
|
import json
|
|
5
5
|
import re
|
|
6
6
|
import curses
|
|
7
|
+
import subprocess
|
|
7
8
|
|
|
8
9
|
# ==============================================================================
|
|
9
|
-
#
|
|
10
|
+
# CONFIGURACIÓN Y RUTAS
|
|
10
11
|
# ==============================================================================
|
|
11
|
-
AVAILABLE_MODELS = [
|
|
12
|
-
"opencode/big-pickle",
|
|
13
|
-
"opencode/deepseek-v4-flash-free",
|
|
14
|
-
"opencode/mimo-v2.5-free",
|
|
15
|
-
"opencode/nemotron-3-ultra-free",
|
|
16
|
-
"opencode/north-mini-code-free",
|
|
17
|
-
"deepseek/deepseek-chat",
|
|
18
|
-
"deepseek/deepseek-reasoner",
|
|
19
|
-
"deepseek/deepseek-v4-flash",
|
|
20
|
-
"deepseek/deepseek-v4-pro",
|
|
21
|
-
"google/gemini-2.5-flash",
|
|
22
|
-
"google/gemini-2.5-flash-image",
|
|
23
|
-
"google/gemini-2.5-flash-lite",
|
|
24
|
-
"google/gemini-2.5-flash-preview-tts",
|
|
25
|
-
"google/gemini-2.5-pro",
|
|
26
|
-
"google/gemini-2.5-pro-preview-tts",
|
|
27
|
-
"google/gemini-3-flash-preview",
|
|
28
|
-
"google/gemini-3-pro-image-preview",
|
|
29
|
-
"google/gemini-3.1-flash-image-preview",
|
|
30
|
-
"google/gemini-3.1-flash-lite",
|
|
31
|
-
"google/gemini-3.1-pro-preview",
|
|
32
|
-
"google/gemini-3.1-pro-preview-customtools",
|
|
33
|
-
"google/gemini-3.5-flash",
|
|
34
|
-
"google/gemini-embedding-001",
|
|
35
|
-
"google/gemini-flash-latest",
|
|
36
|
-
"google/gemini-flash-lite-latest",
|
|
37
|
-
"google/gemma-4-26b-a4b-it",
|
|
38
|
-
"google/gemma-4-31b-it",
|
|
39
|
-
"minimax/MiniMax-M2",
|
|
40
|
-
"minimax/MiniMax-M2.1",
|
|
41
|
-
"minimax/MiniMax-M2.5",
|
|
42
|
-
"minimax/MiniMax-M2.5-highspeed",
|
|
43
|
-
"minimax/MiniMax-M2.7",
|
|
44
|
-
"minimax/MiniMax-M2.7-highspeed",
|
|
45
|
-
"minimax/MiniMax-M3",
|
|
46
|
-
"minimax-coding-plan/MiniMax-M2",
|
|
47
|
-
"minimax-coding-plan/MiniMax-M2.1",
|
|
48
|
-
"minimax-coding-plan/MiniMax-M2.5",
|
|
49
|
-
"minimax-coding-plan/MiniMax-M2.5-highspeed",
|
|
50
|
-
"minimax-coding-plan/MiniMax-M2.7",
|
|
51
|
-
"minimax-coding-plan/MiniMax-M2.7-highspeed",
|
|
52
|
-
"minimax-coding-plan/MiniMax-M3",
|
|
53
|
-
]
|
|
54
|
-
|
|
55
12
|
AGENTS_LIST = ["sdd-orchestrator", "sdd-spec-writer", "sdd-coder", "sdd-tester", "sdd-deployer"]
|
|
56
13
|
|
|
57
14
|
|
|
@@ -64,10 +21,44 @@ def get_paths():
|
|
|
64
21
|
return opencode_json, agents_dir, models_json
|
|
65
22
|
|
|
66
23
|
|
|
24
|
+
def fetch_available_models(silent=False):
|
|
25
|
+
"""Obtiene los modelos disponibles ejecutando el comando de opencode de forma dinámica."""
|
|
26
|
+
if not silent:
|
|
27
|
+
print("🔍 Cargando modelos desde opencode...", end="", flush=True)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Ejecutar 'opencode models' con un timeout de 4 segundos
|
|
31
|
+
res = subprocess.run(["opencode", "models"], capture_output=True, text=True, timeout=4)
|
|
32
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
33
|
+
models = [line.strip() for line in res.stdout.strip().split("\n") if line.strip()]
|
|
34
|
+
if not silent:
|
|
35
|
+
print(" ¡Listo! 🎉")
|
|
36
|
+
return models
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
if not silent:
|
|
41
|
+
print(" (Usando lista de respaldo de emergencia) ⚠️")
|
|
42
|
+
|
|
43
|
+
# Lista de respaldo en caso de que opencode models falle
|
|
44
|
+
return [
|
|
45
|
+
"google/gemini-3.5-flash",
|
|
46
|
+
"google/gemini-3.1-pro-preview",
|
|
47
|
+
"google/gemini-2.5-pro",
|
|
48
|
+
"google/gemini-2.5-flash",
|
|
49
|
+
"deepseek/deepseek-v4-flash",
|
|
50
|
+
"deepseek/deepseek-reasoner",
|
|
51
|
+
"opencode/deepseek-v4-flash-free",
|
|
52
|
+
"opencode/nemotron-3-ultra-free",
|
|
53
|
+
"opencode/mimo-v2.5-free",
|
|
54
|
+
"opencode/big-pickle",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
67
58
|
def load_models_config():
|
|
68
59
|
opencode_json, agents_dir, models_json = get_paths()
|
|
69
60
|
default_config = {
|
|
70
|
-
"global": "
|
|
61
|
+
"global": "google/gemini-3.5-flash",
|
|
71
62
|
"sdd-orchestrator": "",
|
|
72
63
|
"sdd-spec-writer": "",
|
|
73
64
|
"sdd-coder": "",
|
|
@@ -82,7 +73,6 @@ def load_models_config():
|
|
|
82
73
|
except Exception:
|
|
83
74
|
pass
|
|
84
75
|
|
|
85
|
-
# Guardamos el default si no existe
|
|
86
76
|
try:
|
|
87
77
|
with open(models_json, 'w', encoding='utf-8') as f:
|
|
88
78
|
json.dump(default_config, f, indent=2)
|
|
@@ -92,7 +82,7 @@ def load_models_config():
|
|
|
92
82
|
|
|
93
83
|
|
|
94
84
|
def save_models_config(config):
|
|
95
|
-
|
|
85
|
+
_, _, models_json = get_paths()
|
|
96
86
|
try:
|
|
97
87
|
with open(models_json, 'w', encoding='utf-8') as f:
|
|
98
88
|
json.dump(config, f, indent=2)
|
|
@@ -100,14 +90,8 @@ def save_models_config(config):
|
|
|
100
90
|
print(f"Error guardando models.json: {e}", file=sys.stderr)
|
|
101
91
|
|
|
102
92
|
|
|
103
|
-
def search_model(query):
|
|
104
|
-
query = query.lower()
|
|
105
|
-
matches = [m for m in AVAILABLE_MODELS if query in m.lower()]
|
|
106
|
-
return matches
|
|
107
|
-
|
|
108
|
-
|
|
109
93
|
def apply_model(agent_models):
|
|
110
|
-
opencode_json, agents_dir,
|
|
94
|
+
opencode_json, agents_dir, _ = get_paths()
|
|
111
95
|
|
|
112
96
|
if not os.path.exists(opencode_json):
|
|
113
97
|
print(f"Error: No se encontró {opencode_json}", file=sys.stderr)
|
|
@@ -130,7 +114,6 @@ def apply_model(agent_models):
|
|
|
130
114
|
if os.path.exists(agents_dir):
|
|
131
115
|
for filename in os.listdir(agents_dir):
|
|
132
116
|
if filename.endswith(".md"):
|
|
133
|
-
# Quitar extensión .md para comparar con el nombre del agente
|
|
134
117
|
agent_name = filename[:-3]
|
|
135
118
|
if agent_name in agent_models:
|
|
136
119
|
target_model = agent_models[agent_name]
|
|
@@ -162,26 +145,14 @@ def apply_model(agent_models):
|
|
|
162
145
|
print(f"Advertencia: Directorio de agentes {agents_dir} no existe.")
|
|
163
146
|
|
|
164
147
|
|
|
165
|
-
def print_available_models():
|
|
166
|
-
print("\nModelos disponibles:")
|
|
167
|
-
for m in AVAILABLE_MODELS:
|
|
168
|
-
print(f" - {m}")
|
|
169
|
-
print()
|
|
170
|
-
|
|
171
|
-
|
|
172
148
|
# ==============================================================================
|
|
173
149
|
# INTERFAZ INTERACTIVA TUI (CURSES)
|
|
174
150
|
# ==============================================================================
|
|
175
151
|
|
|
176
|
-
def select_model_tui(stdscr, title, current_val, is_global=False):
|
|
177
|
-
|
|
152
|
+
def select_model_tui(stdscr, title, current_val, models_list, is_global=False):
|
|
153
|
+
"""Pantalla interactiva de búsqueda con filtro en tiempo real para seleccionar un modelo."""
|
|
178
154
|
stdscr.keypad(True)
|
|
179
|
-
curses.curs_set(
|
|
180
|
-
|
|
181
|
-
# Colores
|
|
182
|
-
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Seleccionado
|
|
183
|
-
curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) # Buscador
|
|
184
|
-
curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) # Éxito / Info
|
|
155
|
+
curses.curs_set(1) # Mostrar cursor para el buscador
|
|
185
156
|
|
|
186
157
|
query = ""
|
|
187
158
|
selected_idx = 0
|
|
@@ -189,70 +160,72 @@ def select_model_tui(stdscr, title, current_val, is_global=False):
|
|
|
189
160
|
while True:
|
|
190
161
|
stdscr.clear()
|
|
191
162
|
|
|
192
|
-
#
|
|
163
|
+
# 1. Preparar lista de opciones filtrada
|
|
193
164
|
options = []
|
|
194
165
|
if not is_global:
|
|
195
|
-
options.append("")
|
|
166
|
+
options.append("") # Representa [Heredar del Global]
|
|
196
167
|
|
|
197
|
-
|
|
198
|
-
filtered_models = [m for m in AVAILABLE_MODELS if query.lower() in m.lower()]
|
|
168
|
+
filtered_models = [m for m in models_list if query.lower() in m.lower()]
|
|
199
169
|
options.extend(filtered_models)
|
|
200
170
|
|
|
201
|
-
#
|
|
171
|
+
# Ajustar límites de selección
|
|
202
172
|
if selected_idx >= len(options):
|
|
203
173
|
selected_idx = max(0, len(options) - 1)
|
|
204
174
|
|
|
205
|
-
#
|
|
206
|
-
stdscr.addstr(1, 2, f"
|
|
207
|
-
stdscr.addstr(2, 2, "Escribe
|
|
208
|
-
stdscr.addstr(3, 2, "
|
|
175
|
+
# 2. Renderizar interfaz (Estilo minimalista y sobrio)
|
|
176
|
+
stdscr.addstr(1, 2, f"┌─── {title} ───┐", curses.A_BOLD)
|
|
177
|
+
stdscr.addstr(2, 2, "│ Escribe para buscar en tiempo real, [↑/↓] navega, [Enter] confirma. │", curses.A_DIM)
|
|
178
|
+
stdscr.addstr(3, 2, "│ Presiona [ESC] para cancelar y volver atrás sin guardar. │", curses.A_DIM)
|
|
179
|
+
|
|
180
|
+
# Input de búsqueda
|
|
181
|
+
stdscr.addstr(5, 2, "🔍 Buscar: ")
|
|
182
|
+
stdscr.addstr(5, 13, query, curses.A_BOLD)
|
|
209
183
|
|
|
210
|
-
#
|
|
211
|
-
stdscr.addstr(
|
|
212
|
-
stdscr.addstr(5, 12, f" {query}█ ", curses.color_pair(2) | curses.A_BOLD)
|
|
213
|
-
stdscr.addstr(6, 2, "-" * 60, curses.A_DIM)
|
|
184
|
+
# Línea divisoria discreta
|
|
185
|
+
stdscr.addstr(6, 2, "─" * 70, curses.A_DIM)
|
|
214
186
|
|
|
215
|
-
# Mostrar opciones
|
|
187
|
+
# Mostrar opciones paginadas
|
|
216
188
|
start_row = 8
|
|
217
189
|
max_rows = curses.LINES - start_row - 2
|
|
218
190
|
|
|
219
191
|
if not options:
|
|
220
|
-
stdscr.addstr(start_row, 4, "No hay modelos que coincidan con la búsqueda.", curses.
|
|
192
|
+
stdscr.addstr(start_row, 4, "No hay modelos que coincidan con la búsqueda.", curses.A_BOLD)
|
|
221
193
|
else:
|
|
222
|
-
# Mostrar solo lo que quepa en la pantalla
|
|
223
194
|
for i in range(min(len(options), max_rows)):
|
|
224
195
|
opt = options[i]
|
|
225
196
|
row = start_row + i
|
|
226
197
|
|
|
227
|
-
# Formatear el
|
|
198
|
+
# Formatear el texto a mostrar
|
|
228
199
|
if opt == "":
|
|
229
|
-
label = "[ Heredar del Global ]"
|
|
200
|
+
label = " [ Heredar del Modelo Global ]"
|
|
230
201
|
else:
|
|
231
|
-
label = opt
|
|
202
|
+
label = f" {opt}"
|
|
232
203
|
|
|
233
|
-
#
|
|
204
|
+
# Añadir tag si es el actualmente activo
|
|
234
205
|
if opt == current_val:
|
|
235
206
|
label += " (activo)"
|
|
236
207
|
|
|
237
208
|
# Resaltar la opción seleccionada
|
|
238
209
|
if i == selected_idx:
|
|
239
|
-
stdscr.attron(curses.
|
|
240
|
-
stdscr.addstr(row, 2, f"
|
|
241
|
-
stdscr.attroff(curses.
|
|
210
|
+
stdscr.attron(curses.A_REVERSE)
|
|
211
|
+
stdscr.addstr(row, 2, f" {label:<66} ")
|
|
212
|
+
stdscr.attroff(curses.A_REVERSE)
|
|
242
213
|
else:
|
|
243
|
-
stdscr.addstr(row, 2, f"
|
|
214
|
+
stdscr.addstr(row, 2, f" {label:<66}")
|
|
244
215
|
|
|
216
|
+
# Mover el cursor al final de la búsqueda para feedback visual
|
|
217
|
+
stdscr.move(5, 13 + len(query))
|
|
245
218
|
stdscr.refresh()
|
|
246
219
|
|
|
247
|
-
#
|
|
220
|
+
# 3. Procesar teclado
|
|
248
221
|
try:
|
|
249
222
|
key = stdscr.getch()
|
|
250
223
|
except KeyboardInterrupt:
|
|
251
224
|
return None
|
|
252
225
|
|
|
253
|
-
if key in (27, curses.KEY_CANCEL):
|
|
226
|
+
if key in (27, curses.KEY_CANCEL): # ESC
|
|
254
227
|
return None
|
|
255
|
-
elif key in (10, 13, curses.KEY_ENTER):
|
|
228
|
+
elif key in (10, 13, curses.KEY_ENTER): # Enter
|
|
256
229
|
if options:
|
|
257
230
|
return options[selected_idx]
|
|
258
231
|
return None
|
|
@@ -262,24 +235,19 @@ def select_model_tui(stdscr, title, current_val, is_global=False):
|
|
|
262
235
|
elif key == curses.KEY_DOWN:
|
|
263
236
|
if selected_idx < len(options) - 1:
|
|
264
237
|
selected_idx += 1
|
|
265
|
-
elif key in (127, 8, curses.KEY_BACKSPACE):
|
|
238
|
+
elif key in (127, 8, curses.KEY_BACKSPACE): # Backspace
|
|
266
239
|
query = query[:-1]
|
|
267
240
|
selected_idx = 0
|
|
268
|
-
elif 32 <= key <= 126:
|
|
241
|
+
elif 32 <= key <= 126: # Caracteres imprimibles
|
|
269
242
|
query += chr(key)
|
|
270
243
|
selected_idx = 0
|
|
271
244
|
|
|
272
245
|
|
|
273
|
-
def main_tui(stdscr):
|
|
274
|
-
|
|
246
|
+
def main_tui(stdscr, models_list):
|
|
247
|
+
"""Menú de configuración principal."""
|
|
275
248
|
stdscr.keypad(True)
|
|
276
|
-
curses.curs_set(0)
|
|
277
|
-
|
|
278
|
-
# Inicializar pares de colores
|
|
279
|
-
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Selección
|
|
280
|
-
curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) # Alertas
|
|
281
|
-
curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) # Éxito
|
|
282
|
-
curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Warnings
|
|
249
|
+
curses.curs_set(0) # Ocultar cursor físico
|
|
250
|
+
curses.use_default_colors() # Respetar fondo de la terminal del usuario
|
|
283
251
|
|
|
284
252
|
config = load_models_config()
|
|
285
253
|
selected_row = 0
|
|
@@ -287,57 +255,71 @@ def main_tui(stdscr):
|
|
|
287
255
|
while True:
|
|
288
256
|
stdscr.clear()
|
|
289
257
|
|
|
290
|
-
|
|
291
|
-
stdscr.addstr(
|
|
292
|
-
stdscr.addstr(
|
|
293
|
-
stdscr.addstr(
|
|
258
|
+
# Cabecera moderna y minimalista
|
|
259
|
+
stdscr.addstr(1, 2, "🤖 ZUGZBOT: CONFIGURACIÓN DE MODELOS", curses.A_BOLD)
|
|
260
|
+
stdscr.addstr(2, 2, "Navega con [↑/↓], pulsa [Enter] para cambiar el modelo de un agente.", curses.A_DIM)
|
|
261
|
+
stdscr.addstr(3, 2, "Presiona [G] para guardar y aplicar, o [ESC]/[Q] para salir sin cambios.", curses.A_DIM)
|
|
262
|
+
stdscr.addstr(4, 2, "═" * 74, curses.A_BOLD)
|
|
294
263
|
|
|
295
|
-
# Construir items
|
|
296
|
-
global_val = config.get("global", "
|
|
264
|
+
# Construir items de la tabla
|
|
265
|
+
global_val = config.get("global", "google/gemini-3.5-flash")
|
|
297
266
|
|
|
298
267
|
menu_items = [
|
|
299
|
-
("global",
|
|
268
|
+
("global", "Modelo Global (Defecto)", global_val),
|
|
300
269
|
]
|
|
301
270
|
|
|
302
271
|
for agent in AGENTS_LIST:
|
|
303
272
|
val = config.get(agent, "")
|
|
304
|
-
label = val if val else f"
|
|
305
|
-
menu_items.append((agent,
|
|
273
|
+
label = val if val else f"Heredado -> {global_val}"
|
|
274
|
+
menu_items.append((agent, agent, label))
|
|
306
275
|
|
|
307
276
|
# Añadir opciones de acción al menú
|
|
308
|
-
menu_items.append(("save", "[✓] GUARDAR Y APLICAR CAMBIOS"))
|
|
309
|
-
menu_items.append(("cancel", "[✗] CANCELAR Y SALIR"))
|
|
277
|
+
menu_items.append(("save", " [✓] GUARDAR Y APLICAR CAMBIOS", ""))
|
|
278
|
+
menu_items.append(("cancel", " [✗] CANCELAR Y SALIR", ""))
|
|
310
279
|
|
|
280
|
+
# Dibujar cabecera de la tabla
|
|
311
281
|
start_row = 6
|
|
312
|
-
|
|
313
|
-
|
|
282
|
+
stdscr.addstr(start_row, 4, f"{'AGENTE / CONTEXTO':<28} │ {'MODELO ASIGNADO':<35}", curses.A_BOLD)
|
|
283
|
+
stdscr.addstr(start_row + 1, 2, "─" * 74, curses.A_DIM)
|
|
284
|
+
|
|
285
|
+
# Renderizar cada fila
|
|
286
|
+
item_start_row = start_row + 2
|
|
287
|
+
for idx, item in enumerate(menu_items):
|
|
288
|
+
key, col1, col2 = item
|
|
289
|
+
row = item_start_row + idx
|
|
314
290
|
|
|
315
|
-
#
|
|
291
|
+
# Formatear fila
|
|
292
|
+
if key in ("save", "cancel"):
|
|
293
|
+
text = f" {col1:<70} "
|
|
294
|
+
else:
|
|
295
|
+
text = f" {col1:<26} │ {col2:<38} "
|
|
296
|
+
|
|
297
|
+
# Resaltar la fila seleccionada
|
|
316
298
|
if idx == selected_row:
|
|
317
|
-
stdscr.attron(curses.
|
|
318
|
-
stdscr.addstr(row, 2, f"
|
|
319
|
-
stdscr.attroff(curses.
|
|
299
|
+
stdscr.attron(curses.A_REVERSE)
|
|
300
|
+
stdscr.addstr(row, 2, f" {text:<70} ")
|
|
301
|
+
stdscr.attroff(curses.A_REVERSE)
|
|
320
302
|
else:
|
|
321
303
|
if key == "save":
|
|
322
|
-
stdscr.addstr(row, 2, f"
|
|
304
|
+
stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
|
|
323
305
|
elif key == "cancel":
|
|
324
|
-
stdscr.addstr(row, 2, f"
|
|
306
|
+
stdscr.addstr(row, 2, f" {text:<70} ", curses.A_DIM)
|
|
325
307
|
elif key == "global":
|
|
326
|
-
stdscr.addstr(row, 2, f"
|
|
308
|
+
stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
|
|
327
309
|
else:
|
|
328
|
-
stdscr.addstr(row, 2, f"
|
|
310
|
+
stdscr.addstr(row, 2, f" {text:<70} ")
|
|
329
311
|
|
|
330
312
|
stdscr.refresh()
|
|
331
313
|
|
|
332
|
-
# Leer
|
|
314
|
+
# Leer teclas
|
|
333
315
|
try:
|
|
334
316
|
key = stdscr.getch()
|
|
335
317
|
except KeyboardInterrupt:
|
|
336
318
|
break
|
|
337
319
|
|
|
338
|
-
if key in (ord('q'), ord('Q'), 27):
|
|
320
|
+
if key in (ord('q'), ord('Q'), 27): # Q o ESC
|
|
339
321
|
break
|
|
340
|
-
elif key in (ord('g'), ord('G')):
|
|
322
|
+
elif key in (ord('g'), ord('G')): # G para Guardar directo
|
|
341
323
|
apply_and_exit(config)
|
|
342
324
|
break
|
|
343
325
|
elif key == curses.KEY_UP:
|
|
@@ -347,8 +329,7 @@ def main_tui(stdscr):
|
|
|
347
329
|
if selected_row < len(menu_items) - 1:
|
|
348
330
|
selected_row += 1
|
|
349
331
|
elif key in (10, 13, curses.KEY_ENTER):
|
|
350
|
-
|
|
351
|
-
action_key, _ = menu_items[selected_row]
|
|
332
|
+
action_key = menu_items[selected_row][0]
|
|
352
333
|
|
|
353
334
|
if action_key == "save":
|
|
354
335
|
apply_and_exit(config)
|
|
@@ -356,39 +337,39 @@ def main_tui(stdscr):
|
|
|
356
337
|
elif action_key == "cancel":
|
|
357
338
|
break
|
|
358
339
|
elif action_key == "global":
|
|
359
|
-
#
|
|
360
|
-
new_val = select_model_tui(stdscr, "SELECCIONAR MODELO GLOBAL POR DEFECTO", global_val, is_global=True)
|
|
340
|
+
# Cambiar modelo global
|
|
341
|
+
new_val = select_model_tui(stdscr, "SELECCIONAR MODELO GLOBAL POR DEFECTO", global_val, models_list, is_global=True)
|
|
361
342
|
if new_val is not None:
|
|
362
343
|
config["global"] = new_val
|
|
363
344
|
save_models_config(config)
|
|
364
345
|
else:
|
|
365
|
-
#
|
|
346
|
+
# Cambiar modelo de un agente específico
|
|
366
347
|
current_agent_val = config.get(action_key, "")
|
|
367
|
-
new_val = select_model_tui(stdscr, f"SELECCIONAR MODELO PARA {action_key}", current_agent_val, is_global=False)
|
|
348
|
+
new_val = select_model_tui(stdscr, f"SELECCIONAR MODELO PARA {action_key.upper()}", current_agent_val, models_list, is_global=False)
|
|
368
349
|
if new_val is not None:
|
|
369
350
|
config[action_key] = new_val
|
|
370
351
|
save_models_config(config)
|
|
371
352
|
|
|
372
353
|
|
|
373
354
|
def apply_and_exit(config):
|
|
374
|
-
# Guardar la configuración
|
|
375
355
|
save_models_config(config)
|
|
376
356
|
|
|
377
|
-
|
|
378
|
-
global_model = config.get("global", "deepseek/deepseek-v4-flash")
|
|
357
|
+
global_model = config.get("global", "google/gemini-3.5-flash")
|
|
379
358
|
final_agent_models = {}
|
|
380
359
|
|
|
381
|
-
print("\
|
|
360
|
+
print("\n" + "=" * 60)
|
|
361
|
+
print("📋 MAPEO DE MODELOS APLICADO:")
|
|
362
|
+
print("=" * 60)
|
|
382
363
|
for agent_name in AGENTS_LIST:
|
|
383
364
|
custom_val = config.get(agent_name, "")
|
|
384
365
|
final_model = custom_val if custom_val else global_model
|
|
385
366
|
final_agent_models[agent_name] = final_model
|
|
386
|
-
origin = "
|
|
387
|
-
print(f" * {agent_name} -> {final_model} ({origin})")
|
|
367
|
+
origin = "Personalizado" if custom_val else "Heredado del Global"
|
|
368
|
+
print(f" * {agent_name:<18} -> {final_model:<30} ({origin})")
|
|
388
369
|
|
|
389
|
-
print("\nAplicando cambios...")
|
|
370
|
+
print("\nAplicando cambios en los archivos de configuración...")
|
|
390
371
|
apply_model(final_agent_models)
|
|
391
|
-
print("\
|
|
372
|
+
print("\n🎉 Configuración completada con éxito.")
|
|
392
373
|
|
|
393
374
|
|
|
394
375
|
# ==============================================================================
|
|
@@ -397,44 +378,40 @@ def apply_and_exit(config):
|
|
|
397
378
|
|
|
398
379
|
def main():
|
|
399
380
|
args = sys.argv[1:]
|
|
400
|
-
|
|
401
|
-
# Cargar la configuración actual del JSON
|
|
402
381
|
config = load_models_config()
|
|
403
382
|
|
|
404
|
-
# Si se pide listar
|
|
383
|
+
# Si es modo CLI rápido sin argumentos, o se pide listar
|
|
405
384
|
if args and args[0] in ("--list", "-l", "list"):
|
|
406
|
-
|
|
385
|
+
models = fetch_available_models(silent=True)
|
|
386
|
+
print("\nModelos disponibles en opencode:")
|
|
387
|
+
for m in models:
|
|
388
|
+
print(f" - {m}")
|
|
407
389
|
sys.exit(0)
|
|
408
390
|
|
|
409
|
-
# Si se pasan argumentos
|
|
391
|
+
# Si se pasan argumentos, procesa actualización por CLI no-interactiva rápida
|
|
410
392
|
if args:
|
|
411
393
|
query = args[0]
|
|
412
|
-
|
|
394
|
+
models = fetch_available_models(silent=True)
|
|
395
|
+
matches = [m for m in models if query.lower() in m.lower()]
|
|
413
396
|
|
|
414
397
|
if not matches:
|
|
415
|
-
print(
|
|
416
|
-
f"Error: Ningún modelo coincide con '{query}'. Use 'list' para ver todos.",
|
|
417
|
-
file=sys.stderr,
|
|
418
|
-
)
|
|
398
|
+
print(f"Error: Ningún modelo coincide con '{query}'.", file=sys.stderr)
|
|
419
399
|
sys.exit(1)
|
|
420
400
|
elif len(matches) == 1:
|
|
421
|
-
|
|
401
|
+
selected_model = matches[0]
|
|
422
402
|
else:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
print(f"Coincidencias encontradas para '{query}':")
|
|
403
|
+
selected_model = matches[0]
|
|
404
|
+
print(f"Múltiples coincidencias para '{query}':")
|
|
426
405
|
for m in matches:
|
|
427
406
|
print(f" * {m}")
|
|
428
|
-
print(
|
|
429
|
-
f"Seleccionando automáticamente la primera: {global_model_selected}\n"
|
|
430
|
-
)
|
|
407
|
+
print(f"Seleccionando la primera por defecto: {selected_model}\n")
|
|
431
408
|
|
|
432
|
-
|
|
433
|
-
config["global"] = global_model_selected
|
|
409
|
+
config["global"] = selected_model
|
|
434
410
|
apply_and_exit(config)
|
|
435
411
|
else:
|
|
436
|
-
#
|
|
437
|
-
|
|
412
|
+
# Modo interactivo elegante con curses
|
|
413
|
+
models_list = fetch_available_models(silent=False)
|
|
414
|
+
curses.wrapper(main_tui, models_list)
|
|
438
415
|
|
|
439
416
|
|
|
440
417
|
if __name__ == "__main__":
|
package/opencode.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"permission": {
|
|
7
7
|
"webfetch": "deny"
|
|
8
8
|
},
|
|
9
|
-
"model": "
|
|
9
|
+
"model": "google/gemini-3.5-flash"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"permission": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"./.opencode/oh-my-design/packages/mcp/dist/index.mjs"
|
|
44
44
|
],
|
|
45
45
|
"enabled": true,
|
|
46
|
-
"purpose": "Busca, consulta y aplica temas de
|
|
46
|
+
"purpose": "Busca, consulta y aplica temas de dise\u00f1o (Toss, Stripe, Supabase, etc.)."
|
|
47
47
|
},
|
|
48
48
|
"shadcn": {
|
|
49
49
|
"type": "local",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"--isolated"
|
|
77
77
|
],
|
|
78
78
|
"enabled": true,
|
|
79
|
-
"purpose": "Ejecuta pruebas de
|
|
79
|
+
"purpose": "Ejecuta pruebas de regresi\u00f3n visual y capturas de pantalla de Playwright."
|
|
80
80
|
},
|
|
81
81
|
"next-devtools": {
|
|
82
82
|
"type": "local",
|