Skip to content

Commit 08e391d

Browse files
fix: file viewer
1 parent 4e087af commit 08e391d

18 files changed

Lines changed: 1097 additions & 618 deletions

File tree

desktop/src-tauri/src/agent/injector.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ async fn build_v3_uncached(runbox_id: &str, task: &str, agent_id: &str) -> Strin
235235
let block = format!("PROJECT:\n{relevant}\n");
236236
let room = 60usize.min(BUDGET_TOTAL_V3.saturating_sub(used));
237237
if est_tokens(&block) <= room {
238-
sections.push(block);
238+
sections.push(block.clone());
239239
used += est_tokens(&block);
240240
}
241241
}
Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
2-
31
use std::io::Write;
42

53
fn main() {
6-
let url = match std::env::args().nth(1) {
7-
Some(u) if u.starts_with("http://") || u.starts_with("https://") => u,
8-
// Some tools pass the URL as-is without a scheme — prepend https
9-
Some(u) if !u.is_empty() => format!("https://{}", u),
4+
let raw = match std::env::args().nth(1) {
5+
Some(a) if !a.is_empty() => a,
106
_ => return,
117
};
128

9+
let url = normalize_to_url(&raw);
10+
1311
// ── 1. POST to the embedded server → triggers "browser-open-url" Tauri event
1412
let port = 7547u16;
1513
let body = url.as_bytes();
@@ -21,12 +19,53 @@ fn main() {
2119

2220
if let Ok(mut stream) = std::net::TcpStream::connect(format!("127.0.0.1:{port}")) {
2321
let _ = stream.write_all(req.as_bytes());
24-
// Read/discard the response so the server doesn't get a broken-pipe error
2522
let mut buf = [0u8; 256];
2623
let _ = std::io::Read::read(&mut stream, &mut buf);
2724
}
2825

29-
// ── 2. Print to stdout so the URL also appears in the terminal output.
30-
// The PTY reader will pick it up as a fallback if the HTTP post fails.
26+
// ── 2. Print to stdout — PTY reader picks it up as fallback
3127
println!("{}", url);
28+
}
29+
30+
/// Convert anything the shell might pass as BROWSER argument into a URL.
31+
fn normalize_to_url(raw: &str) -> String {
32+
if raw.starts_with("http://") || raw.starts_with("https://") || raw.starts_with("file://") {
33+
return raw.to_string();
34+
}
35+
36+
// Absolute Windows path: C:\... or C:/...
37+
if raw.len() >= 2 && raw.chars().nth(1) == Some(':') {
38+
let normalised = raw.replace('\\', "/");
39+
return format!("file:///{normalised}");
40+
}
41+
42+
let looks_like_path = raw.starts_with('.')
43+
|| raw.starts_with('/')
44+
|| raw.starts_with('\\')
45+
|| has_web_extension(raw);
46+
47+
if looks_like_path {
48+
let base = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
49+
let path = base.join(raw);
50+
51+
if let Ok(canonical) = path.canonicalize() {
52+
if let Ok(url) = url::Url::from_file_path(&canonical) {
53+
return url.to_string();
54+
}
55+
let s = canonical.to_string_lossy().replace('\\', "/");
56+
return format!("file:///{}", s.trim_start_matches('/'));
57+
}
58+
59+
// File doesn't exist yet — build URL anyway
60+
let s = path.to_string_lossy().replace('\\', "/");
61+
return format!("file:///{}", s.trim_start_matches('/'));
62+
}
63+
64+
format!("https://{raw}")
65+
}
66+
67+
fn has_web_extension(s: &str) -> bool {
68+
let lower = s.to_lowercase();
69+
lower.ends_with(".html") || lower.ends_with(".htm")
70+
|| lower.ends_with(".svg") || lower.ends_with(".pdf") || lower.ends_with(".xhtml")
3271
}

desktop/src-tauri/src/browser/webview.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,16 @@ pub fn browser_destroy(app: AppHandle, id: String) -> Result<(), String> {
131131
#[tauri::command]
132132
pub fn browser_navigate(app: AppHandle, id: String, url: String) -> Result<(), String> {
133133
let wv = app.get_webview(&label(&id)).ok_or("webview not found")?;
134-
// .navigate() silently fails on Windows child webviews created via add_child.
135-
// Using window.location.assign() via eval is reliable cross-platform.
134+
135+
// file:// URLs cannot be loaded via window.location.assign() from an http
136+
// context — the browser blocks the cross-origin jump. Use the native Tauri
137+
// navigate() for file:// only; for http/https keep using eval (more reliable
138+
// on Windows child webviews created via add_child).
139+
if url.starts_with("file://") {
140+
let parsed = url.parse::<url::Url>().map_err(|e: url::ParseError| e.to_string())?;
141+
return wv.navigate(parsed).map_err(|e| e.to_string());
142+
}
143+
136144
let escaped = url.replace('\\', "\\\\").replace('\'', "\\'");
137145
wv.eval(&format!("window.location.assign('{}')", escaped))
138146
.map_err(|e| e.to_string())

desktop/src-tauri/src/commands/pty.rs

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ pub async fn pty_spawn(
1616
runbox_id: String,
1717
cwd: String,
1818
agent_cmd: Option<String>,
19-
cols: Option<u16>, // ← added
20-
rows: Option<u16>, // ← added
19+
cols: Option<u16>,
20+
rows: Option<u16>,
21+
docker: Option<bool>, // ← NEW: passed from frontend workspace card
2122
state: tauri::State<'_, AppState>,
2223
) -> Result<(), String> {
23-
pty::spawn(app, session_id, runbox_id, cwd, agent_cmd, cols, rows, &state).await
24+
pty::spawn(app, session_id, runbox_id, cwd, agent_cmd, cols, rows, docker.unwrap_or(false), &state).await
2425
}
2526

2627
#[tauri::command]
@@ -29,23 +30,29 @@ pub fn pty_write(
2930
data: String,
3031
state: tauri::State<'_, AppState>,
3132
) -> Result<(), String> {
32-
let mut inject: Option<(String, String, AgentKind)> = None;
33+
let mut inject: Option<(String, String, AgentKind)> = None;
34+
let mut open_url: Option<String> = None;
35+
let mut send_data: Option<String> = None;
36+
3337
{
3438
let mut sessions = state.sessions.lock().unwrap();
3539
if let Some(s) = sessions.get_mut(&session_id) {
36-
let _ = s.writer.write_all(data.as_bytes());
37-
let _ = s.writer.flush();
3840
for ch in data.chars() {
3941
match ch {
4042
'\r' | '\n' => {
4143
let line = s.input_buf.trim().to_string();
4244
s.input_buf.clear();
4345
if !line.is_empty() {
44-
let token = line.split_whitespace().next().unwrap_or("");
45-
let base_cmd = token.rsplit(['/', '\\']).next().unwrap_or(token);
46-
let kind = AgentKind::detect(base_cmd);
47-
if kind != AgentKind::Shell {
48-
inject = Some((s.runbox_id.clone(), s.cwd.clone(), kind));
46+
if let Some(url) = intercept_start_cmd(&line, &s.cwd) {
47+
open_url = Some(url);
48+
send_data = Some("\x03\r\n".to_string());
49+
} else {
50+
let token = line.split_whitespace().next().unwrap_or("");
51+
let base_cmd = token.rsplit(['/', '\\']).next().unwrap_or(token);
52+
let kind = AgentKind::detect(base_cmd);
53+
if kind != AgentKind::Shell {
54+
inject = Some((s.runbox_id.clone(), s.cwd.clone(), kind));
55+
}
4956
}
5057
}
5158
}
@@ -54,8 +61,17 @@ pub fn pty_write(
5461
_ => {}
5562
}
5663
}
64+
65+
let to_write = send_data.as_deref().unwrap_or(&data);
66+
let _ = s.writer.write_all(to_write.as_bytes());
67+
let _ = s.writer.flush();
5768
}
5869
}
70+
71+
if let Some(url) = open_url {
72+
crate::agent::globals::emit_event("browser-open-url", serde_json::json!(url));
73+
}
74+
5975
if let Some((rb, cwd, kind)) = inject {
6076
let sid = session_id.clone();
6177
let db = state.db.clone();
@@ -68,6 +84,41 @@ pub fn pty_write(
6884
Ok(())
6985
}
7086

87+
fn intercept_start_cmd(line: &str, cwd: &str) -> Option<String> {
88+
let mut parts = line.splitn(2, char::is_whitespace);
89+
let cmd = parts.next().unwrap_or("").to_lowercase();
90+
if cmd != "start" { return None; }
91+
92+
let arg = parts.next()?.trim().trim_matches('"').trim_matches('\'');
93+
if arg.is_empty() { return None; }
94+
95+
let lower = arg.to_lowercase();
96+
if !lower.ends_with(".html") && !lower.ends_with(".htm")
97+
&& !lower.ends_with(".svg") && !lower.ends_with(".pdf") {
98+
return None;
99+
}
100+
101+
if arg.starts_with("file://") { return Some(arg.to_string()); }
102+
103+
if arg.len() >= 2 && arg.chars().nth(1) == Some(':') {
104+
let normalised = arg.replace('\\', "/");
105+
return Some(format!("file:///{normalised}"));
106+
}
107+
108+
let abs = std::path::Path::new(cwd).join(arg);
109+
110+
if let Ok(canonical) = abs.canonicalize() {
111+
if let Ok(url) = url::Url::from_file_path(&canonical) {
112+
return Some(url.to_string());
113+
}
114+
let s = canonical.to_string_lossy().replace('\\', "/");
115+
return Some(format!("file:///{}", s.trim_start_matches('/')));
116+
}
117+
118+
let s = abs.to_string_lossy().replace('\\', "/");
119+
Some(format!("file:///{}", s.trim_start_matches('/')))
120+
}
121+
71122
#[tauri::command]
72123
pub fn pty_resize(
73124
session_id: String, cols: u16, rows: u16,

0 commit comments

Comments
 (0)