From 7aae6a6feb85bbae65a61677bf0603202ea1bf9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 23:14:44 +0000 Subject: [PATCH 1/4] Initial plan From d69f5dc272dd87292d8347d5ac1e3e83204a9451 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 23:27:40 +0000 Subject: [PATCH 2/4] feat: add warning mode reconciliation --- src/background/handlers/messages.ts | 22 +- src/background/tabController.ts | 111 +++++++- src/blocked/index.html | 1 + src/blocked/index.ts | 51 +++- src/options/index.html | 19 +- src/options/index.ts | 45 ++++ src/shared/api/session.ts | 41 +++ src/shared/api/storage.ts | 23 +- src/shared/types/messages.ts | 21 ++ src/shared/types/storage.ts | 8 + src/shared/utils/filteringEngine.ts | 30 ++- test/e2e/blocked.spec.ts | 31 +++ test/e2e/helpers.ts | 3 + test/e2e/options.spec.ts | 35 +++ test/unit/background/messages.test.ts | 49 ++++ test/unit/background/rulesProvider.test.ts | 1 + test/unit/background/tabController.test.ts | 285 +++++++++++++++++++++ test/unit/shared/filteringEngine.test.ts | 65 +++++ test/unit/shared/messages.test.ts | 2 + test/unit/shared/session.test.ts | 18 ++ test/unit/shared/storage.test.ts | 28 +- 21 files changed, 858 insertions(+), 31 deletions(-) diff --git a/src/background/handlers/messages.ts b/src/background/handlers/messages.ts index bdbda19..7131eb4 100644 --- a/src/background/handlers/messages.ts +++ b/src/background/handlers/messages.ts @@ -6,6 +6,7 @@ import { loadData } from '../../shared/api/storage'; import { isCheckUrlMessage, + isContinueActiveTabMessage, isGetBlockedPageStateMessage, isGetDataMessage, isGoBackActiveTabMessage, @@ -40,6 +41,11 @@ export function handleMessage( return true; } + if (isContinueActiveTabMessage(message)) { + void handleContinueActiveTab(sendResponse); + return true; + } + if (isGetBlockedPageStateMessage(message)) { void handleGetBlockedPageState(message.blockId, sender, sendResponse); return true; @@ -65,23 +71,27 @@ async function handleGoBackActiveTab(sendResponse: (response: unknown) => void): sendResponse({ restored: await getTabController().goBackFromActiveTab() }); } +async function handleContinueActiveTab(sendResponse: (response: unknown) => void): Promise { + sendResponse({ continued: await getTabController().continueFromActiveTab() }); +} + async function handleGetBlockedPageState( blockId: string | undefined, sender: chrome.runtime.MessageSender, sendResponse: (response: unknown) => void ): Promise { - if (blockId) { - sendResponse(await getTabController().getBlockedPageStateByBlockId(blockId)); + const senderTabId = sender.tab?.id; + if (typeof senderTabId === 'number') { + sendResponse(await getTabController().getFreshBlockedPageState(senderTabId, sender.tab?.url)); return; } - const senderTabId = sender.tab?.id; - if (typeof senderTabId !== 'number') { - sendResponse({ status: 'unavailable' }); + if (blockId) { + sendResponse(await getTabController().getBlockedPageStateByBlockId(blockId)); return; } - sendResponse(await getTabController().getFreshBlockedPageState(senderTabId, sender.tab?.url)); + sendResponse({ status: 'unavailable' }); } function isInternalSender(sender: chrome.runtime.MessageSender): boolean { diff --git a/src/background/tabController.ts b/src/background/tabController.ts index 4c5596a..09adf51 100644 --- a/src/background/tabController.ts +++ b/src/background/tabController.ts @@ -1,11 +1,14 @@ import { clearBlockedTabState, + clearWarningBypassState, getBlockedPageState, getBlockedTabState, getLastAllowedUrl, + getWarningBypassState, setBlockedPageState, setBlockedTabState, setLastAllowedUrl, + setWarningBypassState, } from '../shared/api/session'; import { getActiveTab, queryTabs, updateTabUrl } from '../shared/api/tabs'; import { getExtensionUrl } from '../shared/api/runtime'; @@ -72,6 +75,14 @@ class TabController { const decision = rules.engine.evaluate(url); if (decision.action === 'block') { + if ( + decision.blockType === 'warning' && + (await this.isWarningBypassed(tabId, url, decision)) + ) { + await this.allowTab(tabId, url, { preserveWarningBypass: true }); + return; + } + await this.blockTab(tabId, url, decision, rules.data); return; } @@ -92,13 +103,17 @@ class TabController { const rules = await this.getRules(); const decision = rules.engine.evaluate(resolvedTarget.targetUrl); - if (decision.action === 'block') { + const bypassed = + decision.action === 'block' && + decision.blockType === 'warning' && + (await this.isWarningBypassed(tabId, resolvedTarget.targetUrl, decision)); + if (decision.action === 'block' && !bypassed) { await this.ensureBlockedState(tabId, resolvedTarget.targetUrl, decision, rules.data); return false; } await updateTabUrl(tabId, resolvedTarget.targetUrl); - await this.allowTab(tabId, resolvedTarget.targetUrl); + await this.allowTab(tabId, resolvedTarget.targetUrl, { preserveWarningBypass: bypassed }); return true; } @@ -123,13 +138,9 @@ class TabController { tabId: number, blockedPageUrl?: string ): Promise { - const blockId = parseBlockedPageBlockId(blockedPageUrl); - if (blockId) { - return this.getBlockedPageStateByBlockId(blockId); - } - if (!Number.isInteger(tabId)) { - return { status: 'unavailable' }; + const blockId = parseBlockedPageBlockId(blockedPageUrl); + return blockId ? this.getBlockedPageStateByBlockId(blockId) : { status: 'unavailable' }; } const resolvedTarget = await this.resolveBlockedTarget(tabId, blockedPageUrl); @@ -200,6 +211,35 @@ class TabController { return true; } + async continueFromActiveTab(): Promise { + const activeTab = await getActiveTab(); + if (!activeTab?.id) { + return false; + } + + const resolvedTarget = await this.resolveBlockedTarget(activeTab.id, activeTab.url); + if (!resolvedTarget) { + return false; + } + + const rules = await this.getRules(); + const decision = rules.engine.evaluate(resolvedTarget.targetUrl); + if (decision.action !== 'block' || decision.blockType !== 'warning') { + return false; + } + + await Promise.all([ + setWarningBypassState(activeTab.id, { + filterId: decision.filterId, + urlKey: getWarningBypassUrlKey(resolvedTarget.targetUrl), + }), + clearBlockedTabState(activeTab.id), + setLastAllowedUrl(activeTab.id, resolvedTarget.targetUrl), + ]); + await updateTabUrl(activeTab.id, resolvedTarget.targetUrl); + return true; + } + private async reconcileTab(tabId: number, url: string): Promise { const blockedPageUrl = getExtensionUrl(PAGES.BLOCKED); if (url.startsWith(blockedPageUrl)) { @@ -222,10 +262,14 @@ class TabController { const rules = await this.getRules(); const decision = rules.engine.evaluate(resolvedTarget.targetUrl); - if (decision.action !== 'block') { + const bypassed = + decision.action === 'block' && + decision.blockType === 'warning' && + (await this.isWarningBypassed(tabId, resolvedTarget.targetUrl, decision)); + if (decision.action !== 'block' || bypassed) { if (resolvedTarget.hasSessionState) { await updateTabUrl(tabId, resolvedTarget.targetUrl); - await this.allowTab(tabId, resolvedTarget.targetUrl); + await this.allowTab(tabId, resolvedTarget.targetUrl, { preserveWarningBypass: bypassed }); } return; } @@ -251,8 +295,21 @@ class TabController { await updateTabUrl(tabId, getBlockedPageUrl(state.tabState.blockId)); } - private async allowTab(tabId: number, url: string): Promise { - await Promise.all([clearBlockedTabState(tabId), setLastAllowedUrl(tabId, url)]); + private async allowTab( + tabId: number, + url: string, + options?: { readonly preserveWarningBypass?: boolean } + ): Promise { + const operations: Promise[] = [clearBlockedTabState(tabId), setLastAllowedUrl(tabId, url)]; + + if (!options?.preserveWarningBypass) { + const warningBypass = await getWarningBypassState(tabId); + if (warningBypass && warningBypass.urlKey !== getWarningBypassUrlKey(url)) { + operations.push(clearWarningBypassState(tabId)); + } + } + + await Promise.all(operations); } private async setBlockedState( @@ -324,7 +381,10 @@ class TabController { ): Promise { const rules = currentRules ?? (await this.getRules()); const decision = rules.engine.evaluate(targetUrl); - if (decision.action !== 'block') { + if ( + decision.action !== 'block' || + (decision.blockType === 'warning' && (await this.isWarningBypassed(tabId, targetUrl, decision))) + ) { await clearBlockedTabState(tabId); return undefined; } @@ -366,6 +426,22 @@ class TabController { private async getRules(): Promise { return this.rulesProvider.loadCurrentRules(); } + + private async isWarningBypassed( + tabId: number, + targetUrl: string, + decision: Extract + ): Promise { + if (decision.blockType !== 'warning') { + return false; + } + + const warningBypass = await getWarningBypassState(tabId); + return ( + warningBypass?.filterId === decision.filterId && + warningBypass.urlKey === getWarningBypassUrlKey(targetUrl) + ); + } } function parseBlockedPageBlockId(tabUrl: string | undefined): string | null { @@ -422,6 +498,15 @@ function isSameBlockedState( ); } +function getWarningBypassUrlKey(targetUrl: string): string { + try { + const parsed = new URL(targetUrl); + return parsed.origin !== 'null' ? parsed.origin : targetUrl; + } catch { + return targetUrl; + } +} + function createFilterSnapshot( filter: Filter | undefined, fallbackFilterId: string diff --git a/src/blocked/index.html b/src/blocked/index.html index f29153b..22b8b49 100644 --- a/src/blocked/index.html +++ b/src/blocked/index.html @@ -37,6 +37,7 @@

Page Blocked

+
diff --git a/src/blocked/index.ts b/src/blocked/index.ts index 6b7162b..86fcb8e 100644 --- a/src/blocked/index.ts +++ b/src/blocked/index.ts @@ -7,6 +7,7 @@ import { openOptionsPage } from '../shared/api/runtime'; import { MessageType, type BlockedPageState, + type ContinueActiveTabResponse, type FilterMatchMode, type GetBlockedPageStateResponse, type GoBackActiveTabResponse, @@ -23,9 +24,7 @@ interface BlockedPageViewModel { * Initialize blocked page */ async function init(): Promise { - const state = await getBlockedPageState(); - renderBlockedUrl(state); - renderResponsibleFilter(state); + await renderPage(); const goBackButton = getElementByIdOrNull('go-back'); goBackButton?.addEventListener('click', () => { @@ -34,6 +33,13 @@ async function init(): Promise { }); }); + const continueButton = getElementByIdOrNull('continue'); + continueButton?.addEventListener('click', () => { + void handleContinue().catch((error: unknown) => { + console.error('Failed to continue past warning:', error); + }); + }); + // Set up options button const openOptionsButton = getElementByIdOrNull('open-options'); openOptionsButton?.addEventListener('click', () => { @@ -41,6 +47,21 @@ async function init(): Promise { console.error('Failed to open options page:', error); }); }); + + chrome.storage.onChanged.addListener((_changes, areaName) => { + if (areaName !== 'sync' && areaName !== 'session') { + return; + } + + void renderPage(); + }); +} + +async function renderPage(): Promise { + const state = await getBlockedPageState(); + renderBlockedUrl(state); + renderResponsibleFilter(state); + renderActions(state); } async function getBlockedPageState(): Promise { @@ -129,6 +150,16 @@ async function handleGoBack(): Promise { } } +async function handleContinue(): Promise { + const response = (await chrome.runtime.sendMessage({ + type: MessageType.CONTINUE_ACTIVE_TAB, + })) as ContinueActiveTabResponse; + + if (!response.continued) { + console.warn('[Teichos] No warning bypass is available for this tab.'); + } +} + function renderBlockedUrl(state: BlockedPageViewModel): void { const blockedUrlElement = getElementByIdOrNull('blocked-url'); if (blockedUrlElement) { @@ -138,7 +169,12 @@ function renderBlockedUrl(state: BlockedPageViewModel): void { function renderResponsibleFilter(state: BlockedPageViewModel): void { const detailSection = getElementByIdOrNull('responsible-filter'); - if (!detailSection || !state.state) { + if (!detailSection) { + return; + } + + if (!state.state) { + detailSection.hidden = true; return; } @@ -154,6 +190,13 @@ function renderResponsibleFilter(state: BlockedPageViewModel): void { detailSection.hidden = false; } +function renderActions(state: BlockedPageViewModel): void { + const continueButton = getElementByIdOrNull('continue'); + if (continueButton) { + continueButton.hidden = state.state?.blockType !== 'warning'; + } +} + function setText(elementId: string, value: string): void { const element = getElementByIdOrNull(elementId); if (element) { diff --git a/src/options/index.html b/src/options/index.html index db664cb..6695b09 100644 --- a/src/options/index.html +++ b/src/options/index.html @@ -134,9 +134,16 @@

Groups

- Export your current Teichos configuration, or import a previous backup to replace the - current settings. + Choose the default block behavior for new and inherited filters, or export/import + your current Teichos configuration.

+
+ + +
+
+ + +