zugzbot 1.0.40 → 1.0.41

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.
@@ -3,7 +3,7 @@ description: Implementa código ajustándose estrictamente a los contratos en sp
3
3
  mode: subagent
4
4
  hidden: true
5
5
  steps: 10
6
- model: deepseek/deepseek-v4-flash
6
+ model: google/gemini-3.5-flash
7
7
  temperature: 0.35
8
8
  frequency_penalty: 0.5
9
9
  presence_penalty: 0.2
@@ -2,7 +2,7 @@
2
2
  description: Limpia Docker y realiza el despliegue local del sistema
3
3
  mode: subagent
4
4
  hidden: true
5
- model: deepseek/deepseek-v4-flash
5
+ model: google/gemini-3.5-flash
6
6
  temperature: 0.1
7
7
  tools:
8
8
  write: true
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Coordinador principal del flujo SDD y del stack cerrado
3
3
  mode: primary
4
- model: deepseek/deepseek-v4-flash
4
+ model: google/gemini-3.5-flash
5
5
  temperature: 0.1
6
6
  tools:
7
7
  write: false
@@ -2,7 +2,7 @@
2
2
  description: Realiza una auditoría/revisión de código contra el contrato y el linter
3
3
  mode: subagent
4
4
  hidden: false
5
- model: deepseek/deepseek-v4-flash
5
+ model: google/gemini-3.5-flash
6
6
  temperature: 0.1
7
7
  tools:
8
8
  write: false
@@ -2,7 +2,7 @@
2
2
  description: Redacta y valida contratos OpenAPI e interfaces en specs/
3
3
  mode: subagent
4
4
  hidden: true
5
- model: deepseek/deepseek-v4-flash
5
+ model: google/gemini-3.5-flash
6
6
  temperature: 0.1
7
7
  tools:
8
8
  write: true
