Hace poco escribí sobre cómo Claude Code y un CLI propio (tv) me ahorran 15 minutos de secretariado por ticket. Funcionó tan bien que me destapó el siguiente cuello de botella: aunque ya no copiaba títulos a mano ni creaba ramas manualmente, mi pantalla seguía teniendo abiertas siete ventanas distintas. Timeview en una pestaña del navegador para ver tareas, GitLab en otra para ver el pipeline, terminal con Claude Code corriendo, VSCode al lado, notas sueltas en otra app y Gemini cuando quería mejorar algún texto.
El secretariado por ticket lo había resuelto. El context-switching seguía intacto.
Spren es lo que construí para resolverlo: una app de escritorio que junta en una sola pantalla todo lo que mi día a día necesita. Terminal con tabs, estado de git en tiempo real, el panel de tareas de Timeview, las notificaciones, el estado del pipeline en GitLab, mi cuota de Claude, un widget de Gemini para preguntas rápidas, y un par de cosas más (TODO, pomodoro, recordatorio para beber agua). En este artículo te cuento cómo está montada y, sobre todo, por qué cada decisión de diseño es la que es, por si te da ideas para hacer tu propio workspace.
El problema: la app perfecta no existía
Cada uno tiene su forma de trabajar.
Warp es una buena terminal. Raycast es un buen launcher. Notion es buena para notas. Pero ninguna de estas resolvía mi problema: yo no necesitaba otra app más, necesitaba una app menos. Una que sustituyera a varias.
El problema con las soluciones existentes:
- Demasiado generales: dashboards corporativos que muestran 200 cosas que no me importan.
- Demasiado específicas: cada app hace bien una cosa, y cuando combinas cinco apps tienes el mismo problema de partida.
- Sin integración con mi stack real: ninguna se conecta a Timeview ni a mi GitLab self-hosted.
Quería algo hecho a medida de mi flujo. Y la única forma de conseguirlo, era creando a Spren.
En el Cosmere, los spren son entidades que se manifiestan cuando las necesitas y se vinculan a quien las invoca
Las decisiones de stack que importan
Spren es una app de escritorio para macOS hecha con Electron + React + JavaScript. No TypeScript, no Tauri, no nada exótico. Te explico por qué cada elección.
Electron sobre Tauri: Tauri es más ligero pero su ecosistema de Node es menor. Yo necesitaba node-pty para terminales reales, simple-git para integración nativa con git, chokidar para watching del filesystem, y la API de safeStorage de Electron para cifrar tokens. Todo eso son problemas resueltos en Electron y problemas a resolver en Tauri. Para una app personal, era el camino claro.
JavaScript sobre TypeScript: decisión consciente. Iteración rápida, pocos colaboradores, y una codebase que no espero que crezca a 50.000 líneas. El coste de tipar todo no compensa el beneficio. (Si esto fuera una app pública con equipo, sería TypeScript sin pensarlo).
electron-vite como starter: HMR funcional, configuración mínima, no me peleo con webpack ni con builds.
TanStack Query para todo lo que viene de red: cache, retry, refetch, polling adaptativo. Si tu UI hace peticiones HTTP, TanStack Query te ahorra el 80% del código de estado... y me encanta!
xterm.js + node-pty para las terminales: las terminales son reales, con shell, history, ANSI colors, todo. Cada tab tiene su propio proceso pty.
safeStorage para tokens (no keytar): la primera versión usaba keytar y dio problemas con el code-signing. safeStorage de Electron es nativo, sin dependencias compiladas, y suficiente para uso personal. Los tokens viven cifrados en ~/Library/Application Support/spren/secrets.json.
Sin SQLite: lo consideré pero no lo necesité. Para los datos que persisto (TODO items, settings, code projects, mappings) un JSON plano vía un módulo settingsManager es más que suficiente. Si en el futuro quiero historial de pomodoros con gráficas, ahí sí entrará SQLite.
La arquitectura, en una imagen mental
Spren tiene tres procesos, como cualquier app de Electron:
- Main process: todo lo sensible vive aquí — credenciales, llamadas HTTP a APIs externas, ejecución de procesos pty, watchers del filesystem, lectura del Keychain.
-
Preload: un puente tipado vía
contextBridgeque expone al renderer solo lo que tiene que poder llamar (window.api.timeview.getMyTasks(...)y similares). - Renderer: solo React, solo UI. El renderer nunca toca tokens, nunca lee archivos del usuario directamente, nunca llama a APIs externas. Todo va por IPC.
Esta separación es innegociable y vale la pena explicarla porque es lo que permite que una app así sea segura. Si el renderer pudiera acceder a tokens, cualquier vulnerabilidad de XSS comprometería tus credenciales de Timeview, GitLab y Gemini de golpe. Como no puede, el peor caso es que un script malicioso pida llamadas a tu API, pero nunca verá los tokens.
El layout: cuatro columnas que cuentan una historia
La pantalla principal (HOME) son cuatro columnas verticales:
TERMINAL (flex) | GIT (320px) | TIMEVIEW (400px) | WIDGETS (400px)
El orden no es casual. Cuenta una historia de izquierda a derecha: dónde estoy escribiendo código, qué estado tiene mi código localmente, qué tengo pendiente y qué está pasando en mi sistema de tareas, y herramientas auxiliares.
Columna 1: TERMINAL
Multi-tab con Cmd+T, Cmd+W, Cmd+1..9. Cada tab es un pty real con su propio shell, su propio cwd, su propio history. Cuando hago cd en una tab, el resto sigue donde estaba.
Lo más importante: el cwd activo de la tab actual alimenta a las demás columnas. La columna GIT mira al repo donde estoy. La columna TIMEVIEW resalta el proyecto correspondiente. Si cambio de tab, todo se actualiza.
Columna 2: GIT
Lee el repo del cwd actual con simple-git y vigila cambios con chokidar. Muestra:
- Branch actual, ahead/behind, tracking de upstream.
- Archivos modificados / staged / untracked, con counters.
- **Diff: puedo ver que ha cambiado en cada fichero sin abrir el IDE.
-
Bloque GITLAB: estado del MR de la rama actual (draft / opened / merged), último pipeline del proyecto (running / passed / failed), con botón "open in browser" que dispara
shell.openExternal.
Detalle de UX: el bloque GITLAB solo aparece si hay datos relevantes. Si estoy en main sin MR, no aparece. Cero ruido.
Columna 3: TIMEVIEW
Tres bloques verticales:
- MY TASKS: lista paginada de mis tareas, con toggle entre "asignadas a mí" y "sin asignar". Cada item muestra estado, id, título, tags y a quién está asignado. Paginación con scroll infinito vía TanStack Query.
- UNREADS COUNTERS: cuatro contadores horizontales (rooms / tasks / notifs / docs) que vienen de de Timeview. Sirven como panorama general.
- NOTIFICATIONS: lista de notificaciones, con borde izquierdo rojo destacado para las no leídas.
Click en una tarea me lleva al detalle. Y desde el detalle puedo escribir comentarios para "preparar el terreno" antes de delegar la tarea a Claude. Eso enlaza con el punto siguiente.
Columna 4: WIDGETS
Una pila vertical: TODO, Gemini, Claude Usage, Pomodoro, Water. Cada uno hace una cosa y la hace bien. Aquí los más interesantes:
-
TODO: items locales con
done: false/true, ordenados pendientes-arriba completados-abajo, con botón "clear done" para limpiar de golpe. Persistencia ensettingsManager. -
Gemini: input + send + área de respuesta con streaming. Para "cambia esta frase a algo más profesional" o "tradúceme esto rápido". Modelo hardcoded a
gemini-2.5-flashcon tier gratuito. Evita salir a otra pestaña. - Claude Usage: muestra mi cuota actual (5h / 7d / opus) leída del endpoint OAuth interno de Claude Code. Barras ASCII, color verde / ámbar / rojo según umbral. Toggleable porque es un endpoint undocumented.
La pieza que cierra el bucle: "Trabaja en esta tarea"
El flujo completo entre Spren y mi CLI tv es esto:
- Llega una notificación de Timeview a la columna correspondiente.
- Click en la tarea, se abre el detalle.
- Si necesito añadir contexto antes de delegar, escribo un comentario directamente desde Spren (pasa por la API de Timeview).
- Cuando estoy listo, click en "trabaja en esta tarea".
- Spren abre una tab nueva con el cwd en el code project que elija (los tengo configurados en Settings, uno por repo local), ejecuta
claudecon un prompt que invocatv tasks work <id>, y a partir de ahí Claude se encarga del resto del flujo descrito en el artículo anterior.
Un detalle interesante es el selector de code project. Una tarea de Timeview no tiene relación 1:1 con un repo. La tarea es "implementar X feature"; el repo donde se resuelve depende de qué área toques (frontend, backend, desktop, mobile). En mi equipo, una misma tarea puede resolverse en timeview/desktop, timeview/app o timeview/api según corresponda. Y cada developer tiene su propia ruta local para cada repo.
Por eso Spren guarda una lista de "code projects" — pares nombre + ruta local que tú das de alta. Cuando le doy a "trabaja en esta tarea", el botón muestra "trabaja en esta tarea en <nombre>" y, si tengo más de uno, aparece un dropdown para cambiar antes de pulsar. Recuerda mi última elección entre sesiones.
Decisiones de diseño que cambiaron el resultado
Tres elecciones que parecen menores pero hicieron mucho:
1. Cero polling agresivo, refresh inteligente.
Mi primera versión hacía polling cada 30 segundos a Timeview, GitLab y Gemini. Resultado: la app consumía mucho innecesariamente y Anthropic / GitLab / Timeview veían tráfico constante mío.
La versión actual usa refetchInterval adaptativo de TanStack Query. Por ejemplo: el pipeline de GitLab se refresca cada 10 segundos si está running, cada 60 segundos si está en estado terminal. Las notificaciones de Timeview, refresh manual con botón. La cuota de Claude, cada 5 minutos.
Misma sensación de "estoy al día", una décima parte del tráfico.
2. Keep-alive vs unmount.
Spren tiene tres secciones (HOME, TIMEVIEW, SETTINGS). Cuando cambias de sección, la anterior no se desmonta. Sigue viva pero invisible (visibility: hidden + position: absolute). Las terminales siguen corriendo, las queries mantienen su estado, no se pierde nada.
Esto resolvió un bug famoso de xterm: si la columna terminal se desmontaba con display: none, al volver el contenedor tenía dimensiones 0×0, fitAddon.fit() calculaba 5 filas, y el pty perdía scrollback. Visualmente parecía que la terminal se había encogido. La solución de visibility: hidden + position: absolute mantiene dimensiones reales todo el tiempo.
3. Errores tipados en cada cliente HTTP.
Cada integración (Timeview, GitLab, Gemini) tiene sus propios errores tipados con un campo .code: TIMEVIEW_AUTH_FAILED, GITLAB_NETWORK, GEMINI_RATE_LIMIT, etc. Los IPC handlers nunca lanzan a través del bridge — siempre devuelven { ok: true, data } o { ok: false, error: { code, message } }.
Cuando veo un error en Spren, se exactamente qué pasa: "TOKEN no es válido" en lugar de "Network error". Y el código de UI puede mapear cada code a un mensaje específico y a una acción ("ir a Settings", "reintentar", "abrir Claude Code").
Lecciones inespereadas
Tres cosas que no esperaba al construir Spren:
1. El context-switching no se mide en clicks, se mide en pestañas abiertas.
No es solo el coste de cambiar de Timeview a GitLab. Es el coste acumulado de tener N pestañas abiertas todo el día, cada una pidiéndote atención visual aunque estés en otra. Ir cambiando cada cierto tiempo por si no has visto alguna notificación. Reducir mi número total de pestañas de 7 a 2 (Spren + el navegador) tiene un impacto en concentración que no esperaba.
2. Construir tu propia app te obliga a definir tu propio flujo.
Igual que escribir el CLAUDE.md me obligó a documentar reglas implícitas, construir Spren me obligó a definir qué información necesito ver constantemente y cuál no. Resultado: descubrí que no necesitaba ver "todas las tareas" — necesitaba ver "las mías". No necesitaba ver "todos los pipelines" — necesitaba ver "el último del proyecto en el que trabajo". El proceso de decidir qué meter en cada columna fue el ejercicio de productividad más útil que he hecho en mucho tiempo.
3. Una app personal no necesita ser perfecta; necesita ser tuya.
Spren no es accesible. No tiene tests automáticos. No tiene auto-update. No está firmada para distribución. No funciona en Windows ni Linux. Es deliberadamente cruda: tema oscuro hardcoded, fuente monoespaciada para todo, casi sin animaciones, una densidad de información que asustaría a un diseñador.
Precisamente por eso me funciona. Cada decisión está optimizada para mi caso y solo el mío. No hay tradeoffs con casos de uso que no son los míos. Esa es la diferencia entre una app personal y un producto.
Y me encanta abrirla, me encanta saber que todo lo que tengo delante es útil y cada vez que salto al navegador con frecuencia, pienso si puedo incluirlo en Spren, porque yo decido.
Lo que viene
Lo siguiente que he pensado incluir es agent harness: un panel "AGENTS" en Spren donde pueda lanzar varias instancias de Claude Code en modo no interactivo (claude -p) sobre distintas tareas, y monitorizar su progreso desde Spren — qué archivos están tocando, cuánto llevan, si terminaron OK o con error, con notificación nativa cuando completan.
La idea es pasar de "lanzo Claude en una tab y miro su output, tomando decisiones" a "delego cinco tareas en paralelo y vuelvo cuando estén listas". Si funciona, escribo el siguiente artículo, pero para conseguir eso, es necesario darle a los agentes skills, tools y tareas muy claras.
Cierre
No te voy a decir que tienes que construir tu propia app de escritorio. Lo que sí te digo es: si te encuentras con siete pestañas abiertas y notas que pierdes foco saltando entre ellas, el problema no es disciplina personal. Es que tu herramienta de trabajo está fragmentada en N apps diseñadas por personas distintas con prioridades distintas.
A veces la solución no es encontrar la app perfecta. Es construirla.
Si quieres ver el código de Spren, escribeme. Es deliberadamente personal — no busco contribuciones ni mantenimiento — pero las ideas son "robables". Si construyes algo similar, cuéntame!





Top comments (0)