Community程式設計與開發github.com

danmactough/glimpse

從腳本和代理程式顯示原生 UI — 對話框、表單、視覺化、浮動小工具、游標伴隨。支援 macOS、Linux 和 Windows。當你需要向使用者顯示 HTML、收集輸入、顯示圖表、渲染 Markdown 或建立任何無需瀏覽器的視覺互動時使用。

glimpse 是什麼?

Glimpse 是一個能在 50 毫秒內開啟原生視窗並嵌入 WebView 的微 UI 工具,支援 macOS(WKWebView)、Linux(WebKitGTK)和 Windows(WebView2)。它透過 `window.glimpse.send()`(WebView → Node)和 `.send(js)`(Node → WebView)實現雙向通訊,讓 AI 代理程式能直接建立豐富的視覺互動而無需依賴瀏覽器。主要應用場景包括:表單輸入、確認對話框、Markdown/圖表顯示、滾動輸出、浮動通知和游標跟隨元件。Glimpse 可作為獨立套件安裝(透過 `glimpse-everything` 插件),也相容於 Claude 桌面應用和 Codex。其兩個核心 API 分別為:用於一次性對話的 `prompt()` 函式,以及用於持久化互動視窗的 `open()` 函式。開發者可以用標準 HTML、CSS 和 JavaScript 定義 UI,並透過 Glimpse 的 API 與主邏輯進行非同步資料交換。

相容平台✓Claude Code✓Codex CLI✓Cursor
npx skills add https://github.com/danmactough/glimpse-everything/tree/HEAD/plugin/skills/glimpse

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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 } });

相關技能