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
Title tag relabeled session->ai-thread + a floating owner chip (John: 'call it ai-thread then and just do the visual parts i see and yes make a small floating in-window chip. it cant get in the way'). (1) The visible title suffix is now '(ai-thread: <name>)' not '(session: <name>)' so it reads as pup metadata, not the page's own title - routed through a single TITLE_LABEL constant across all 31 write-sites (the injector, retag, and every desktop_find_window/titleContains matcher), while EVERY read/parse/strip regex (SESSION_TAG_RE, the injector strip, the z-order check, the dashboard re-assert) is tolerant of BOTH labels so windows tagged by the running build still resolve during the switchover, and the re-assert now strips any existing tag of either label before writing so old+new can never stack. (2) A tiny non-interactive owner chip in each window's bottom-left names the owning ai-thread and profile: attached to <html> inside a CLOSED shadow root (page CSS/JS cannot see or affect it, and it cannot affect the page), pointer-events:none so clicks pass through, one-shot with no observers fighting the page, self-dimming after 4.5s - designed so it can never get in the way of John's vibe-coded pages. Gated by the ownerChip setting (default on, bottom of the dialog) with immediate add/remove on toggle.
4 files changed
+131−49
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.282",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.282/adom-bridge-puppeteer-v1.9.282.zip",- "sha256": "0200b39cd13e7be8f4e459f1e7f36f470b319443a21f80c383fe2d766d138ea4",- "size": 713017,+ "version": "1.9.284",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.284/adom-bridge-puppeteer-v1.9.284.zip",+ "sha256": "ef008a387279af50b8b10d213941b8807eff86ae9b8636bfb984f8030b6b204d",+ "size": 715560, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.282+1.9.284
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.282",+ "version": "1.9.284", "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+125−43@@ -511,7 +511,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // enforcer at document-start on EVERY navigation (before page scripts), so the // suffix reappears the instant the page sets its title — closing that gap. try {- page.evaluateOnNewDocument((sid, minimal) => {+ page.evaluateOnNewDocument((sid, minimal, label) => { // v1.9.185: carry minimal-touch into the page so the title observer below can skip installing // itself (see the note at `observe`). Read at document-start, before any page script runs. try { if (minimal) window.__pupMinimalTouch = true; } catch (e) {}@@ -522,8 +522,9 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // two MutationObservers appending different strings ping-pong forever, peg the // renderer, and flood the CDP pipe (which HUNG browser_open_tab on Edge and // poisoned the whole bridge). Matching on the marker PREFIX makes it cooperate.- const MARKER = ' (session: ' + sid; // no closing paren — matches both forms- const BASE = ' (session: ' + sid + ')';+ const LBL = label || 'ai-thread';+ const MARKER = ' (' + LBL + ': ' + sid; // no closing paren — matches both forms+ const BASE = ' (' + LBL + ': ' + sid + ')'; // v1.9.49: do NOT require a pre-existing title. This guard used to be // if (document.title && ...) // so a page that never sets a <title> (plenty of app routes and SPA shells serve one) kept an@@ -543,7 +544,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // by "(session: <id>" — the same duplicate-tag class that previously made branding land on the // WRONG window. So: count OUR OWN tag and normalise to exactly one. Only our own sid is touched, // so an adopted tab's re-tag (retagSessionTitle) is never fought.- const RE_MINE = new RegExp('\\s*\\(session: ' + sid.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^)]*)\\)', 'g');+ const RE_MINE = new RegExp('\\s*\\((?:session|ai-thread): ' + sid.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^)]*)\\)', 'g'); const ensure = () => { try { const cur = String(document.title || '');@@ -568,7 +569,44 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user }; ensure(); if (!observe()) document.addEventListener('DOMContentLoaded', () => { observe(); ensure(); });- }, sessionId, minimalTouch()).catch(() => {});+ }, sessionId, minimalTouch(), TITLE_LABEL).catch(() => {});+ } catch (e) {}+ // v1.9.284 (John: "a small floating in-window chip that indicates what ai-thread owns this tab,+ // what profile ... it can't get in the way though"). A tiny corner pill showing owner-thread and+ // profile. Design so it can NEVER get in the way or disturb his vibe-coded pages:+ // - attached to <html> (survives SPA <body> swaps) inside a CLOSED shadow root, so page CSS/JS+ // can neither style it nor see it, and it cannot affect the page's layout or selectors;+ // - pointer-events:none, so every click passes straight through it;+ // - one-shot (no observers/intervals fighting the page), and it self-dims after a few seconds.+ // Gated by the ownerChip setting (default on). Value strips a leading "ai-" so it reads as the+ // plain thread name, matching the new "ai-thread" title label.+ try {+ if (pupSettings().ownerChip === 'on') {+ const _threadDisp = String(sessionId).replace(/^ai-/, '');+ const _prof = (session && session.profileName) || 'adom-you';+ page.evaluateOnNewDocument((thread, profile) => {+ try {+ const add = () => {+ try {+ const root = document.documentElement; if (!root) return;+ if (root.querySelector('div[data-pup-owner]')) return;+ const host = document.createElement('div');+ host.setAttribute('data-pup-owner', '1');+ host.style.cssText = 'position:fixed;left:8px;bottom:8px;z-index:2147483647;pointer-events:none;margin:0;padding:0;';+ const sh = host.attachShadow ? host.attachShadow({ mode: 'closed' }) : null;+ const html = '<style>.c{font:11px/1.35 "JetBrains Mono",ui-monospace,SFMono-Regular,monospace;color:#7fe9e2;background:rgba(13,17,23,.72);border:1px solid rgba(0,184,177,.45);border-radius:6px;padding:2px 8px;white-space:nowrap;opacity:.9;transition:opacity .5s ease;user-select:none}.c b{color:#e6edf3;font-weight:600}.dim{opacity:.28}</style>'+ + '<div class="c" id="pupc">⬢ <b>' + String(thread) + '</b> · ' + String(profile) + '</div>';+ if (sh) { sh.innerHTML = html; } else { host.innerHTML = html; }+ root.appendChild(host);+ const el = sh ? sh.getElementById('pupc') : host.querySelector('#pupc');+ setTimeout(() => { try { if (el) el.classList.add('dim'); } catch (e) {} }, 4500);+ } catch (e) {}+ };+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', add, { once: true });+ else add();+ } catch (e) {}+ }, _threadDisp, _prof).catch(() => {});+ } } catch (e) {} // v1.8.21: install the shadow-DOM + scroll helpers on every document so browser_eval // can use $deep / $$deep / deepText without hand-rolling a shadow-root walk.@@ -1102,7 +1140,11 @@ function isProcessAlive(pid) { // scenarios. Without this tag (e.g. about:blank pages), the page is // considered an orphan and will be adopted under a generated sessionId // if rescan is asked to.-const SESSION_TAG_RE = /\(session:\s*([^|)]+?)(?:\s*\|.*)?\)\s*$/;+// v1.9.284 (John: "call it ai-thread ... and just do the visual parts i see"): the visible title+// tag label. WRITE side uses this; READ side (parse/strip/match) is TOLERANT of the legacy "session"+// label so windows tagged by an older build still resolve during the switchover.+const TITLE_LABEL = 'ai-thread';+const SESSION_TAG_RE = /\((?:session|ai-thread):\s*([^|)]+?)(?:\s*\|.*)?\)\s*$/; function parseSessionIdFromTitle(title) { if (!title) return null; const m = String(title).match(SESSION_TAG_RE);@@ -1964,7 +2006,7 @@ async function sessionHwnd(sessionId) { if (_s) { const h = await resolveSessionHwndByBounds(_s, sessionId); if (h) return h; } } catch (e) {} try {- const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 5000, caller: sessionCaller(sessions.get(sessionId)) }));+ const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 5000, caller: sessionCaller(sessions.get(sessionId)) })); const b = fw && (fw.best || (fw.data && fw.data.best)); return b && (b.hwnd != null ? Number(b.hwnd) : null); } catch (e) { return null; }@@ -2624,7 +2666,7 @@ async function unstickIfHiddenWhileFront(sessionId, session, fgHwnd, hwnd) { } _unstickClickAt.set(sessionId, Date.now()); try {- const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 5000 }));+ const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 5000 })); const r = fw && (fw.best || (fw.data && fw.data.best)); const rect = r && r.rect; if (rect && rect.width > 600) { const cx = Math.round(rect.left + rect.width - 300);@@ -2728,6 +2770,7 @@ function pupSettings() { // v1.9.275 (John: 'shouldn't there be a setting for whether to show the full jump list or not?') D.jlViewTasks = 'on'; D.jlCloseAll = 'on'; D.notMineCaption = 'on'; // v1.9.282 (John): the 'NOT foregrounded by Adom' caption, default on+ D.ownerChip = 'on'; // v1.9.284 (John): the floating owner-thread/profile chip, default on if (_pupSettings.autoLogin === 'off' && !chosen.autoLogin) _pupSettings.autoLogin = 'on'; // v1.9.260 (John: "2s feels too fast cuz i can't read"): readable default. A stored 2000 was the // old default, not a choice made in the dialog (it shipped hours ago) - migrate it up once.@@ -3005,7 +3048,7 @@ async function stampPupIdentity(sessionId, opts) { if (pupSettings().jumpLists === 'on') { try { await updateWikiJumplist(sess, sessionId, { appId }); } catch (e) {} } } const r = await chrome.adCommand('desktop_set_window_identity', {- titleContains: `(session: ${sessionId}`, cacheKey: `pup-${sessionId}-${cat}`,+ titleContains: `(${TITLE_LABEL}: ${sessionId}`, cacheKey: `pup-${sessionId}-${cat}`, appId, displayName: pupDisplayName(sessionId), iconPath: pupIconPath(sessionId), // v1.9.11 (AD ≥1.9.141): brand the taskbar RIGHT-CLICK MENU HEADER row (it showed the // chrome.exe icon for months). There is NO "ApplicationIcon" registry value on AUMID keys@@ -3319,7 +3362,7 @@ async function updateWikiJumplist(session, sessionId, opts) { // code — no more "right-click it and tell me what you see". let hwnd = null; try {- const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))}));+ const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))})); hwnd = fw && fw.best && fw.best.hwnd; } catch (e) {} try {@@ -3426,7 +3469,7 @@ async function setSessionProgress(sessionId, busy) { if (process.platform !== 'win32') return; try { await chrome.adCommand('desktop_taskbar', {- titleContains: `(session: ${sessionId}`,+ titleContains: `(${TITLE_LABEL}: ${sessionId}`, progress: busy ? { state: 'indeterminate' } : { state: 'none' }, }, { timeoutMs: 4000 }); } catch (e) {}@@ -3585,7 +3628,7 @@ async function updateThumbnailTooltip(session, sessionId) { if (session._thumbTip === tip) return; // idempotent: only push real changes try { const r = await chrome.adCommand('desktop_taskbar', {- titleContains: `(session: ${sessionId}`, thumbnailTooltip: tip,+ titleContains: `(${TITLE_LABEL}: ${sessionId}`, thumbnailTooltip: tip, }, { timeoutMs: 5000, caller: sessionCaller(session) }); const raw = r && (typeof r.output === 'string' ? r.output : JSON.stringify(r.output || '')); if (_isUnknownVerb(r, raw) || /unknown (arg|field)|unexpected (arg|field)/i.test(raw || '')) {@@ -3648,8 +3691,8 @@ async function updateTabCountBadge(session, sessionId) { else if (totalTabs >= 2) icon = tabBadgeIcon(totalTabs); // single window: plain tab count for (const [sid2] of sessions) { const args2 = icon- ? { titleContains: `(session: ${sid2}`, overlay: { icon, tooltip: `Adom Pup — ${wins} window${wins === 1 ? '' : 's'}, ${totalTabs} tab${totalTabs === 1 ? '' : 's'}` } }- : { titleContains: `(session: ${sid2}`, overlay: { badge: 'none' } };+ ? { titleContains: `(${TITLE_LABEL}: ${sid2}`, overlay: { icon, tooltip: `Adom Pup — ${wins} window${wins === 1 ? '' : 's'}, ${totalTabs} tab${totalTabs === 1 ? '' : 's'}` } }+ : { titleContains: `(${TITLE_LABEL}: ${sid2}`, overlay: { badge: 'none' } }; try { await chrome.adCommand('desktop_taskbar', args2, { timeoutMs: 5000 }); } catch (e) {} } return;@@ -3667,12 +3710,12 @@ async function updateTabCountBadge(session, sessionId) { if (!icon) return; session._overlayDesc = `count:${count} (no favicon on the active tab, so the tab-count badge)`; await chrome.adCommand('desktop_taskbar', {- titleContains: `(session: ${sessionId}`,+ titleContains: `(${TITLE_LABEL}: ${sessionId}`, overlay: { icon, tooltip: `Adom Pup — ${count} tabs` }, }, { timeoutMs: 5000 }); } else { await chrome.adCommand('desktop_taskbar', {- titleContains: `(session: ${sessionId}`,+ titleContains: `(${TITLE_LABEL}: ${sessionId}`, overlay: { badge: 'none' }, }, { timeoutMs: 5000 }); }@@ -3706,10 +3749,16 @@ async function brandSweep(reason) { if (!tab || !tab.page) continue; // re-assert the title suffix so AD's resolver can find the window even after title churn try {- await tab.page.evaluate((suffix, sid2) => {- try { if (document.title.indexOf(suffix) === -1) document.title += suffix + ')'; } catch (e) {}+ await tab.page.evaluate((sfx, sid2) => {+ try {+ const esc = sid2.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');+ const re = new RegExp('\\s*\\((?:session|ai-thread): ' + esc + '(?:[^)]*)\\)', 'g');+ const base = String(document.title || '').replace(re, ''); // drop any existing tag (either label)+ const want = base + sfx + ')';+ if (document.title !== want) document.title = want; // exactly one, new label+ } catch (e) {} try { if (window.name !== 'pup:' + sid2) window.name = 'pup:' + sid2; } catch (e) {}- }, ` (session: ${sid}`, sid);+ }, ` (${TITLE_LABEL}: ${sid}`, sid); } catch (e) {} const ok = await brandPupWindow(sid); if (ok) n++;@@ -4542,7 +4591,7 @@ async function osRaiseSessionWindow(session, sessionId, reason) { // exactly: resolve THIS session's hwnd by its "(session: <id>)" title, then have AD foreground // that specific hwnd. Precise, browser-agnostic, and no PID guessing. try {- const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))}));+ const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))})); const hwnd = fw && fw.best && fw.best.hwnd; if (hwnd) { await chrome.adCommand('desktop_bring_to_front', { hwnd }, { timeoutMs: 6000 });@@ -4744,7 +4793,7 @@ async function resolveSessionHwndByBounds(session, sessionId) { // exact where it exists - resolve by title FIRST; geometry stays as the fallback for windows // that cannot carry the tag (PDF viewer). try {- const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 5000, caller: sessionCaller(session) }));+ const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 5000, caller: sessionCaller(session) })); const b = fw && (fw.best || (fw.data && fw.data.best)); if (b && b.hwnd) { session._hwnd = Number(b.hwnd); console.log(`[hwnd] "${sessionId}" resolved by TITLE tag -> ${b.hwnd}`); return Number(b.hwnd); } } catch (e) {}@@ -4909,6 +4958,37 @@ async function adoptDraggedOutTabs(sessionId) { // match reality. It NEVER kills a process (that stays in the gated reclaim path); it only corrects // pup's own bookkeeping and the taskbar identity, both cheap and idempotent. let _reconcileBusy = false;+// v1.9.284: apply an ownerChip toggle to already-open pages immediately (off removes it; on adds it),+// so the user does not have to navigate for the change to show. New navigations are handled by the+// inject-time gate in attachTab.+async function applyOwnerChipSetting(on) {+ if (process.platform !== 'win32') return;+ for (const [sid, s] of [...sessions]) {+ for (const t of (s.tabs || [])) {+ if (!t || !t.page) continue;+ try {+ if (!on) {+ await t.page.evaluate(() => { try { const el = document.documentElement.querySelector('div[data-pup-owner]'); if (el) el.remove(); } catch (e) {} });+ } else {+ const thread = String(sid).replace(/^ai-/, '');+ const prof = s.profileName || 'adom-you';+ await t.page.evaluate((thread2, profile) => {+ try {+ const root = document.documentElement; if (!root || root.querySelector('div[data-pup-owner]')) return;+ const host = document.createElement('div'); host.setAttribute('data-pup-owner', '1');+ host.style.cssText = 'position:fixed;left:8px;bottom:8px;z-index:2147483647;pointer-events:none;margin:0;padding:0;';+ const sh = host.attachShadow ? host.attachShadow({ mode: 'closed' }) : null;+ const html = '<style>.c{font:11px/1.35 "JetBrains Mono",ui-monospace,monospace;color:#7fe9e2;background:rgba(13,17,23,.72);border:1px solid rgba(0,184,177,.45);border-radius:6px;padding:2px 8px;white-space:nowrap;opacity:.9;user-select:none}.c b{color:#e6edf3;font-weight:600}</style>'+ + '<div class="c">⬢ <b>' + thread2 + '</b> · ' + profile + '</div>';+ if (sh) sh.innerHTML = html; else host.innerHTML = html;+ root.appendChild(host);+ } catch (e) {}+ }, thread, prof);+ }+ } catch (e) {}+ }+ }+} async function reconcileModel(reason) { if (process.platform !== 'win32' || _reconcileBusy) return; _reconcileBusy = true;@@ -5188,8 +5268,8 @@ async function drainWindowOpenedEvents() { // was not shotlog). Strips ANY existing pup suffix, installs the new one, and re-arms the observer. async function retagSessionTitle(page, sessionId, profileName) { const suffix = profileName && profileName !== sessionId- ? ` (session: ${sessionId} | profile: ${profileName})`- : ` (session: ${sessionId})`;+ ? ` (${TITLE_LABEL}: ${sessionId} | profile: ${profileName})`+ : ` (${TITLE_LABEL}: ${sessionId})`; try { await page.evaluate((sfx, sid) => { // v1.9.151: window.name is the DURABLE session carrier — it survives same-tab navigations@@ -5201,7 +5281,7 @@ async function retagSessionTitle(page, sessionId, profileName) { // observer had already re-appended its suffix, so a trailing-only strip missed and we stacked // two: "BMV080 (session: thr) (session: thr-split1-mkh7)". The window then still answered to // the PARENT's suffix, collided with it, and its own branding landed unreliably.- const strip = (t) => String(t || '').replace(/\s*\(session:[^)]*\)/g, '').trim();+ const strip = (t) => String(t || '').replace(/\s*\((?:session|ai-thread):[^)]*\)/g, '').trim(); try { if (window.__pupTitleObs) { window.__pupTitleObs.disconnect(); window.__pupTitleObs = null; } } catch (e) {} document.title = strip(document.title) + sfx; const obs = new MutationObserver(() => {@@ -5257,7 +5337,7 @@ async function badgePupTaskbar(sessionId) { // silently never appeared and every later attempt was debounced away. try { const r = await chrome.adCommand('desktop_taskbar', {- titleContains: `(session: ${sessionId}`,+ titleContains: `(${TITLE_LABEL}: ${sessionId}`, overlay: { icon, tooltip: 'Adom pup — driven from the cloud' }, }, { timeoutMs: 6000 }); const raw = r && (typeof r.output === 'string' ? r.output : JSON.stringify(r.output || r));@@ -5871,7 +5951,7 @@ async function parkSessionWindowDirect(page, sessionId) { if (!mine) return false; if (Number(mine.z) === 0) return false; // at least one NON-pup window above ours- return ws.some(w => Number(w.z) < Number(mine.z) && !/\(session: /.test(w.title || ''));+ return ws.some(w => Number(w.z) < Number(mine.z) && !/\((?:session|ai-thread): /.test(w.title || '')); } catch (e) { return false; } }; // hwnd via AD (native) — the title suffix "(session: <id>" is set by the welcome page.@@ -5900,7 +5980,7 @@ async function parkSessionWindowDirect(page, sessionId) { // At launch, foreground is NOT a user signal (post-restart reassignment), so pup always // forces. On AD 1.9.114 the arg is ignored (skip-on-foreground) and the wasForeground // fallback below covers it; that fallback auto-obsoletes as desktops update.- const r = await chrome.adCommand('desktop_set_window_state', { titleContains: `(session: ${sessionId}`, state: 'bottom', force: true }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))});+ const r = await chrome.adCommand('desktop_set_window_state', { titleContains: `(${TITLE_LABEL}: ${sessionId}`, state: 'bottom', force: true }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))}); if (r && r.success === false) return { ok: false, err: r.error || 'error' }; const raw = r && (typeof r.output === 'string' ? r.output : JSON.stringify(r.output || '')); const o = raw ? JSON.parse(raw) : {};@@ -5922,7 +6002,7 @@ async function parkSessionWindowDirect(page, sessionId) { if (z.wasForeground || /invalid|unknown|must be one of|maximize\|minimize/i.test(lastErr)) { let hwnd = null; try {- const r = await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))});+ const r = await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))}); const o = r && (typeof r.output === 'string' ? JSON.parse(r.output) : r.output || r); hwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null; } catch (e) {}@@ -5937,7 +6017,7 @@ async function parkSessionWindowDirect(page, sessionId) { // On-screen full-size WITHOUT activation — AD MoveWindow preserves z-order (we're at bottom). if (!parkHwnd) { try {- const r = await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))});+ const r = await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))}); const o = r && (typeof r.output === 'string' ? JSON.parse(r.output) : r.output || r); parkHwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null; } catch (e) {}@@ -6020,7 +6100,7 @@ async function reassertBottomAfterNav(sessionId) { } } catch (e) {} try {- await chrome.adCommand('desktop_set_window_state', { titleContains: `(session: ${sessionId}`, state: 'bottom', force: false }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))});+ await chrome.adCommand('desktop_set_window_state', { titleContains: `(${TITLE_LABEL}: ${sessionId}`, state: 'bottom', force: false }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))}); } catch (e) {} } @@ -6188,7 +6268,7 @@ async function flashViaAD(sessionId, opts) { // window). Never flash a bare recycled hwnd - same Edge-protection guarantee as the overlay paint. let _flashHwndOk = false; if (session && session._hwnd) { try { _flashHwndOk = await hwndBelongsToPup(session._hwnd); } catch (e) {} if (!_flashHwndOk) session._hwnd = null; }- const tgt = _flashHwndOk ? { hwnd: session._hwnd } : { titleContains: `(session: ${sessionId}` };+ const tgt = _flashHwndOk ? { hwnd: session._hwnd } : { titleContains: `(${TITLE_LABEL}: ${sessionId}` }; if (!opts.stop && session && session._hwnd) _flashedHwnds.add(session._hwnd); const args = opts.stop ? { ...tgt, stop: true } : { ...tgt, mode: 'count', count: 4 }; try { return await chrome.adCommand('desktop_flash_window', args, { timeoutMs: 6000 }); }@@ -7220,8 +7300,8 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st // Append sessionId (and profile if different) to the page title so user can identify windows try { const titleSuffix = profileName !== sessionId- ? ` (session: ${sessionId} | profile: ${profileName})`- : ` (session: ${sessionId})`;+ ? ` (${TITLE_LABEL}: ${sessionId} | profile: ${profileName})`+ : ` (${TITLE_LABEL}: ${sessionId})`; await page.evaluate((suffix, sid) => { // v1.9.43: ONE named handle for the title observer. An adopted (dragged-out) tab must be able // to disconnect the PREVIOUS session's observer, or that observer keeps re-appending the old@@ -7229,7 +7309,7 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st // v1.9.153: window.name = durable session carrier (the LAUNCH-path injector was the third, // unstamped one — why docs-writer still mis-bound after 1.9.151). try { window.name = 'pup:' + sid; } catch (e) {}- const strip = (t) => String(t || '').replace(/\s*\(session:[^)]*\)/g, '').trim();+ const strip = (t) => String(t || '').replace(/\s*\((?:session|ai-thread):[^)]*\)/g, '').trim(); try { if (window.__pupTitleObs) { window.__pupTitleObs.disconnect(); window.__pupTitleObs = null; } } catch (e) {} document.title = strip(document.title) + suffix; const obs = new MutationObserver(() => {@@ -8740,6 +8820,7 @@ const SETTINGS_DEF=[ {k:'sleepRecovery',l:'Sleep and crash recovery',d:'After the laptop wakes or Chrome dies, automatically bring every pup window back at its last page, signed in. Never restarts a live Chrome.'}, {k:'autoLogin',l:'Auto-fill saved logins',d:'Fill (never submit) a matching saved login when a login page loads in a pup window.'}, {k:'reportIssues',l:'Issue-report hints',d:'Let pup invite AIs to file a wiki issue when they hit something broken.'},+ {k:'ownerChip',l:'Floating owner chip in each window',d:'Show a tiny non-interactive chip in the bottom-left of each pup window naming the AI thread that owns it and its profile. It cannot be clicked through to (pointer-events off) and lives in an isolated layer so it never affects the page. Turn off if you do not want any overlay in your windows.'}, {k:'notMineCaption',l:'"Not foregrounded by Adom" caption',d:'Low importance. When a window comes to the front on its own (JavaScript, a focus change, Chrome, or your own click), briefly caption that Adom did NOT do it. Default on so you can see it; turn off once you trust that pup keeps windows in the background.'}, {k:'minimalTouch',l:'Minimal-touch mode',d:'The safety mode from the Chrome crash hunt. ON means pup keeps its hands off your windows: it only opens them and drives the pages inside, and skips the extra Windows machinery (identity stamps, window sweeps, shell churn) that was suspected of freezing Chrome. The toggles above are the real per-feature controls now and they win over this switch. Rule of thumb: leave this ON; if pup ever gets crashy, make sure it is ON and turn features back on one at a time to find the culprit.'}, ];@@ -8943,7 +9024,7 @@ const server = http.createServer(async (req, res) => { taskbarGrouping: ['split', 'grouped'], aumidIconStyle: ['standard', 'category'], jlViewTasks: ['on', 'off'], jlCloseAll: ['on', 'off'],- notMineCaption: ['on', 'off'],+ notMineCaption: ['on', 'off'], ownerChip: ['on', 'off'], }; const key = String(a.key || ''); let val = a.value; if (key === 'fgCaptionMs') {@@ -8957,6 +9038,7 @@ const server = http.createServer(async (req, res) => { // v1.9.279: a taskbar-identity setting must take effect on EXISTING windows immediately, // not just future ones. Reconcile now re-stamps every live window to match (idempotent). if (['aumidIcons', 'taskbarGrouping', 'aumidIconStyle'].includes(key)) reconcileModel('setting ' + key).catch(() => {});+ if (key === 'ownerChip') applyOwnerChipSetting(val === 'on').catch(() => {}); return done({ ok: true, settings: pupSettings() }); } if (act === 'fg' && sid) {@@ -9655,7 +9737,7 @@ const server = http.createServer(async (req, res) => { // force:false touches nothing but z, and respects a user-held window. if (!foreground && sized) { try {- await chrome.adCommand('desktop_set_window_state', { titleContains: `(session: ${sessionId}`, state: 'bottom', force: false }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))});+ await chrome.adCommand('desktop_set_window_state', { titleContains: `(${TITLE_LABEL}: ${sessionId}`, state: 'bottom', force: false }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))}); } catch (e) {} } // v1.8.26: record whether this window is meant to be on screen — gates every@@ -10257,7 +10339,7 @@ const server = http.createServer(async (req, res) => { let _visible = false; for (let i = 0; i < 8 && !_visible; i++) { try {- const fr = await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))});+ const fr = await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))}); const fraw = fr && (typeof fr.output === 'string' ? fr.output : JSON.stringify(fr.output || '')); const fo = fraw ? JSON.parse(fraw) : {}; _visible = !!((fo.data || fo).best);@@ -10514,7 +10596,7 @@ const server = http.createServer(async (req, res) => { id: toastId, level: 'info', scenario: 'reminder', title: 'A pup window wants your attention', body: `Session "${sid}" has something to show — click to bring it up.`,- focus: { titleContains: `(session: ${sid}` },+ focus: { titleContains: `(${TITLE_LABEL}: ${sid}` }, }, { timeoutMs: 6000 }); applied = !!(r2 && r2.success !== false); } catch (e) {}@@ -10673,7 +10755,7 @@ const server = http.createServer(async (req, res) => { // raise resolves the SAME window regardless of what its title currently says. let hwnd = await sessionHwnd(sessionId); if (!hwnd) {- const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))}));+ const fw = adPayload(await chrome.adCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 5000 , caller: sessionCaller(sessions.get(sessionId))})); hwnd = fw && fw.best && fw.best.hwnd; } if (hwnd) {@@ -11257,7 +11339,7 @@ const server = http.createServer(async (req, res) => { return; } await pupCaption(`Adom is clicking ${_label} now.`, 5000);- const shot = adPayload(await chrome.adCommand('desktop_screenshot_window', { titleContains: `(session: ${resolvedId}` }, { timeoutMs: 8000, caller: sessionCaller(session) }));+ const shot = adPayload(await chrome.adCommand('desktop_screenshot_window', { titleContains: `(${TITLE_LABEL}: ${resolvedId}` }, { timeoutMs: 8000, caller: sessionCaller(session) })); const shotId = shot && (shot.shotId || (shot.screenshots && shot.screenshots[0] && shot.screenshots[0].shotId)); if (!shotId) { sendJSON(res, { success: false, error: 'could not capture the window to aim the click' }); return; } const X = Math.round(Number(args.x)), Y = Math.round(Number(args.y));@@ -12002,10 +12084,10 @@ const server = http.createServer(async (req, res) => { const pName = session.profileName; // When there's more than one tab, include tabId in the title suffix const titleSuffix = session.tabs.length > 1- ? ` (session: ${resolvedId} | ${tabId})`+ ? ` (${TITLE_LABEL}: ${resolvedId} | ${tabId})` : (pName !== resolvedId- ? ` (session: ${resolvedId} | profile: ${pName})`- : ` (session: ${resolvedId})`);+ ? ` (${TITLE_LABEL}: ${resolvedId} | profile: ${pName})`+ : ` (${TITLE_LABEL}: ${resolvedId})`); await tab.page.evaluate((suffix, sid) => { try { window.name = 'pup:' + sid; } catch (e) {} // v1.9.153: durable carrier on tab retitles too document.title = document.title + suffix;