Glimpse — 原生微 UI
Glimpse 能在 50 毫秒内打开一个带有 WebView 的原生窗口。你编写 HTML,用户瞬间看到。通过 window.glimpse.send()(WebView → Node)和 .send(js)(Node → WebView)实现双向通信。适用于 macOS(WKWebView)、Linux(WebKitGTK)和 Windows(WebView2)。
何时使用 Glimpse:
- 你需要用户输入,不限于是/否(表单、选择、文本输入)
- 你想展示视觉内容(图表、Markdown、图像、差异对比)
- 你想用一个合适的对话框确认破坏性操作
- 你想要一个浮动指示器、通知或伴侣小部件
- 你需要用户与丰富内容交互
桌面应用: 检查你是否正在一个可以在对话中内联显示 HTML、图表和表单的桌面应用中运行:
- Claude 桌面应用:
CLAUDE_CODE_ENTRYPOINT是claude-desktop。 - ChatGPT 桌面应用中的 Codex:
CODEX_INTERNAL_ORIGINATOR_OVERRIDE是Codex Desktop或codex_work_desktop。其内联显示是捆绑的visualize技能。
echo "claude=${CLAUDE_CODE_ENTRYPOINT:-} codex=${CODEX_INTERNAL_ORIGINATOR_OVERRIDE:-}"
在桌面应用中,优先使用应用的内联显示。只有在内联显示无法做到以下情况时,才在此处使用 Glimpse:一个会阻塞直到用户回答的运行脚本、一个在应用外保持可见的窗口(浮动、状态项、跟随光标)、或者为无代理运行的脚本提供 UI。在终端中(任何其他值,或两个变量均未设置),按如下所述使用 Glimpse。
设置(glimpse-everything 插件): glimpseui 在首次使用时会安装到此插件中。在编写任何 Glimpse 脚本之前,每个会话运行一次此命令。它会安装 glimpseui(如果需要),构建其原生辅助程序,并在标准输出上打印已安装包的绝对路径。该打印路径在下面各处都是 <RESOLVED_PATH>。
bash "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-}}/bin/glimpse-path.sh"
如果 CLAUDE_PLUGIN_ROOT 和 CODEX_PLUGIN_ROOT 都未设置,则相对于此技能文件的目录运行 ../../bin/glimpse-path.sh。首次运行可能需要一些时间(npm install 加上原生构建);进度会输出到 stderr。如果它以非零状态退出,向用户显示其 stderr,其中会指出缺少的构建工具。
导入: 始终使用已安装包内 glimpse.mjs 的绝对路径——当脚本从 /tmp 或任何没有 node_modules 的目录运行时,裸的 'glimpseui' 说明符会失败。
在 macOS/Linux 上,使用直接路径:
import { open, prompt } from '<RESOLVED_PATH>/src/glimpse.mjs';
在 Windows 上,ESM 导入需要 file:/// URL:
import { open, prompt } from 'file:///<RESOLVED_PATH>/src/glimpse.mjs';
// 例如 import { open, prompt } from 'file:///C:/Users/me/.pi/agent/.../glimpseui/src/glimpse.mjs';
快速参考
一次性对话框 (prompt)
import { prompt } from '<RESOLVED_PATH>/src/glimpse.mjs';
const answer = await prompt(html, {
width: 400, height: 300, // 窗口大小
title: 'My Dialog', // 标题栏文本
frameless: true, // 无标题栏
transparent: true, // 透明背景
});
// answer = 来自 window.glimpse.send() 的数据,如果用户关闭窗口则为 null
持久窗口 (open)
import { open } from '<RESOLVED_PATH>/src/glimpse.mjs';
const win = open(html, options);
win.on('ready', (info) => {}); // HTML 已加载 — info 包含屏幕、外观、光标
win.on('message', data => {}); // 用户交互
win.on('info', info => {}); // 新鲜的系统信息(在 getInfo() 之后)
win.on('closed', () => {}); // 窗口已关闭
win.send('document.title = "Hi"'); // 在 WebView 中执行 JS
win.setHTML('<h1>New content</h1>'); // 替换 HTML
win.info; // 最后已知的系统信息
win.getInfo(); // 请求最新信息
win.close(); // 关闭窗口
所有选项
{
width, height, // 像素(默认:800×600)
title, // 窗口标题(默认:"Glimpse")
frameless: true, // 无标题栏,可通过背景拖动
floating: true, // 总是置顶
transparent: true, // 透明窗口背景
clickThrough: true, // 鼠标穿透窗口
followCursor: true, // 窗口跟随鼠标光标
followMode: 'spring', // 'snap'(即时,默认)或 'spring'(弹性)
cursorAnchor: 'top-right', // 吸附点:top-left、top-right、right、bottom-right、bottom-left、left
cursorOffset: {x, y}, // 相对于光标的偏移(默认:20, -20)
openLinks: true, // 在默认浏览器中打开点击的 http/https 链接
openLinksApp: '/Applications/Google Chrome.app', // 可选的应用程序包路径
autoClose: true, // 在第一次消息后关闭
noDock: true, // 无 Dock 图标或应用切换器条目(macOS)
x, y, // 精确屏幕位置
timeout, // 仅用于 prompt() — 超时前等待的毫秒数
}
系统信息(在 ready 时可用)
win.on('ready', (info) => {
// 屏幕
info.screen.width // 2560(全分辨率)
info.screen.height // 1440
info.screen.scaleFactor // 2(Retina)
info.screen.visibleWidth // 2560(排除 Dock)
info.screen.visibleHeight // 1367(排除菜单栏)
// 外观
info.appearance.darkMode // true
info.appearance.accentColor // "#007AFF"
info.appearance.reduceMotion // false
info.appearance.increaseContrast // false
// 光标
info.cursor.x // 500
info.cursor.y // 800
// 所有显示器
info.screens // [{ x, y, width, height, scaleFactor, ... }, ...]
});
// 在 ready 后随时访问:
win.info.screen.width
win.info.appearance.darkMode
页面内 JavaScript 桥接
window.glimpse.send(data) // 发送数据到 Node(任何 JSON 可序列化的值)
window.glimpse.close() // 从 JS 关闭窗口
模式
1. 确认对话框
最简单的用例。询问是/否,得到答案,继续。
const answer = await prompt(`
<body style="font-family: system-ui; padding: 24px; background: white;">
<h2 style="margin-top: 0;">删除 47 个文件?</h2>
<p style="color: #666;">此操作无法撤销。</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;">
取消
</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;">
删除
</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: '确认' });
if (answer?.ok) { /* 继续执行删除 */ }
2. 文本输入表单
收集结构化输入。Enter 提交,Escape 取消。
const result = 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;">新组件</h3>
<input id="name" placeholder="组件名称" autofocus />
<select id="type">
<option value="page">页面</option>
<option value="component">组件</option>
<option value="layout">布局</option>
</select>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button onclick="window.glimpse.send(null)" style="background: #eee;">取消</button>
<button onclick="submit()" style="background: #4299e1; color: white;">创建</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: '创建' });
if (result) console.log(`正在创建 ${result.type}: ${result.name}`);
3. 选择列表
从选项列表中选取。点击或使用方向键 + Enter。
function pickFromList(title, items) {
const itemsHTML = items.map((item, i) =>
`<div class="item${i === 0 ? ' selected' : ''}" data-index="${i}" onclick="pick(${i})">${item}</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">${title}</div>
<div id="list">${itemsHTML}</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 + items.length * 38, frameless: true });
}
const choice = await pickFromList('选择一个框架', ['React', 'Vue', 'Svelte', 'Solid', 'Angular']);
4. Markdown / 富内容查看器
显示格式化内容。对于预览生成的文档、README 或差异对比很有用。
// 使用 CDN 库渲染 Markdown
const markdown = `# Hello World\n\n这是 **粗体** 而这是 \`代码\`。`;
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: '预览' });
5. 实时进度 / 流式输出
随着工作进展向窗口推送更新。
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 = (text) => {
document.getElementById('log').textContent += text + '\\n';
window.scrollTo(0, document.body.scrollHeight);
};
</script>
</body>
`, { width: 500, height: 300, title: '构建进度' });
win.on('ready', () => {
win.send(`appendLog('开始构建...')`);
// 随着进程运行,推送更新:
win.send(`appendLog('✓ 编译 42 个文件')`);
win.send(`appendLog('✓ 测试通过')`);
win.send(`appendLog('✓ 完成!')`);
});
6. 自适应深色/浅色模式
使用系统信息来样式化 UI 以匹配用户的外观设置。
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};">自适应 UI</h2>
<p>深色模式:${appearance.darkMode ? '开启' : '关闭'}</p>
<p>屏幕:${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;">
关闭
</button>
</body>
`);
});
7. 浮动通知
一个简短、自动消失的吐司提示。无需交互。
function notify(message, durationMs = 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);
">${message}</div>
</body>
`, { width: 300, height: 60, frameless: true, transparent: true, floating: true, clickThrough: true });
win.on('ready', () => setTimeout(() => win.close(), durationMs));
}
notify('✅ 已部署到生产环境');
8. 光标伴侣
跟随光标的视觉元素。非常适合代理状态指示器。
// 跟随光标的旋转环
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="
5
…
[截断以进行翻译]