Community编程与开发github.com

danmactough/glimpse

从脚本和AI代理中显示原生用户界面——对话框、表单、可视化内容、浮动小部件、光标伴侣。支持macOS、Linux和Windows。当你需要向用户显示HTML、收集输入、展示图表、渲染Markdown或创建任何无需浏览器的视觉交互时使用。

glimpse 是什么?

Glimpse 是一个为 AI 代理(如 Claude Code 和 Codex)设计的原生微 UI 技能。它能在 50 毫秒内打开一个带有 WebView 的原生窗口,让你编写 HTML,用户能立即看到。它支持通过 `window.glimpse.send()`(WebView → Node)和 `.send(js)`(Node → WebView)进行双向通信,使代理能够收集输入、显示可视化内容、确认操作或创建浮动小部件——所有这一切都无需浏览器。Glimpse 适用于 macOS、Linux 和 Windows,利用每个平台的原生渲染引擎(WKWebView、WebKitGTK、WebView2)。它包括一个一次性对话框(`prompt`)和一个持久窗口(`open`)API,提供对窗口大小、位置、透明度、跟随光标行为以及主题感知样式(自动深色/浅色模式检测)的控制。该技能还检测它是否在桌面应用(Claude Desktop 应用或 ChatGPT 桌面应用中的 Codex)中运行,并建议在这些情况下优先使用内联显示,仅在需要脚本阻塞交互、代理外窗口或类似通知的浮动 UI 时回退到 Glimpse。

兼容平台✓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这是 **粗体** 而这是 \`代码\`。`;

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

…

[截断以进行翻译]

相关技能