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