refector ui ux

This commit is contained in:
Douglas 2026-04-02 16:45:32 +01:00
parent 3ef1cee4e8
commit 121fbf4092
11 changed files with 1730 additions and 26 deletions

View file

@ -237,13 +237,37 @@ async function handleServerMessage(message) {
});
break;
case 'ACTION':
case 'ACTION': {
broadcastToPopup({
type: 'ACTION',
action: message.action,
detail: message.detail,
});
const actionTabId = message.tabId || getDefaultTabId();
const actionDetail = message.detail || message.action || '';
// Forward summary text to overlay
sendOverlayEvent(actionTabId, {
type: 'OVERLAY_SUMMARY',
text:
actionDetail.length > 60
? message.action || actionDetail.slice(0, 60)
: actionDetail,
});
// Extract element ref from detail (e.g. "ref=e46, text=...") and resolve via CDP
const refMatch = actionDetail.match(/ref=(e\d+)/);
if (refMatch && actionTabId) {
resolveElementRect(refMatch[1], actionTabId).then((rect) => {
if (rect) {
sendOverlayEvent(actionTabId, {
type: 'OVERLAY_CURSOR_MOVE',
x: rect.cx,
y: rect.cy,
});
}
});
}
break;
}
case 'ACTION_COMPLETE':
broadcastToPopup({
@ -251,28 +275,63 @@ async function handleServerMessage(message) {
success: message.success,
result: message.result,
});
// Notify overlay that action is done
sendOverlayEvent(message.tabId || getDefaultTabId(), {
type: 'OVERLAY_AGENT_STEP',
stepId: message.id || '',
state: message.success ? 'done' : 'error',
summary: message.success ? 'Done' : 'Action failed',
});
break;
case 'CDP_COMMAND': {
// Execute CDP command via chrome.debugger, routed by tabId
const targetTabId = message.tabId || getDefaultTabId();
try {
// Check if we should highlight before this action
// Highlight before this action — prefer overlay content script, fallback to CDP
if (message.highlight && message.highlight.selector) {
await highlightElement(
message.highlight.selector,
message.highlight.duration || 1500,
targetTabId
);
const overlayHighlighted = await sendOverlayEvent(targetTabId, {
type: 'OVERLAY_HIGHLIGHT',
selector: message.highlight.selector,
duration: message.highlight.duration || 1500,
});
if (!overlayHighlighted) {
// Fallback to CDP-injected highlight
await highlightElement(
message.highlight.selector,
message.highlight.duration || 1500,
targetTabId
);
}
if (message.highlight.summary) {
sendOverlayEvent(targetTabId, {
type: 'OVERLAY_SUMMARY',
text: message.highlight.summary,
});
}
// Small delay to let user see the highlight
await new Promise((resolve) => setTimeout(resolve, 300));
await new Promise((resolve) => setTimeout(resolve, 100));
}
const result = await executeCdpCommand(
message.method,
message.params || {},
targetTabId
);
// Extract cursor position from CDP commands that have coordinates
const params = message.params || {};
const method = message.method || '';
if (
method === 'Input.dispatchMouseEvent' &&
params.x != null &&
params.y != null
) {
if (params.type === 'mousePressed') {
sendOverlayEvent(targetTabId, {
type: 'OVERLAY_CURSOR_MOVE',
x: params.x,
y: params.y,
});
await new Promise((resolve) => setTimeout(resolve, 80));
}
}
const result = await executeCdpCommand(method, params, targetTabId);
// Send result back to server with tabId
sendToServer({
@ -357,20 +416,38 @@ async function handleServerMessage(message) {
break;
}
case 'TASK_COMPLETE':
case 'TASK_COMPLETE': {
broadcastToPopup({
type: 'TASK_COMPLETE',
result: message.result,
});
// Hide all overlay — agent session ended
const completeTabId = message.tabId || getDefaultTabId();
sendOverlayEvent(completeTabId, {
type: 'OVERLAY_STATE',
auroraVisible: false,
cursorVisible: false,
summaryText: '',
});
// Don't detach all tabs on task complete - let server manage tab lifecycle
break;
}
case 'TASK_ERROR':
case 'TASK_ERROR': {
broadcastToPopup({
type: 'TASK_ERROR',
error: message.error,
});
// Hide all overlay — agent session ended with error
const errorTabId = message.tabId || getDefaultTabId();
sendOverlayEvent(errorTabId, {
type: 'OVERLAY_STATE',
auroraVisible: false,
cursorVisible: false,
summaryText: '',
});
break;
}
case 'STREAM_TEXT':
{
@ -422,21 +499,35 @@ async function handleServerMessage(message) {
break;
case 'HIGHLIGHT': {
// Highlight an element on the page
console.log('Received HIGHLIGHT message:', message);
// Resolve element position via CDP, move cursor there, then highlight
const hlTabId = message.tabId || getDefaultTabId();
try {
const highlightResult = await highlightElement(
// Resolve element rect in page main world
const rect = await resolveElementRect(message.selector, hlTabId);
if (rect) {
// Move overlay cursor to element center
sendOverlayEvent(hlTabId, {
type: 'OVERLAY_CURSOR_MOVE',
x: rect.cx,
y: rect.cy,
});
// Show highlight rect via overlay
sendOverlayEvent(hlTabId, {
type: 'OVERLAY_HIGHLIGHT_RECT',
rect: rect,
duration: message.duration || 2000,
});
}
// Also run the existing CDP highlight (red ring)
await highlightElement(
message.selector,
message.duration || 2000,
hlTabId
);
console.log('Highlight completed:', highlightResult);
sendToServer({
type: 'HIGHLIGHT_RESULT',
id: message.id,
success: true,
result: highlightResult,
tabId: hlTabId,
});
} catch (error) {
@ -476,6 +567,78 @@ async function enableCdpDomains(tabId) {
}
}
// Resolve element ref to bounding rect via CDP (runs in page main world)
// Returns { x, y, width, height, cx, cy } or null
async function resolveElementRect(selector, tabId) {
const targetTabId = tabId || getDefaultTabId();
if (!targetTabId) return null;
const script = `
(function() {
const sel = ${JSON.stringify(selector)};
let element = null;
// Method 1: ariaSnapshot
if (typeof __ariaSnapshot !== 'undefined' && __ariaSnapshot.getElementByRef) {
try { element = __ariaSnapshot.getElementByRef(sel, document.body); } catch(e) {}
}
// Method 2: _ariaRef DOM walk
if (!element) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
let node;
while (node = walker.nextNode()) {
if (node._ariaRef && node._ariaRef.ref === sel) { element = node; break; }
}
}
// Method 3: data attributes
if (!element) {
const refNum = sel.replace(/^e/, '');
const selectors = [
'[data-ref="' + sel + '"]', '[data-ref="' + refNum + '"]',
'[ref="' + sel + '"]', '[aria-ref="' + sel + '"]',
'[data-camel-ref="' + sel + '"]', '[data-camel-ref="' + refNum + '"]'
];
for (const s of selectors) {
try { element = document.querySelector(s); if (element) break; } catch(e) {}
}
}
// Method 4: CSS selector
if (!element && (sel.includes('[') || sel.includes('.') || sel.includes('#'))) {
try { element = document.querySelector(sel); } catch(e) {}
}
if (!element) return null;
const rect = element.getBoundingClientRect();
return {
x: rect.left, y: rect.top, width: rect.width, height: rect.height,
cx: rect.left + rect.width / 2, cy: rect.top + rect.height / 2
};
})();
`;
try {
const result = await executeCdpCommand(
'Runtime.evaluate',
{
expression: script,
returnByValue: true,
},
targetTabId
);
if (result && result.result && result.result.value) {
return result.result.value;
}
} catch (e) {
console.log('resolveElementRect failed:', e.message);
}
return null;
}
// Highlight element on page with animation
async function highlightElement(selector, duration = 600, tabId = null) {
const targetTabId = tabId || getDefaultTabId();
@ -573,9 +736,9 @@ async function highlightElement(selector, duration = 600, tabId = null) {
style.id = '__agent_highlight_styles__';
style.textContent = \`
@keyframes __agent_pulse__ {
0% { box-shadow: 0 0 0 4px rgba(255, 68, 68, 1), 0 0 15px rgba(255, 68, 68, 0.7); }
50% { box-shadow: 0 0 0 6px rgba(255, 68, 68, 0.7), 0 0 25px rgba(255, 68, 68, 0.5); }
100% { box-shadow: 0 0 0 4px rgba(255, 68, 68, 1), 0 0 15px rgba(255, 68, 68, 0.7); }
0% { box-shadow: 0 0 0 4px rgba(21, 93, 252, 1), 0 0 15px rgba(21, 93, 252, 0.7); }
50% { box-shadow: 0 0 0 6px rgba(21, 93, 252, 0.7), 0 0 25px rgba(21, 93, 252, 0.5); }
100% { box-shadow: 0 0 0 4px rgba(21, 93, 252, 1), 0 0 15px rgba(21, 93, 252, 0.7); }
}
@keyframes __agent_ripple__ {
0% { transform: scale(0.8); opacity: 1; }
@ -601,9 +764,9 @@ async function highlightElement(selector, duration = 600, tabId = null) {
left: \${rect.left - 8}px;
width: \${rect.width + 16}px;
height: \${rect.height + 16}px;
border: 4px solid #ff4444;
border: 4px solid #155DFC;
border-radius: 8px;
background: rgba(255, 68, 68, 0.15);
background: rgba(21, 93, 252, 0.15);
pointer-events: none;
z-index: 2147483647;
animation: __agent_pulse__ 0.2s ease-in-out infinite;
@ -620,7 +783,7 @@ async function highlightElement(selector, duration = 600, tabId = null) {
left: \${rect.left + rect.width/2 - 25}px;
width: 50px;
height: 50px;
border: 3px solid #ff4444;
border: 3px solid #155DFC;
border-radius: 50%;
pointer-events: none;
z-index: 2147483646;
@ -841,6 +1004,19 @@ function broadcastToPopup(message) {
});
}
// Send overlay event to content script on a specific tab
// Returns true if message was delivered, false if no content script listening
async function sendOverlayEvent(tabId, event) {
if (!tabId) return false;
try {
await chrome.tabs.sendMessage(tabId, event);
return true;
} catch (e) {
// Content script not injected on this tab (restricted page, etc.)
return false;
}
}
// Listen for debugger events - forward from ALL attached tabs
chrome.debugger.onEvent.addListener((source, method, params) => {
if (attachedTabs.has(source.tabId)) {
@ -1044,6 +1220,13 @@ async function executeTask(task, tabId, url) {
message: 'Sending task to AI...',
});
// Show aurora overlay — agent session started
sendOverlayEvent(tabId, {
type: 'OVERLAY_STATE',
enabled: true,
auroraVisible: true,
});
sendToServer({
type: 'START_TASK',
task: task,

View file

@ -0,0 +1,295 @@
// Content script — Shadow DOM overlay mount + orchestrator
// Runs after all overlay/*.js modules are loaded by manifest
// Prevent double injection
if (document.getElementById('eigent-agent-overlay')) {
// already injected
} else if (
window.location.href.startsWith('chrome://') ||
window.location.href.startsWith('chrome-extension://') ||
window.location.href.startsWith('edge://') ||
window.location.href.startsWith('about:')
) {
// skip restricted pages
} else {
console.log('[Eigent Cursor] Initializing content script...');
// Verify all modules loaded
const modules = {
OverlayStore,
OverlayEvents,
OverlayMotion,
OverlayAurora,
OverlayCursor,
OverlaySummary,
OverlayHighlight,
};
for (const [name, mod] of Object.entries(modules)) {
if (!mod) {
console.error(`[Eigent Cursor] Module ${name} not loaded!`);
} else {
console.log(`[Eigent Cursor] Module ${name} OK`);
}
}
// Create overlay host — full viewport, no interaction blocking
const host = document.createElement('div');
host.id = 'eigent-agent-overlay';
host.style.cssText =
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 2147483646; pointer-events: none; overflow: visible;';
document.documentElement.appendChild(host);
// Attach Shadow DOM for style isolation
const shadow = host.attachShadow({ mode: 'open' });
// Inject all overlay styles into shadow root
const styleEl = document.createElement('style');
styleEl.textContent = [
OverlayAurora.getStyles(),
OverlayCursor.getStyles(),
OverlaySummary.getStyles(),
OverlayHighlight.getStyles(),
].join('\n');
shadow.appendChild(styleEl);
console.log(
'[Eigent Cursor] Styles injected, length:',
styleEl.textContent.length
);
// Detect reduced motion preference
const reducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
OverlayStore.update({ reducedMotion });
window
.matchMedia('(prefers-reduced-motion: reduce)')
.addEventListener('change', (e) => {
OverlayStore.update({ reducedMotion: e.matches });
});
// Initialize all overlay layers (order matters for z-stacking)
OverlayAurora.init(shadow);
OverlayHighlight.init(shadow);
OverlayCursor.init(shadow);
OverlaySummary.init(shadow);
console.log('[Eigent Cursor] All layers initialized');
console.log(
'[Eigent Cursor] Shadow root children:',
shadow.childNodes.length
);
console.log(
'[Eigent Cursor] Host element in DOM:',
!!document.getElementById('eigent-agent-overlay')
);
// Start listening for messages from background
OverlayEvents.listen();
OverlayEvents.notifyReady();
// --- DOM Target Resolution (Milestone 2) ---
const OverlayDomResolver = {
resolve(target) {
const element = OverlayHighlight.resolveElement(target);
if (!element) {
return { found: false, target };
}
const rect = element.getBoundingClientRect();
// Scroll into view if off-screen
if (
rect.top < 0 ||
rect.bottom > window.innerHeight ||
rect.left < 0 ||
rect.right > window.innerWidth
) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
// Re-measure after scroll
return new Promise((resolve) => {
setTimeout(() => {
const newRect = element.getBoundingClientRect();
resolve({
found: true,
target,
rect: {
x: newRect.left,
y: newRect.top,
width: newRect.width,
height: newRect.height,
},
center: {
x: newRect.left + newRect.width / 2,
y: newRect.top + newRect.height / 2,
},
});
}, 400);
});
}
return {
found: true,
target,
rect: {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
},
center: {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
},
};
},
};
// Expose resolver globally for events.js
window.__eigentDomResolver = OverlayDomResolver;
// --- Scripted Demo Mode ---
window.__eigentOverlayDemo = async function () {
const store = OverlayStore;
const motion = OverlayMotion;
const cursor = OverlayCursor;
const highlight = OverlayHighlight;
console.log('[Eigent Demo] Starting overlay demo...');
// Step 1: Show aurora (simulates agent session start)
store.update({ aurora: { visible: true }, enabled: true });
await sleep(800);
// Step 2: Show cursor at top-left area
store.update({ cursor: { x: 100, y: 100, visible: true, state: 'idle' } });
motion.setPosition(100, 100);
await sleep(600);
// Step 3: Look for a search input or first input on page
const searchInput =
document.querySelector('input[type="search"]') ||
document.querySelector('input[type="text"]') ||
document.querySelector('input:not([type="hidden"])') ||
document.querySelector('textarea');
if (searchInput) {
const rect = searchInput.getBoundingClientRect();
const targetX = rect.left + rect.width / 2;
const targetY = rect.top + rect.height / 2;
// Move cursor to search input
store.update({
summary: { text: 'Looking for search field\u2026', visible: true },
});
await motion.moveCursor(targetX, targetY);
await motion.settle(180);
// Highlight target
highlight.highlightSelector(
'input[type="search"], input[type="text"], input:not([type="hidden"]), textarea',
2500
);
store.update({ cursor: { state: 'hovering' } });
await sleep(800);
// Click
store.update({
summary: { text: 'Typing search query\u2026', visible: true },
});
cursor.animateClick();
await sleep(1200);
} else {
// Fallback: move to center of page
store.update({ summary: { text: 'Scanning page\u2026', visible: true } });
await motion.moveCursor(window.innerWidth / 2, window.innerHeight / 3);
await sleep(1000);
}
// Step 4: Find a button or link
const button =
document.querySelector('button:not([disabled])') ||
document.querySelector('a[href]') ||
document.querySelector('[role="button"]');
if (button) {
const rect = button.getBoundingClientRect();
const targetX = rect.left + rect.width / 2;
const targetY = rect.top + rect.height / 2;
store.update({
summary: {
text:
'Clicking ' + (button.textContent || 'button').trim().slice(0, 30),
visible: true,
},
});
await motion.moveCursor(targetX, targetY);
await motion.settle(150);
highlight.highlightSelector(button.tagName.toLowerCase(), 2000);
cursor.animateClick();
await sleep(1500);
}
// Step 5: Done
store.update({
summary: { text: 'Done', visible: true },
cursor: { state: 'complete' },
});
await sleep(2000);
// Fade out everything (simulates agent session end)
store.update({
summary: { visible: false },
cursor: { visible: false },
highlight: null,
aurora: { visible: false },
});
console.log('[Eigent Demo] Demo complete.');
};
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Listen for custom events from the MAIN world bridge (overlay/bridge.js)
document.addEventListener('eigent-cursor-demo', () => {
console.log('[Eigent Cursor] Demo event received from bridge');
if (typeof window.__eigentOverlayDemo === 'function') {
window.__eigentOverlayDemo();
}
});
document.addEventListener('eigent-cursor-cmd', (e) => {
const detail = e.detail || {};
switch (detail.cmd) {
case 'showCursor':
OverlayStore.update({
cursor: {
x: detail.x || 200,
y: detail.y || 200,
visible: true,
state: 'idle',
},
});
OverlayMotion.setPosition(detail.x || 200, detail.y || 200);
break;
case 'hideCursor':
OverlayStore.update({ cursor: { visible: false } });
break;
case 'summary':
OverlayStore.update({
summary: { text: detail.text || '', visible: !!detail.text },
});
break;
}
});
console.log('[Eigent Cursor] Content script loaded, overlay mounted.');
} // end else block

View file

@ -19,6 +19,28 @@
"action": {
"default_title": "Open ADGM Co-work Agent"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": [
"overlay/store.js",
"overlay/events.js",
"overlay/motion.js",
"overlay/aurora.js",
"overlay/cursor.js",
"overlay/summary.js",
"overlay/highlight.js",
"content.js"
],
"run_at": "document_idle"
},
{
"matches": ["<all_urls>"],
"js": ["overlay/bridge.js"],
"run_at": "document_idle",
"world": "MAIN"
}
],
"background": {
"service_worker": "background.js"
},

View file

@ -0,0 +1,144 @@
// Aurora gradient corner effect — soft ambient glow at all 4 corners
const OverlayAurora = (() => {
let container = null;
let blobs = [];
let reducedMotion = false;
// Each corner: a triangle of color that fades diagonally to transparent.
// The opposite corner of the rectangle is fully transparent.
const BLOB_CONFIGS = [
{
corner: 'top-left',
css: 'top: 0; left: 0; width: 40vw; height: 50vh;',
gradient: `linear-gradient(135deg, rgba(150,130,255,0.9) 0%, rgba(120,180,255,0.4) 25%, transparent 50%)`,
delay: '0s',
},
{
corner: 'top-right',
css: 'top: 0; right: 0; width: 40vw; height: 50vh;',
gradient: `linear-gradient(225deg, rgba(120,170,255,0.9) 0%, rgba(150,120,255,0.4) 25%, transparent 50%)`,
delay: '-2s',
},
{
corner: 'bottom-left',
css: 'bottom: 0; left: 0; width: 40vw; height: 50vh;',
gradient: `linear-gradient(45deg, rgba(120,190,255,0.9) 0%, rgba(155,130,255,0.4) 25%, transparent 50%)`,
delay: '-4s',
},
{
corner: 'bottom-right',
css: 'bottom: 0; right: 0; width: 40vw; height: 50vh;',
gradient: `linear-gradient(315deg, rgba(255,130,210,0.9) 0%, rgba(150,120,255,0.4) 25%, transparent 50%)`,
delay: '-6s',
},
];
function getStyles() {
return `
.eigent-aurora-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 2147483646;
opacity: 0;
transition: opacity 0.8s ease;
}
.eigent-aurora-container--visible {
opacity: 1;
}
.eigent-aurora-blob {
position: absolute;
will-change: opacity;
animation: eigent-aurora-glow 8s ease-in-out infinite;
}
@keyframes eigent-aurora-glow {
0%, 100% { opacity: var(--aurora-intensity); }
50% { opacity: calc(var(--aurora-intensity) * 0.5); }
}
`;
}
function init(shadowRoot) {
reducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
window
.matchMedia('(prefers-reduced-motion: reduce)')
.addEventListener('change', (e) => {
reducedMotion = e.matches;
updateAnimation();
});
container = document.createElement('div');
container.className = 'eigent-aurora-container';
const state = OverlayStore.getState();
const intensity = state.aurora.intensity || 0.7;
BLOB_CONFIGS.forEach((config) => {
const blob = document.createElement('div');
blob.className = 'eigent-aurora-blob';
blob.style.cssText = `
${config.css}
background: ${config.gradient};
--aurora-intensity: ${intensity};
animation-delay: ${config.delay};
${reducedMotion ? 'animation: none; opacity: ' + intensity + ';' : ''}
`;
blobs.push(blob);
container.appendChild(blob);
});
shadowRoot.appendChild(container);
OverlayStore.subscribe((state, changed) => {
if (changed.aurora !== undefined || changed.enabled !== undefined) {
updateVisibility();
}
});
updateVisibility();
}
function updateVisibility() {
if (!container) return;
const state = OverlayStore.getState();
const visible = state.enabled && state.aurora.visible;
container.classList.toggle('eigent-aurora-container--visible', visible);
const intensity = state.aurora.intensity || 0.7;
blobs.forEach((blob) => {
blob.style.setProperty('--aurora-intensity', String(intensity));
});
}
function updateAnimation() {
const state = OverlayStore.getState();
const intensity = state.aurora.intensity || 0.7;
blobs.forEach((blob) => {
if (reducedMotion) {
blob.style.animation = 'none';
blob.style.opacity = String(intensity);
} else {
blob.style.animation = '';
}
});
}
function destroy() {
if (container) {
container.remove();
container = null;
blobs = [];
}
}
return { init, getStyles, destroy };
})();

View file

@ -0,0 +1,35 @@
// Bridge script — runs in the MAIN world (page context)
// Exposes __eigentOverlayDemo to DevTools console by dispatching
// a custom DOM event that the content script (isolated world) listens for.
window.__eigentOverlayDemo = function () {
document.dispatchEvent(new CustomEvent('eigent-cursor-demo'));
console.log('[Eigent] Demo triggered');
};
window.__eigentOverlay = {
demo: function () {
document.dispatchEvent(new CustomEvent('eigent-cursor-demo'));
},
showCursor: function (x, y) {
document.dispatchEvent(
new CustomEvent('eigent-cursor-cmd', {
detail: { cmd: 'showCursor', x: x, y: y },
})
);
},
hideCursor: function () {
document.dispatchEvent(
new CustomEvent('eigent-cursor-cmd', {
detail: { cmd: 'hideCursor' },
})
);
},
summary: function (text) {
document.dispatchEvent(
new CustomEvent('eigent-cursor-cmd', {
detail: { cmd: 'summary', text: text },
})
);
},
};

View file

@ -0,0 +1,157 @@
// Agent cursor — custom SVG cursor with state-based animations
const OverlayCursor = (() => {
let cursorEl = null;
let shadowRoot = null;
// Custom cursor SVG from design
const CURSOR_SVG = `
<svg width="32" height="32" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_cursor)">
<g filter="url(#filter0_cursor)">
<path d="M43.1519 31.002C48.4436 28.1565 51.0894 26.7338 51.7589 25.021C52.3399 23.5348 52.1764 21.8606 51.3191 20.5147C50.3311 18.9638 47.4601 18.0794 41.7181 16.3106L20.0154 9.62537C15.1706 8.13297 12.7482 7.3868 11.1463 7.99175C9.75183 8.51835 8.66505 9.63962 8.18225 11.0498C7.62763 12.6699 8.44913 15.0678 10.0922 19.8636L17.37 41.1073C19.3909 47.0063 20.4014 49.956 22.0131 50.8935C23.4108 51.7065 25.1154 51.7935 26.5886 51.1268C28.2874 50.358 29.5926 47.5265 32.2034 41.8635L34.1321 37.6798C34.5476 36.7783 34.7554 36.3275 35.0399 35.934C35.2921 35.5845 35.5886 35.2695 35.9216 34.996C36.2971 34.688 36.7341 34.453 37.6084 33.983L43.1519 31.002Z" fill="#155DFC"/>
<path d="M43.1519 31.002C48.4436 28.1565 51.0894 26.7338 51.7589 25.021C52.3399 23.5348 52.1764 21.8606 51.3191 20.5147C50.3311 18.9638 47.4601 18.0794 41.7181 16.3106L20.0154 9.62537C15.1706 8.13297 12.7482 7.3868 11.1463 7.99175C9.75183 8.51835 8.66505 9.63962 8.18225 11.0498C7.62763 12.6699 8.44913 15.0678 10.0922 19.8636L17.37 41.1073C19.3909 47.0063 20.4014 49.956 22.0131 50.8935C23.4108 51.7065 25.1154 51.7935 26.5886 51.1268C28.2874 50.358 29.5926 47.5265 32.2034 41.8635L34.1321 37.6798C34.5476 36.7783 34.7554 36.3275 35.0399 35.934C35.2921 35.5845 35.5886 35.2695 35.9216 34.996C36.2971 34.688 36.7341 34.453 37.6084 33.983L43.1519 31.002Z" stroke="white" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
<defs>
<filter id="filter0_cursor" x="0.509766" y="2.28125" width="59.0918" height="58.7903" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.113725 0 0 0 0 0.113725 0 0 0 0 0.113725 0 0 0 0.4 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_cursor"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_cursor" result="shape"/>
</filter>
<clipPath id="clip0_cursor">
<rect width="60" height="60" fill="white"/>
</clipPath>
</defs>
</svg>
`;
function getStyles() {
return `
.eigent-cursor {
position: fixed;
top: 0;
left: 0;
width: 32px;
height: 32px;
pointer-events: none;
z-index: 2147483647;
will-change: transform, opacity;
transition: opacity 0.2s ease;
opacity: 0;
}
.eigent-cursor--visible {
opacity: 1;
}
.eigent-cursor--idle {
animation: eigent-cursor-pulse 2s ease-in-out infinite;
}
.eigent-cursor--clicking {
animation: eigent-cursor-click 0.3s ease forwards;
}
.eigent-cursor--thinking {
animation: eigent-cursor-think 1.2s linear infinite;
}
@keyframes eigent-cursor-pulse {
0%, 100% { transform: var(--cursor-translate) scale(1); }
50% { transform: var(--cursor-translate) scale(1.08); }
}
@keyframes eigent-cursor-click {
0% { transform: var(--cursor-translate) scale(1); }
40% { transform: var(--cursor-translate) scale(0.82); }
100% { transform: var(--cursor-translate) scale(1); }
}
@keyframes eigent-cursor-think {
0% { transform: var(--cursor-translate) rotate(0deg); }
25% { transform: var(--cursor-translate) rotate(6deg); }
75% { transform: var(--cursor-translate) rotate(-6deg); }
100% { transform: var(--cursor-translate) rotate(0deg); }
}
`;
}
function init(root) {
shadowRoot = root;
cursorEl = document.createElement('div');
cursorEl.className = 'eigent-cursor';
cursorEl.innerHTML = CURSOR_SVG;
shadowRoot.appendChild(cursorEl);
OverlayStore.subscribe((state, changed) => {
if (changed.cursor !== undefined || changed.enabled !== undefined) {
render();
}
});
render();
}
function render() {
if (!cursorEl) return;
const { cursor, enabled, reducedMotion } = OverlayStore.getState();
const visible = enabled && cursor.visible;
cursorEl.classList.toggle('eigent-cursor--visible', visible);
cursorEl.style.setProperty(
'--cursor-translate',
`translate3d(${cursor.x}px, ${cursor.y}px, 0)`
);
cursorEl.style.transform = `translate3d(${cursor.x}px, ${cursor.y}px, 0)`;
const states = [
'idle',
'moving',
'hovering',
'clicking',
'thinking',
'complete',
'error',
];
for (const s of states) {
cursorEl.classList.toggle(`eigent-cursor--${s}`, cursor.state === s);
}
if (reducedMotion) {
cursorEl.style.animation = 'none';
}
}
function animateClick() {
OverlayStore.update({ cursor: { state: 'clicking' } });
setTimeout(() => {
OverlayStore.update({ cursor: { state: 'idle' } });
}, 300);
}
function show() {
OverlayStore.update({ cursor: { visible: true } });
}
function hide() {
OverlayStore.update({ cursor: { visible: false } });
}
function destroy() {
if (cursorEl) {
cursorEl.remove();
cursorEl = null;
}
}
return { init, getStyles, animateClick, show, hide, destroy };
})();

View file

@ -0,0 +1,213 @@
// Event types and message bridge between background ↔ content script
const OverlayEvents = (() => {
// Message types: Background → Content
const TYPES = {
AGENT_STEP: 'OVERLAY_AGENT_STEP',
CURSOR_MOVE: 'OVERLAY_CURSOR_MOVE',
HIGHLIGHT: 'OVERLAY_HIGHLIGHT',
SUMMARY: 'OVERLAY_SUMMARY',
STATE: 'OVERLAY_STATE',
RESOLVE_TARGET: 'OVERLAY_RESOLVE_TARGET',
RESOLVE_AND_MOVE: 'OVERLAY_RESOLVE_AND_MOVE',
// Content → Background
READY: 'OVERLAY_READY',
ELEMENT_RESOLVED: 'OVERLAY_ELEMENT_RESOLVED',
};
function listen() {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || !message.type || !message.type.startsWith('OVERLAY_')) {
return;
}
switch (message.type) {
case TYPES.AGENT_STEP:
handleAgentStep(message);
break;
case 'OVERLAY_DEMO':
if (typeof window.__eigentOverlayDemo === 'function') {
window.__eigentOverlayDemo();
}
break;
case TYPES.CURSOR_MOVE:
console.log(
'[Eigent Cursor] CURSOR_MOVE received:',
message.x,
message.y
);
OverlayStore.update({
cursor: {
x: message.x,
y: message.y,
state: 'moving',
visible: true,
},
});
if (typeof OverlayMotion !== 'undefined') {
OverlayMotion.moveCursor(message.x, message.y, message.duration);
}
break;
case TYPES.HIGHLIGHT:
if (typeof OverlayHighlight !== 'undefined') {
OverlayHighlight.highlightSelector(
message.selector,
message.duration
);
}
break;
case 'OVERLAY_HIGHLIGHT_RECT':
// Highlight at a pre-resolved rect (from CDP resolution)
if (typeof OverlayHighlight !== 'undefined' && message.rect) {
OverlayHighlight.showRect(message.rect);
if (message.duration > 0) {
setTimeout(() => OverlayHighlight.hide(), message.duration);
}
}
break;
case TYPES.SUMMARY:
OverlayStore.update({
summary: { text: message.text, visible: !!message.text },
});
break;
case TYPES.STATE: {
const stateUpdate = {};
if (message.enabled !== undefined) {
stateUpdate.enabled = message.enabled;
}
if (message.auroraVisible !== undefined) {
stateUpdate.aurora = { visible: message.auroraVisible };
}
if (message.cursorVisible !== undefined) {
stateUpdate.cursor = { visible: message.cursorVisible };
}
if (message.summaryText !== undefined) {
stateUpdate.summary = {
text: message.summaryText,
visible: !!message.summaryText,
};
}
OverlayStore.update(stateUpdate);
break;
}
case TYPES.RESOLVE_AND_MOVE: {
// Resolve element by ARIA ref and move cursor to its center
const ref = message.ref;
console.log('[Eigent Cursor] RESOLVE_AND_MOVE for ref:', ref);
const el = resolveElementByRef(ref);
if (el) {
const rect = el.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
console.log('[Eigent Cursor] Resolved ref', ref, 'to', cx, cy);
OverlayStore.update({
cursor: { visible: true, state: 'moving' },
});
if (typeof OverlayMotion !== 'undefined') {
OverlayMotion.moveCursor(cx, cy);
}
} else {
console.log('[Eigent Cursor] Could not resolve ref:', ref);
}
break;
}
case TYPES.RESOLVE_TARGET:
if (typeof OverlayDomResolver !== 'undefined') {
const result = OverlayDomResolver.resolve(message.target);
sendResponse(result);
}
return true; // async response
}
});
}
function handleAgentStep(msg) {
if (msg.summary) {
OverlayStore.update({ summary: { text: msg.summary, visible: true } });
}
if (msg.cursor) {
OverlayStore.update({ cursor: { visible: true } });
if (typeof OverlayMotion !== 'undefined') {
OverlayMotion.moveCursor(msg.cursor.x, msg.cursor.y);
}
}
if (msg.highlight) {
OverlayStore.update({ highlight: msg.highlight });
}
if (msg.state === 'done' || msg.state === 'error') {
setTimeout(() => {
OverlayStore.update({
cursor: { state: msg.state === 'done' ? 'complete' : 'error' },
summary: { visible: false },
highlight: null,
});
}, 1200);
}
}
// Resolve an element by ARIA ref (e.g. "e46") — same methods as the CDP highlight system
function resolveElementByRef(ref) {
if (!ref) return null;
// Method 1: __ariaSnapshot.getElementByRef
if (
typeof __ariaSnapshot !== 'undefined' &&
__ariaSnapshot.getElementByRef
) {
try {
const el = __ariaSnapshot.getElementByRef(ref, document.body);
if (el) return el;
} catch (e) {}
}
// Method 2: Walk DOM for _ariaRef property
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT,
null,
false
);
let node;
while ((node = walker.nextNode())) {
if (node._ariaRef && node._ariaRef.ref === ref) {
return node;
}
}
// Method 3: data attributes
const refNum = ref.replace(/^e/, '');
const selectors = [
`[data-ref="${ref}"]`,
`[data-ref="${refNum}"]`,
`[ref="${ref}"]`,
`[aria-ref="${ref}"]`,
`[data-camel-ref="${ref}"]`,
`[data-camel-ref="${refNum}"]`,
];
for (const sel of selectors) {
try {
const el = document.querySelector(sel);
if (el) return el;
} catch (e) {}
}
// Method 4: Try as CSS selector directly
if (ref.includes('[') || ref.includes('.') || ref.includes('#')) {
try {
return document.querySelector(ref);
} catch (e) {}
}
return null;
}
function sendToBackground(type, data = {}) {
chrome.runtime.sendMessage({ type, ...data }).catch(() => {});
}
function notifyReady() {
sendToBackground(TYPES.READY);
}
return { TYPES, listen, sendToBackground, notifyReady };
})();

View file

@ -0,0 +1,264 @@
// Target element highlight — Atlas-style blue glow
const OverlayHighlight = (() => {
let highlightEl = null;
let shadowRoot = null;
let scrollListener = null;
let resizeListener = null;
let currentTarget = null; // DOM element being tracked
let updateFrame = null;
function getStyles() {
return `
.eigent-highlight {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
z-index: 2147483646;
border: 2px solid rgba(120, 180, 255, 0.6);
border-radius: 8px;
background: rgba(120, 180, 255, 0.08);
box-shadow: 0 0 12px rgba(120, 180, 255, 0.3), inset 0 0 8px rgba(120, 180, 255, 0.1);
will-change: transform, opacity, width, height;
opacity: 0;
transition: opacity 0.2s ease, transform 0.15s ease, width 0.15s ease, height 0.15s ease;
}
.eigent-highlight--visible {
opacity: 1;
}
.eigent-highlight--pulse {
animation: eigent-highlight-pulse 1.4s ease-in-out infinite;
}
@keyframes eigent-highlight-pulse {
0%, 100% {
box-shadow: 0 0 12px rgba(120, 180, 255, 0.3), inset 0 0 8px rgba(120, 180, 255, 0.1);
}
50% {
box-shadow: 0 0 20px rgba(120, 180, 255, 0.5), inset 0 0 12px rgba(120, 180, 255, 0.15);
}
}
`;
}
function init(root) {
shadowRoot = root;
highlightEl = document.createElement('div');
highlightEl.className = 'eigent-highlight';
shadowRoot.appendChild(highlightEl);
// Track scroll and resize for position updates
scrollListener = () => updatePosition();
resizeListener = () => updatePosition();
window.addEventListener('scroll', scrollListener, {
passive: true,
capture: true,
});
window.addEventListener('resize', resizeListener, { passive: true });
OverlayStore.subscribe((state, changed) => {
if (changed.highlight !== undefined) {
if (state.highlight) {
showRect(state.highlight);
} else {
hide();
}
}
if (changed.enabled !== undefined && !state.enabled) {
hide();
}
});
}
// Show highlight at a specific rect { x, y, width, height }
function showRect(rect) {
if (!highlightEl) return;
const padding = 6;
highlightEl.style.transform = `translate3d(${rect.x - padding}px, ${rect.y - padding}px, 0)`;
highlightEl.style.width = `${rect.width + padding * 2}px`;
highlightEl.style.height = `${rect.height + padding * 2}px`;
highlightEl.classList.add(
'eigent-highlight--visible',
'eigent-highlight--pulse'
);
}
// Highlight a DOM element by selector — resolves and tracks it
function highlightSelector(selector, duration = 2000) {
const element = resolveElement(selector);
if (!element) {
console.warn('[Eigent Cursor] Highlight target not found:', selector);
hide();
return null;
}
currentTarget = element;
updatePosition();
highlightEl.classList.add(
'eigent-highlight--visible',
'eigent-highlight--pulse'
);
// Update store with rect
const rect = element.getBoundingClientRect();
OverlayStore.update({
highlight: {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
},
});
// Auto-hide after duration
if (duration > 0) {
setTimeout(() => {
if (currentTarget === element) {
hide();
}
}, duration);
}
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
// Resolve element from various descriptor types
function resolveElement(descriptor) {
if (typeof descriptor === 'string') {
// Try CSS selector
try {
const el = document.querySelector(descriptor);
if (el && isVisible(el)) return el;
} catch (e) {}
// Try text content match
return findByText(descriptor);
}
if (typeof descriptor === 'object') {
// Try selector first
if (descriptor.selector) {
try {
const el = document.querySelector(descriptor.selector);
if (el && isVisible(el)) return el;
} catch (e) {}
}
// Try ARIA label
if (descriptor.ariaLabel) {
const el = document.querySelector(
`[aria-label="${CSS.escape(descriptor.ariaLabel)}"]`
);
if (el && isVisible(el)) return el;
}
// Try role + text
if (descriptor.role) {
const candidates = document.querySelectorAll(
`[role="${descriptor.role}"]`
);
for (const el of candidates) {
if (
descriptor.text &&
el.textContent.includes(descriptor.text) &&
isVisible(el)
) {
return el;
}
if (!descriptor.text && isVisible(el)) return el;
}
}
// Try text
if (descriptor.text) {
return findByText(descriptor.text);
}
}
return null;
}
function findByText(text) {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT
);
let node;
while ((node = walker.nextNode())) {
if (node.children.length === 0 || node.childNodes.length === 1) {
const content = (node.textContent || '').trim();
if (content === text || content.includes(text)) {
if (isVisible(node)) return node;
}
}
}
return null;
}
function isVisible(el) {
const style = getComputedStyle(el);
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0'
) {
return false;
}
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function updatePosition() {
if (!currentTarget || !highlightEl) return;
if (updateFrame) cancelAnimationFrame(updateFrame);
updateFrame = requestAnimationFrame(() => {
if (!currentTarget) return;
const rect = currentTarget.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) {
hide();
return;
}
const padding = 6;
highlightEl.style.transform = `translate3d(${rect.left - padding}px, ${rect.top - padding}px, 0)`;
highlightEl.style.width = `${rect.width + padding * 2}px`;
highlightEl.style.height = `${rect.height + padding * 2}px`;
});
}
function hide() {
if (!highlightEl) return;
highlightEl.classList.remove(
'eigent-highlight--visible',
'eigent-highlight--pulse'
);
currentTarget = null;
// Only update store if highlight isn't already null (avoid re-entrant loop)
if (OverlayStore.getState().highlight !== null) {
OverlayStore.update({ highlight: null });
}
}
function destroy() {
if (scrollListener)
window.removeEventListener('scroll', scrollListener, { capture: true });
if (resizeListener) window.removeEventListener('resize', resizeListener);
if (updateFrame) cancelAnimationFrame(updateFrame);
if (highlightEl) {
highlightEl.remove();
highlightEl = null;
}
currentTarget = null;
}
return {
init,
getStyles,
highlightSelector,
showRect,
hide,
resolveElement,
destroy,
};
})();

View file

@ -0,0 +1,147 @@
// Animation utilities — spring easing, cursor motion, tab visibility
const OverlayMotion = (() => {
let cursorAnimFrame = null;
let cursorCurrentX = 0;
let cursorCurrentY = 0;
let tabVisible = true;
// Track tab visibility to pause animations
document.addEventListener('visibilitychange', () => {
tabVisible = !document.hidden;
});
// Easing functions
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function easeOutQuart(t) {
return 1 - Math.pow(1 - t, 4);
}
// Spring approximation — overdamp for smooth settle
function springEase(t) {
const c4 = (2 * Math.PI) / 3;
return t === 0
? 0
: t === 1
? 1
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
}
// Compute duration based on distance (300ms min, 900ms max)
function durationForDistance(x1, y1, x2, y2) {
const dist = Math.hypot(x2 - x1, y2 - y1);
return Math.min(900, Math.max(300, dist * 1.2));
}
// Animate a value from → to using requestAnimationFrame
function animate({
from,
to,
duration,
easing = easeOutCubic,
onUpdate,
onComplete,
}) {
const start = performance.now();
let frame;
function tick(now) {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easing(progress);
// Interpolate
if (typeof from === 'number') {
onUpdate(from + (to - from) * easedProgress);
} else {
// Object with x, y
onUpdate({
x: from.x + (to.x - from.x) * easedProgress,
y: from.y + (to.y - from.y) * easedProgress,
});
}
if (progress < 1 && tabVisible) {
frame = requestAnimationFrame(tick);
} else if (progress >= 1) {
if (onComplete) onComplete();
}
}
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}
// Move cursor to target position with smooth animation
function moveCursor(targetX, targetY, customDuration) {
if (cursorAnimFrame) {
cancelAnimationFrame(cursorAnimFrame);
cursorAnimFrame = null;
}
const duration =
customDuration ||
durationForDistance(cursorCurrentX, cursorCurrentY, targetX, targetY);
const startX = cursorCurrentX;
const startY = cursorCurrentY;
const startTime = performance.now();
return new Promise((resolve) => {
function tick(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = easeOutQuart(progress);
cursorCurrentX = startX + (targetX - startX) * eased;
cursorCurrentY = startY + (targetY - startY) * eased;
// Update store (cursor module reads from store)
OverlayStore.update({
cursor: {
x: cursorCurrentX,
y: cursorCurrentY,
state: progress < 1 ? 'moving' : 'idle',
},
});
if (progress < 1 && tabVisible) {
cursorAnimFrame = requestAnimationFrame(tick);
} else {
cursorAnimFrame = null;
resolve();
}
}
cursorAnimFrame = requestAnimationFrame(tick);
});
}
// Settle delay — pause before click action
function settle(ms = 150) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getCurrentPosition() {
return { x: cursorCurrentX, y: cursorCurrentY };
}
function setPosition(x, y) {
cursorCurrentX = x;
cursorCurrentY = y;
}
return {
animate,
moveCursor,
settle,
easeOutCubic,
easeOutQuart,
springEase,
getCurrentPosition,
setPosition,
durationForDistance,
};
})();

View file

@ -0,0 +1,70 @@
// Overlay state store — simple pub/sub, no framework
// Shared across all overlay modules via content script injection order
const OverlayStore = (() => {
const state = {
enabled: true,
reducedMotion: false,
aurora: { visible: false, intensity: 0.7 },
cursor: { x: 0, y: 0, state: 'idle', visible: false },
summary: { text: '', visible: false },
highlight: null, // { x, y, width, height } or null
};
const listeners = new Set();
let notifying = false;
let pendingUpdate = null;
function getState() {
return state;
}
function update(partial) {
// Apply state changes immediately
for (const key of Object.keys(partial)) {
if (
typeof partial[key] === 'object' &&
partial[key] !== null &&
!Array.isArray(partial[key])
) {
state[key] = { ...state[key], ...partial[key] };
} else {
state[key] = partial[key];
}
}
// Re-entrancy guard: if a subscriber calls update(), batch it
if (notifying) {
pendingUpdate = pendingUpdate || {};
Object.assign(pendingUpdate, partial);
return;
}
notifying = true;
try {
for (const fn of listeners) {
try {
fn(state, partial);
} catch (e) {
console.error('[Eigent Cursor] Store listener error:', e);
}
}
} finally {
notifying = false;
}
// Flush any updates that were queued during notification
if (pendingUpdate) {
const queued = pendingUpdate;
pendingUpdate = null;
update(queued);
}
}
function subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
return { getState, update, subscribe };
})();

View file

@ -0,0 +1,174 @@
// Floating action summary bubble — attached near cursor
const OverlaySummary = (() => {
let bubbleEl = null;
let currentText = '';
let followTimer = null;
let bubbleX = 0;
let bubbleY = 0;
const OFFSET_X = 20; // px right of cursor
const OFFSET_Y = 28; // px below cursor
const MAX_CHARS = 64;
const FOLLOW_LAG = 0; // no lag, follow cursor immediately
function getStyles() {
return `
.eigent-summary {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
z-index: 2147483647;
will-change: transform, opacity;
max-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font: 13px/1.3 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
color: #fff;
background: #155DFC;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 9999px;
padding: 8px 18px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
opacity: 0;
transition: opacity 0.18s ease;
}
.eigent-summary--visible {
opacity: 1;
}
.eigent-summary--entering {
transform: translate3d(var(--summary-x), var(--summary-y), 0) translateY(4px);
}
.eigent-summary--active {
transform: translate3d(var(--summary-x), var(--summary-y), 0);
}
`;
}
function init(shadowRoot) {
bubbleEl = document.createElement('div');
bubbleEl.className = 'eigent-summary';
shadowRoot.appendChild(bubbleEl);
OverlayStore.subscribe((state, changed) => {
if (changed.summary !== undefined) {
updateText(state.summary);
}
if (changed.cursor !== undefined) {
scheduleFollow(state.cursor);
}
if (changed.enabled !== undefined && !state.enabled) {
hide();
}
});
}
function updateText(summary) {
if (!bubbleEl) return;
if (!summary.visible || !summary.text) {
hide();
return;
}
const text =
summary.text.length > MAX_CHARS
? summary.text.slice(0, MAX_CHARS - 1) + '\u2026'
: summary.text;
if (text !== currentText) {
// Fade out, swap, fade in
bubbleEl.classList.remove('eigent-summary--active');
bubbleEl.classList.add('eigent-summary--entering');
setTimeout(
() => {
bubbleEl.textContent = text;
currentText = text;
// Position at center-top if cursor not visible
const { cursor } = OverlayStore.getState();
if (!cursor.visible) {
positionCenterTop();
}
bubbleEl.classList.add('eigent-summary--visible');
requestAnimationFrame(() => {
bubbleEl.classList.remove('eigent-summary--entering');
bubbleEl.classList.add('eigent-summary--active');
});
},
currentText ? 120 : 0
);
}
}
function scheduleFollow(cursor) {
if (cursor.visible) {
// Cursor is active (clicking action) — follow it
setPosition(cursor.x + OFFSET_X, cursor.y + OFFSET_Y);
} else {
// No cursor (non-click action) — center top of screen, 10vh from top
positionCenterTop();
}
}
function positionCenterTop() {
if (!bubbleEl) return;
const vw = window.innerWidth;
const rect = bubbleEl.getBoundingClientRect();
const w = rect.width || 200;
const x = (vw - w) / 2;
const y = window.innerHeight * 0.1; // 10vh from top
bubbleX = x;
bubbleY = y;
bubbleEl.style.setProperty('--summary-x', `${bubbleX}px`);
bubbleEl.style.setProperty('--summary-y', `${bubbleY}px`);
bubbleEl.style.transform = `translate3d(${bubbleX}px, ${bubbleY}px, 0)`;
}
function setPosition(x, y) {
if (!bubbleEl) return;
bubbleX = x;
bubbleY = y;
// Clamp to viewport
const vw = window.innerWidth;
const vh = window.innerHeight;
const rect = bubbleEl.getBoundingClientRect();
const w = rect.width || 200;
const h = rect.height || 30;
if (bubbleX + w > vw - 8) bubbleX = vw - w - 8;
if (bubbleY + h > vh - 8) bubbleY = vh - h - 8;
if (bubbleX < 8) bubbleX = 8;
if (bubbleY < 8) bubbleY = 8;
bubbleEl.style.setProperty('--summary-x', `${bubbleX}px`);
bubbleEl.style.setProperty('--summary-y', `${bubbleY}px`);
bubbleEl.style.transform = `translate3d(${bubbleX}px, ${bubbleY}px, 0)`;
}
function hide() {
if (!bubbleEl) return;
bubbleEl.classList.remove(
'eigent-summary--visible',
'eigent-summary--active'
);
currentText = '';
}
function destroy() {
if (followTimer) clearTimeout(followTimer);
if (bubbleEl) {
bubbleEl.remove();
bubbleEl = null;
}
}
return { init, getStyles, hide, destroy };
})();