Skip to content

Commit b5ae662

Browse files
committed
Merge pull request #15 from itskolbyk/fix/token-expired-notification
2 parents 3bc9be4 + 1d5c46f commit b5ae662

9 files changed

Lines changed: 96 additions & 19 deletions

File tree

src/localization/english.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ pub(super) const STRINGS: Strings = Strings {
3434
day_suffix: "d",
3535
hour_suffix: "h",
3636
minute_suffix: "m",
37+
token_expired_title: "Claude token expired",
38+
token_expired_body: "Your session has expired. Run 'claude logout' then 'claude login' in a terminal, then restart this app.",
3739
second_suffix: "s",
3840
};

src/localization/french.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ pub(super) const STRINGS: Strings = Strings {
3434
day_suffix: "j",
3535
hour_suffix: "h",
3636
minute_suffix: "m",
37+
token_expired_title: "Claude token expired",
38+
token_expired_body: "Your session has expired. Run 'claude logout' then 'claude login' in a terminal, then restart this app.",
3739
second_suffix: "s",
3840
};

src/localization/german.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ pub(super) const STRINGS: Strings = Strings {
3434
day_suffix: "T",
3535
hour_suffix: "h",
3636
minute_suffix: "m",
37+
token_expired_title: "Claude token expired",
38+
token_expired_body: "Your session has expired. Run 'claude logout' then 'claude login' in a terminal, then restart this app.",
3739
second_suffix: "s",
3840
};

src/localization/japanese.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ pub(super) const STRINGS: Strings = Strings {
3434
day_suffix: "日",
3535
hour_suffix: "時間",
3636
minute_suffix: "分",
37+
token_expired_title: "Claude token expired",
38+
token_expired_body: "Your session has expired. Run 'claude logout' then 'claude login' in a terminal, then restart this app.",
3739
second_suffix: "秒",
3840
};

src/localization/korean.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ pub(super) const STRINGS: Strings = Strings {
3434
day_suffix: "일",
3535
hour_suffix: "시간",
3636
minute_suffix: "분",
37+
token_expired_title: "Claude token expired",
38+
token_expired_body: "Your session has expired. Run 'claude logout' then 'claude login' in a terminal, then restart this app.",
3739
second_suffix: "초",
3840
};

src/localization/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ pub struct Strings {
145145
pub hour_suffix: &'static str,
146146
pub minute_suffix: &'static str,
147147
pub second_suffix: &'static str,
148+
pub token_expired_title: &'static str,
149+
pub token_expired_body: &'static str,
148150
}
149151

150152
pub fn resolve_language(language_override: Option<LanguageId>) -> LanguageId {

src/localization/spanish.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ pub(super) const STRINGS: Strings = Strings {
3434
day_suffix: "d",
3535
hour_suffix: "h",
3636
minute_suffix: "m",
37+
token_expired_title: "Claude token expired",
38+
token_expired_body: "Your session has expired. Run 'claude logout' then 'claude login' in a terminal, then restart this app.",
3739
second_suffix: "s",
3840
};

src/tray_icon.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use windows::Win32::Foundation::*;
22
use windows::Win32::Graphics::Gdi::*;
33
use windows::Win32::System::LibraryLoader::GetModuleFileNameW;
44
use windows::Win32::UI::Shell::{
5-
ExtractIconExW, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY,
6-
NOTIFYICONDATAW, Shell_NotifyIconW,
5+
ExtractIconExW, NIF_ICON, NIF_INFO, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY,
6+
NIIF_WARNING, NOTIFYICONDATAW, Shell_NotifyIconW,
77
};
88
use windows::Win32::UI::WindowsAndMessaging::*;
99
use windows::core::PCWSTR;
@@ -249,6 +249,35 @@ fn load_embedded_app_icon() -> HICON {
249249
}
250250
}
251251

