Community코딩 & 개발github.com

Glimpse — 네이티브 마이크로 UI

스크립트와 AI 에이전트에서 네이티브 UI(대화상자, 폼, 시각화, 떠 있는 위젯, 커서 컴패니언)를 표시합니다. macOS, Linux, Windows를 지원하며, 브라우저 없이 HTML을 보여주거나 입력을 받거나 차트/마크다운 등을 렌더링해야 할 때 사용합니다.

Glimpse — 네이티브 마이크로 UI란 무엇인가요?

Glimpse는 50ms 미만의 속도로 네이티브 웹뷰 창을 열어주는 라이브러리로, AI 에이전트(Claude Code, Codex 등)나 스크립트에서 사용자와의 시각적 상호작용을 가능하게 합니다. `prompt()` 함수로 한 번의 대화상자를 띄우거나, `open()` 함수로 지속적인 창을 관리할 수 있으며, 양방향 통신(JavaScript → Node.js, Node.js → JavaScript)을 지원합니다. macOS(WKWebView), Linux(WebKitGTK), Windows(WebView2)에서 모두 동작하며, 창 크기, 투명도, 플로팅, 커서 추적 등 다양한 옵션을 제공합니다. 데스크톱 앱(Claude, Codex 등) 내에서는 앱 자체 인라인 표시를 우선 사용하되, 사용자 입력이 차단되는 스크립트나 앱 외부에 떠 있는 위젯이 필요할 때 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에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Glimpse — 네이티브 마이크로 UI은(는) 무엇을 하나요?

Glimpse는 50ms 미만의 속도로 네이티브 웹뷰 창을 엽니다. HTML을 작성하면 사용자가 즉시 볼 수 있습니다. window.glimpse.send() (웹뷰 → Node)와 .send(js) (Node → 웹뷰)를 통한 양방향 통신을 지원합니다. macOS(WKWebView), Linux(WebKitGTK), Windows(WebView2)에서 작동합니다.

Glimpse를 사용해야 할 경우:

  • 예/아니오 이상의 사용자 입력이 필요할 때 (폼, 선택, 텍스트 입력)
  • 시각적인 내용(차트, 마크다운, 이미지, diff)을 보여주고 싶을 때
  • 중요한 작업을 적절한 대화상자로 확인하고 싶을 때
  • 떠 있는 표시기, 알림, 또는 컴패니언 위젯이 필요할 때
  • 사용자가 풍부한 콘텐츠와 상호작용해야 할 때

데스크톱 앱: 현재 실행 중인 환경이 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를 설치하고, 네이티브 헬퍼를 빌드하며, 설치된 패키지의 절대 경로를 stdout에 출력합니다. 그 출력된 경로가 아래 모든 곳에서 <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로 출력됩니다. 종료 코드가 0이 아니면 사용자에게 stderr를 보여주세요. 여기에는 누락된 빌드 도구 이름이 표시됩니다.

가져오기: 설치된 패키지 내의 glimpse.mjs에 대한 절대 경로를 항상 사용하세요. 'glimpseui'와 같은 단순한 지정자는 스크립트가 /tmp나 node_modules가 없는 곳에서 실행될 때 실패합니다.

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: '내 대화상자',         // 제목 표시줄 텍스트
  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"');   // 웹뷰에서 JS 실행
win.setHTML('<h1>새 내용</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() 전용 — ms 후 거부
}

시스템 정보 (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. 마크다운 / 리치 콘텐츠 뷰어

포맷된 콘텐츠를 표시합니다. 생성된 문서, README, diff 미리보기에 유용합니다.

// CDN 라이브러리로 마크다운 렌더링
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="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
    </circle>
  </svg>
</body>
`, {
  width: 50, height: 50, frameless: true, transparent: true,
  floating: true, clickThrough: true, followCursor: true,
  cursorAnchor: 'right', cursorOffset: { x: 30, y: 30 },
});

9. 절전 페이드 (macOS)

macOS에서 에너지를 절약하기 위해 숨겨진 창을 감지하여 WebKit 렌더링을 일시 중지합니다.

let hiddenWindows = [];

win.on('info', (info) => {
  const hw = new Set(hiddenWindows);
  const windows = []; // 앱이 추적 중인 모든 창 목록
  for (const w of windows) {
    // 창이 화면 밖에 있거나 다른 앱에 의해 가려진 경우
    if (isHidden(w, info)) {
      if (!hw.has(w)) { w.send('document.hidden = true;'); hiddenWindows.push(w); }
    } else {
      if (hw.has(w)) { w.send('document.hidden = false;'); hiddenWindows = hiddenWindows.filter(x => x !== w); }
    }
  }
});

관련 스킬