¿Qué hace Glimpse — Micro-UI Nativa?
Glimpse abre una ventana nativa con una webview en menos de 50ms. Tú escribes HTML, el usuario lo ve al instante. Comunicación bidireccional mediante window.glimpse.send() (webview → Node) y .send(js) (Node → webview). Funciona en macOS (WKWebView), Linux (WebKitGTK) y Windows (WebView2).
Cuándo usar Glimpse:
- Necesitas entrada del usuario más allá de sí/no (formularios, selecciones, entrada de texto)
- Quieres mostrar algo visual (gráficos, Markdown, imágenes, diffs)
- Quieres confirmar una acción destructiva con un diálogo adecuado
- Quieres un indicador flotante, notificación o widget compañero
- Necesitas que el usuario interactúe con contenido enriquecido
Aplicaciones de escritorio: Verifica si te estás ejecutando dentro de una aplicación de escritorio que pueda mostrar HTML, gráficos y formularios en línea en la conversación:
- Aplicación de escritorio Claude:
CLAUDE_CODE_ENTRYPOINTesclaude-desktop. - Codex en la aplicación de escritorio ChatGPT:
CODEX_INTERNAL_ORIGINATOR_OVERRIDEesCodex Desktopocodex_work_desktop. Su visualización en línea es la skillvisualizeincluida.
echo "claude=${CLAUDE_CODE_ENTRYPOINT:-} codex=${CODEX_INTERNAL_ORIGINATOR_OVERRIDE:-}"
En una aplicación de escritorio, prefiere la visualización en línea de la aplicación. Usa Glimpse allí solo cuando necesites algo que la visualización en línea no puede hacer: un script en ejecución que bloquea hasta que el usuario responda, una ventana que permanece visible fuera de la aplicación (flotante, elemento de estado, seguir cursor), o una interfaz para un script que se ejecuta sin el agente. En una terminal (cualquier otro valor, o ambas variables sin establecer), usa Glimpse como se describe a continuación.
Configuración (plugin glimpse-everything): glimpseui se instala en este plugin en el primer uso. Antes de escribir cualquier script de Glimpse, ejecuta esto una vez por sesión. Instala glimpseui si es necesario, construye su helper nativo e imprime la ruta absoluta del paquete instalado en stdout. Esa ruta impresa es <RUTA_RESUELTA> en todas partes a continuación.
bash "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-}}/bin/glimpse-path.sh"
Si ni CLAUDE_PLUGIN_ROOT ni CODEX_PLUGIN_ROOT están establecidos, ejecuta ../../bin/glimpse-path.sh resuelto en relación al directorio de este archivo de skill. La primera ejecución puede llevar un tiempo (npm install más una compilación nativa); el progreso va a stderr. Si sale con código distinto de cero, muestra al usuario su stderr, que nombra la herramienta de compilación faltante.
Importación: Siempre usa la ruta absoluta a glimpse.mjs dentro del paquete instalado — el especificador simple 'glimpseui' falla cuando los scripts se ejecutan desde /tmp o cualquier lugar sin node_modules.
En macOS/Linux, usa una ruta directa:
import { open, prompt } from '<RUTA_RESUELTA>/src/glimpse.mjs';
En Windows, las importaciones ESM requieren una URL file:///:
import { open, prompt } from 'file:///<RUTA_RESUELTA>/src/glimpse.mjs';
// ej. import { open, prompt } from 'file:///C:/Users/yo/.pi/agent/.../glimpseui/src/glimpse.mjs';
Referencia Rápida
Diálogo de Una Sola Toma (prompt)
import { prompt } from '<RUTA_RESUELTA>/src/glimpse.mjs';
const respuesta = await prompt(html, {
width: 400, height: 300, // tamaño de ventana
title: 'Mi Diálogo', // texto de la barra de título
frameless: true, // sin barra de título
transparent: true, // fondo transparente
});
// respuesta = datos de window.glimpse.send(), o null si el usuario cerró la ventana
Ventana Persistente (open)
import { open } from '<RUTA_RESUELTA>/src/glimpse.mjs';
const win = open(html, opciones);
win.on('ready', (info) => {}); // HTML cargado — info tiene pantalla, apariencia, cursor
win.on('message', datos => {}); // interacción del usuario
win.on('info', info => {}); // información fresca del sistema (después de getInfo())
win.on('closed', () => {}); // ventana cerrada
win.send('document.title = "Hola"'); // evaluar JS en webview
win.setHTML('<h1>Nuevo contenido</h1>'); // reemplazar HTML
win.info; // última información conocida del sistema
win.getInfo(); // solicitar información fresca
win.close(); // cerrar ventana
Todas las Opciones
{
width, height, // píxeles (predeterminado: 800×600)
title, // título de ventana (predeterminado: "Glimpse")
frameless: true, // sin barra de título, arrastrable por fondo
floating: true, // siempre al frente
transparent: true, // fondo de ventana transparente
clickThrough: true, // el ratón atraviesa la ventana
followCursor: true, // la ventana sigue el cursor del ratón
followMode: 'spring', // 'snap' (instantáneo, predeterminado) o 'spring' (elástico)
cursorAnchor: 'top-right', // punto de anclaje: top-left, top-right, right, bottom-right, bottom-left, left
cursorOffset: {x, y}, // desplazamiento desde el cursor (predeterminado: 20, -20)
openLinks: true, // abrir enlaces http/https cliqueados en el navegador predeterminado
openLinksApp: '/Applications/Google Chrome.app', // ruta opcional del paquete de aplicación
autoClose: true, // cerrar después del primer mensaje
noDock: true, // sin icono de dock ni entrada en el selector de aplicaciones (macOS)
x, y, // posición exacta en pantalla
timeout, // para prompt() solamente — ms antes de rechazar
}
Información del Sistema (disponible en ready)
win.on('ready', (info) => {
// Pantalla
info.screen.width // 2560 (resolución completa)
info.screen.height // 1440
info.screen.scaleFactor // 2 (Retina)
info.screen.visibleWidth // 2560 (excluyendo dock)
info.screen.visibleHeight // 1367 (excluyendo barra de menú)
// Apariencia
info.appearance.darkMode // true
info.appearance.accentColor // "#007AFF"
info.appearance.reduceMotion // false
info.appearance.increaseContrast // false
// Cursor
info.cursor.x // 500
info.cursor.y // 800
// Todos los monitores
info.screens // [{ x, y, width, height, scaleFactor, ... }, ...]
});
// Acceder en cualquier momento después de ready:
win.info.screen.width
win.info.appearance.darkMode
Puente JavaScript en la Página
window.glimpse.send(datos) // enviar datos a Node (cualquier valor serializable en JSON)
window.glimpse.close() // cerrar la ventana desde JS
Patrones
1. Diálogo de Confirmación
El caso de uso más simple. Pregunta sí/no, obtén la respuesta, continúa.
const respuesta = await prompt(`
<body style="font-family: system-ui; padding: 24px; background: white;">
<h2 style="margin-top: 0;">¿Eliminar 47 archivos?</h2>
<p style="color: #666;">Esto no se puede deshacer.</p>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button onclick="window.glimpse.send({ok: false})"
style="padding: 10px 20px; font-size: 14px; border: 1px solid #ddd; border-radius: 8px; background: white; cursor: pointer;">
Cancelar
</button>
<button onclick="window.glimpse.send({ok: true})" autofocus
style="padding: 10px 20px; font-size: 14px; border: none; border-radius: 8px; background: #e53e3e; color: white; cursor: pointer;">
Eliminar
</button>
</div>
<script>
document.addEventListener('keydown', e => {
if (e.key === 'Escape') window.glimpse.send({ok: false});
if (e.key === 'Enter') window.glimpse.send({ok: true});
});
</script>
</body>
`, { width: 340, height: 180, title: 'Confirmar' });
if (respuesta?.ok) { /* proceder con la eliminación */ }
2. Formulario de Entrada de Texto
Recopila entrada estructurada. Enter envía, Escape cancela.
const resultado = await prompt(`
<body style="font-family: system-ui; padding: 24px; background: white;">
<style>
input, select { padding: 8px 12px; font-size: 14px; border: 1px solid #ddd; border-radius: 6px; width: 100%; margin-bottom: 12px; }
input:focus, select:focus { outline: none; border-color: #4299e1; box-shadow: 0 0 0 3px rgba(66,153,225,0.15); }
button { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; }
</style>
<h3 style="margin-top: 0;">Nuevo Componente</h3>
<input id="name" placeholder="Nombre del componente" autofocus />
<select id="type">
<option value="page">Página</option>
<option value="component">Componente</option>
<option value="layout">Diseño</option>
</select>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button onclick="window.glimpse.send(null)" style="background: #eee;">Cancelar</button>
<button onclick="submit()" style="background: #4299e1; color: white;">Crear</button>
</div>
<script>
function submit() {
window.glimpse.send({
name: document.getElementById('name').value,
type: document.getElementById('type').value,
});
}
document.getElementById('name').addEventListener('keydown', e => {
if (e.key === 'Enter') submit();
if (e.key === 'Escape') window.glimpse.send(null);
});
</script>
</body>
`, { width: 380, height: 260, title: 'Crear' });
if (resultado) console.log(`Creando ${resultado.type}: ${resultado.name}`);
3. Lista de Selección
Elige de una lista de opciones. Haz clic o usa las teclas de flecha + Enter.
function seleccionarDeLista(titulo, elementos) {
const elementosHTML = elementos.map((elem, i) =>
`<div class="item${i === 0 ? ' selected' : ''}" data-index="${i}" onclick="pick(${i})">${elem}</div>`
).join('');
return prompt(`
<body style="font-family: system-ui; margin: 0; background: white;">
<style>
.header { padding: 12px 16px; font-size: 13px; color: #888; border-bottom: 1px solid #eee; }
.item { padding: 10px 16px; cursor: pointer; font-size: 14px; }
.item:hover, .item.selected { background: #4299e1; color: white; }
</style>
<div class="header">${titulo}</div>
<div id="list">${elementosHTML}</div>
<script>
const items = document.querySelectorAll('.item');
let sel = 0;
function pick(i) { window.glimpse.send({index: i, value: items[i].textContent}); }
document.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { items[sel].classList.remove('selected'); sel = (sel+1) % items.length; items[sel].classList.add('selected'); }
if (e.key === 'ArrowUp') { items[sel].classList.remove('selected'); sel = (sel-1+items.length) % items.length; items[sel].classList.add('selected'); }
if (e.key === 'Enter') pick(sel);
if (e.key === 'Escape') window.glimpse.send(null);
e.preventDefault();
});
</script>
</body>
`, { width: 300, height: 40 + elementos.length * 38, frameless: true });
}
const eleccion = await seleccionarDeLista('Elige un framework', ['React', 'Vue', 'Svelte', 'Solid', 'Angular']);
4. Visor de Markdown / Contenido Enriquecido
Muestra contenido formateado. Útil para previsualizar documentos generados, READMEs o diffs.
// Renderizar markdown con una librería CDN
const markdown = `# Hola Mundo\n\nEsto es **negrita** y esto es \`código\`.`;
const win = open(`
<body style="font-family: system-ui; padding: 24px; background: white;">
<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.getElementById('content').innerHTML = marked.parse(${JSON.stringify(markdown)});
</script>
</body>
`, { width: 600, height: 400, title: 'Vista Previa' });
5. Progreso en Vivo / Salida Continua
Empuja actualizaciones a la ventana a medida que avanza el trabajo.
const win = open(`
<body style="font-family: monospace; padding: 16px; background: #1a1a2e; color: #0f0; font-size: 13px;">
<div id="log" style="white-space: pre-wrap;"></div>
<script>
window.appendLog = (texto) => {
document.getElementById('log').textContent += texto + '\\n';
window.scrollTo(0, document.body.scrollHeight);
};
</script>
</body>
`, { width: 500, height: 300, title: 'Progreso de Compilación' });
win.on('ready', () => {
win.send(`appendLog('Iniciando compilación...')`);
// A medida que tu proceso se ejecuta, empuja actualizaciones:
win.send(`appendLog('✓ Compilados 42 archivos')`);
win.send(`appendLog('✓ Pruebas pasadas')`);
win.send(`appendLog('✓ Listo!')`);
});
6. Modo Oscuro/Claro Adaptativo
Usa la información del sistema para diseñar la interfaz de usuario que coincida con la apariencia del usuario.
const win = open('', { width: 400, height: 200 });
win.on('ready', ({ appearance, screen }) => {
const bg = appearance.darkMode ? '#1a1a2e' : '#ffffff';
const fg = appearance.darkMode ? '#ffffff' : '#333333';
const accent = appearance.accentColor;
win.setHTML(`
<body style="font-family: system-ui; padding: 24px; background: ${bg}; color: ${fg};">
<h2 style="color: ${accent};">IU Adaptativa</h2>
<p>Modo oscuro: ${appearance.darkMode ? 'activado' : 'desactivado'}</p>
<p>Pantalla: ${screen.width}×${screen.height} @${screen.scaleFactor}x</p>
<button onclick="window.glimpse.close()"
style="padding: 8px 16px; background: ${accent}; color: white; border: none; border-radius: 6px; cursor: pointer;">
Cerrar
</button>
</body>
`);
});
7. Notificación Flotante
Un mensaje breve que se cierra automáticamente. Sin interacción necesaria.
function notificar(mensaje, duracionMs = 3000) {
const win = open(`
<body style="margin: 0; background: transparent !important;">
<div style="
background: rgba(0,0,0,0.85); color: white; padding: 12px 20px;
border-radius: 10px; font-family: system-ui; font-size: 14px;
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
">${mensaje}</div>
</body>
`, { width: 300, height: 60, frameless: true, transparent: true, floating: true, clickThrough: true });
win.on('ready', () => setTimeout(() => win.close(), duracionMs));
}
notificar('✅ Desplegado en producción');
8. Compañero de Cursor
Un elemento visual que sigue al cursor. Ideal para indicadores de estado del agente.
// Anillo giratorio que sigue al cursor
const win = open(`
<body style="background: transparent !important; margin: 0;">
<svg width="50" height="50" style="filter: drop-shadow(0 0 6px rgba(0,255,200,0.5));">
<circle cx="25" cy="25" r="18" fill="none" stroke="cyan" stroke-width="2" stroke-dasharray="20 60">
<animateTransform attributeName="transform" type="rotate"
from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
</circle>
</svg>
</body>
`, { width: 50, height: 50, frameless: true, transparent: true, floating: true, followCursor: true });
9. Diálogo con Entrada de Archivo
Un diálogo que acepta arrastrar y soltar archivos o selecciona archivos mediante un input nativo.
const archivos = await prompt(`
<body style="font-family: system-ui; padding: 24px; background: white;">
<style>
.dropzone { border: 2px dashed #4299e1; border-radius: 12px; padding: 40px 20px; text-align: center; cursor: pointer; transition: all 0.2s; }
.dropzone.drag-over { background: #ebf8ff; border-color: #2b6cb0; }
</style>
<h3 style="margin-top: 0;">Seleccionar Archivos</h3>
<div class="dropzone" id="dropzone" onclick="document.getElementById('fileInput').click()">
<p>Arrastra archivos aquí o haz clic para seleccionar</p>
<input type="file" id="fileInput" multiple style="display: none;" onchange="handleFiles(this.files)" />
</div>
<div id="fileList" style="margin-top: 12px;"></div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;">
<button onclick="window.glimpse.send(null)" style="background: #eee; padding: 8px 16px; border: none; border-radius: 6px;">Cancelar</button>
</div>
<script>
const dropzone = document.getElementById('dropzone');
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('drag-over'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over'));
dropzone.addEventListener('drop', e => {
e.preventDefault();
dropzone.classList.remove('drag-over');
handleFiles(e.dataTransfer.files);
});
function handleFiles(archivos) {
const nombres = Array.from(archivos).map(f => f.name);
document.getElementById('fileList').innerHTML = nombres.map(n => '<div>📄 ' + n + '</div>').join('');
window.glimpse.send({ archivos: nombres });
}
</script>
</body>
`, { width: 400, height: 300, title: 'Cargar Archivos' });
Referencia de API
| Función | Descripción |
|---|---|
prompt(html, opciones?) | Muestra HTML. Devuelve una promesa que se resuelve con los primeros datos enviados desde el webview, o null si se cerró. |
open(html, opciones?) | Abre una ventana persistente conectada a un EventEmitter. |
win.on(evt, cb) | Escucha eventos ready, message, info, closed. |
win.send(js) | Ejecuta JavaScript en el webview. |
win.setHTML(html) | Reemplaza el contenido completo del webview. |
win.getInfo() | Solicita información actualizada del sistema (desencadena evento info). |
win.close() | Cierra la ventana. |
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
width | numero | 800 | Ancho de la ventana en píxeles |
height | numero | 600 | Alto de la ventana en píxeles |
title | string | 'Glimpse' | Título de la ventana |
frameless | boolean | false | Oculta la barra de título; arrastrable por el fondo |
floating | boolean | false | Se mantiene sobre otras ventanas |
transparent | boolean | false | Fondo transparente; el HTML debe establecer el suyo propio |
clickThrough | boolean | false | Los clics pasan a través de la ventana |
followCursor | boolean | false | La ventana sigue la posición del cursor |
x, y | numero | — | Posición absoluta en pantalla |
timeout | numero | — | (solo prompt) Tiempo en ms antes de rechazar la promesa |
Notas de la plataforma: Escritura de contenido HTML de marcado para una ventana sin marcos, el fondo es arrastrable normalmente. En macOS, establece -webkit-app-region: drag en los elementos del fondo. En Windows, comportamiento similar con selección predeterminada de cursor. Las ventanas transparentes requieren que el HTML de la página tenga un background explícito o use transparent !important. Los iconos de aplicación y dock se pueden ocultar con noDock: true (macOS). La compilación nativa requiere Xcode Command Line Tools en macOS, build-essential en Linux, y la cadena de herramientas de VS Build en Windows.