@@ -3,7 +3,7 @@ description: Diseña y corre pruebas unitarias, integración y visuales (Playwri
3
3
  mode: subagent
4
4
  hidden: true
5
5
  steps: 15
6
- model: deepseek/deepseek-v4-flash
6
+ model: google/gemini-3.5-flash
7
7
  temperature: 0.35
8
8
  frequency_penalty: 0.5
9
9
  presence_penalty: 0.2
@@ -22,11 +22,12 @@ 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 (dashboard, sidebar, login, etc.)
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
- - **Implementación por Distribución y Composición**: El Coder (F2) **debe** instalar estos bloques usando `npx shadcn@latest add <block-name>` (ej: `dashboard-01`, `login-03`) en su proyecto, integrando y distribuyendo los elementos con el estado y lógica del backend, sin rediseñar o codificar layouts que ya existen de forma oficial. El Coder puede usar `shadcn_get_item_examples_from_registries` para ver ejemplos de implementación realistas y aplicarlos limpiamente a la primera, reduciendo el desvío visual.
29
+ - **Inyección Zero-Touch y Migración Física en Fase A**: El Coder (F2) **debe** instalar el bloque usando `npx shadcn@latest add <block-name>`. Los archivos se descargarán en `src/components/blocks/<block-name>/`. Queda **estrictamente prohibido** importar el `page.tsx` del bloque como un simple componente hijo (ej: `return <BlockPage />` desde la raíz de la app), ya que esto rompe el router, layouts y metadata de Next.js. El Coder debe **copiar íntegramente el contenido del código** de `src/components/blocks/<block-name>/page.tsx` hacia el `page.tsx` de la ruta destino, y **reescribir todos los imports relativos** de componentes y datos locales (ej. `./components/...` o `./data.json`) para usar aliases absolutos que apunten a la ruta física descargada por Shadcn (ej. `@/components/blocks/<block-name>/components/...` o `@/components/blocks/<block-name>/data.json`). El archivo de datos locales `data.json` debe preservarse e importarse para garantizar un renderizado visual clonado perfecto.
30
+ - **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
31
  - **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
32
 
32
33
  ## 4. Soporte para Subdirectorios, Monorepos (targetDir) y Estándar Corporativo `src/`
@@ -72,14 +72,31 @@ 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. **No estás restringido a ningún subconjunto fijo de componentes**: tienes total libertad para buscar en el catálogo completo de 97+ bloques oficiales y componentes primitivos de Shadcn:
77
- - **Dashboards**: Utiliza `dashboard-01` o cualquier variante de panel de control interactivo oficial.
78
- - **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.
79
- - **Login / Registro**: Utiliza `login-01` al `05` y `signup-01` al `05` para flujos de login y registro de usuarios.
80
- - **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.
81
-
82
- Busca layouts de dashboards o formularios complejos en el registro mediante el MCP `shadcn` antes de escribir cualquier código personalizado. Una vez agregado el bloque, distribuye y adapta sus elementos con el estado y la lógica de tu aplicación. Te motivamos a explorar todo el ecosistema de bloques pre-creados por Shadcn para armar una UI robusta y moderna.
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 Migración Física)**:
81
+ - El Coder (F2) **debe** instalar el bloque de Shadcn correspondiente mediante la CLI (`npx shadcn@latest add <block-name>`).
82
+ - El bloque se descargará en `src/components/blocks/<block-name>/`. El Coder tiene **estrictamente prohibido** importar el `page.tsx` del bloque como un simple componente hijo (ej: `return <BlockPage />` desde la raíz de la app), ya que esto rompe la estructura del router de Next.js, layouts y metadata.
83
+ - **Acción Obligatoria**: El Coder debe **copiar el contenido íntegro del código** de `src/components/blocks/<block-name>/page.tsx` hacia el `page.tsx` de tu ruta destino (por ejemplo, `src/app/page.tsx` o `src/app/dashboard/page.tsx`).
84
+ - **Corrección de Imports de forma Manual**: Al mover el código de la ruta de componentes al App Router de Next.js, los imports relativos en ese archivo se romperán. El Coder tiene la obligación de **reescribir todos los imports relativos de componentes e información locales** (ej. `./components/app-sidebar` o `./data.json`) para usar aliases absolutos que apunten a la ubicación física descargada por Shadcn (ej. `@/components/blocks/<block-name>/components/app-sidebar` o `@/components/blocks/<block-name>/data.json`).
85
+ - **Preservación de Datos Mock/Dummy**: El archivo de datos del bloque (por ejemplo, `data.json` o constantes internas) **debe ser preservado e importado de manera intacta** durante esta Fase A para garantizar que la vista renderice con datos idénticos al demo original en el navegador.
86
+ - **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.
87
+
88
+ 2. **Sustitución de Datos Atómica (Fase B - Conexión de Lógica)**:
89
+ - 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).
90
+ - 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.
91
+
92
+ 3. **Prevención de Parpadeos (Flicker) de Sidebar y Providers**:
93
+ - 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.
94
+
95
+ 4. **Catálogo de Bloques a Explorar**:
96
+ - **Dashboards**: Utiliza `dashboard-01` o cualquier variante de panel de control interactivo oficial.
97
+ - **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.
98
+ - **Login / Registro**: Utiliza `login-01` al `05` y `signup-01` al `05` para flujos de login y registro de usuarios.
99
+ - **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
100
 
84
101
  **REGLA CRÍTICA DE RUTAS DUPLICADAS POR SHADCN CLI**:
85
102
  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.
@@ -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
- # LISTA DE MODELOS DISPONIBLES EN OPENCODE
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": "deepseek/deepseek-v4-flash",
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
- opencode_json, agents_dir, models_json = get_paths()
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, models_json = get_paths()
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
- # Habilitar modo keypad para recibir flechas de cursor
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(0) # Ocultar cursor físico
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
- # Obtener lista de modelos y añadir "Heredar del Global" si no es la config global
163
+ # 1. Preparar lista de opciones filtrada
193
164
  options = []
194
165
  if not is_global:
195
- options.append("") # Representa [Heredar del Global]
166
+ options.append("") # Representa [Heredar del Global]
196
167
 
