app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
rust: Phase 4 parity gate (tools/parity.py) green 37/37 on winvm; Node response envelope ported into dispatch (verb cat…
f389b1e
2mo ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
//! The highFps lease (Node: highFpsGrant/Extend/Info/Downgrade): a background window renders full
//! rate only while a lease runs (20 min default, 120 max); a lapsed lease relaunches the window
//! without the anti-throttle flags so a forgotten demo window can never flatten a battery (issue #14).
use crate::Ctx;
use pup_core::{err, ok, Caller};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
struct LoopTick(Instant);
impl Drop for LoopTick { fn drop(&mut self) { pup_core::loops::tick("highfps-lease-janitor", 60_000, self.0.elapsed().as_millis() as u64); } }
pub const DEFAULT_MIN: u64 = 20;
pub const MAX_MIN: u64 = 120;
#[derive(Clone)]
pub struct Lease { pub minutes: u64, pub granted: Instant, pub expires: Instant, pub warned: bool }
pub type Leases = Arc<Mutex<HashMap<String, Lease>>>;
pub fn grant(ctx: &Ctx, sid: &str, minutes: u64) -> Lease {
let m = minutes.clamp(1, MAX_MIN);
let l = Lease { minutes: m, granted: Instant::now(), expires: Instant::now() + Duration::from_secs(m * 60), warned: false };
ctx.highfps.lock().unwrap().insert(sid.into(), l.clone());
pup_core::log(format!("[highfps] \"{sid}\" lease granted: {m} min"));
l
}
pub fn info(ctx: &Ctx, sid: &str) -> Value {
match ctx.highfps.lock().unwrap().get(sid) { Some(l) => json!({ "active": true, "minutes": l.minutes, "msRemaining": l.expires.saturating_duration_since(Instant::now()).as_millis() as u64, "recordingActive": ctx.tab_recs.lock().unwrap().contains_key(sid) }), None => Value::Null }
}
pub fn extend(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {
let s = match crate::resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };
let minutes = args["minutes"].as_u64().unwrap_or(DEFAULT_MIN).clamp(1, MAX_MIN);
let mut m = ctx.highfps.lock().unwrap();
let Some(l) = m.get_mut(&s.id) else { return err("not_high_fps", format!("window \"{}\" is not a high-fps window — nothing to extend", s.id), Some("Open with pup_open_window {highFps:true} (or start a recording) to get a lease.")) };
l.expires = Instant::now() + Duration::from_secs(minutes * 60); l.minutes = minutes; l.warned = false;
drop(m);
pup_core::log(format!("[highfps] \"{}\" lease extended: +{minutes} min", s.id));
ok(json!({ "success": true, "output": json!({ "ok": true, "sessionId": s.id, "highFps": info(ctx, &s.id) }).to_string(), "highFps": info(ctx, &s.id), "_hint": format!("High-fps lease extended to {minutes} min. While a recording runs the lease cannot lapse.") }))
}
/// Every minute: warn 2 min before expiry, downgrade (relaunch without the flags) on expiry.
pub fn start_loop(ctx: Arc<Ctx>) {
std::thread::Builder::new().name("highfps".into()).spawn(move || loop {
std::thread::sleep(Duration::from_secs(60));
let now = Instant::now();
let _t0 = Instant::now();
let _guard = LoopTick(_t0);
let expired: Vec<String> = { let mut m = ctx.highfps.lock().unwrap(); let recording: Vec<String> = ctx.tab_recs.lock().unwrap().keys().cloned().collect();
for (sid, l) in m.iter_mut() { if recording.contains(sid) { l.expires = now + Duration::from_secs(5 * 60); } else if !l.warned && l.expires.saturating_duration_since(now) < Duration::from_secs(120) { l.warned = true; pup_core::log(format!("[highfps] \"{sid}\" lease expires in ~2 min — pup_highfps_extend {{sessionId,minutes}} keeps it")); } }
m.iter().filter(|(_, l)| l.expires <= now).map(|(s, _)| s.clone()).collect() };
for sid in expired {
ctx.highfps.lock().unwrap().remove(&sid);
let Some(s) = ctx.sessions.get(&sid) else { continue };
let urls: Vec<String> = s.tabs.iter().map(|t| t.url.clone()).filter(|u| u != "about:blank").collect();
pup_core::log(format!("[highfps] \"{sid}\" DOWNGRADING to normal power (lease lapsed) — relaunching {} tab(s) without the high-fps flags", urls.len()));
let caller = Caller { thread: s.owner.clone().unwrap_or_default(), reason: "highFps lease lapsed".into(), ..Default::default() };
let _ = crate::close_window(&ctx, &caller, &json!({ "window": sid, "takeover": true }));
let first = urls.first().cloned().unwrap_or_else(|| "about:blank".into());
let _ = crate::open_window(&ctx, &caller, &json!({ "window": sid, "url": first, "profile": s.profile, "takeover": true }));
for u in urls.iter().skip(1) { let _ = crate::tabs::open_tab(&ctx, &caller, &json!({ "window": sid, "url": u, "takeover": true })); }
}
}).ok();
}