Implement managed browser security, individual agents and observability

This commit is contained in:
Karl committed 2026-10-13 16:58:15 +02:00
1 parent ff482ebe23
commit ea448de2d4
172 files changed
+5404 -347

No files matched your search

+79 -13
View File
@@ -4,12 +4,25 @@ const preference=(key,fallback)=>{try{return JSON.parse(localStorage.getItem(`sb
const remember=(key,value)=>{try{localStorage.setItem(`sbrowser.${key}`,JSON.stringify(value));}catch{}};
const safeURL=value=>{try{const u=new URL(value);if(!['https:','http:'].includes(u.protocol)||u.username||u.password)return null;u.search='';u.hash='';return u.href;}catch{return null;}};
const metadata=list=>Array.isArray(list)?list.slice(0,100).filter(v=>v&&safeURL(v.url)&&typeof v.title==='string').map(v=>({url:safeURL(v.url),title:v.title.slice(0,160)})):[];
let bookmarks=[],readingList=metadata(preference('reading-list',[])),groups=preference('groups',[]);groups=Array.isArray(groups)?groups.filter(v=>typeof v==='string'&&v.length<=40).slice(0,20):[];
// Company artwork is signed policy data, never a remote image or executable SVG.
function renderCompany(s){
const name=s.brand?.name==='SafetyNet'?'S Browser':s.brand?.name||'S Browser',logo=typeof s.brand?.logoDataUrl==='string'&&/^data:image\/png;base64,[A-Za-z0-9+/=]+$/.test(s.brand.logoDataUrl)&&s.brand.logoDataUrl.length<=180000?s.brand.logoDataUrl:null;
document.documentElement.style.setProperty('--company-accent',/^#[0-9a-f]{6}$/i.test(s.brand?.color||'')?s.brand.color:'var(--accent)');
const setLogo=(image)=>{image.hidden=!logo;if(logo)image.src=logo;else image.removeAttribute('src');image.alt=logo?`${name} logo`:'';};
setLogo($('start-company-logo'));$('start-brand-name').textContent=name;
const monogram=document.querySelector('.brand-monogram');monogram.replaceChildren();if(logo){const image=document.createElement('img');image.className='company-logo';setLogo(image);monogram.append(image);}else monogram.textContent=name.charAt(0).toUpperCase();
for(const id of ['welcome','settings','ai','vault']){const host=$(id);if(!host)continue;let mark=host.querySelector('.company-surface-brand');if(!mark){mark=document.createElement('div');mark.className='company-surface-brand';mark.append(document.createElement('img'),document.createElement('span'));host.prepend(mark);}setLogo(mark.firstElementChild);mark.firstElementChild.className='company-logo';mark.lastElementChild.textContent=name;}
const start=s.startPage||{};$('start-title').textContent=start.title||'Your space to explore.';$('start-subtitle').textContent=start.subtitle||'';$('start-subtitle').hidden=!start.subtitle;
$('company-news').hidden=!start.newsEnabled;const list=$('company-news-items');list.replaceChildren();
if(start.newsEnabled)for(const item of (Array.isArray(start.news)?start.news:[]).slice(0,12)){let url;try{url=new URL(item.url);if(url.protocol!=='https:'||url.username||url.password)continue;}catch{continue;}if(typeof item.title!=='string')continue;const card=button('',()=>navigateTab(url.href),'company-news-card'),title=document.createElement('strong');title.textContent=item.title.slice(0,240);card.append(title);if(item.publishedAt){const date=new Date(item.publishedAt);if(Number.isFinite(date.getTime())){const time=document.createElement('time');time.dateTime=date.toISOString();time.textContent=date.toLocaleDateString();card.append(time);}}list.append(card);}
$('company-news-empty').hidden=!!list.children.length;
}
let bookmarks=[],readingList=metadata(preference('reading-list',[])),groups=[];
function currentTab(){return tabs.find(t=>t.id===activeTab)||{id:null,url:'',title:'New Tab',group:'',startPage:true};}
async function updateChrome(){
document.body.classList.toggle('sidebar-open',sidebarOpen);if($('library'))$('library').hidden=!sidebarOpen;if($('start-page'))$('start-page').hidden=!startPageOpen||overviewOpen||!lastState?.ready;if($('overview'))$('overview').hidden=!overviewOpen||!lastState?.ready;
for(const id of ['bookmark','reading-add','share-url'])if($(id))$(id).disabled=!lastState?.ready||startPageOpen||overviewOpen||!currentTab().url||(id==='bookmark'&&(!sectionEditable('library')||!lastState?.bookmarks?.allowUserAdd))||(id==='reading-add'&&!sectionEditable('library'));
const height=Math.ceil($('chrome')?.getBoundingClientRect().bottom||104),layout={height,sidebarOpen,startPageOpen:startPageOpen&&!!lastState?.ready,overviewOpen:overviewOpen&&!!lastState?.ready,toolsOpen:!!($('tools-menu')&&!$('tools-menu').hidden)||!!$('bookmark-editor')?.open},key=JSON.stringify(layout);document.documentElement.style.setProperty('--chrome-height',`${height}px`);if(window.safetynet.setChromeLayout&&key!==lastLayoutKey){lastLayoutKey=key;try{await call('setChromeLayout',layout);}catch(e){lastLayoutKey='';throw e;}}
const height=Math.ceil($('chrome')?.getBoundingClientRect().bottom||104),layout={height,sidebarOpen,startPageOpen:startPageOpen&&!!lastState?.ready,overviewOpen:overviewOpen&&!!lastState?.ready,toolsOpen:!!($('tools-menu')&&!$('tools-menu').hidden)||!!$('bookmark-editor')?.open||!!$('bookmark-manager')?.open||!!$('agent-pairing')?.open},key=JSON.stringify(layout);document.documentElement.style.setProperty('--chrome-height',`${height}px`);if(window.safetynet.setChromeLayout&&key!==lastLayoutKey){lastLayoutKey=key;try{await call('setChromeLayout',layout);}catch(e){lastLayoutKey='';throw e;}}
}
async function navigateTab(value){
const token=++navigationRevision,tab=currentTab();if(!tab.id)throw Error('No managed tab is available');pendingNavigation={token,tabId:tab.id};overviewOpen=false;
@@ -27,7 +40,7 @@ function renderTabs(){
const select=button(tab.title||'New Tab',()=>selectTab(tab.id),'tab-label'),close=button('×',()=>closeTab(tab.id),'tab-close');close.setAttribute('aria-label',`Close ${tab.title}`);select.disabled=!lastState?.ready;close.disabled=!lastState?.ready;el.dataset.tabId=tab.id;el.append(select,close);strip.append(el);}
if($('overview-grid')){$('overview-grid').replaceChildren();for(const tab of tabs){const card=button('',()=>selectTab(tab.id),'overview-card');const title=document.createElement('strong'),url=document.createElement('span');title.textContent=tab.title;url.textContent=safeURL(tab.url)||'Favorites and a fresh start';card.append(title,url);$('overview-grid').append(card);}}
}
function renderLinks(id,items){if(!$(id))return;$(id).replaceChildren();for(const item of items){const b=button('',()=>navigateTab(item.url),'favorite-card');const initial=document.createElement('span'),label=document.createElement('strong');initial.className='favorite-icon';initial.textContent=new URL(item.url).hostname.charAt(0).toUpperCase();label.textContent=item.title||new URL(item.url).hostname;if(item.managed){const badge=document.createElement('small');badge.textContent='Company';b.append(badge);}b.title=item.url;b.append(initial,label);$(id).append(b);}}
function renderLinks(id,items){if(!$(id))return;$(id).replaceChildren();for(const item of items){const b=button('',()=>navigateTab(item.url),'favorite-card');const initial=document.createElement('span'),label=document.createElement('strong');initial.className='favorite-icon';initial.textContent=new URL(item.url).hostname.charAt(0).toUpperCase();label.textContent=item.title||new URL(item.url).hostname;b.title=item.url;b.append(initial,label);const badge=document.createElement('small');badge.className='favorite-badge';badge.textContent=item.managed?'Company':'';b.append(badge);$(id).append(b);}}
let bookmarkEditingId=null;
async function openBookmarkEditor(item){
requireSection('library',true);const rules=lastState?.bookmarks;
@@ -59,9 +72,9 @@ $('bookmark-form').onsubmit=async event=>{event.preventDefault();const submit=$(
requireSection('library',true);await mutateBookmark(bookmarkEditingId?'update':'add',{id:bookmarkEditingId||crypto.randomUUID(),title:$('bookmark-title').value,url:$('bookmark-url').value,folder:$('bookmark-folder').value});$('bookmark-editor').close();
}catch(e){$('bookmark-editor-error').textContent=e.message;}finally{submit.disabled=false;}};
function renderLibrary(){
const key=JSON.stringify({bookmarks,bookmarkPermissions:lastState?.bookmarks,settings:lastState?.settings,readingList,groups,tabs,activeTab});if(key===lastLibraryKey)return;lastLibraryKey=key;
const favorites=sectionVisible('library')?bookmarks.slice(0,12):[];renderLinks('favorites-grid',favorites);renderLinks('library-favorites',favorites);renderBookmarkLibrary();renderLinks('library-reading',readingList);
if($('library-groups')){$('library-groups').replaceChildren();$('library-groups').append(button('All tabs',()=>{groupFilter='';renderTabs();},'group-item'));for(const group of groups)$('library-groups').append(button(group,()=>{groupFilter=group;renderTabs();},'group-item'));}
const key=JSON.stringify({bookmarks,bookmarkPermissions:lastState?.bookmarks,libraryAI:lastState?.libraryAI,settings:lastState?.settings,readingList,groups,tabs,activeTab});if(key===lastLibraryKey)return;lastLibraryKey=key;
const favorites=sectionVisible('library')?bookmarks.slice(0,12):[];renderLinks('favorites-grid',favorites);renderLinks('library-favorites',favorites);renderBookmarkLibrary();if($('bookmark-manager')?.open){libraryProposal=null;$('manager-proposal').hidden=true;renderBookmarkManager();}renderLinks('library-reading',readingList);
if($('library-groups')){$('library-groups').replaceChildren();$('library-groups').append(button('All tabs',()=>{groupFilter='';renderTabs();},'group-item'));for(const group of groups){const row=document.createElement('div');row.className='group-manager-row';row.append(button(group,()=>{groupFilter=group;renderTabs();},'group-item'));if(sectionEditable('library')){const rename=button('Rename',async()=>{const label=$('group-name').value.trim();if(!label||label.length>40)throw Error('Enter a new group name, up to 40 characters');for(const tab of [...tabs].filter(t=>t.group===group))show(await call('groupTab',{id:tab.id,group:label}));groupFilter=label;renderTabs();});rename.setAttribute('aria-label',`Rename group ${group}`);const remove=button('Ungroup',async()=>{for(const tab of [...tabs].filter(t=>t.group===group))show(await call('groupTab',{id:tab.id,group:''}));groupFilter='';renderTabs();});remove.setAttribute('aria-label',`Ungroup tabs in ${group}`);row.append(rename,remove);}$('library-groups').append(row);}}
if($('favorites-empty'))$('favorites-empty').hidden=!!favorites.length;
if($('library-tabs')){$('library-tabs').replaceChildren();for(const tab of tabs)$('library-tabs').append(button(tab.title,()=>selectTab(tab.id),`library-tab${tab.id===activeTab?' active':''}`));}
renderLinks('recent-list',tabs.filter(t=>t.url).slice(-5).map(t=>({url:safeURL(t.url),title:t.title})).filter(t=>t.url));
@@ -97,16 +110,17 @@ function applyManagedSettings(){
const settings=lastState?.settings,visible=!!settings?.available&&settings.visible;
$('tools-toggle').hidden=!visible;$('tools-toggle').disabled=!visible;
if(!visible){$('tools-menu').hidden=true;$('tools-toggle').setAttribute('aria-expanded','false');}
const sections={appearance:['appearance-section'],library:['bookmark','reading-add','sidebar-toggle'],ai:['ai-toggle','sidebar'],passwords:['passwords'],documentScan:['inspect'],agents:['agents']};
const sections={appearance:['appearance-section'],library:['bookmark','reading-add','sidebar-toggle','bookmark-manager-open','favorites-manage'],ai:['ai-toggle','sidebar'],passwords:['passwords'],documentScan:['inspect'],agents:['agents']};
for(const [name,ids] of Object.entries(sections))for(const id of ids){const el=$(id);if(!el)continue;el.hidden=!sectionVisible(name);if(!sectionVisible(name)||(['library','ai','passwords','documentScan','agents'].includes(name)&&!sectionEditable(name)))el.disabled=true;}
for(const id of ['theme-mode','accent-mode','tab-mode'])$(id).disabled=!sectionEditable('appearance');
for(const id of ['bookmark','reading-add','group-add','group-name'])if($(id))$(id).disabled=!sectionEditable('library')||!lastState?.ready||(id==='bookmark'&&!lastState?.bookmarks?.allowUserAdd);
$('send').disabled=!lastState?.aiEnabled||!sectionEditable('ai');$('prompt').disabled=!sectionEditable('ai');
for(const [quick,original,name,enabled] of [['quick-ai','ai-toggle','ai',lastState?.aiEnabled],['quick-passwords','passwords','passwords',lastState?.ready&&lastState?.passwordManagerEnabled!==false],['quick-inspect','inspect','documentScan',lastState?.documentScanEnabled],['quick-agents','agents','agents',lastState?.agentsEnabled]]){const el=$(quick);el.hidden=visible||!sectionVisible(name)||!enabled;el.disabled=!sectionEditable(name)||!enabled;el.setAttribute('aria-expanded',$(original).getAttribute('aria-expanded')||'false');}
$('settings-managed').hidden=!sectionVisible('appearance')||sectionEditable('appearance');
if(!sectionVisible('library')){sidebarOpen=false;$('sidebar-toggle').setAttribute('aria-expanded','false');}
if(!sectionVisible('library')){if($('bookmark-manager')?.open)$('bookmark-manager').close();sidebarOpen=false;$('sidebar-toggle').setAttribute('aria-expanded','false');}
if(!sectionEditable('ai')&&panel)closeAI().catch(()=>{});
if(!sectionEditable('passwords')&&vaultPanel)closeVault().catch(()=>{});
if($('agent-pairing')?.open&&(!lastState?.ready||!lastState?.agentsEnabled||!sectionEditable('agents')))$('agent-pairing').close();
applyAppearance();
}
@@ -118,9 +132,9 @@ async function openAI(){requireSection('ai',true);if(vaultPanel)await closeVault
function show(s){
// A delayed invoke response must not replace a newer tab switch or navigation event.
if(Number.isSafeInteger(s.tabsRevision)&&s.tabsRevision<nativeTabsRevision)return;
if(Array.isArray(s.tabs)){nativeTabsRevision=s.tabsRevision??nativeTabsRevision;tabs=s.tabs;activeTab=s.activeTabId;startPageOpen=!!currentTab().startPage;if(groupFilter&&currentTab().group!==groupFilter)groupFilter=currentTab().group;}
if(Array.isArray(s.tabs)){nativeTabsRevision=s.tabsRevision??nativeTabsRevision;tabs=s.tabs;groups=[...new Set(tabs.map(t=>t.group).filter(Boolean))];activeTab=s.activeTabId;startPageOpen=!!currentTab().startPage;if(groupFilter&&currentTab().group!==groupFilter)groupFilter=currentTab().group;}
document.body.classList.toggle('window-maximized',!!s.windowMaximized);
lastState=s;bookmarks=[...(s.bookmarks?.managed||[]).map(item=>({...item,managed:true})),...(s.bookmarks?.personal?.items||[])];if(!s.ready||!sectionEditable('library')||($('bookmark-editor').open&&!(bookmarkEditingId?s.bookmarks?.allowUserEdit:s.bookmarks?.allowUserAdd)))$('bookmark-editor').close();const brand=s.brand.name==='SafetyNet'?'S Browser':s.brand.name;$('brand').textContent=brand;document.title=brand;$('managed').textContent=s.ready?(s.loginCheckPending?'Login check required':`Managed · policy ${s.policyVersion}`):'Access paused';$('managed').parentElement.classList.toggle('paused',!s.ready);
lastState=s;renderCompany(s);bookmarks=[...(s.bookmarks?.managed||[]).map(item=>({...item,managed:true})),...(s.bookmarks?.personal?.items||[])];if(!s.ready||!sectionEditable('library')||($('bookmark-editor').open&&!(bookmarkEditingId?s.bookmarks?.allowUserEdit:s.bookmarks?.allowUserAdd)))$('bookmark-editor').close();const brand=s.brand.name==='SafetyNet'?'S Browser':s.brand.name;$('brand').textContent=brand;document.title=brand;$('managed').textContent=s.ready?(s.loginCheckPending?'Login check required':`Managed · policy ${s.policyVersion}`):'Access paused';$('managed').parentElement.classList.toggle('paused',!s.ready);
$('welcome').hidden=s.ready;$('enrollment').hidden=s.enrolled;$('message').textContent=s.message;
if(document.activeElement!==$('address'))$('address').value=startPageOpen?'':s.url||'';
$('managed').title=s.message;$('address-form').querySelector('.address-lock').src=s.url?.startsWith('https:')?'icons/lock-keyhole.svg':'icons/globe.svg';
@@ -135,7 +149,7 @@ function show(s){
if($('start-greeting'))$('start-greeting').textContent='A fresh place to begin';
if($('sidebar-toggle'))$('sidebar-toggle').disabled=!s.ready;
if($('new-tab'))$('new-tab').disabled=!s.ready;if($('tab-overview'))$('tab-overview').disabled=!s.ready;
applyManagedSettings();updateChrome().catch(()=>{});renderTabs();renderLibrary();
reconcileAgentPairing();applyManagedSettings();updateChrome().catch(()=>{});renderTabs();renderLibrary();
}
function task(fn){return async event=>{event?.preventDefault();$('error').textContent='';try{await fn();}catch(e){$('error').textContent=e.message;$('managed').textContent=e.message;}};}
function vaultTask(fn){return async event=>{event?.preventDefault();if(vaultBusy)return;vaultBusy=true;$('vault-error').textContent='';try{await fn();}catch{$('vault-error').textContent='The vault action could not be completed. Check your password and company policy.';}finally{vaultBusy=false;$('vault-master').value='';$('vault-confirm').value='';}};}
@@ -150,7 +164,43 @@ $('inspect').onclick=task(async()=>{
requireSection('documentScan',true);$('inspect').disabled=true;try{const result=await call('inspectFile');if(!result)return;await openAI();entry(`Inspection: ${result.name}\nDecision: ${result.decision}\nDetector: ${result.ruleId||'No match'}\nType: ${result.contentType}\nComplete coverage: ${result.coverage.complete}\n${result.model?`Model: ${result.model}`:''}\nAnalyzed locally. This result does not authorize a website upload.`);}finally{show(await call('state'));}
});
function entry(text){const empty=$('conversation').querySelector('.empty-chat');empty?.remove();const e=document.createElement('div');e.className='entry';e.textContent=text;$('conversation').append(e);e.scrollIntoView();}
$('agents').onclick=task(async()=>{requireSection('agents',true);const connection=await call('agentConnection');await openAI();entry(`Local agent API\nEndpoint: ${connection.address}\nEncrypted pairing file: ${connection.credentialPath}\nUse the trusted local helper. Every action follows company policy.`);});
// Pairing selects a signed central identity. It never creates a local grant or exposes tokens.
let agentPairingRevision=0,agentPairingFingerprint='';
function agentGrantFingerprint(){return JSON.stringify({mode:lastState?.agentIdentityMode,grants:approvedAgentGrants()});}
function reconcileAgentPairing(){if($('agent-pairing')?.open&&agentPairingFingerprint!==agentGrantFingerprint())$('agent-pairing').close();}
setInterval(reconcileAgentPairing,1000);
function approvedAgentGrants(){return (Array.isArray(lastState?.agentGrants)?lastState.agentGrants:[]).filter(g=>g&&typeof g.id==='string'&&typeof g.name==='string'&&Number.isSafeInteger(g.generation)&&g.generation>=1&&Date.parse(g.expiresAt)>Date.now());}
function agentPairingDialog(){
let dialog=$('agent-pairing');if(dialog)return dialog;
dialog=document.createElement('dialog');dialog.id='agent-pairing';dialog.setAttribute('aria-labelledby','agent-pairing-title');
const title=document.createElement('h2');title.id='agent-pairing-title';title.textContent='Connect a local agent';
const description=document.createElement('p');description.id='agent-pairing-description';dialog.setAttribute('aria-describedby',description.id);
const list=document.createElement('div');list.id='agent-grant-list';list.className='agent-grant-list';
const result=document.createElement('pre');result.id='agent-pairing-result';result.setAttribute('role','status');result.setAttribute('aria-live','polite');
const close=button('Close',()=>dialog.close(),'secondary');close.setAttribute('aria-label','Close agent connection');
dialog.append(title,description,list,result,close);document.body.append(dialog);
dialog.addEventListener('close',()=>{agentPairingRevision++;agentPairingFingerprint='';list.replaceChildren();result.textContent='';updateChrome().catch(()=>{});});
return dialog;
}
async function openAgentPairing(){
requireSection('agents',true);if(!lastState?.ready||!lastState?.agentsEnabled)throw Error('Local agent control is disabled by your administrator.');
const dialog=agentPairingDialog(),revision=++agentPairingRevision,mode=lastState.agentIdentityMode==='individual'?'individual':'legacy',list=$('agent-grant-list'),result=$('agent-pairing-result');list.replaceChildren();result.textContent='';
$('agent-pairing-description').textContent=mode==='individual'?'Choose an identity approved by your administrator. Each identity has its own permissions and encrypted pairing file.':'Shared legacy credential: all connected agents share the same permissions. Your administrator can require individual identities.';
const pair=async grant=>{
requireSection('agents',true);if(revision!==agentPairingRevision)return;
if(mode==='individual'&&!approvedAgentGrants().some(g=>g.id===grant.id&&g.generation===grant.generation))throw Error('This agent identity is no longer approved.');
const connection=await call('agentConnection',grant?.id);
requireSection('agents',true);if(revision!==agentPairingRevision||!dialog.open)return;
if(!lastState?.ready||!lastState?.agentsEnabled||(lastState.agentIdentityMode==='individual'?'individual':'legacy')!==mode||(grant&&!approvedAgentGrants().some(g=>g.id===grant.id&&g.generation===grant.generation)))throw Error('Agent permissions changed. Open the connection dialog again.');
result.textContent=`${grant?`Identity: ${grant.name}\n`:'Shared legacy credential\n'}Endpoint: ${connection.address}\nEncrypted pairing file: ${connection.credentialPath}\nUse the trusted local helper. Every action follows company policy.`;
};
if(mode==='individual'){
const grants=approvedAgentGrants();if(!grants.length){const empty=document.createElement('p');empty.textContent='No active agent identities are approved. Ask your administrator to publish an agent grant.';list.append(empty);}
for(const grant of grants){const choice=button('',()=>pair(grant),'agent-grant-choice'),name=document.createElement('strong'),detail=document.createElement('small');name.textContent=grant.name;detail.textContent=`Expires ${new Date(grant.expiresAt).toLocaleString()} · generation ${grant.generation}`;choice.append(name,detail);list.append(choice);}
}else list.append(button('Get shared legacy connection',()=>pair(null),'agent-grant-choice'));
agentPairingFingerprint=agentGrantFingerprint();dialog.showModal();await updateChrome();
}
$('agents').onclick=task(openAgentPairing);
$('ai-form').onsubmit=task(async()=>{const prompt=$('prompt').value;if(!prompt.trim())return;entry(`You: ${prompt}`);$('send').disabled=true;try{const result=await call('ai',prompt);entry(result.answer);$('prompt').value='';}catch(e){entry(e.message);}finally{$('send').disabled=!lastState?.aiEnabled;}});
async function refreshVault(){
const revision=++vaultRevision;const status=await call('vaultStatus');if(!vaultPanel||revision!==vaultRevision)return;const previous=vaultState;vaultState=status;
@@ -183,7 +233,7 @@ for(const action of ['minimize','maximize','close'])wire(`window-${action}`,()=>
wire('share-url',()=>call('shareURL'));
wire('bookmark',async()=>{requireSection('library',true);if(!lastState?.bookmarks?.allowUserAdd)throw Error('Adding bookmarks is disabled by your administrator');await openBookmarkEditor();});
wire('reading-add',async()=>{requireSection('library',true);await call('authorizeSetting','library');requireSection('library',true);if(!lastState?.ready)throw Error('Open an approved page first');const url=safeURL(lastState.url);if(!url)throw Error('This page cannot be saved');if(!readingList.some(b=>b.url===url)){readingList.push({url,title:String(lastState.pageTitle||new URL(url).hostname).slice(0,160)});readingList=readingList.slice(-100);remember('reading-list',readingList);renderLibrary();}});
wire('group-add',async()=>{requireSection('library',true);await call('authorizeSetting','library');requireSection('library',true);const field=$('group-name'),label=field?.value.trim();if(!label)throw Error('Enter a tab group name');if(label.length>40)throw Error('Tab group names are limited to 40 characters');if(!groups.includes(label)){if(groups.length>=20)throw Error('At most 20 tab groups are supported');groups.push(label);remember('groups',groups);}show(await call('groupTab',{id:currentTab().id,group:label}));groupFilter=label;field.value='';renderLibrary();renderTabs();});
wire('group-add',async()=>{requireSection('library',true);await call('authorizeSetting','library');requireSection('library',true);const field=$('group-name'),label=field?.value.trim();if(!label)throw Error('Enter a tab group name');if(label.length>40)throw Error('Tab group names are limited to 40 characters');if(!groups.includes(label)){if(groups.length>=20)throw Error('At most 20 tab groups are supported');groups.push(label);}show(await call('groupTab',{id:currentTab().id,group:label}));groupFilter=label;field.value='';renderLibrary();renderTabs();});
for(const [quick,original] of [['quick-ai','ai-toggle'],['quick-passwords','passwords'],['quick-inspect','inspect'],['quick-agents','agents']])wire(quick,()=>$(original).click());
wire('tools-toggle',async()=>{if(!lastState?.settings?.available||!lastState.settings.visible)throw Error('Settings are managed by your administrator.');const menu=$('tools-menu');if(menu){menu.hidden=!menu.hidden;$('tools-toggle').setAttribute('aria-expanded',String(!menu.hidden));await updateChrome();}});
for(const id of ['theme-mode','accent-mode','tab-mode'])$(id).onchange=task(async()=>{
@@ -196,3 +246,19 @@ document.addEventListener('click',event=>{const menu=$('tools-menu');if(!menu||m
window.addEventListener('keydown',event=>{if(event.ctrlKey&&event.key.toLowerCase()==='t'&&lastState?.ready){event.preventDefault();newTab().catch(()=>{});}if(event.key==='Escape'&&overviewOpen){overviewOpen=false;startPageOpen=!!currentTab().startPage;updateChrome().catch(()=>{});}});
applyAppearance();renderTabs();renderLibrary();
window.safetynet.onState(show);call('state').then(show).catch(e=>{$('error').textContent=e.message;});
let libraryProposal=null;
function renderBookmarkManager(){
const query=$('bookmark-search').value.trim().toLowerCase(),list=$('manager-list');list.replaceChildren();
const matches=bookmarks.filter(item=>[item.title,item.url,item.folder||''].some(value=>value.toLowerCase().includes(query)));
let section='';for(const item of matches){const key=`${item.managed?'Company':'Personal'} - ${item.folder||'Unfiled'}`;if(key!==section){const label=document.createElement('h3');label.className='manager-folder';label.textContent=key;list.append(label);section=key;}
const row=document.createElement('div');row.className='manager-row';row.dataset.bookmarkId=item.id;const link=button('',()=>navigateTab(item.url),'manager-link'),title=document.createElement('strong'),detail=document.createElement('small');title.textContent=item.title;detail.textContent=item.url;link.append(title,detail);row.append(link);
if(item.managed){const badge=document.createElement('span');badge.className='manager-managed';badge.textContent='Managed';row.append(badge);}else if(sectionEditable('library')){const actions=document.createElement('div');actions.className='manager-row-actions';if(lastState.bookmarks.allowUserEdit)actions.append(button('Edit / move',()=>openBookmarkEditor(item)));if(lastState.bookmarks.allowUserDelete)actions.append(button('Remove',async()=>{await mutateBookmark('delete',null,item.id);renderBookmarkManager();}));row.append(actions);}list.append(row);}
$('manager-empty').hidden=matches.length>0;$('manager-add').disabled=!sectionEditable('library')||!lastState?.bookmarks?.allowUserAdd;$('manager-ai').disabled=!sectionEditable('library')||!lastState?.libraryAI?.suggestionsEnabled||!!lastState?.libraryAI?.busy;
$('manager-policy').textContent=sectionEditable('library')?'Company bookmarks are read-only. Personal bookmarks follow company policy. AI organization shares titles and website domains with your approved model.':'Your administrator manages this library. Changes are disabled.';
}
async function openBookmarkManager(){requireSection('library');$('tools-menu').hidden=true;renderBookmarkManager();$('bookmark-manager').showModal();await updateChrome();$('bookmark-search').focus();}
for(const id of ['bookmark-manager-open','favorites-manage'])wire(id,openBookmarkManager);
$('bookmark-search').addEventListener('input',renderBookmarkManager);$('bookmark-manager-close').onclick=()=>$('bookmark-manager').close();$('bookmark-manager').addEventListener('close',()=>updateChrome().catch(()=>{}));wire('manager-add',()=>openBookmarkEditor());
wire('manager-ai',async()=>{requireSection('library',true);libraryProposal=await call('organizeLibrary');const diff=$('manager-diff');diff.replaceChildren();for(const [index,item] of (libraryProposal.plan.bookmarks||[]).entries()){const existing=bookmarks.find(b=>b.id===item.id);if(!existing||existing.managed)throw Error('Organization proposal no longer matches your library');const row=document.createElement('p');row.textContent=`${index+1}. ${existing.title}: ${existing.folder||'Unfiled'} -> ${item.folder||'Unfiled'}`;diff.append(row);}for(const item of libraryProposal.plan.tabs||[]){const existing=tabs.find(t=>t.id===item.id);if(!existing)throw Error('Organization proposal no longer matches your tabs');const row=document.createElement('p');row.textContent=`Tab ${existing.title}: ${existing.group||'Ungrouped'} -> ${item.group||'Ungrouped'}`;diff.append(row);}$('manager-proposal').hidden=false;});
wire('manager-apply',async()=>{if(!libraryProposal)throw Error('Request a fresh organization proposal');await call('applyLibraryOrganization',{proposalId:libraryProposal.proposalId,confirm:true});libraryProposal=null;$('manager-proposal').hidden=true;show(await call('state'));renderBookmarkManager();});$('manager-discard').onclick=()=>{libraryProposal=null;$('manager-proposal').hidden=true;};