diff --git a/packages/extension/src/content-script.ts b/packages/extension/src/content-script.ts index 0a3bd40..c016245 100644 --- a/packages/extension/src/content-script.ts +++ b/packages/extension/src/content-script.ts @@ -202,9 +202,14 @@ function renderState(state: PublicState): void { status.textContent = "Waiting for Claude Code"; hint.textContent = "Open a terminal and start Claude Code"; } else { + const idleSessions = state.sessions - state.working; message.textContent = "Your job finished!"; setDotColor(dot, "green"); - status.textContent = `${state.sessions} session${state.sessions > 1 ? "s" : ""} idle`; + if (state.sessions === 1) { + status.textContent = "1 session idle"; + } else { + status.textContent = `${idleSessions} of ${state.sessions} sessions idle`; + } hint.textContent = "Type a prompt in Claude Code to unblock"; } } diff --git a/packages/extension/src/options.css b/packages/extension/src/options.css index 10ff207..a90c6be 100644 --- a/packages/extension/src/options.css +++ b/packages/extension/src/options.css @@ -475,6 +475,90 @@ footer p { letter-spacing: 0.3px; } +/* Mode Toggle Section */ +.mode-section { + background: var(--bg-base); +} + +.mode-toggle { + margin-top: 8px; +} + +.toggle-container { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 20px; + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.toggle-container:hover { + background: var(--bg-hover); + border-color: var(--border-default); +} + +.toggle-container input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: relative; + flex-shrink: 0; + width: 44px; + height: 24px; + background: var(--bg-elevated); + border: 1px solid var(--border-default); + border-radius: 12px; + transition: all 0.2s ease; +} + +.toggle-slider::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + background: var(--text-muted); + border-radius: 50%; + transition: all 0.2s ease; +} + +.toggle-container input:checked + .toggle-slider { + background: var(--accent-green); + border-color: var(--accent-green); +} + +.toggle-container input:checked + .toggle-slider::after { + transform: translateX(20px); + background: #fff; +} + +.toggle-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.toggle-label { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); +} + +.toggle-desc { + font-size: 13px; + color: var(--text-muted); + line-height: 1.4; +} + /* Responsive */ @media (max-width: 600px) { .container { diff --git a/packages/extension/src/options.html b/packages/extension/src/options.html index a42ca41..4eceee5 100644 --- a/packages/extension/src/options.html +++ b/packages/extension/src/options.html @@ -52,6 +52,27 @@

Claude Blocker

+ +
+
+
+

Blocking Mode

+

How strictly should Claude Blocker enforce focus

+
+
+ +
+ +
+
+
diff --git a/packages/extension/src/options.ts b/packages/extension/src/options.ts index bdbf57b..28da3c8 100644 --- a/packages/extension/src/options.ts +++ b/packages/extension/src/options.ts @@ -30,6 +30,7 @@ const siteCount = document.getElementById("site-count") as HTMLElement; const bypassBtn = document.getElementById("bypass-btn") as HTMLButtonElement; const bypassText = document.getElementById("bypass-text") as HTMLElement; const bypassStatus = document.getElementById("bypass-status") as HTMLElement; +const alwaysWorkingToggle = document.getElementById("always-working-toggle") as HTMLInputElement; let bypassCountdown: ReturnType | null = null; let currentDomains: string[] = []; @@ -48,6 +49,24 @@ async function loadDomains(): Promise { }); } +// Load always working setting +async function loadAlwaysWorking(): Promise { + return new Promise((resolve) => { + chrome.storage.sync.get(["alwaysWorking"], (result) => { + resolve(result.alwaysWorking ?? false); + }); + }); +} + +// Save always working setting +async function saveAlwaysWorking(enabled: boolean): Promise { + return new Promise((resolve) => { + chrome.storage.sync.set({ alwaysWorking: enabled }, () => { + resolve(); + }); + }); +} + // Save domains to storage async function saveDomains(domains: string[]): Promise { return new Promise((resolve) => { @@ -249,6 +268,10 @@ bypassBtn.addEventListener("click", () => { }); }); +alwaysWorkingToggle.addEventListener("change", async () => { + await saveAlwaysWorking(alwaysWorkingToggle.checked); +}); + // Listen for state broadcasts chrome.runtime.onMessage.addListener((message) => { if (message.type === "STATE") { @@ -260,6 +283,10 @@ chrome.runtime.onMessage.addListener((message) => { async function init(): Promise { currentDomains = await loadDomains(); renderDomains(); + + // Load always working setting + alwaysWorkingToggle.checked = await loadAlwaysWorking(); + refreshState(); } diff --git a/packages/extension/src/service-worker.ts b/packages/extension/src/service-worker.ts index 4a5088f..7668a07 100644 --- a/packages/extension/src/service-worker.ts +++ b/packages/extension/src/service-worker.ts @@ -12,6 +12,7 @@ interface State { working: number; waitingForInput: number; bypassUntil: number | null; + alwaysWorking: boolean; } const state: State = { @@ -20,6 +21,7 @@ const state: State = { working: 0, waitingForInput: 0, bypassUntil: null, + alwaysWorking: false, }; let websocket: WebSocket | null = null; @@ -34,12 +36,28 @@ chrome.storage.sync.get(["bypassUntil"], (result) => { } }); +// Load always working setting from storage on startup +chrome.storage.sync.get(["alwaysWorking"], (result) => { + state.alwaysWorking = result.alwaysWorking ?? false; +}); + // Compute derived state function getPublicState() { const bypassActive = state.bypassUntil !== null && state.bypassUntil > Date.now(); - // Don't block if waiting for input - only block when truly idle - const isIdle = state.working === 0 && state.waitingForInput === 0; - const shouldBlock = !bypassActive && (isIdle || !state.serverConnected); + + // Calculate blocking based on mode + let shouldBlock: boolean; + if (state.alwaysWorking) { + // Always Working mode: block unless ALL sessions are working + // Also block if no sessions exist + shouldBlock = !bypassActive && ( + state.sessions === 0 || state.working < state.sessions || !state.serverConnected + ); + } else { + // Standard mode: block only when NO session is working + const isIdle = state.working === 0 && state.waitingForInput === 0; + shouldBlock = !bypassActive && (isIdle || !state.serverConnected); + } return { serverConnected: state.serverConnected, @@ -49,6 +67,7 @@ function getPublicState() { blocked: shouldBlock, bypassActive, bypassUntil: state.bypassUntil, + alwaysWorking: state.alwaysWorking, }; } @@ -178,5 +197,13 @@ setInterval(() => { } }, 5000); +// Listen for settings changes +chrome.storage.onChanged.addListener((changes, area) => { + if (area === "sync" && changes.alwaysWorking !== undefined) { + state.alwaysWorking = changes.alwaysWorking.newValue ?? false; + broadcast(); + } +}); + // Start connect();