Skip to content

Commit b764ba4

Browse files
committed
feat: implement automation server with websocket support and modular handlers
1 parent 54a3815 commit b764ba4

8 files changed

Lines changed: 1734 additions & 8 deletions

File tree

File renamed without changes.

src/automation/handle.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use std::sync::{Arc, RwLock};
2+
use std::sync::atomic::{AtomicU64, Ordering};
3+
use tokio::sync::{mpsc, broadcast};
4+
use crate::ShellState;
5+
use crate::cache::{AssetMetadata, AssetStore};
6+
use crate::config::{CacheConfig, TerminalConfig};
7+
use crate::permissions::PermissionPolicy;
8+
use crate::platform_paths::RuntimePaths;
9+
use crate::session::SessionSnapshot;
10+
use super::types::*;
11+
12+
#[derive(Clone)]
13+
pub struct AutomationHandle {
14+
pub(crate) snapshot: Arc<RwLock<AutomationSnapshot>>,
15+
pub(crate) command_tx: mpsc::UnboundedSender<AutomationCommand>,
16+
pub(crate) event_tx: broadcast::Sender<AutomationEvent>,
17+
#[allow(dead_code)]
18+
pub(crate) cache_config: CacheConfig,
19+
pub(crate) runtime_paths: RuntimePaths,
20+
pub(crate) profile_id: String,
21+
pub(crate) permissions: PermissionPolicy,
22+
pub(crate) expose_tab_api: bool,
23+
#[allow(dead_code)]
24+
pub(crate) expose_cache_api: bool,
25+
pub(crate) terminal_config: TerminalConfig,
26+
pub mount_manager: crate::mounts::MountManager,
27+
pub(crate) activity_counter: Arc<AtomicU64>,
28+
pub(crate) egui_ctx: Arc<RwLock<Option<eframe::egui::Context>>>,
29+
}
30+
31+
impl AutomationHandle {
32+
pub fn new(
33+
snapshot: Arc<RwLock<AutomationSnapshot>>,
34+
command_tx: mpsc::UnboundedSender<AutomationCommand>,
35+
event_tx: broadcast::Sender<AutomationEvent>,
36+
config: &crate::config::BrazenConfig,
37+
paths: &RuntimePaths,
38+
mount_manager: crate::mounts::MountManager,
39+
) -> Self {
40+
Self {
41+
snapshot,
42+
command_tx,
43+
event_tx,
44+
cache_config: config.cache.clone(),
45+
runtime_paths: paths.clone(),
46+
profile_id: config.profiles.active_profile.clone(),
47+
permissions: config.permissions.clone(),
48+
expose_tab_api: config.automation.expose_tab_api,
49+
expose_cache_api: config.automation.expose_cache_api,
50+
terminal_config: config.terminal.clone(),
51+
mount_manager,
52+
activity_counter: Arc::new(AtomicU64::new(0)),
53+
egui_ctx: Arc::new(RwLock::new(None)),
54+
}
55+
}
56+
57+
pub fn set_egui_context(&self, ctx: eframe::egui::Context) {
58+
let mut lock = self.egui_ctx.write().expect("egui ctx lock");
59+
*lock = Some(ctx);
60+
}
61+
62+
pub fn request_repaint(&self) {
63+
if let Some(ctx) = self.egui_ctx.read().expect("egui ctx lock").as_ref() {
64+
ctx.request_repaint();
65+
}
66+
}
67+
68+
pub fn request_shutdown(&self) -> Result<(), String> {
69+
let ctx = self
70+
.egui_ctx
71+
.read()
72+
.expect("egui ctx lock")
73+
.as_ref()
74+
.cloned()
75+
.ok_or_else(|| "egui context not available".to_string())?;
76+
ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
77+
Ok(())
78+
}
79+
80+
pub fn snapshot(&self) -> AutomationSnapshot {
81+
self.snapshot.read().expect("automation snapshot lock").clone()
82+
}
83+
84+
pub fn update_snapshot(&self, shell_state: &ShellState, cache: &AssetStore) {
85+
let mut snapshot = self.snapshot.write().expect("automation snapshot lock");
86+
let session = shell_state.session.read().expect("session lock");
87+
snapshot.tabs = build_tab_list(&session);
88+
snapshot.active_tab_index = session
89+
.active_tab()
90+
.and_then(|tab| {
91+
session
92+
.windows
93+
.get(session.active_window)
94+
.and_then(|window| window.tabs.iter().position(|t| t.id == tab.id))
95+
})
96+
.unwrap_or(0);
97+
snapshot.active_tab_id = session
98+
.active_tab()
99+
.map(|tab| tab.id.0.to_string());
100+
snapshot.address_bar = shell_state.address_bar_input.clone();
101+
snapshot.page_title = shell_state.page_title.clone();
102+
snapshot.last_committed_url = shell_state.last_committed_url.clone();
103+
snapshot.load_status = shell_state
104+
.load_status
105+
.map(|status| status.as_str().to_string());
106+
snapshot.load_progress = shell_state.load_progress;
107+
snapshot.document_ready = shell_state.document_ready;
108+
snapshot.can_go_back = shell_state.can_go_back;
109+
snapshot.can_go_forward = shell_state.can_go_forward;
110+
snapshot.engine_status = shell_state.engine_status.to_string();
111+
snapshot.upstream_active = shell_state.upstream_active;
112+
snapshot.upstream_last_error = shell_state.upstream_last_error.clone();
113+
snapshot.last_security_warning = shell_state
114+
.last_security_warning
115+
.as_ref()
116+
.map(|(kind, message)| format!("{kind:?}: {message}"));
117+
snapshot.last_crash = shell_state.last_crash.clone();
118+
snapshot.log_panel_open = shell_state.log_panel_open;
119+
snapshot.permission_panel_open = shell_state.permission_panel_open;
120+
snapshot.find_panel_open = shell_state.find_panel_open;
121+
snapshot.cache_stats = cache.stats();
122+
snapshot.cache_entries = cache
123+
.entries()
124+
.iter()
125+
.take(512)
126+
.map(asset_summary_from_metadata)
127+
.collect();
128+
snapshot.last_event_log_len = shell_state.event_log.len();
129+
snapshot.tts_queue_len = shell_state.tts_queue.len();
130+
snapshot.tts_playing = shell_state.tts_playing;
131+
snapshot.reading_queue_len = shell_state.reading_queue.len();
132+
snapshot.reading_queue_urls = shell_state
133+
.reading_queue
134+
.iter()
135+
.take(16)
136+
.map(|item| item.url.clone())
137+
.collect();
138+
snapshot.reader_mode_open = shell_state.reader_mode_open;
139+
snapshot.reader_mode_source_url = shell_state.reader_mode_source_url.clone();
140+
snapshot.reader_mode_text_len = shell_state.reader_mode_text.len();
141+
snapshot.visit_total = shell_state.visit_total;
142+
snapshot.revisit_total = shell_state.revisit_total;
143+
snapshot.unique_visit_urls = shell_state.visit_counts.len();
144+
}
145+
146+
pub fn publish_navigation(&self, event: AutomationNavigationEvent) {
147+
let _ = self.event_tx.send(AutomationEvent::Navigation(event));
148+
}
149+
150+
pub fn publish_capability(&self, event: AutomationCapabilityEvent) {
151+
let _ = self.event_tx.send(AutomationEvent::Capability(event));
152+
}
153+
154+
pub fn record_activity(&self, activity: AutomationActivity) {
155+
let mut snapshot = self.snapshot.write().expect("automation snapshot lock");
156+
if snapshot.activities.len() >= 128 {
157+
snapshot.activities.pop_front();
158+
}
159+
snapshot.activities.push_back(activity);
160+
}
161+
162+
pub fn take_command_sender(&self) -> mpsc::UnboundedSender<AutomationCommand> {
163+
self.command_tx.clone()
164+
}
165+
166+
pub fn next_activity_id(&self) -> String {
167+
self.activity_counter.fetch_add(1, Ordering::SeqCst).to_string()
168+
}
169+
}
170+
171+
pub(crate) fn build_tab_list(session: &SessionSnapshot) -> Vec<AutomationTab> {
172+
let mut tabs = Vec::new();
173+
if let Some(window) = session.windows.get(session.active_window) {
174+
for (index, tab) in window.tabs.iter().enumerate() {
175+
tabs.push(AutomationTab {
176+
tab_id: tab.id.0.to_string(),
177+
index,
178+
title: tab.title.clone(),
179+
url: tab.url.clone(),
180+
zoom: tab.zoom_level,
181+
pinned: tab.pinned,
182+
muted: tab.muted,
183+
});
184+
}
185+
}
186+
tabs
187+
}
188+
189+
pub(crate) fn asset_summary_from_metadata(entry: &AssetMetadata) -> AutomationAssetSummary {
190+
AutomationAssetSummary {
191+
asset_id: entry.asset_id.clone(),
192+
url: entry.url.clone(),
193+
status_code: entry.status_code,
194+
mime: entry.mime.clone(),
195+
size_bytes: entry.size_bytes,
196+
hash: entry.hash.clone(),
197+
created_at: entry.created_at.clone(),
198+
session_id: entry.session_id.clone(),
199+
tab_id: entry.tab_id.clone(),
200+
}
201+
}

0 commit comments

Comments
 (0)