Skip to content

Commit 93e1ba5

Browse files
author
Ö. Efe D.
committed
Handle Gemini-disabled 403 with clear errors and TUI warning popup
1 parent 9c9e0ae commit 93e1ba5

File tree

4 files changed

+173
-1
lines changed

4 files changed

+173
-1
lines changed

src/cloudcode/client.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ pub const ENDPOINTS: &[&str] = &[
2525
"https://cloudcode-pa.googleapis.com",
2626
];
2727

28+
const GEMINI_DISABLED_ERROR_MARKER: &str = "gemini has been disabled in this account";
29+
const GEMINI_DISABLED_WARNING: &str = "Gemini has been disabled in this Google account for a Terms of Service violation. Requests cannot continue until access is restored. Contact Google Cloud Support or email gemini-code-assist-user-feedback@google.com.";
30+
2831
/// HTTP client for Google Cloud Code API with retry logic and rate limiting.
2932
///
3033
/// Features:
@@ -597,6 +600,15 @@ fn map_http_error(status: u16, message: &str, model: Option<&str>) -> Error {
597600
size: 0,
598601
max: 10 * 1024 * 1024,
599602
}),
603+
403 if message
604+
.to_ascii_lowercase()
605+
.contains(GEMINI_DISABLED_ERROR_MARKER) =>
606+
{
607+
Error::Api(ApiError::ServerError {
608+
status,
609+
message: GEMINI_DISABLED_WARNING.to_string(),
610+
})
611+
}
600612
500..=599 => Error::Api(ApiError::ServerError {
601613
status,
602614
message: message.to_string(),
@@ -630,3 +642,34 @@ fn map_google_error(code: i32, message: &str) -> Error {
630642
}),
631643
}
632644
}
645+
646+
#[cfg(test)]
647+
mod tests {
648+
use super::*;
649+
650+
#[test]
651+
fn test_map_http_error_gemini_disabled_returns_403_warning() {
652+
let upstream_error = r#"{
653+
"error": {
654+
"code": 403,
655+
"message": "Gemini has been disabled in this account for violation of Terms of\n\t\tService. If you believe this is an error, please contact Google Cloud Support, or email\n\t\tgemini-code-assist-user-feedback@google.com.",
656+
"status": "PERMISSION_DENIED"
657+
}
658+
}"#;
659+
660+
let error = map_http_error(403, upstream_error, Some("gpt-5.3-codex"));
661+
662+
match error {
663+
Error::Api(ApiError::ServerError { status, message }) => {
664+
assert_eq!(status, 403);
665+
assert!(message.contains("Gemini has been disabled"));
666+
assert!(message.contains("Google Cloud Support"));
667+
assert!(
668+
!message.contains("\"error\":"),
669+
"message should be concise, not raw JSON"
670+
);
671+
}
672+
other => panic!("expected 403 server error warning, got {other:?}"),
673+
}
674+
}
675+
}

src/tui/app.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ use super::theme;
2323
const MIN_WIDTH: u16 = 60;
2424
/// Minimum terminal height for proper display
2525
const MIN_HEIGHT: u16 = 15;
26+
/// Marker used by Google's raw 403 payload.
27+
const GEMINI_DISABLED_MARKER_RAW: &str = "gemini has been disabled in this account";
28+
/// Marker used by AGCP's mapped 403 message.
29+
const GEMINI_DISABLED_MARKER_MAPPED: &str = "gemini has been disabled in this google account";
30+
/// Popup message shown when Gemini access is disabled on the account.
31+
const GEMINI_DISABLED_WARNING_MESSAGE: &str = "Google has disabled Gemini access for this account due to a Terms of Service violation. Requests will continue to fail until access is restored. Contact Google Cloud Support or email gemini-code-assist-user-feedback@google.com.";
2632

2733
/// Linearly interpolate between two u64 values.
2834
fn lerp_u64(from: u64, to: u64, t: f64) -> u64 {
@@ -33,6 +39,19 @@ fn lerp_u64(from: u64, to: u64, t: f64) -> u64 {
3339
}
3440
}
3541

