← Commit history

Update 5 file(s)

John Lauer ·784d73ffc3 ·2mo ago ·parent fbc1bd2
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"+  }+}\ No newline at end of file
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, '&lt;') + '</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(() => {});+});