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這是 **粗體** 而這是 \`code\`。`;
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. 浮動通知
一個短暫、自動關閉的 Toast。無需互動。
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="0 25 25" to="360 25 25" dur="2s" repeatCount="indefinite" />
</circle>
</svg>
</body>
`, { width: 50, height: 50, frameless: true, transparent: true, floating: true, clickThrough: true, followCursor: true, cursorAnchor: 'top-right', cursorOffset: { x: -20, y: -20 } });