42+
/// Detect a high-priority runtime warning from newly appended log entries.
43+
fn detect_runtime_warning_message(entries: &[super::data::LogEntry]) -> Option<&'static str> {
44+
for entry in entries.iter().rev() {
45+
let line = entry.line.to_ascii_lowercase();
46+
if line.contains(GEMINI_DISABLED_MARKER_RAW) || line.contains(GEMINI_DISABLED_MARKER_MAPPED)
47+
{
48+
return Some(GEMINI_DISABLED_WARNING_MESSAGE);
49+
}
50+
}
51+
52+
None
53+
}
54+
3655
/// Available tabs in the TUI
3756
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3857
pub enum Tab {
@@ -254,6 +273,8 @@ pub struct App {
254273
pub startup_warnings: Vec<super::widgets::StartupWarning>,
255274
/// Whether to show the startup warnings popup
256275
pub show_startup_warnings: bool,
276+
/// Runtime warning popup message for high-priority live errors
277+
pub runtime_warning_message: Option<String>,
257278
/// Receiver for background startup warnings collection
258279
startup_warnings_receiver: Option<mpsc::Receiver<Vec<super::widgets::StartupWarning>>>,
259280
/// About page: cached inner area for mouse detection
@@ -364,10 +385,12 @@ impl App {
364385
let log_path = super::data::DataProvider::get_log_path();
365386

366387
// Single pass: read last N log lines and find server start line
367-
let (logs, server_start_line) =
388+
let (mut logs, server_start_line) =
368389
super::log_reader::read_last_lines_and_start(&log_path, 500);
369390
let daemon_start_time =
370391
server_start_line.and_then(|line| super::data::parse_daemon_start_from_line(&line));
392+
let runtime_warning_message =
393+
detect_runtime_warning_message(logs.make_contiguous()).map(str::to_string);
371394

372395
let log_count = logs.len();
373396

@@ -423,6 +446,7 @@ impl App {
423446
// Startup warnings deferred -- populated by background thread
424447
startup_warnings: Vec::new(),
425448
show_startup_warnings: false,
449+
runtime_warning_message,
426450
startup_warnings_receiver: None,
427451
about_area: Rect::default(),
428452
about_link_hovered: false,
@@ -500,6 +524,9 @@ impl App {
500524
/// Refresh logs from file and update cached stats
501525
pub fn refresh_logs(&mut self) {
502526
let new_entries = self.log_tailer.read_new_lines();
527+
if let Some(message) = detect_runtime_warning_message(&new_entries) {
528+
self.runtime_warning_message = Some(message.to_string());
529+
}
503530
let new_count = new_entries.len();
504531
super::log_reader::append_entries(&mut self.logs, new_entries);
505532

@@ -1276,6 +1303,17 @@ impl App {
12761303
return;
12771304
}
12781305

1306+
// Handle runtime warning popup (blocks other input)
1307+
if self.runtime_warning_message.is_some() {
1308+
match code {
1309+
KeyCode::Enter | KeyCode::Esc => {
1310+
self.runtime_warning_message = None;
1311+
}
1312+
_ => {}
1313+
}
1314+
return;
1315+
}
1316+
12791317
// Handle account dropdown when open (blocks other Logs input)
12801318
if self.log_account_dropdown_open {
12811319
match code {
@@ -2741,6 +2779,8 @@ fn render(frame: &mut Frame, app: &mut App, elapsed: Duration) {
27412779
// Startup warnings popup (rendered on top of everything)
27422780
if app.show_startup_warnings {
27432781
super::widgets::startup_warnings::render(frame, area, &app.startup_warnings);
2782+
} else if let Some(message) = &app.runtime_warning_message {
2783+
super::widgets::runtime_warning::render(frame, area, message);
27442784
}
27452785

27462786
// Process effects
@@ -2772,3 +2812,44 @@ fn calculate_tab_areas(tabs_area: Rect) -> Vec<Rect> {
27722812

27732813
areas
27742814
}
2815+
2816+
#[cfg(test)]
2817+
mod tests {
2818+
use super::*;
2819+
2820+
#[test]
2821+
fn test_detect_runtime_warning_message_raw_gemini_disabled_log() {
2822+
let entries = vec![super::super::data::LogEntry::new(
2823+
r#"WARN Request error status=502 error=http error: HTTP 403: {"error":{"code":403,"message":"Gemini has been disabled in this account for violation of Terms of Service.","status":"PERMISSION_DENIED"}}"#.to_string(),
2824+
)];
2825+
2826+
let warning = detect_runtime_warning_message(&entries);
2827+
assert!(
2828+
warning.is_some(),
2829+
"expected runtime warning for raw Gemini-disabled 403"
2830+
);
2831+
}
2832+
2833+
#[test]
2834+
fn test_detect_runtime_warning_message_mapped_gemini_disabled_log() {
2835+
let entries = vec![super::super::data::LogEntry::new(
2836+
"WARN Request error status=403 error=server error (403): Gemini has been disabled in this Google account for a Terms of Service violation.".to_string(),
2837+
)];
2838+
2839+
let warning = detect_runtime_warning_message(&entries);
2840+
assert!(
2841+
warning.is_some(),
2842+
"expected runtime warning for mapped Gemini-disabled 403"
2843+
);
2844+
}
2845+
2846+
#[test]
2847+
fn test_detect_runtime_warning_message_ignores_other_errors() {
2848+
let entries = vec![super::super::data::LogEntry::new(
2849+
"WARN Request error status=429 error=rate limited".to_string(),
2850+
)];
2851+
2852+
let warning = detect_runtime_warning_message(&entries);
2853+
assert!(warning.is_none());
2854+
}
2855+
}

src/tui/widgets/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod donut_chart;
33
mod footer;
44
mod header;
55
pub mod help;
6+
pub mod runtime_warning;
67
pub mod startup_warnings;
78
mod stats_panel;
89
mod status_panel;

src/tui/widgets/runtime_warning.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! Runtime warning popup widget.
2+
//!
3+
//! Displays high-priority warnings detected while the daemon is running.
4+
5+
use ratatui::prelude::*;
6+
use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};
7+
8+
use crate::tui::theme;
9+
10+
/// Render a centered runtime warning popup.
11+
pub fn render(frame: &mut Frame, area: Rect, message: &str) {
12+
let popup_width = 90.min(area.width.saturating_sub(4));
13+
let popup_height = 10.min(area.height.saturating_sub(4));
14+
15+
let popup_area = Rect {
16+
x: area.x + (area.width.saturating_sub(popup_width)) / 2,
17+
y: area.y + (area.height.saturating_sub(popup_height)) / 2,
18+
width: popup_width,
19+
height: popup_height,
20+
};
21+
22+
frame.render_widget(Clear, popup_area);
23+
24+
let block = Block::default()
25+
.title(" Runtime Warning ")
26+
.title_style(theme::warning().add_modifier(Modifier::BOLD))
27+
.borders(Borders::ALL)
28+
.border_type(BorderType::Rounded)
29+
.border_style(theme::warning())
30+
.style(theme::surface());
31+
32+
let inner = block.inner(popup_area);
33+
frame.render_widget(block, popup_area);
34+
35+
let lines = vec![
36+
Line::from(Span::styled("Gemini Access Disabled", theme::warning())),
37+
Line::from(""),
38+
Line::from(Span::styled(message, Style::default().fg(theme::TEXT))),
39+
Line::from(""),
40+
Line::from(Span::styled("Press Enter or Esc to dismiss", theme::dim())),
41+
];
42+
43+
frame.render_widget(
44+
Paragraph::new(lines).wrap(ratatui::widgets::Wrap { trim: true }),
45+
inner,
46+
);
47+
}

0 commit comments

Comments
 (0)