diff --git a/assets/claude-usage-screenshot.jpg b/assets/claude-usage-screenshot.jpg
index c84e577..d49cce2 100644
Binary files a/assets/claude-usage-screenshot.jpg and b/assets/claude-usage-screenshot.jpg differ
diff --git a/main.js b/main.js
index 40380a9..e0b06f1 100644
--- a/main.js
+++ b/main.js
@@ -9,6 +9,7 @@ const store = new Store({
let mainWindow = null;
let loginWindow = null;
+let silentLoginWindow = null;
let tray = null;
// Window configuration
@@ -176,6 +177,149 @@ function createLoginWindow() {
});
}
+// Attempt silent login in a hidden browser window
+async function attemptSilentLogin() {
+ console.log('[Main] Attempting silent login...');
+
+ // Notify renderer that we're trying to auto-login
+ if (mainWindow) {
+ mainWindow.webContents.send('silent-login-started');
+ }
+
+ return new Promise((resolve) => {
+ silentLoginWindow = new BrowserWindow({
+ width: 800,
+ height: 700,
+ show: false, // Hidden window
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true
+ }
+ });
+
+ silentLoginWindow.loadURL('https://claude.ai');
+
+ let loginCheckInterval = null;
+ let hasLoggedIn = false;
+ const SILENT_LOGIN_TIMEOUT = 15000; // 15 seconds timeout
+
+ // Function to check login status
+ async function checkLoginStatus() {
+ if (hasLoggedIn || !silentLoginWindow) return;
+
+ try {
+ const cookies = await session.defaultSession.cookies.get({
+ url: 'https://claude.ai',
+ name: 'sessionKey'
+ });
+
+ if (cookies.length > 0) {
+ const sessionKey = cookies[0].value;
+ console.log('[Main] Silent login: Session key found, attempting to get org ID...');
+
+ // Fetch org ID from API
+ let orgId = null;
+ try {
+ const response = await axios.get('https://claude.ai/api/organizations', {
+ headers: {
+ 'Cookie': `sessionKey=${sessionKey}`,
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ });
+
+ if (response.data && Array.isArray(response.data) && response.data.length > 0) {
+ orgId = response.data[0].uuid || response.data[0].id;
+ console.log('[Main] Silent login: Org ID fetched from API:', orgId);
+ }
+ } catch (err) {
+ console.log('[Main] Silent login: API not ready yet:', err.message);
+ }
+
+ if (sessionKey && orgId) {
+ hasLoggedIn = true;
+ if (loginCheckInterval) {
+ clearInterval(loginCheckInterval);
+ loginCheckInterval = null;
+ }
+
+ console.log('[Main] Silent login successful!');
+ store.set('sessionKey', sessionKey);
+ store.set('organizationId', orgId);
+
+ if (mainWindow) {
+ mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId });
+ }
+
+ silentLoginWindow.close();
+ resolve(true);
+ }
+ }
+ } catch (error) {
+ console.error('[Main] Silent login check error:', error);
+ }
+ }
+
+ // Check on page load
+ silentLoginWindow.webContents.on('did-finish-load', async () => {
+ const url = silentLoginWindow.webContents.getURL();
+ console.log('[Main] Silent login page loaded:', url);
+
+ if (url.includes('claude.ai')) {
+ await checkLoginStatus();
+ }
+ });
+
+ // Also check on navigation
+ silentLoginWindow.webContents.on('did-navigate', async (event, url) => {
+ console.log('[Main] Silent login navigated to:', url);
+ if (url.includes('claude.ai')) {
+ await checkLoginStatus();
+ }
+ });
+
+ // Poll periodically
+ loginCheckInterval = setInterval(async () => {
+ if (!hasLoggedIn && silentLoginWindow) {
+ await checkLoginStatus();
+ } else if (loginCheckInterval) {
+ clearInterval(loginCheckInterval);
+ loginCheckInterval = null;
+ }
+ }, 1000);
+
+ // Timeout - if silent login doesn't work, fall back to visible login
+ setTimeout(() => {
+ if (!hasLoggedIn) {
+ console.log('[Main] Silent login timeout, falling back to visible login...');
+ if (loginCheckInterval) {
+ clearInterval(loginCheckInterval);
+ loginCheckInterval = null;
+ }
+ if (silentLoginWindow) {
+ silentLoginWindow.close();
+ }
+
+ // Notify renderer that silent login failed
+ if (mainWindow) {
+ mainWindow.webContents.send('silent-login-failed');
+ }
+
+ // Open visible login window
+ createLoginWindow();
+ resolve(false);
+ }
+ }, SILENT_LOGIN_TIMEOUT);
+
+ silentLoginWindow.on('closed', () => {
+ if (loginCheckInterval) {
+ clearInterval(loginCheckInterval);
+ loginCheckInterval = null;
+ }
+ silentLoginWindow = null;
+ });
+ });
+}
+
function createTray() {
try {
tray = new Tray(path.join(__dirname, 'assets/tray-icon.png'));
@@ -331,7 +475,18 @@ ipcMain.handle('fetch-usage-data', async () => {
if (error.response) {
console.error('[Main] Response status:', error.response.status);
if (error.response.status === 401 || error.response.status === 403) {
- throw new Error('Unauthorized');
+ // Session expired - attempt silent re-login
+ console.log('[Main] Session expired, attempting silent re-login...');
+ store.delete('sessionKey');
+ store.delete('organizationId');
+
+ // Don't clear cookies - we need them for silent login to work with OAuth
+ // The silent login will use existing Google/OAuth session if available
+
+ // Attempt silent login (will notify renderer appropriately)
+ attemptSilentLogin();
+
+ throw new Error('SessionExpired');
}
}
throw error;
diff --git a/package.json b/package.json
index f7c2040..1439796 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "claude-usage-widget",
- "version": "1.0.0",
+ "version": "1.3.0",
"description": "Desktop widget for Claude.ai usage monitoring",
"main": "main.js",
"scripts": {
@@ -47,4 +47,4 @@
"!*.md"
]
}
-}
+}
\ No newline at end of file
diff --git a/preload.js b/preload.js
index cb54db6..63ac749 100644
--- a/preload.js
+++ b/preload.js
@@ -24,6 +24,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
onRefreshUsage: (callback) => {
ipcRenderer.on('refresh-usage', () => callback());
},
+ onSessionExpired: (callback) => {
+ ipcRenderer.on('session-expired', () => callback());
+ },
+ onSilentLoginStarted: (callback) => {
+ ipcRenderer.on('silent-login-started', () => callback());
+ },
+ onSilentLoginFailed: (callback) => {
+ ipcRenderer.on('silent-login-failed', () => callback());
+ },
// API
fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'),
diff --git a/src/renderer/app.js b/src/renderer/app.js
index 1dbcb8b..7c3c51e 100644
--- a/src/renderer/app.js
+++ b/src/renderer/app.js
@@ -10,6 +10,7 @@ const elements = {
loadingContainer: document.getElementById('loadingContainer'),
loginContainer: document.getElementById('loginContainer'),
noUsageContainer: document.getElementById('noUsageContainer'),
+ autoLoginContainer: document.getElementById('autoLoginContainer'),
mainContent: document.getElementById('mainContent'),
loginBtn: document.getElementById('loginBtn'),
refreshBtn: document.getElementById('refreshBtn'),
@@ -103,6 +104,25 @@ function setupEventListeners() {
window.electronAPI.onRefreshUsage(async () => {
await fetchUsageData();
});
+
+ // Listen for session expiration events (403 errors) - only used as fallback
+ window.electronAPI.onSessionExpired(() => {
+ console.log('Session expired event received');
+ credentials = { sessionKey: null, organizationId: null };
+ showLoginRequired();
+ });
+
+ // Listen for silent login attempts
+ window.electronAPI.onSilentLoginStarted(() => {
+ console.log('Silent login started...');
+ showAutoLoginAttempt();
+ });
+
+ // Listen for silent login failures (falls back to visible login)
+ window.electronAPI.onSilentLoginFailed(() => {
+ console.log('Silent login failed, manual login required');
+ showLoginRequired();
+ });
}
// Fetch usage data from Claude API
@@ -122,9 +142,11 @@ async function fetchUsageData() {
updateUI(data);
} catch (error) {
console.error('Error fetching usage data:', error);
- if (error.message.includes('Unauthorized')) {
- await window.electronAPI.deleteCredentials();
- showLoginRequired();
+ if (error.message.includes('SessionExpired') || error.message.includes('Unauthorized')) {
+ // Session expired - silent login attempt is in progress
+ // Show auto-login UI while waiting
+ credentials = { sessionKey: null, organizationId: null };
+ showAutoLoginAttempt();
} else {
showError('Failed to fetch usage data');
}
@@ -139,7 +161,7 @@ function hasNoUsage(data) {
const weeklyResetsAt = data.seven_day?.resets_at;
return sessionUtilization === 0 && !sessionResetsAt &&
- weeklyUtilization === 0 && !weeklyResetsAt;
+ weeklyUtilization === 0 && !weeklyResetsAt;
}
// Update UI with usage data
@@ -316,6 +338,7 @@ function showLoading() {
elements.loadingContainer.style.display = 'block';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'none';
+ elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
}
@@ -323,6 +346,7 @@ function showLoginRequired() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'flex'; // Use flex to preserve centering
elements.noUsageContainer.style.display = 'none';
+ elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
stopAutoUpdate();
}
@@ -331,13 +355,24 @@ function showNoUsage() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'flex';
+ elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'none';
}
+function showAutoLoginAttempt() {
+ elements.loadingContainer.style.display = 'none';
+ elements.loginContainer.style.display = 'none';
+ elements.noUsageContainer.style.display = 'none';
+ elements.autoLoginContainer.style.display = 'flex';
+ elements.mainContent.style.display = 'none';
+ stopAutoUpdate();
+}
+
function showMainContent() {
elements.loadingContainer.style.display = 'none';
elements.loginContainer.style.display = 'none';
elements.noUsageContainer.style.display = 'none';
+ elements.autoLoginContainer.style.display = 'none';
elements.mainContent.style.display = 'block';
}
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 35557f4..9c3dc38 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -69,6 +69,17 @@
+
+
+
+
+
+
Trying to auto-login...
+
Please wait while we reconnect
+
+
+
+
@@ -114,16 +125,18 @@
×
- Disclaimer: Unofficial tool not affiliated with Anthropic. Use at your own discretion.
+ Disclaimer: Unofficial tool not affiliated with Anthropic. Use at your own
+ discretion.
-
-
-
-
-
-
+
+
+
+
+
+
If you find this useful, buy me a coffee!
diff --git a/src/renderer/styles.css b/src/renderer/styles.css
index d654c8d..1713f4e 100644
--- a/src/renderer/styles.css
+++ b/src/renderer/styles.css
@@ -230,6 +230,48 @@ body {
margin-bottom: 0;
}
+/* Auto-Login Attempt State */
+.auto-login-container {
+ padding: 0 16px;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.auto-login-content {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ text-align: left;
+ width: 100%;
+}
+
+.auto-login-content .spinner {
+ width: 32px;
+ height: 32px;
+ flex-shrink: 0;
+ margin: 0;
+}
+
+.auto-login-text-group {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.auto-login-content h3 {
+ color: #e0e0e0;
+ font-size: 14px;
+ margin-bottom: 2px;
+}
+
+.auto-login-content p {
+ color: #a0a0a0;
+ font-size: 11px;
+ margin-bottom: 0;
+}
+
/* Main Content */
.content {
padding: 20px;
@@ -530,4 +572,4 @@ body {
.timer-progress.danger {
stroke: #ef4444;
-}
+}
\ No newline at end of file