252+
/// Show a Windows balloon notification from the tray icon.
253+
/// Used to alert the user when re-authentication is required.
254+
pub fn notify_balloon(hwnd: HWND, title: &str, message: &str) {
255+
unsafe {
256+
let mut nid: NOTIFYICONDATAW = std::mem::zeroed();
257+
nid.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
258+
nid.hWnd = hwnd;
259+
nid.uID = TRAY_ICON_ID;
260+
nid.uFlags = NIF_INFO;
261+
nid.dwInfoFlags = NIIF_WARNING;
262+
copy_wide(title, &mut nid.szInfoTitle);
263+
copy_wide_256(message, &mut nid.szInfo);
264+
let _ = Shell_NotifyIconW(NIM_MODIFY, &nid);
265+
}
266+
}
267+
268+
/// Copy a string into a fixed-size wide buffer (truncates to fit).
269+
fn copy_wide<const N: usize>(s: &str, buf: &mut [u16; N]) {
270+
let wide: Vec<u16> = s.encode_utf16().collect();
271+
let len = wide.len().min(N - 1);
272+
buf[..len].copy_from_slice(&wide[..len]);
273+
buf[len] = 0;
274+
}
275+
276+
/// Copy a string into a 256-wide buffer.
277+
fn copy_wide_256(s: &str, buf: &mut [u16; 256]) {
278+
copy_wide(s, buf)
279+
}
280+
252281
/// Register the tray icon with the shell.
253282
pub fn add(hwnd: HWND, percent: Option<f64>, tooltip: &str) {
254283
let hicon = create_icon(percent);

src/window.rs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,24 +1265,58 @@ fn do_poll(send_hwnd: SendHwnd) {
12651265
let _ = PostMessageW(hwnd, WM_APP_USAGE_UPDATED, WPARAM(0), LPARAM(0));
12661266
}
12671267
}
1268-
Err(_e) => {
1269-
// Show refresh indicator — retry will recover silently
1270-
let mut state = lock_state();
1271-
if let Some(s) = state.as_mut() {
1272-
s.session_text = "...".to_string();
1273-
s.weekly_text = "...".to_string();
1274-
s.last_poll_ok = false;
1275-
1276-
// Exponential backoff retry: 30s, 60s, 120s, ... up to poll_interval
1277-
s.retry_count = s.retry_count.saturating_add(1);
1278-
let backoff = RETRY_BASE_MS
1279-
.saturating_mul(1u32.checked_shl(s.retry_count - 1).unwrap_or(u32::MAX));
1280-
let retry_ms = backoff.min(s.poll_interval_ms);
1268+
Err(e) => {
1269+
// Distinguish token expiry (needs user action) from transient errors (retry helps).
1270+
let notify_expired = {
1271+
let mut state = lock_state();
1272+
let mut should_notify = false;
1273+
if let Some(s) = state.as_mut() {
1274+
s.last_poll_ok = false;
1275+
match e {
1276+
poller::PollError::TokenExpired => {
1277+
// Only show the balloon on the first failure so it doesn't spam.
1278+
if s.retry_count == 0 {
1279+
should_notify = true;
1280+
}
1281+
s.session_text = "auth?".to_string();
1282+
s.weekly_text = "re-login".to_string();
1283+
s.retry_count = s.retry_count.saturating_add(1);
1284+
// Retry every 5 minutes — polling more often won't help until
1285+
// the user re-authenticates via 'claude logout && claude login'.
1286+
unsafe {
1287+
let _ = KillTimer(hwnd, TIMER_RESET_POLL);
1288+
SetTimer(hwnd, TIMER_POLL, 5 * 60 * 1000, None);
1289+
}
1290+
}
1291+
_ => {
1292+
// Transient network / credential-missing errors: exponential backoff.
1293+
s.session_text = "...".to_string();
1294+
s.weekly_text = "...".to_string();
1295+
s.retry_count = s.retry_count.saturating_add(1);
1296+
let backoff = RETRY_BASE_MS
1297+
.saturating_mul(1u32.checked_shl(s.retry_count - 1).unwrap_or(u32::MAX));
1298+
let retry_ms = backoff.min(s.poll_interval_ms);
1299+
unsafe {
1300+
let _ = KillTimer(hwnd, TIMER_RESET_POLL);
1301+
SetTimer(hwnd, TIMER_POLL, retry_ms, None);
1302+
}
1303+
}
1304+
}
1305+
}
1306+
should_notify
1307+
};
12811308

1282-
unsafe {
1283-
// Kill the 5-second reset poll so it doesn't bypass backoff
1284-
let _ = KillTimer(hwnd, TIMER_RESET_POLL);
1285-
SetTimer(hwnd, TIMER_POLL, retry_ms, None);
1309+
if notify_expired {
1310+
let strings = {
1311+
let state = lock_state();
1312+
state.as_ref().map(|s| s.language.strings())
1313+
};
1314+
if let Some(strings) = strings {
1315+
tray_icon::notify_balloon(
1316+
hwnd,
1317+
strings.token_expired_title,
1318+
strings.token_expired_body,
1319+
);
12861320
}
12871321
}
12881322

0 commit comments

Comments
 (0)