app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
Update 5 file(s)
4 files changed
+239
src/extension/manifest.jsonadded+34@@ -0,0 +1,34 @@+{+ "manifest_version": 3,+ "name": "Adom Pup",+ "version": "0.1.0",+ "description": "Toolbar for Adom Pup browser windows: annotate, snip, view toggle, dashboard, owner info. Thin trigger surface - the pup bridge is the engine.",+ "action": {+ "default_popup": "popup.html",+ "default_title": "Adom Pup"+ },+ "background": {+ "service_worker": "worker.js"+ },+ "permissions": [+ "storage",+ "tabs",+ "alarms"+ ],+ "host_permissions": [+ "http://127.0.0.1/*"+ ],+ "commands": {+ "pup-snip": {+ "suggested_key": {+ "default": "Ctrl+Shift+A"+ },+ "description": "Snip a region of this pup window"+ }+ },+ "icons": {+ "16": "icon16.png",+ "48": "icon48.png",+ "128": "icon128.png"+ }+}
src/extension/popup.htmladded+60@@ -0,0 +1,60 @@+<!doctype html>+<html>+<head>+<meta charset="utf-8">+<style>+ :root { color-scheme: dark; }+ body {+ margin: 0; width: 248px; background: #0d1117; color: #e5e7eb;+ font: 13px "Segoe UI", system-ui, sans-serif; border: 1px solid #2dd4bf44;+ }+ .hdr {+ display: flex; align-items: center; gap: 8px; padding: 10px 12px 8px;+ border-bottom: 1px solid #ffffff12;+ }+ .hdr .logo { width: 18px; height: 18px; border-radius: 5px; background: #0f766e;+ display: grid; place-items: center; }+ .hdr .logo span { color: #5eead4; font-size: 11px; letter-spacing: 1px; }+ .hdr .t { font-weight: 600; color: #7fe9e2; }+ .hdr .owner { margin-left: auto; font-size: 11px; color: #9ca3af; max-width: 110px;+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }+ .owner b { color: #fbbf24; font-weight: 600; }+ .menu { padding: 6px; }+ button.row {+ display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;+ background: none; border: none; border-radius: 8px; color: #e5e7eb;+ padding: 8px 10px; font: inherit; cursor: pointer;+ }+ button.row:hover { background: #ffffff10; }+ button.row .ic { width: 18px; text-align: center; color: #5eead4; }+ button.row .kbd { margin-left: auto; font-size: 10px; color: #6b7280;+ border: 1px solid #ffffff1c; border-radius: 4px; padding: 1px 5px; }+ button.row.danger .ic { color: #f87171; }+ button.row.danger:hover { background: #f8717118; }+ .sep { height: 1px; background: #ffffff12; margin: 4px 8px; }+ .foot { padding: 6px 12px 9px; font-size: 10.5px; color: #6b7280; }+ .foot .dot { color: #34d399; }+ .off { padding: 14px 12px; color: #9ca3af; font-size: 12px; display: none; }+</style>+</head>+<body>+ <div class="hdr">+ <div class="logo"><span>⠿</span></div>+ <div class="t">Adom Pup</div>+ <div class="owner" id="owner">…</div>+ </div>+ <div class="menu" id="menu">+ <button class="row" data-act="annotate"><span class="ic">✎</span>Annotate this page</button>+ <button class="row" data-act="snip"><span class="ic">▣</span>Snip a region<span class="kbd">Ctrl·Shift·A</span></button>+ <div class="sep"></div>+ <button class="row" data-act="view"><span class="ic">◐</span><span id="viewLabel">Switch to public view</span></button>+ <button class="row" data-act="dashboard"><span class="ic">▦</span>Open pup dashboard</button>+ <button class="row" data-act="info"><span class="ic">ⓘ</span>Window info card</button>+ <div class="sep"></div>+ <button class="row danger" data-act="close"><span class="ic">✕</span>Close this window</button>+ </div>+ <div class="off" id="off">The Adom toolbar is turned off in pup's Settings, or the pup bridge isn't running.</div>+ <div class="foot" id="foot"><span class="dot">●</span> pup connected</div>+ <script src="popup.js"></script>+</body>+</html>
src/extension/popup.jsadded+76@@ -0,0 +1,76 @@+// Adom Pup popup — thin trigger surface. Finds pup's loopback port (cached), resolves which pup+// session owns THIS tab's window, and fires the same /command verbs the taskbar jump lists use,+// with caller identity 'user-toolbar' (ownership-gate exempt on the pup side: USER_CALLERS).+const CANDIDATE_PORTS = [64230, 8851]; // ab-assigned stable port first, legacy default second++async function findPup() {+ const { pupPort } = await chrome.storage.local.get('pupPort');+ const ports = pupPort ? [pupPort, ...CANDIDATE_PORTS.filter(p => p !== pupPort)] : CANDIDATE_PORTS;+ for (const port of ports) {+ try {+ const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1200) });+ const j = await r.json();+ if (j && j.bridge === 'puppeteer') { chrome.storage.local.set({ pupPort: port }); return { port, health: j }; }+ } catch (e) {}+ }+ return null;+}++async function cmd(port, command, args) {+ const r = await fetch(`http://127.0.0.1:${port}/command`, {+ method: 'POST',+ headers: { 'content-type': 'application/json', 'x-adom-caller-thread': 'user-toolbar', 'x-adom-caller-reason': 'toolbar popup click' },+ body: JSON.stringify({ command, args: { ...args, caller: { aiThread: 'user-toolbar' } } }),+ });+ return r.json();+}++(async () => {+ const menu = document.getElementById('menu');+ // render-first preview (?preview=1): mock state so the UI can be reviewed without a bridge+ if (new URLSearchParams(location.search).get('preview')) {+ document.getElementById('owner').innerHTML = 'owned by <b>kicad-dev</b>';+ document.getElementById('foot').innerHTML = '<span class="dot">●</span> pup 2.0.50 · port 64230';+ return;+ }+ const off = document.getElementById('off');+ const foot = document.getElementById('foot');+ const ownerEl = document.getElementById('owner');++ const pup = await findPup();+ if (!pup) { menu.style.display = 'none'; off.style.display = 'block'; foot.innerHTML = '<span style="color:#f87171">●</span> pup not reachable'; return; }+ const { port, health } = pup;++ // which session owns THIS window? Match the active tab's URL against pup's session list.+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });+ let sid = null, owner = null, view = null;+ try {+ const sessions = (health.sessions || []);+ const hit = sessions.find(s => s.url && tab && tab.url && (s.url === tab.url || tab.url.startsWith((s.url || '').split('?')[0])));+ if (hit) sid = hit.sessionId;+ if (sid) {+ const d = await cmd(port, 'pup_list_windows', {});+ const rows = JSON.parse(d.output || '{}').sessions || d.sessions || [];+ const row = rows.find(r2 => r2.sessionId === sid) || (Array.isArray(rows) ? rows.find(r2 => r2.sessionId === sid) : null);+ if (row) { owner = row.owner; view = row.wikiView || row.view || null; }+ }+ } catch (e) {}+ ownerEl.innerHTML = owner ? 'owned by <b>' + owner.replace(/</g, '<') + '</b>' : (sid ? sid : 'not a pup window');+ if (view === 'public') document.getElementById('viewLabel').textContent = 'Switch to logged-in view';+ foot.innerHTML = '<span class="dot">●</span> pup ' + (health.version || '') + ' · port ' + port;++ menu.addEventListener('click', async (e) => {+ const btn = e.target.closest('button.row');+ if (!btn) return;+ const act = btn.dataset.act;+ try {+ if (act === 'annotate') await cmd(port, 'pup_annotate', { sessionId: sid });+ else if (act === 'snip') await cmd(port, 'pup_annotate', { sessionId: sid, tool: 'snip' });+ else if (act === 'view') await cmd(port, 'pup_wiki_set_view', { sessionId: sid, view: view === 'public' ? 'authed' : 'public' });+ else if (act === 'dashboard') await cmd(port, 'pup_dashboard', {});+ else if (act === 'info') await cmd(port, 'pup_window_info', { sessionId: sid });+ else if (act === 'close') { if (confirm('Close ' + (owner ? owner + "'s" : 'this') + ' pup window?')) await cmd(port, 'pup_close_window', { sessionId: sid }); }+ } catch (err) {}+ window.close();+ });+})();
src/extension/worker.jsadded+69@@ -0,0 +1,69 @@+// Adom Pup service worker — per-tab owner badge + the snip keyboard command.+// Polls pup /health (cheap, cached port) and badges each tab whose URL matches a pup session+// with the owning thread's initials + a stable per-thread color. Settings-aware: pup's+// toolbarExtension/tbOwnerBadge prefs ride /health; OFF clears badges and stands down.+const CANDIDATE_PORTS = [64230, 8851];++async function findPort() {+ const { pupPort } = await chrome.storage.local.get('pupPort');+ const ports = pupPort ? [pupPort, ...CANDIDATE_PORTS.filter(p => p !== pupPort)] : CANDIDATE_PORTS;+ for (const port of ports) {+ try {+ const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1200) });+ const j = await r.json();+ if (j && j.bridge === 'puppeteer') { chrome.storage.local.set({ pupPort: port }); return { port, health: j }; }+ } catch (e) {}+ }+ return null;+}++function threadColor(name) {+ let h = 0;+ for (let i = 0; i < (name || '').length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;+ const palette = ['#f59e0b', '#38bdf8', '#39d98a', '#c084fc', '#fb7185', '#5eead4'];+ return palette[h % palette.length];+}+function initials(name) {+ const parts = String(name || '').split(/[\s-_]+/).filter(Boolean);+ return (parts.length >= 2 ? parts[0][0] + parts[1][0] : String(name || '??').slice(0, 2)).toUpperCase();+}++async function sweepBadges() {+ const found = await findPort();+ if (!found) return;+ const sessions = found.health.sessions || [];+ const tabs = await chrome.tabs.query({});+ for (const tab of tabs) {+ if (!tab.url) continue;+ const hit = sessions.find(s => s.url && (s.url === tab.url || tab.url.startsWith((s.url || '').split('?')[0])));+ try {+ if (hit && hit.owner) {+ chrome.action.setBadgeText({ tabId: tab.id, text: initials(hit.owner) });+ chrome.action.setBadgeBackgroundColor({ tabId: tab.id, color: threadColor(hit.owner) });+ chrome.action.setTitle({ tabId: tab.id, title: `Adom Pup — ${hit.owner}'s window` });+ } else {+ chrome.action.setBadgeText({ tabId: tab.id, text: '' });+ }+ } catch (e) {}+ }+}++chrome.alarms.create('pup-badges', { periodInMinutes: 0.5 });+chrome.alarms.onAlarm.addListener(a => { if (a.name === 'pup-badges') sweepBadges(); });+chrome.tabs.onActivated.addListener(() => sweepBadges());+chrome.runtime.onStartup.addListener(() => sweepBadges());+sweepBadges();++chrome.commands.onCommand.addListener(async (command) => {+ if (command !== 'pup-snip') return;+ const found = await findPort();+ if (!found) return;+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });+ const hit = (found.health.sessions || []).find(s => s.url && tab && tab.url && (s.url === tab.url || tab.url.startsWith((s.url || '').split('?')[0])));+ if (!hit) return;+ await fetch(`http://127.0.0.1:${found.port}/command`, {+ method: 'POST',+ headers: { 'content-type': 'application/json', 'x-adom-caller-thread': 'user-toolbar', 'x-adom-caller-reason': 'Ctrl+Shift+A snip hotkey' },+ body: JSON.stringify({ command: 'pup_annotate', args: { sessionId: hit.sessionId, tool: 'snip', caller: { aiThread: 'user-toolbar' } } }),+ }).catch(() => {});+});