Glimpse — ネイティブマイクロUI は何をしますか?
Glimpseは、50ミリ秒未満でWebビューを備えたネイティブウィンドウを開きます。HTMLを記述すれば、ユーザーはそれを即座に表示できます。window.glimpse.send()(Webビュー→Node)および.send(js)(Node→Webビュー)を介した双方向通信が可能です。macOS(WKWebView)、Linux(WebKitGTK)、Windows(WebView2)で動作します。
Glimpseを使用するタイミング:
- はい/いいえ以外のユーザー入力が必要な場合(フォーム、選択、テキスト入力)
- 視覚的なものを表示したい場合(チャート、マークダウン、画像、差分)
- 適切なダイアログで破壊的な操作を確認したい場合
- フローティングインジケーター、通知、またはコンパニオンウィジェットが必要な場合
- リッチコンテンツとユーザーがインタラクションする必要がある場合
デスクトップアプリ: 自分がデスクトップアプリ内で実行しているかどうかを確認し、そのアプリが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スクリプトを作成する前に、セッションごとに1回これを実行してください。必要に応じて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とネイティブビルド)。進捗は標準エラー出力に出力されます。終了コードが0以外の場合は、標準エラー出力をユーザーに表示します。これには欠落しているビルドツールの名前が含まれています。
インポート: インストールされたパッケージ内の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: '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にはscreen, appearance, cursorが含まれる
win.on('message', data => {}); // ユーザーインタラクション
win.on('info', info => {}); // 新しいシステム情報(getInfo()後)
win.on('closed', () => {}); // ウィンドウが閉じられた
win.send('document.title = "Hi"'); // Webビュー内で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()用 — ミリ秒後に拒否
}
システム情報(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(`Creating ${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、差分のプレビューに便利です。
// CDNライブラリでマークダウンをレンダリング
const markdown = `# Hello World\n\nThis is **bold** and this is \`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. フローティング通知
簡潔で自動的に閉じるトースト。インタラクション不要。
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, followCursor: true, clickThrough: true });
9. 差分ビューアー
gitの差分やテキストの変更をインラインで表示します。
function showDiff(oldText, newText) {
// 簡単な差分を作成(実際の使用ではdiffライブラリを使用)
const lines = [];
const oldLines = oldText.split('\\n');
const newLines = newText.split('\\n');
const maxLen = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLen; i++) {
if (oldLines[i] === newLines[i]) {
lines.push(`<div style="padding: 2px 12px; font-size: 13px;">${oldLines[i] || ''}</div>`);
} else {
if (oldLines[i] !== undefined) lines.push(`<div style="padding: 2px 12px; font-size: 13px; background: #fdd; color: #c33;">- ${oldLines[i]}</div>`);
if (newLines[i] !== undefined) lines.push(`<div style="padding: 2px 12px; font-size: 13px; background: #dfd; color: #3c3;">+ ${newLines[i]}</div>`);
}
}
open(`
<body style="font-family: 'SF Mono', Monaco, monospace; margin: 0; background: white;">
<div style="padding: 8px 12px; background: #f6f8fa; border-bottom: 1px solid #eee; font-size: 12px; color: #666;">
差分: ${oldLines.length}→${newLines.length}行
</div>
${lines.join('')}
</body>
`, { width: 600, height: 400, title: '差分' });
}
showDiff('Hello World', 'Hello Glimpse!');
ベストプラクティス
- HTMLはインラインで記述する — GlimpseのウィンドウはローカルのHTML文字列を受け取ります。外部ファイルをロードするよりも、HTMLをテンプレートリテラルに直接記述してください。
window.glimpse.send()を使用する — これがGlimpseのデータ送信方法です。フォームの送信やページ遷移は使用しないでください。- ユーザーの外観を尊重する — 利用可能な場合は
info.appearanceを使用して、ダークモードとアクセントカラーに合わせてUIを調整します。 - ウィンドウを小さく保つ — ネイティブな外観にするには、控えめなサイズ(400×300など)と最小限の装飾を使用します。
- キーボードナビゲーションを考慮する — ユーザーの操作性を高めるために、
Enterで送信、Escapeでキャンセルを実装します。 transparent: trueで背景をリセットする — 透過ウィンドウを使用する場合は、bodyにbackground: transparent !importantを適用してデフォルトの白地を上書きします。- HTMLをエスケープする — ユーザーが提供したコンテンツをHTMLに挿入する場合は、XSSを防ぐために必ずエスケープ処理を行ってください。
- プラグインの互換性を確認する —
CLAUDE_PLUGIN_ROOTとCODEX_PLUGIN_ROOTの両方をチェックし、適切なプラグインルートを使用します。
トラブルシューティング
問題: glimpse-path.shが存在しないと表示される
解決策: スキルファイルが正しいプラグインディレクトリに配置されていることを確認してください。CLAUDE_PLUGIN_ROOTまたはCODEX_PLUGIN_ROOTが正しく設定されている必要があります。
問題: npm install中にネイティブビルドが失敗する
解決策: システムにC++コンパイラとビルドツールがインストールされていることを確認してください(macOSではXcode CLI Tools、Linuxではbuild-essential、WindowsではVisual Studio Build Tools)。
問題: import { open } from 'glimpseui'がERR_MODULE_NOT_FOUNDで失敗する
解決策: 常にインポートに絶対パスを使用してください。ベア指定子'glimpseui'は、スクリプトが/tmpから実行される場合など、Node.jsのモジュール解決パス外では機能しません。
問題: ウィンドウが表示されるがコンテンツが空白
解決策: HTMLが正しくフォーマットされていることを確認してください。テンプレートリテラル内の引用符をエスケープします。win.send()でJavaScriptを評価する場合は、'の代わりにバックティックを使用します。