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
7 changes: 6 additions & 1 deletion packages/extension/src/content-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Expand Down
84 changes: 84 additions & 0 deletions packages/extension/src/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions packages/extension/src/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ <h1>Claude Blocker</h1>
</div>
</section>

<!-- Blocking Mode Section -->
<section class="mode-section">
<div class="section-header">
<div class="section-title">
<h2>Blocking Mode</h2>
<p class="section-desc">How strictly should Claude Blocker enforce focus</p>
</div>
</div>

<div class="mode-toggle">
<label class="toggle-container">
<input type="checkbox" id="always-working-toggle" />
<span class="toggle-slider"></span>
<div class="toggle-content">
<span class="toggle-label">Always Working Mode</span>
<span class="toggle-desc">Block sites unless ALL Claude Code sessions are actively working</span>
</div>
</label>
</div>
</section>

<!-- Blocked Sites Section -->
<section class="sites-section">
<div class="section-header">
Expand Down
27 changes: 27 additions & 0 deletions packages/extension/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof setInterval> | null = null;
let currentDomains: string[] = [];
Expand All @@ -48,6 +49,24 @@ async function loadDomains(): Promise<string[]> {
});
}

// Load always working setting
async function loadAlwaysWorking(): Promise<boolean> {
return new Promise((resolve) => {
chrome.storage.sync.get(["alwaysWorking"], (result) => {
resolve(result.alwaysWorking ?? false);
});
});
}

// Save always working setting
async function saveAlwaysWorking(enabled: boolean): Promise<void> {
return new Promise((resolve) => {
chrome.storage.sync.set({ alwaysWorking: enabled }, () => {
resolve();
});
});
}

// Save domains to storage
async function saveDomains(domains: string[]): Promise<void> {
return new Promise((resolve) => {
Expand Down Expand Up @@ -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") {
Expand All @@ -260,6 +283,10 @@ chrome.runtime.onMessage.addListener((message) => {
async function init(): Promise<void> {
currentDomains = await loadDomains();
renderDomains();

// Load always working setting
alwaysWorkingToggle.checked = await loadAlwaysWorking();

refreshState();
}

Expand Down
33 changes: 30 additions & 3 deletions packages/extension/src/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface State {
working: number;
waitingForInput: number;
bypassUntil: number | null;
alwaysWorking: boolean;
}

const state: State = {
Expand All @@ -20,6 +21,7 @@ const state: State = {
working: 0,
waitingForInput: 0,
bypassUntil: null,
alwaysWorking: false,
};

let websocket: WebSocket | null = null;
Expand All @@ -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,
Expand All @@ -49,6 +67,7 @@ function getPublicState() {
blocked: shouldBlock,
bypassActive,
bypassUntil: state.bypassUntil,
alwaysWorking: state.alwaysWorking,
};
}

Expand Down Expand Up @@ -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();