Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions extensions/chrome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@
"side_panel": {
"default_path": "panel.html"
},
"permissions": ["sidePanel"],
"host_permissions": ["https://*.iblai.app/*", "https://*.ibl.ai/*"],
"permissions": ["sidePanel", "identity", "scripting", "tabs"],
"host_permissions": [
"https://*.iblai.app/*",
"https://*.ibl.ai/*",
"http://localhost:3001/*",
"https://*.ngrok-free.app/*",
"http://*/*",
"https://*/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.iblai.app https://*.ibl.ai; frame-src https://*.iblai.app https://*.ibl.ai https://accounts.google.com https://appleid.apple.com; child-src https://*.iblai.app https://*.ibl.ai"
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.iblai.app https://*.ibl.ai http://localhost:3001 https://*.ngrok-free.app; frame-src https://*.iblai.app https://*.ibl.ai https://accounts.google.com https://appleid.apple.com http://localhost:3001 https://*.ngrok-free.app; child-src https://*.iblai.app https://*.ibl.ai http://localhost:3001 https://*.ngrok-free.app"
}
}
4 changes: 3 additions & 1 deletion extensions/chrome/panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}

/* The agent fills the full height of the side panel. */
/* The agent fills the entire side panel — no fixed width or overlay. The
component sizes its own (shadow-DOM) iframe to 100%, so this is all we need. */
agent-ai {
display: block;
width: 100%;
Expand Down
22 changes: 8 additions & 14 deletions extensions/chrome/panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,22 @@
<title>ibl.ai Agent</title>
</head>
<body>
<!-- Set tenant + mentor for your agent. -->
<!-- Set tenant + mentor for your agent. Sizing lives in panel.css so the
component fills the side panel (no fixed width / overlay positioning). -->
<agent-ai
style="
position: fixed;
top: 0;
right: 0;
width: 400px;
height: 100vh;
z-index: 1000;
"
mentorurl="https://mentorai.iblai.app"
mentorurl="https://78a2-3-209-13-28.ngrok-free.app"
authurl="https://login.iblai.app"
lmsurl="https://learn.iblai.app"
tenant="iblai"
mentor="744a3672-0a59-4cc9-ad5a-03ada402a658"
theme="dark"
component="chat"
redirecttoken="8b2c592b-2ddb-4768-ab3d-29cd2921cc11"
iscontextaware
authrelyonhost
></agent-ai>

<!-- Vendored from @iblai/agent-ai (self-registers <agent-ai>). -->
<script src="vendor/agent-ai.umd.js"></script>
<!-- Host-side auth: obtains the ibl.ai session token via
chrome.identity.launchWebAuthFlow and stores it for
<agent-ai authrelyonhost>. External file — MV3 forbids inline scripts. -->
<script src="panel.js"></script>
</body>
</html>
142 changes: 142 additions & 0 deletions extensions/chrome/panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Host-side authentication for the ibl.ai side-panel agent.
//
// <agent-ai authrelyonhost> reads `axd_token` / `axd_token_expires` from THIS
// page's localStorage and forwards them to the mentor iframe (no in-iframe auth
// redirect — which browsers block in extension side panels via storage
// partitioning). We obtain those tokens here with chrome.identity.launchWebAuthFlow.
//
// Must be an external script: MV3's `script-src 'self'` forbids inline scripts.

const AUTH_URL = 'https://login.iblai.app';

function isAuthed() {
return Boolean(localStorage.getItem('axd_token'));
}

// Extract ibl's session payload (a JSON `data` param — the same shape the web
// app's /mobile-sso-login consumes) and store its keys in localStorage.
function storeSessionFromRedirect(responseUrl) {
const u = new URL(responseUrl);
const data =
u.searchParams.get('data') ||
new URLSearchParams(u.hash.replace(/^#/, '')).get('data');
if (!data) {
throw new Error(`no "data" param in auth redirect: ${responseUrl}`);
}
const session = JSON.parse(data);
Object.entries(session).forEach(([key, value]) =>
localStorage.setItem(key, String(value)),
);
}

async function authenticate() {
// Chrome intercepts navigations to this URL and hands the full URL back to us.
const redirectUri = chrome.identity.getRedirectURL(); // https://<id>.chromiumapp.org/
const authUrl =
`${AUTH_URL}/login` + `?redirect-to=${encodeURIComponent(redirectUri)}`;

const responseUrl = await chrome.identity.launchWebAuthFlow({
url: authUrl,
interactive: true,
});
if (!responseUrl) throw new Error('auth flow returned no redirect URL');
storeSessionFromRedirect(responseUrl);
}

(async () => {
if (isAuthed()) return; // <agent-ai authrelyonhost> reads the stored token
try {
await authenticate();
// Re-init <agent-ai> now that the token is in localStorage.
location.reload();
} catch (err) {
console.error('[ibl.ai panel] sign-in failed:', err);
}
})();

// ---- Page context: feed the ACTIVE TAB's content to the mentor iframe --------
// The side panel runs in its own document, so <agent-ai> can only see panel.html
// (that's why `iscontextaware` is left OFF — it would otherwise flood the mentor
// with the panel's own DOM every second). Instead we read the browsed tab with
// chrome.scripting and post the mentor's own MENTOR:CONTEXT_UPDATE message
// straight into its (open) shadow-DOM iframe.

// Runs in the target tab's context.
function extractPageContent() {
const text = document.body ? document.body.innerText : '';
return {
title: document.title,
href: location.href,
text: text.slice(0, 100000),
};
}

async function readActiveTab() {
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
// Can't script the browser's own pages, the web store, or extension pages.
if (
!tab?.id ||
!tab.url ||
/^(chrome|edge|about|chrome-extension|view-source|devtools):/.test(tab.url)
) {
return null;
}
try {
const [injection] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: extractPageContent,
});
return injection?.result ?? null;
} catch (err) {
console.warn('[ibl.ai panel] cannot read tab content:', err);
return null;
}
}

function mentorIframe() {
return document
.querySelector('agent-ai')
?.shadowRoot?.querySelector('#ibl-chat-widget-container iframe');
}

let latestContext = null;

function pushContext() {
const iframe = mentorIframe();
if (!iframe?.contentWindow || !latestContext) return;
iframe.contentWindow.postMessage(
{
type: 'MENTOR:CONTEXT_UPDATE',
hostInfo: { title: latestContext.title, href: latestContext.href },
pageContent: latestContext.text,
},
'*',
);
}

async function refreshContext() {
const content = await readActiveTab();
if (content) {
latestContext = content;
pushContext();
}
}

// Refresh as the user switches tabs or a page finishes loading, plus a periodic
// re-read to catch in-page (SPA) navigations — mirrors the SDK's own cadence.
chrome.tabs.onActivated.addListener(() => refreshContext());
chrome.tabs.onUpdated.addListener((_id, info, tab) => {
if (info.status === 'complete' && tab.active) refreshContext();
});
setInterval(refreshContext, 5000);

// The mentor iframe posts a `loaded` signal to this window when ready; (re)push
// the current context immediately so it lands as soon as the chat is up.
window.addEventListener('message', (event) => {
if (event.data?.loaded || event.data?.type === 'MENTOR:READY') pushContext();
});

refreshContext();
319 changes: 183 additions & 136 deletions extensions/chrome/vendor/agent-ai.umd.js

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions scripts/check-test-coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ SKIP_COVERAGE_FILES=(
"[sessionId]/[tenantKey]/[mentorId]/page.tsx"
"[sessionId]/page.tsx"
"[sessionId]/share-chat-redirect-content.tsx"
# Third-party vendored bundle and the chrome extension service worker
# (uses chrome.* APIs) are not unit-testable.
# Third-party vendored bundle and the chrome extension scripts (service
# worker + side-panel host: chrome.* APIs, shadow-DOM postMessage glue) are
# not unit-testable in the app's vitest/jsdom environment.
"extensions/chrome/background.js"
"extensions/chrome/panel.js"
"extensions/chrome/vendor/agent-ai.umd.js"
# i18n/locale runtime glue (server-only request config, cookie helpers).
"i18n/config.ts"
Expand Down
Loading