Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
360f462
perf(tauri): add minimal desktop transport benchmark
pascalandr May 11, 2026
090efe9
fix(tauri): restore desktop transport heartbeat parity
pascalandr May 9, 2026
74fa1c3
fix(bench): gate perf242 transport harness
pascalandr May 1, 2026
0049c7b
fix(tauri): authenticate desktop heartbeat pongs
pascalandr May 1, 2026
39d0ae1
feat(settings): add Tauri transport fallback toggle
pascalandr May 9, 2026
843a47c
fix(settings): keep Tauri transport fallback local
pascalandr May 10, 2026
c225204
Merge upstream/dev into pagec/rust-desktop-event-transport for task 055
pascalandr May 16, 2026
654b424
fix(bench): align transport harness message loading
pascalandr May 16, 2026
e915985
refactor(tauri): remove temporary transport benchmark harness
pascalandr May 16, 2026
88dc49c
Merge branch 'dev' into pagec/rust-desktop-event-transport
pascalandr May 16, 2026
8e50a8e
fix(ui): ignore stale event stream connect failures
pascalandr May 16, 2026
251d689
Merge upstream/dev into pagec/rust-desktop-event-transport for task 0…
pascalandr May 16, 2026
27d7430
Merge branch 'dev' into pagec/rust-desktop-event-transport
pascalandr May 18, 2026
a1db48d
fix: task-078 restore UI typecheck on integrated batch
pascalandr Jun 5, 2026
b2d1c92
fix: task-078 merge upstream dev into PR 242
pascalandr Jun 5, 2026
1f79720
fix: task-083 address PR242 transport review feedback
pascalandr Jun 6, 2026
3007657
fix: task-084 address Greptile transport rerun feedback
pascalandr Jun 6, 2026
4878fe8
Merge branch 'dev' into pagec/rust-desktop-event-transport
pascalandr Jun 6, 2026
fc62ce2
merge: task-087 refresh PR242 onto upstream dev
pascalandr Jun 7, 2026
998d515
Merge branch 'dev' into pagec/rust-desktop-event-transport
pascalandr Jun 7, 2026
503d86f
Merge branch 'dev' into pagec/rust-desktop-event-transport
pascalandr Jun 8, 2026
df1948c
fix: task-080 scope native transport to local Tauri windows
pascalandr Jun 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 154 additions & 19 deletions packages/tauri-app/src-tauri/src/cli_manager.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::desktop_event_transport::DesktopEventStreamConfig;
use crate::managed_node::resolve_bundled_node_binary;
use dirs::home_dir;
use parking_lot::Mutex;
Expand Down Expand Up @@ -185,12 +186,13 @@ fn kill_process_tree_windows(pid: u32, force: bool) -> bool {
}
fn navigate_main(app: &AppHandle, url: &str) {
if let Some(win) = app.webview_windows().get("main") {
let mut display = url.to_string();
let final_url = augment_launch_url(url);
let mut display = final_url.clone();
if let Some(hash_index) = display.find('#') {
display.replace_range(hash_index + 1.., "[REDACTED]");
}
log_line(&format!("navigating main to {display}"));
if let Ok(parsed) = Url::parse(url) {
if let Ok(parsed) = Url::parse(&final_url) {
let _ = win.navigate(parsed);
} else {
log_line("failed to parse URL for navigation");
Expand All @@ -200,6 +202,31 @@ fn navigate_main(app: &AppHandle, url: &str) {
}
}

fn augment_launch_url(base_url: &str) -> String {
let launch_query = std::env::var("CODENOMAD_UI_LAUNCH_QUERY")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());

let Some(launch_query) = launch_query else {
return base_url.to_string();
};

if base_url.contains('?') {
return format!(
"{}&{}",
base_url,
launch_query.trim_start_matches(['?', '#'])
);
}

format!(
"{}?{}",
base_url,
launch_query.trim_start_matches(['?', '#'])
)
}

fn extract_cookie_value(set_cookie: &str, name: &str) -> Option<String> {
let prefix = format!("{name}=");
let cookie_kv = set_cookie.split(';').next()?.trim();
Expand Down Expand Up @@ -298,6 +325,15 @@ fn generate_auth_cookie_name() -> String {
format!("{SESSION_COOKIE_NAME_PREFIX}_{pid}_{timestamp}")
}

fn generate_transport_connection_id() -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let tid = std::thread::current().id();
format!("tauri-{}-{:?}", ts, tid)
}

const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -456,6 +492,8 @@ pub struct CliProcessManager {
job: Arc<Mutex<Option<WindowsJobObject>>>,
ready: Arc<AtomicBool>,
bootstrap_token: Arc<Mutex<Option<String>>>,
session_cookie: Arc<Mutex<Option<String>>>,
auth_cookie_name: Arc<Mutex<Option<String>>>,
}

impl CliProcessManager {
Expand All @@ -467,6 +505,8 @@ impl CliProcessManager {
job: Arc::new(Mutex::new(None)),
ready: Arc::new(AtomicBool::new(false)),
bootstrap_token: Arc::new(Mutex::new(None)),
session_cookie: Arc::new(Mutex::new(None)),
auth_cookie_name: Arc::new(Mutex::new(None)),
}
}

Expand All @@ -475,6 +515,8 @@ impl CliProcessManager {
self.stop()?;
self.ready.store(false, Ordering::SeqCst);
*self.bootstrap_token.lock() = None;
*self.session_cookie.lock() = None;
*self.auth_cookie_name.lock() = None;
{
let mut status = self.status.lock();
status.state = CliState::Starting;
Expand All @@ -491,6 +533,8 @@ impl CliProcessManager {
let job_arc = self.job.clone();
let ready_flag = self.ready.clone();
let token_arc = self.bootstrap_token.clone();
let session_cookie_arc = self.session_cookie.clone();
let auth_cookie_name_arc = self.auth_cookie_name.clone();
thread::spawn(move || {
if let Err(err) = Self::spawn_cli(
app.clone(),
Expand All @@ -500,6 +544,8 @@ impl CliProcessManager {
job_arc,
ready_flag,
token_arc,
session_cookie_arc,
auth_cookie_name_arc,
dev,
) {
log_line(&format!("cli spawn failed: {err}"));
Expand Down Expand Up @@ -594,6 +640,7 @@ impl CliProcessManager {
status.port = None;
status.url = None;
status.error = None;
*self.session_cookie.lock() = None;

Ok(())
}
Expand All @@ -602,13 +649,35 @@ impl CliProcessManager {
self.status.lock().clone()
}

pub fn desktop_event_stream_config(&self) -> Option<DesktopEventStreamConfig> {
let base_url = self.status.lock().url.clone()?;
let events_url = format!("{}/api/events", base_url.trim_end_matches('/'));
let client_id = format!("tauri-{}", std::process::id());
let cookie_name = self
.auth_cookie_name
.lock()
.clone()
.unwrap_or_else(|| SESSION_COOKIE_NAME_PREFIX.to_string());

Some(DesktopEventStreamConfig {
base_url,
events_url,
client_id,
connection_id: generate_transport_connection_id(),
cookie_name,
session_cookie: self.session_cookie.lock().clone(),
})
}

fn spawn_cli(
app: AppHandle,
status: Arc<Mutex<CliStatus>>,
child_holder: Arc<Mutex<Option<Child>>>,
#[cfg(windows)] job_holder: Arc<Mutex<Option<WindowsJobObject>>>,
ready: Arc<AtomicBool>,
bootstrap_token: Arc<Mutex<Option<String>>>,
session_cookie: Arc<Mutex<Option<String>>>,
auth_cookie_name_holder: Arc<Mutex<Option<String>>>,
dev: bool,
) -> anyhow::Result<()> {
log_line("resolving CLI entry");
Expand All @@ -619,6 +688,7 @@ impl CliProcessManager {
resolution.runner, resolution.entry, host
));
let auth_cookie_name = Arc::new(generate_auth_cookie_name());
*auth_cookie_name_holder.lock() = Some(auth_cookie_name.as_str().to_string());
let args = resolution.build_args(dev, &host, auth_cookie_name.as_str());
log_line(&format!("CLI args: {:?}", args));
if dev {
Expand Down Expand Up @@ -723,6 +793,7 @@ impl CliProcessManager {
let app_clone = app.clone();
let ready_clone = ready.clone();
let token_clone = bootstrap_token.clone();
let session_cookie_clone = session_cookie.clone();
let auth_cookie_name_clone = auth_cookie_name.clone();

thread::spawn(move || {
Expand All @@ -742,6 +813,7 @@ impl CliProcessManager {
let status = status_clone.clone();
let ready = ready_clone.clone();
let token = token_clone.clone();
let session_cookie = session_cookie_clone.clone();
let auth_cookie_name = auth_cookie_name_clone.clone();
thread::spawn(move || {
Self::process_stream(
Expand All @@ -751,6 +823,7 @@ impl CliProcessManager {
&status,
&ready,
&token,
&session_cookie,
auth_cookie_name.as_str(),
);
});
Expand All @@ -761,6 +834,7 @@ impl CliProcessManager {
let status = status_clone.clone();
let ready = ready_clone.clone();
let token = token_clone.clone();
let session_cookie = session_cookie_clone.clone();
let auth_cookie_name = auth_cookie_name_clone.clone();
thread::spawn(move || {
Self::process_stream(
Expand All @@ -770,6 +844,7 @@ impl CliProcessManager {
&status,
&ready,
&token,
&session_cookie,
auth_cookie_name.as_str(),
);
});
Expand Down Expand Up @@ -894,6 +969,7 @@ impl CliProcessManager {
status: &Arc<Mutex<CliStatus>>,
ready: &Arc<AtomicBool>,
bootstrap_token: &Arc<Mutex<Option<String>>>,
session_cookie: &Arc<Mutex<Option<String>>>,
auth_cookie_name: &str,
) {
let mut buffer = String::new();
Expand Down Expand Up @@ -946,6 +1022,7 @@ impl CliProcessManager {
status,
ready,
bootstrap_token,
session_cookie,
auth_cookie_name,
url,
);
Expand All @@ -963,6 +1040,7 @@ impl CliProcessManager {
status: &Arc<Mutex<CliStatus>>,
ready: &Arc<AtomicBool>,
bootstrap_token: &Arc<Mutex<Option<String>>>,
session_cookie: &Arc<Mutex<Option<String>>>,
auth_cookie_name: &str,
base_url: String,
) {
Expand Down Expand Up @@ -995,6 +1073,7 @@ impl CliProcessManager {
log_line(&format!("failed to set session cookie: {err}"));
navigate_main(app, &format!("{base_url}/login"));
} else {
*session_cookie.lock() = Some(session_id.clone());
navigate_main(app, &base_url);
}
}
Expand Down Expand Up @@ -1215,31 +1294,37 @@ fn resolve_dev_entry(_app: &AppHandle) -> Option<String> {
}

fn resolve_prod_entry(_app: &AppHandle) -> Option<String> {
let base = workspace_root();
let exe_dir = std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.to_path_buf()));

first_existing(prod_entry_candidates(exe_dir, base))
}

fn prod_entry_candidates(
exe_dir: Option<PathBuf>,
workspace: Option<PathBuf>,
) -> Vec<Option<PathBuf>> {
let mut candidates = Vec::new();

if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
candidates.push(Some(dir.join("resources/server/dist/bin.js")));
if let Some(dir) = exe_dir {
candidates.push(Some(dir.join("resources/server/dist/bin.js")));

let resources = dir.join("../Resources");
candidates.push(Some(resources.join("server/dist/bin.js")));
candidates.push(Some(resources.join("resources/server/dist/bin.js")));
let resources = dir.join("../Resources");
candidates.push(Some(resources.join("server/dist/bin.js")));
candidates.push(Some(resources.join("resources/server/dist/bin.js")));

let linux_resource_roots = [dir.join("../lib/CodeNomad"), dir.join("../lib/codenomad")];
for root in linux_resource_roots {
candidates.push(Some(root.join("server/dist/bin.js")));
candidates.push(Some(root.join("resources/server/dist/bin.js")));
}
let linux_resource_roots = [dir.join("../lib/CodeNomad"), dir.join("../lib/codenomad")];
for root in linux_resource_roots {
candidates.push(Some(root.join("server/dist/bin.js")));
candidates.push(Some(root.join("resources/server/dist/bin.js")));
}
}

let base = workspace_root();
candidates.push(
base.as_ref()
.map(|p| p.join("packages/server/dist/bin.js")),
);
candidates.push(workspace.map(|p| p.join("packages/server/dist/bin.js")));

first_existing(candidates)
candidates
}

fn build_shell_command_string(
Expand Down Expand Up @@ -1355,3 +1440,53 @@ fn normalize_path(path: PathBuf) -> String {
rendered
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;

static ENV_LOCK: StdMutex<()> = StdMutex::new(());

#[test]
fn prod_entry_candidates_prefer_exe_relative_before_workspace_fallback() {
let exe_dir = PathBuf::from("/opt/codenomad/bin");
let workspace = PathBuf::from("/workspace/codenomad");

let candidates = prod_entry_candidates(Some(exe_dir.clone()), Some(workspace.clone()))
.into_iter()
.flatten()
.collect::<Vec<_>>();

assert_eq!(
candidates.first(),
Some(&exe_dir.join("resources/server/dist/bin.js"))
);
assert_eq!(
candidates.last(),
Some(&workspace.join("packages/server/dist/bin.js"))
);
}

#[test]
fn augment_launch_url_trims_leading_fragment_marker() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
std::env::set_var("CODENOMAD_UI_LAUNCH_QUERY", "#debug=true");

let augmented = augment_launch_url("http://127.0.0.1:3000");

std::env::remove_var("CODENOMAD_UI_LAUNCH_QUERY");
assert_eq!(augmented, "http://127.0.0.1:3000?debug=true");
}

#[test]
fn augment_launch_url_trims_fragment_marker_when_query_exists() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
std::env::set_var("CODENOMAD_UI_LAUNCH_QUERY", "#debug=true");

let augmented = augment_launch_url("http://127.0.0.1:3000?existing=true");

std::env::remove_var("CODENOMAD_UI_LAUNCH_QUERY");
assert_eq!(augmented, "http://127.0.0.1:3000?existing=true&debug=true");
}
}
Loading
Loading