use std::time::Instant;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::text::Line;
use super::{
overlay_panel::{PanelBody, PanelKeyOutcome, PanelState},
panel_text::{heading_with_status, indented_wrapped_lines},
theme::Theme,
App, ComposerMode, PanelOverlay,
};
use crate::tools::computer_use::{
desktop_warning, ComputerUseControl, ComputerUseStatus, InstallKind, UpdateCheckStatus,
UpdateOutcome,
};
const TITLE: &str = "Computer use";
const FOOTER_INSTALLING: &str = "r cancel setup · Enter/Esc close";
const FOOTER_UPDATING: &str = "r cancel update · Enter/Esc close";
const FOOTER_REVOCABLE: &str = "r revoke · Enter/Esc close";
const FOOTER_CHECKABLE: &str = "u check for driver updates · Enter/Esc close";
const FOOTER: &str = "Enter/Esc close";
pub(super) struct ComputerOverlay {
control: Option<ComputerUseControl>,
panel: PanelState,
}
impl std::fmt::Debug for ComputerOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ComputerOverlay")
.field("status", &self.status())
.field("panel", &self.panel)
.finish()
}
}
impl ComputerOverlay {
fn status(&self) -> ComputerUseStatus {
self.control
.as_ref()
.map(ComputerUseControl::status)
.unwrap_or(ComputerUseStatus::Off)
}
fn revocable(&self) -> bool {
self.control.as_ref().is_some_and(|control| {
control.installation_pending()
|| matches!(
control.status(),
ComputerUseStatus::Connecting | ComputerUseStatus::Connected
)
})
}
fn status_lines(&self, width: usize) -> Vec<Line<'static>> {
let state = self
.control
.as_ref()
.map(ComputerUseControl::status)
.unwrap_or(ComputerUseStatus::Off);
let updating = matches!(
self.control
.as_ref()
.and_then(ComputerUseControl::pending_install),
Some(InstallKind::Update { .. })
);
let (status, access, next) = match state {
ComputerUseStatus::Installing if updating => (
"Updating",
"Cua Driver update pending; desktop access remains off",
"r or /computer off cancels the update",
),
ComputerUseStatus::Installing => (
"Installing",
"Cua Driver installation pending; desktop access remains off",
"r or /computer off cancels installation",
),
ComputerUseStatus::Off => (
"Off",
"No desktop access granted",
"/computer on grant access and connect",
),
ComputerUseStatus::Connecting => (
"Connecting",
"Desktop access granted; connecting to driver",
"/computer off cancel connection and revoke access",
),
ComputerUseStatus::Connected => (
"Driver connected",
"Desktop access granted for this session",
"/computer off revoke access and disconnect",
),
ComputerUseStatus::Closing => (
"Access off · disconnecting",
"Access revoked; driver connection is closing",
"Wait for closing to finish before /computer on",
),
};
let mut lines = vec![heading_with_status("Session", status, width)];
lines.extend(indented_wrapped_lines(access, 0, width, Theme::text()));
lines.extend(indented_wrapped_lines(
"Desktop permissions: not checked",
0,
width,
Theme::dim(),
));
if let Some(reason) = self
.control
.as_ref()
.and_then(ComputerUseControl::revocation_reason)
{
lines.extend(indented_wrapped_lines(
&format!("Access revoked: {reason}"),
0,
width,
Theme::warning(),
));
}
if let Some(warning) = desktop_warning() {
lines.push(Line::default());
lines.push(heading_with_status("Display warning", "", width));
lines.extend(indented_wrapped_lines(warning, 0, width, Theme::warning()));
}
if let Some(error) = self
.control
.as_ref()
.and_then(ComputerUseControl::terminal_error)
{
lines.push(Line::default());
lines.push(heading_with_status("Connection error", "", width));
lines.extend(indented_wrapped_lines(&error, 0, width, Theme::error()));
}
lines.push(Line::default());
let driver = self
.control
.as_ref()
.and_then(ComputerUseControl::driver_path);
lines.push(heading_with_status(
"Cua Driver",
if driver.is_some() {
"Detected"
} else {
"Not detected"
},
width,
));
let path = driver
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| {
"/computer setup installs the missing driver after separate consent".into()
});
lines.extend(indented_wrapped_lines(&path, 0, width, Theme::dim()));
if driver.is_some() {
self.update_lines(&self.update_status(), &mut lines, width);
}
lines.extend(indented_wrapped_lines(
"cua-driver doctor installation diagnostics",
0,
width,
Theme::dim(),
));
if cfg!(target_os = "macos") {
lines.extend(indented_wrapped_lines(
"cua-driver permissions status check permissions after the daemon starts",
0,
width,
Theme::dim(),
));
}
lines.push(Line::default());
lines.push(heading_with_status("Access and privacy", "", width));
for note in [
"Enabling grants local desktop access, including signed-in apps.",
"No per-action Rho approval, including in supervised mode.",
"Captured images go to your model provider and session history.",
"This machine saves each session's access separately. Explicit on/off also sets the default for new sessions. Resumed sessions keep their own choice.",
] {
lines.extend(indented_wrapped_lines(note, 0, width, Theme::text()));
}
lines.push(Line::default());
lines.push(heading_with_status("Commands", "", width));
for command in [
next,
"/computer setup install, configure and verify connection",
"/computer update check for and install a newer driver",
] {
lines.extend(indented_wrapped_lines(command, 0, width, Theme::text()));
}
lines
}
fn update_status(&self) -> UpdateCheckStatus {
self.control.as_ref().map_or(
UpdateCheckStatus::NotChecked,
ComputerUseControl::update_check_status,
)
}
fn checkable(&self, update: &UpdateCheckStatus) -> bool {
self.control.as_ref().is_some_and(|control| {
control.driver_path().is_some()
&& control.status() != ComputerUseStatus::Installing
&& !matches!(update, UpdateCheckStatus::Checking)
})
}
fn update_lines(
&self,
update: &UpdateCheckStatus,
lines: &mut Vec<Line<'static>>,
width: usize,
) {
let hint = self
.control
.as_ref()
.map_or("", ComputerUseControl::update_hint);
let (text, notes, style) = match update {
UpdateCheckStatus::NotChecked => (
"Updates: not checked · u check (runs cua-driver check-update)".to_owned(),
None,
Theme::dim(),
),
UpdateCheckStatus::Checking => ("Updates: checking…".to_owned(), None, Theme::dim()),
UpdateCheckStatus::Failed(error) => (
format!("Updates: check failed: {error} · u retry"),
None,
Theme::warning(),
),
UpdateCheckStatus::Checked(UpdateOutcome::Unavailable { current, reason }) => (
format!("Version {current} · update check unavailable: {reason}"),
None,
Theme::warning(),
),
UpdateCheckStatus::Checked(UpdateOutcome::Available {
current,
latest,
notes,
}) => (
format!("Version {current} · {latest} available · {hint}"),
notes.as_deref(),
Theme::warning(),
),
UpdateCheckStatus::Checked(UpdateOutcome::UpToDate { current }) => (
format!("Version {current} · up to date"),
None,
Theme::dim(),
),
};
lines.extend(indented_wrapped_lines(&text, 0, width, style));
if let Some(notes) = notes {
lines.extend(indented_wrapped_lines(notes, 0, width, Theme::dim()));
}
}
}
impl PanelBody for ComputerOverlay {
fn state(&self) -> &PanelState {
&self.panel
}
fn state_mut(&mut self) -> &mut PanelState {
&mut self.panel
}
fn title(&self) -> &str {
TITLE
}
fn footer(&self) -> &str {
match &self.control {
Some(control) => match control.pending_install() {
Some(InstallKind::Install) => FOOTER_INSTALLING,
Some(InstallKind::Update { .. }) => FOOTER_UPDATING,
None if self.revocable() => FOOTER_REVOCABLE,
None if self.checkable(&self.update_status()) => FOOTER_CHECKABLE,
None => FOOTER,
},
None => FOOTER,
}
}
fn body_lines(&self, width: usize, _now: Instant) -> Vec<Line<'static>> {
self.status_lines(width)
}
fn handle_key(&mut self, key: KeyEvent) -> PanelKeyOutcome {
if !key.modifiers.is_empty() {
return PanelKeyOutcome::Unhandled;
}
match key.code {
KeyCode::Char('r') if self.revocable() => PanelKeyOutcome::Run(|app| {
app.revoke_computer_preference();
app.show_computer_off();
}),
KeyCode::Char('u') if self.checkable(&self.update_status()) => {
PanelKeyOutcome::Run(App::check_computer_update)
}
KeyCode::Char('r' | 'u') => PanelKeyOutcome::Handled,
_ => PanelKeyOutcome::Unhandled,
}
}
}
impl App {
pub(super) fn show_computer_status(&mut self) {
self.input_ui
.set_composer(ComposerMode::Panel(PanelOverlay::Computer(
ComputerOverlay {
control: self.computer_use.clone(),
panel: PanelState::default(),
},
)));
}
pub(super) fn sync_computer_overlay(&mut self) {
let control = self.computer_use.clone();
if let ComposerMode::Panel(PanelOverlay::Computer(overlay)) = self.input_ui.composer_mut() {
overlay.control = control;
}
}
}