197
- # Filtrar modelos según query
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
- # Validar límites de selección
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
- # Títulos
206
- stdscr.addstr(1, 2, f"=== {title} ===", curses.A_BOLD)
207
- stdscr.addstr(2, 2, "Escribe caracteres para buscar en tiempo real.", curses.A_DIM)
208
- stdscr.addstr(3, 2, "Usa [↑/↓] para navegar y [Enter] para confirmar. [ESC] para volver.", curses.A_DIM)
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
- # Caja de búsqueda
211
- stdscr.addstr(5, 2, "Buscador: ", curses.A_BOLD)
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 con paginación simple de terminal
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.color_pair(2))
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 label
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
- # Indicar si es el activo actual
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.color_pair(1))
240
- stdscr.addstr(row, 2, f" > {label:<50} ")
241
- stdscr.attroff(curses.color_pair(1))
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" {label:<50}")
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
- # Leer tecla
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): # ESC
226
+ if key in (27, curses.KEY_CANCEL): # ESC
254
227
  return None
255
- elif key in (10, 13, curses.KEY_ENTER): # 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): # 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: # Carácter imprimible
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
- # Habilitar keypad
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
- stdscr.addstr(1, 2, "🤖 ZUGZBOT: CONFIGURACIÓN INTERACTIVA DE MODELOS", curses.A_BOLD)
291
- stdscr.addstr(2, 2, "Navega con [↑/↓], pulsa [Enter] para editar un agente.", curses.A_DIM)
292
- stdscr.addstr(3, 2, "Presiona [G] para Guardar y Aplicar, o [Q]/[ESC] para Salir.", curses.A_DIM)
293
- stdscr.addstr(4, 2, "" * 70)
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 del menú interactivo
296
- global_val = config.get("global", "deepseek/deepseek-v4-flash")
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", f"Modelo Global: {global_val}"),
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"[Hereda del global: {global_val}]"
305
- menu_items.append((agent, f"{agent}: {label}"))
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
- for idx, (key, text) in enumerate(menu_items):
313
- row = start_row + idx
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
- # Resaltar la opción actual
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.color_pair(1))
318
- stdscr.addstr(row, 2, f" > {text:<65} ")
319
- stdscr.attroff(curses.color_pair(1))
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" {text}", curses.color_pair(3) | curses.A_BOLD)
304
+ stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
323
305
  elif key == "cancel":
324
- stdscr.addstr(row, 2, f" {text}", curses.color_pair(2))
306
+ stdscr.addstr(row, 2, f" {text:<70} ", curses.A_DIM)
325
307
  elif key == "global":
326
- stdscr.addstr(row, 2, f" {text}", curses.color_pair(3))
308
+ stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
327
309
  else:
328
- stdscr.addstr(row, 2, f" {text}")
310
+ stdscr.addstr(row, 2, f" {text:<70} ")
329
311
 
330
312
  stdscr.refresh()
331
313
 
332
- # Leer acción del usuario
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): # Q o ESC
320
+ if key in (ord('q'), ord('Q'), 27): # Q o ESC
339
321
  break
340
- elif key in (ord('g'), ord('G')): # G de Guardar
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
- # Obtener el item seleccionado
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
- # Editar el modelo global
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
- # Editar un agente específico
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
- # Aplicar el mapeo de modelos final en cascada
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("\nMapeo de modelos resultante:")
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 = "personalizado" if custom_val else "global"
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("\nModelos configurados exitosamente! 🎉")
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 modelos por CLI
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
- print_available_models()
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 por parámetro, corre en modo CLI no-interactivo rápido
391
+ # Si se pasan argumentos, procesa actualización por CLI no-interactiva rápida
410
392
  if args:
411
393
  query = args[0]
412
- matches = search_model(query)
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
- global_model_selected = matches[0]
401
+ selected_model = matches[0]
422
402
  else:
423
- # Si hay múltiples coincidencias, tomamos la primera pero listamos las demás
424
- global_model_selected = matches[0]
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
- # Actualizar el global_model en config y persistir
433
- config["global"] = global_model_selected
409
+ config["global"] = selected_model
434
410
  apply_and_exit(config)
435
411
  else:
436
- # Si NO se pasan argumentos, inicia la TUI interactiva
437
- curses.wrapper(main_tui)
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": "deepseek/deepseek-v4-flash"
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 diseño (Toss, Stripe, Supabase, etc.)."
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 regresión visual y capturas de pantalla de Playwright."
79
+ "purpose": "Ejecuta pruebas de regresi\u00f3n visual y capturas de pantalla de Playwright."
80
80
  },
81
81
  "next-devtools": {
82
82
  "type": "local",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zugzbot",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
4
4
  "description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
5
5
  "type": "module",
6
6
  "bin": {