Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Hardcoded nvm Node version in PATH augmentation
- Replaced the version-specific ~/.nvm/versions/node/v24.13.0/bin entry with ~/.nvm/current/bin so PATH follows nvm’s active default when that symlink exists.
- ✅ Fixed: New browser webview navigates to URL twice
- ensure_browser now returns whether it created the webview, and browser_navigate skips navigate() on creation since WebviewUrl::External already loads the URL once.
Or push these changes by commenting:
@cursor push 77b4184628
Preview (77b4184628)
diff --git a/native/macos/comux-tauri/src-tauri/src/lib.rs b/native/macos/comux-tauri/src-tauri/src/lib.rs
--- a/native/macos/comux-tauri/src-tauri/src/lib.rs
+++ b/native/macos/comux-tauri/src-tauri/src/lib.rs
@@ -246,9 +246,9 @@
w: f64,
h: f64,
url: &str,
-) -> Result<(), String> {
+) -> Result<bool, String> {
if app.webviews().keys().any(|existing| existing == label) {
- return Ok(());
+ return Ok(false);
}
let main = app
@@ -318,7 +318,7 @@
)
.map_err(|e| e.to_string())?;
- Ok(())
+ Ok(true)
}
fn hide_webview(webview: &tauri::Webview) -> Result<(), String> {
@@ -342,12 +342,14 @@
h: f64,
) -> Result<(), String> {
let label = safe_browser_label(label);
- ensure_browser(&app, &label, x, y, w, h, &url)?;
- let webview = app
- .get_webview(&label)
- .ok_or_else(|| "browser webview missing".to_string())?;
- let parsed_url = Url::parse(&url).map_err(|e| e.to_string())?;
- webview.navigate(parsed_url).map_err(|e| e.to_string())?;
+ let created = ensure_browser(&app, &label, x, y, w, h, &url)?;
+ if !created {
+ let webview = app
+ .get_webview(&label)
+ .ok_or_else(|| "browser webview missing".to_string())?;
+ let parsed_url = Url::parse(&url).map_err(|e| e.to_string())?;
+ webview.navigate(parsed_url).map_err(|e| e.to_string())?;
+ }
Ok(())
}
@@ -734,7 +736,7 @@
for suffix in [
".cargo/bin",
".local/bin",
- ".nvm/versions/node/v24.13.0/bin",
+ ".nvm/current/bin",
".volta/bin",
".bun/bin",
".rbenv/shims",You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Code Review
This pull request implements a new Tauri-based macOS workspace shell, providing a unified interface for terminal management and web browsing. The core logic includes a Rust backend for PTY session handling and browser pane integration, complemented by a vanilla JavaScript frontend that supports slash commands and project-scoped browser tabs. Review feedback highlights a bug in process exit code retrieval, security concerns regarding the Content Security Policy, and opportunities to reduce brittleness in hardcoded environment paths and test assertions.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Sort/dedup key mismatch causes incomplete deduplication
- Updated
agent_skillsto sort by(name, kind, source)sodedup_byon(name, kind)compares consecutive duplicates.
- Updated
- ✅ Fixed: Global session lock held during PTY write I/O
pty_writenow clones the writerArcunderSESSIONSthen drops the map lock beforewrite_all/flushon the per-session writer mutex.
Or push these changes by commenting:
@cursor push a9f9546622
Preview (a9f9546622)
diff --git a/native/macos/comux-tauri/src-tauri/src/lib.rs b/native/macos/comux-tauri/src-tauri/src/lib.rs
--- a/native/macos/comux-tauri/src-tauri/src/lib.rs
+++ b/native/macos/comux-tauri/src-tauri/src/lib.rs
@@ -190,11 +190,14 @@
#[tauri::command]
fn pty_write(thread_id: String, bytes: Vec<u8>) -> Result<(), String> {
- let guard = SESSIONS.lock();
- let session = guard
- .get(&thread_id)
- .ok_or_else(|| format!("thread '{}' not found", thread_id))?;
- let mut writer = session.writer.lock();
+ let writer = {
+ let guard = SESSIONS.lock();
+ let session = guard
+ .get(&thread_id)
+ .ok_or_else(|| format!("thread '{}' not found", thread_id))?;
+ Arc::clone(&session.writer)
+ };
+ let mut writer = writer.lock();
writer.write_all(&bytes).map_err(|e| e.to_string())?;
writer.flush().map_err(|e| e.to_string())?;
Ok(())
@@ -503,7 +506,7 @@
}
}
- out.sort_by(|a, b| a.name.cmp(&b.name).then(a.source.cmp(&b.source)));
+ out.sort_by(|a, b| a.name.cmp(&b.name).then(a.kind.cmp(&b.kind)).then(a.source.cmp(&b.source)));
out.dedup_by(|a, b| a.name == b.name && a.kind == b.kind);
out
}You can send follow-ups to the cloud agent here.
95a9bb2 to
053abd7
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: PTY session insertion races with exit watcher thread
- The session is now inserted into SESSIONS before the exit watcher thread is spawned, so remove always runs after insert and cannot miss a fast-exiting child.
Or push these changes by commenting:
@cursor push d8c90225e9
Preview (d8c90225e9)
diff --git a/__tests__/tauriDesktopTabs.test.ts b/__tests__/tauriDesktopTabs.test.ts
--- a/__tests__/tauriDesktopTabs.test.ts
+++ b/__tests__/tauriDesktopTabs.test.ts
@@ -6,22 +6,62 @@
const mainJs = readFileSync(join(repoRoot, 'native/macos/comux-tauri/web/main.js'), 'utf8');
const stylesCss = readFileSync(join(repoRoot, 'native/macos/comux-tauri/web/styles.css'), 'utf8');
const tauriLib = readFileSync(join(repoRoot, 'native/macos/comux-tauri/src-tauri/src/lib.rs'), 'utf8');
+const indexHtml = readFileSync(join(repoRoot, 'native/macos/comux-tauri/web/index.html'), 'utf8');
+const tauriConfig = JSON.parse(
+ readFileSync(join(repoRoot, 'native/macos/comux-tauri/src-tauri/tauri.conf.json'), 'utf8')
+);
describe('Tauri desktop tab shortcuts', () => {
it('routes Command+T based on the last focused desktop surface', () => {
- expect(mainJs).toContain('var activeSurface = "terminal";');
- expect(mainJs).toContain('function createContextualTab()');
- expect(mainJs).toContain('markActiveSurface("terminal")');
- expect(mainJs).toContain('markActiveSurface("browser")');
- expect(mainJs).toContain('if (activeSurface === "browser") openBlankBrowserTab(); else spawnDefaultThread();');
+ expect(mainJs).toMatch(/var\s+activeSurface\s*=\s*"terminal";/);
+ expect(mainJs).toMatch(/function\s+createContextualTab\(\)/);
+ expect(mainJs).toMatch(/markActiveSurface\(\s*"terminal"\s*\)/);
+ expect(mainJs).toMatch(/markActiveSurface\(\s*"browser"\s*\)/);
+ expect(mainJs).toMatch(
+ /if\s*\(\s*activeSurface\s*===\s*"browser"\s*\)\s*openBlankBrowserTab\(\);\s*else\s*spawnDefaultThread\(\);/
+ );
});
it('lets embedded browser webviews request a new browser tab with Command+T', () => {
- expect(tauriLib).toContain('browser:shortcut-new-tab');
- expect(tauriLib).toContain('event.key.toLowerCase() === "t"');
- expect(mainJs).toContain('listen("browser:shortcut-new-tab"');
+ expect(tauriLib).toMatch(/browser:shortcut-new-tab/);
+ expect(tauriLib).toMatch(/event\.key\.toLowerCase\(\)\s*===\s*"t"/);
+ expect(tauriLib).toMatch(/function\(browserLabel\)/);
+ expect(tauriLib).not.toMatch(/label_json,\s*label_json/);
+ expect(mainJs).toMatch(/listen\(\s*"browser:shortcut-new-tab"/);
});
+ it('keeps browser navigation single-shot for newly created webviews', () => {
+ expect(tauriLib).toMatch(/fn\s+ensure_browser[\s\S]*?->\s*Result<bool,\s*String>/);
+ expect(tauriLib).toMatch(/return\s+Ok\(false\);/);
+ expect(tauriLib).toMatch(/let\s+created\s*=\s*ensure_browser\(/);
+ expect(tauriLib).toMatch(/if\s+!created\s*\{[\s\S]*?webview\.navigate\(parsed_url\)/);
+ });
+
+ it('reports PTY exit codes and avoids machine-specific nvm paths', () => {
+ expect(tauriLib).toMatch(/status\.ok\(\)\.map\(\|s\|\s*s\.exit_code\(\)\s+as\s+i32\)/);
+ expect(tauriLib).toMatch(/fn\s+newest_nvm_node_bin\(/);
+ expect(tauriLib).not.toMatch(/\.nvm\/versions\/node\/v\d+\.\d+\.\d+\/bin/);
+ });
+
+ it('keeps Tauri backend shared-state operations grouped correctly', () => {
+ expect(tauriLib).toMatch(
+ /let\s+writer\s*=\s*\{[\s\S]*?let\s+guard\s*=\s*SESSIONS\.lock\(\);[\s\S]*?Arc::clone\(&session\.writer\)[\s\S]*?\};[\s\S]*?let\s+mut\s+writer\s*=\s*writer\.lock\(\);/
+ );
+ expect(tauriLib).toMatch(
+ /out\.sort_by\(\|a,\s*b\|\s*\{[\s\S]*?a\.name[\s\S]*?\.cmp\(&b\.name\)[\s\S]*?\.then\(a\.kind\.cmp\(&b\.kind\)\)[\s\S]*?\.then\(a\.source\.cmp\(&b\.source\)\)/
+ );
+ expect(tauriLib).toMatch(/out\.dedup_by\(\|a,\s*b\|\s*a\.name\s*==\s*b\.name\s*&&\s*a\.kind\s*==\s*b\.kind\)/);
+ });
+
+ it('keeps the Tauri app CSP free of broad unsafe allowances', () => {
+ const csp = tauriConfig.app.security.csp as string;
+ expect(csp).toMatch(/style-src\s+'self'/);
+ expect(csp).toMatch(/script-src\s+'self'/);
+ expect(csp).not.toMatch(/'unsafe-inline'|'unsafe-eval'/);
+ expect(indexHtml).not.toMatch(/\sstyle=/);
+ expect(mainJs).not.toMatch(/style\.cssText/);
+ });
+
it('keeps browser tabs thin and collapsible under narrow browser panes', () => {
expect(stylesCss).toMatch(/--browser-tab-h:\s*22px;/);
expect(stylesCss).toMatch(/grid-template-rows:\s*var\(--browser-bar-h\) var\(--browser-tab-h\) 1fr;/);
diff --git a/native/macos/comux-tauri/src-tauri/src/lib.rs b/native/macos/comux-tauri/src-tauri/src/lib.rs
--- a/native/macos/comux-tauri/src-tauri/src/lib.rs
+++ b/native/macos/comux-tauri/src-tauri/src/lib.rs
@@ -8,8 +8,8 @@
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
use serde::{Deserialize, Serialize};
use tauri::{
- webview::{PageLoadEvent, WebviewBuilder}, AppHandle, Emitter, LogicalPosition, LogicalSize,
- Manager, Url, WebviewUrl,
+ webview::{PageLoadEvent, WebviewBuilder},
+ AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Url, WebviewUrl,
};
const BROWSER_LABEL_PREFIX: &str = "comux-browser-";
@@ -21,7 +21,11 @@
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.take(64)
.collect();
- format!("{}{}", BROWSER_LABEL_PREFIX, if safe.is_empty() { "default" } else { &safe })
+ format!(
+ "{}{}",
+ BROWSER_LABEL_PREFIX,
+ if safe.is_empty() { "default" } else { &safe }
+ )
}
// ----------------------------------------------------------------------------
@@ -88,9 +92,7 @@
})
.map_err(|e| e.to_string())?;
- let command = options
- .command
- .unwrap_or_else(|| "/bin/zsh".to_string());
+ let command = options.command.unwrap_or_else(|| "/bin/zsh".to_string());
let args = options.args.unwrap_or_else(|| vec!["-l".to_string()]);
let mut cmd = CommandBuilder::new(command);
cmd.args(args);
@@ -134,7 +136,24 @@
cmd.env_remove("PREFIX");
let mut child = pair.slave.spawn_command(cmd).map_err(|e| e.to_string())?;
+
+ let mut reader = pair.master.try_clone_reader().map_err(|e| e.to_string())?;
+ let writer = pair.master.take_writer().map_err(|e| e.to_string())?;
+
let exit_thread_id = thread_id.clone();
+ let data_thread_id = thread_id.clone();
+
+ {
+ let mut guard = SESSIONS.lock();
+ guard.insert(
+ thread_id,
+ PtySession {
+ master: pair.master,
+ writer: Arc::new(Mutex::new(writer)),
+ },
+ );
+ }
+
let app_for_exit = app.clone();
std::thread::spawn(move || {
let status = child.wait();
@@ -142,12 +161,7 @@
let mut guard = SESSIONS.lock();
guard.remove(&exit_thread_id);
}
- let code = status.ok().and_then(|s| {
- // ExitStatus is opaque; best-effort extraction.
- #[allow(unused_variables)]
- let success = s.success();
- None
- });
+ let code = status.ok().map(|s| s.exit_code() as i32);
let _ = app_for_exit.emit(
"pty:exit",
PtyExitEvent {
@@ -157,10 +171,6 @@
);
});
- let mut reader = pair.master.try_clone_reader().map_err(|e| e.to_string())?;
- let writer = pair.master.take_writer().map_err(|e| e.to_string())?;
-
- let data_thread_id = thread_id.clone();
let app_for_data = app.clone();
std::thread::spawn(move || {
let mut buf = [0u8; 4096];
@@ -179,27 +189,19 @@
}
});
- {
- let mut guard = SESSIONS.lock();
- guard.insert(
- thread_id,
- PtySession {
- master: pair.master,
- writer: Arc::new(Mutex::new(writer)),
- },
- );
- }
-
Ok(())
}
#[tauri::command]
fn pty_write(thread_id: String, bytes: Vec<u8>) -> Result<(), String> {
- let guard = SESSIONS.lock();
- let session = guard
- .get(&thread_id)
- .ok_or_else(|| format!("thread '{}' not found", thread_id))?;
- let mut writer = session.writer.lock();
+ let writer = {
+ let guard = SESSIONS.lock();
+ let session = guard
+ .get(&thread_id)
+ .ok_or_else(|| format!("thread '{}' not found", thread_id))?;
+ Arc::clone(&session.writer)
+ };
+ let mut writer = writer.lock();
writer.write_all(&bytes).map_err(|e| e.to_string())?;
writer.flush().map_err(|e| e.to_string())?;
Ok(())
@@ -246,9 +248,9 @@
w: f64,
h: f64,
url: &str,
-) -> Result<(), String> {
+) -> Result<bool, String> {
if app.webviews().keys().any(|existing| existing == label) {
- return Ok(());
+ return Ok(false);
}
let main = app
@@ -275,7 +277,7 @@
if matches!(payload.event(), PageLoadEvent::Finished) {
let label_json = serde_json::to_string(&browser_label).unwrap_or_else(|_| "null".to_string());
let script = format!(
- r#"(function() {{
+ r#"(function(browserLabel) {{
try {{
var emit = function(name, payload) {{
if (window.__TAURI__ && window.__TAURI__.event) {{
@@ -283,7 +285,7 @@
}}
}};
var title = document.title || location.hostname || location.href;
- emit("browser:title", {{ label: {}, title: title, url: location.href }});
+ emit("browser:title", {{ label: browserLabel, title: title, url: location.href }});
if (!window.__COMUX_BROWSER_SHORTCUTS_INSTALLED__) {{
window.__COMUX_BROWSER_SHORTCUTS_INSTALLED__ = true;
window.addEventListener("keydown", function(event) {{
@@ -291,20 +293,20 @@
if ((event.metaKey || event.ctrlKey) && event.key && event.key.toLowerCase() === "t") {{
event.preventDefault();
event.stopPropagation();
- emit("browser:shortcut-new-tab", {{ label: {}, url: location.href }});
+ emit("browser:shortcut-new-tab", {{ label: browserLabel, url: location.href }});
}}
}} catch (_) {{}}
}}, true);
window.addEventListener("pointerdown", function() {{
- emit("browser:focus", {{ label: {}, url: location.href }});
+ emit("browser:focus", {{ label: browserLabel, url: location.href }});
}}, true);
window.addEventListener("focusin", function() {{
- emit("browser:focus", {{ label: {}, url: location.href }});
+ emit("browser:focus", {{ label: browserLabel, url: location.href }});
}}, true);
}}
}} catch (_) {{}}
- }})();"#,
- label_json, label_json, label_json, label_json
+ }})({});"#,
+ label_json
);
let _ = webview.eval(&script);
}
@@ -318,7 +320,7 @@
)
.map_err(|e| e.to_string())?;
- Ok(())
+ Ok(true)
}
fn hide_webview(webview: &tauri::Webview) -> Result<(), String> {
@@ -342,12 +344,14 @@
h: f64,
) -> Result<(), String> {
let label = safe_browser_label(label);
- ensure_browser(&app, &label, x, y, w, h, &url)?;
- let webview = app
- .get_webview(&label)
- .ok_or_else(|| "browser webview missing".to_string())?;
- let parsed_url = Url::parse(&url).map_err(|e| e.to_string())?;
- webview.navigate(parsed_url).map_err(|e| e.to_string())?;
+ let created = ensure_browser(&app, &label, x, y, w, h, &url)?;
+ if !created {
+ let webview = app
+ .get_webview(&label)
+ .ok_or_else(|| "browser webview missing".to_string())?;
+ let parsed_url = Url::parse(&url).map_err(|e| e.to_string())?;
+ webview.navigate(parsed_url).map_err(|e| e.to_string())?;
+ }
Ok(())
}
@@ -385,7 +389,8 @@
fn browser_hide_all_except(app: AppHandle, label: Option<String>) -> Result<(), String> {
let keep = label.map(|raw| safe_browser_label(Some(raw)));
for (existing_label, webview) in app.webviews() {
- if existing_label.starts_with(BROWSER_LABEL_PREFIX) && Some(existing_label.clone()) != keep {
+ if existing_label.starts_with(BROWSER_LABEL_PREFIX) && Some(existing_label.clone()) != keep
+ {
hide_webview(&webview)?;
}
}
@@ -506,7 +511,12 @@
}
}
- out.sort_by(|a, b| a.name.cmp(&b.name).then(a.source.cmp(&b.source)));
+ out.sort_by(|a, b| {
+ a.name
+ .cmp(&b.name)
+ .then(a.kind.cmp(&b.kind))
+ .then(a.source.cmp(&b.source))
+ });
out.dedup_by(|a, b| a.name == b.name && a.kind == b.kind);
out
}
@@ -594,7 +604,11 @@
/// the walk so we never recurse into node_modules or git history.
fn scan_claude_plugins(root: &Path, out: &mut Vec<AgentSkillEntry>) {
fn plugin_name_from_manifest(dir: &Path) -> Option<String> {
- for rel in [".plugin/plugin.json", ".claude-plugin/plugin.json", "package.json"] {
+ for rel in [
+ ".plugin/plugin.json",
+ ".claude-plugin/plugin.json",
+ "package.json",
+ ] {
let path = dir.join(rel);
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
@@ -667,11 +681,7 @@
break;
}
if let Some(rest) = line.strip_prefix("description:") {
- let value = rest
- .trim()
- .trim_matches('"')
- .trim_matches('\'')
- .to_string();
+ let value = rest.trim().trim_matches('"').trim_matches('\'').to_string();
if !value.is_empty() {
return Some(truncate_oneline(&value));
}
@@ -731,26 +741,68 @@
}
// Plus common user-installed runtime managers on macOS.
if let Ok(home) = std::env::var("HOME") {
+ let home_path = Path::new(&home);
for suffix in [
".cargo/bin",
".local/bin",
- ".nvm/versions/node/v24.13.0/bin",
".volta/bin",
".bun/bin",
".rbenv/shims",
".pyenv/shims",
] {
- let candidate = format!("{}/{}", home, suffix);
- if std::path::Path::new(&candidate).is_dir()
- && !parts.iter().any(|p| p == &candidate)
- {
- parts.push(candidate);
- }
+ push_path_if_dir(&mut parts, home_path.join(suffix));
}
+ if let Some(nvm_bin) = newest_nvm_node_bin(home_path) {
+ push_path_if_dir(&mut parts, nvm_bin);
+ }
}
parts.join(":")
}
+fn push_path_if_dir(parts: &mut Vec<String>, candidate: PathBuf) {
+ if !candidate.is_dir() {
+ return;
+ }
+ let candidate = candidate.to_string_lossy().to_string();
+ if !parts.iter().any(|p| p == &candidate) {
+ parts.push(candidate);
+ }
+}
+
+fn newest_nvm_node_bin(home: &Path) -> Option<PathBuf> {
+ let versions_dir = home.join(".nvm").join("versions").join("node");
+ let mut newest: Option<((u32, u32, u32), PathBuf)> = None;
+ for entry in std::fs::read_dir(versions_dir).ok()? {
+ let Ok(entry) = entry else {
+ continue;
+ };
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+ let name = entry.file_name();
+ let Some(version) = parse_nvm_node_version(&name.to_string_lossy()) else {
+ continue;
+ };
+ if newest
+ .as_ref()
+ .map_or(true, |(current, _)| version > *current)
+ {
+ newest = Some((version, path));
+ }
+ }
+ newest.map(|(_, path)| path.join("bin"))
+}
+
+fn parse_nvm_node_version(name: &str) -> Option<(u32, u32, u32)> {
+ let trimmed = name.strip_prefix('v').unwrap_or(name);
+ let mut parts = trimmed.split('.');
+ let major = parts.next()?.parse().ok()?;
+ let minor = parts.next().unwrap_or("0").parse().ok()?;
+ let patch = parts.next().unwrap_or("0").parse().ok()?;
+ Some((major, minor, patch))
+}
+
fn which_on_path(binary: &str) -> Option<String> {
for dir in augmented_path().split(':') {
let candidate = std::path::Path::new(dir).join(binary);
@@ -817,7 +869,9 @@
.setup(|app| {
#[cfg(target_os = "macos")]
{
- use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial, NSVisualEffectState};
+ use window_vibrancy::{
+ apply_vibrancy, NSVisualEffectMaterial, NSVisualEffectState,
+ };
if let Some(window) = app.get_webview_window("main") {
let _ = apply_vibrancy(
&window,
diff --git a/native/macos/comux-tauri/src-tauri/tauri.conf.json b/native/macos/comux-tauri/src-tauri/tauri.conf.json
--- a/native/macos/comux-tauri/src-tauri/tauri.conf.json
+++ b/native/macos/comux-tauri/src-tauri/tauri.conf.json
@@ -28,7 +28,7 @@
"withGlobalTauri": true,
"macOSPrivateApi": true,
"security": {
- "csp": "default-src 'self'; img-src 'self' data: https: http:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; frame-src https: http:; connect-src 'self' ipc: http://ipc.localhost https: http:"
+ "csp": "default-src 'self'; img-src 'self' data: https: http:; style-src 'self'; script-src 'self'; frame-src https: http:; connect-src 'self' ipc: http://ipc.localhost https: http:"
}
},
"bundle": {
diff --git a/native/macos/comux-tauri/web/index.html b/native/macos/comux-tauri/web/index.html
--- a/native/macos/comux-tauri/web/index.html
+++ b/native/macos/comux-tauri/web/index.html
@@ -39,7 +39,7 @@
</div>
</header>
- <main class="detail" id="detail" data-layout="terminal" data-browser-side="right" style="--split-frac: 0.6;">
+ <main class="detail" id="detail" data-layout="terminal" data-browser-side="right">
<section class="terminal-area">
<div class="tab-strip" id="tab-strip"></div>
<div class="terminal-host" id="terminal-host"></div>
diff --git a/native/macos/comux-tauri/web/main.js b/native/macos/comux-tauri/web/main.js
--- a/native/macos/comux-tauri/web/main.js
+++ b/native/macos/comux-tauri/web/main.js
@@ -14,8 +14,7 @@
if (!host) return;
host.innerHTML = "";
var pre = document.createElement("pre");
- pre.style.cssText =
- "color:#ff6b6b;padding:24px;font-family:ui-monospace,SF Mono,Menlo,monospace;white-space:pre-wrap;";
+ pre.className = "boot-error";
pre.textContent = "comux boot error\n\n" + msg;
host.appendChild(pre);
}
@@ -254,10 +253,35 @@
var browserCollapseBtn = document.getElementById("browser-collapse");
var browserCycleSideBtn = document.getElementById("browser-cycle-side");
var BROWSER_SIDES = ["right", "bottom", "left", "top"];
+ var detailStyleRule = null;
function currentLayout() { return detail.dataset.layout || "terminal"; }
function currentSide() { return detail.dataset.browserSide || "right"; }
- function currentSplitFrac() { return parseFloat(detail.style.getPropertyValue("--split-frac")) || 0.6; }
+ function getDetailStyleRule() {
+ if (detailStyleRule) return detailStyleRule;
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ var rules;
+ try { rules = document.styleSheets[i].cssRules; } catch (_) { continue; }
+ for (var j = 0; j < rules.length; j++) {
+ if (rules[j].selectorText === ".detail") {
+ detailStyleRule = rules[j];
+ return detailStyleRule;
+ }
+ }
+ }
+ return null;
+ }
+ function setDetailSplitFrac(value) {
+ var rule = getDetailStyleRule();
+ if (rule) rule.style.setProperty("--split-frac", String(value));
+ else detail.style.setProperty("--split-frac", String(value));
+ }
+ function currentSplitFrac() {
+ var rule = getDetailStyleRule();
+ var value = rule ? rule.style.getPropertyValue("--split-frac") : "";
+ if (!value) value = window.getComputedStyle(detail).getPropertyValue("--split-frac");
+ return parseFloat(value) || 0.6;
+ }
function ensureProjectLayout(project) {
if (!project) return null;
if (!project.layout) project.layout = { mode: "terminal", side: "right", splitFrac: 0.6 };
@@ -275,7 +299,7 @@
function restoreProjectLayout(project) {
var layout = ensureProjectLayout(project);
if (!layout) return;
- detail.style.setProperty("--split-frac", String(layout.splitFrac || 0.6));
+ setDetailSplitFrac(layout.splitFrac || 0.6);
applyLayout(layout.mode || "terminal", { side: layout.side || "right", persist: false });
}
@@ -1446,7 +1470,7 @@
function syncUrlInput() {
var tab = currentBrowserTab();
if (urlInput) urlInput.value = tab && tab.url !== "about:blank" ? tab.url : "";
- if (previewEmpty) previewEmpty.style.display = tab && tab.created ? "none" : "";
+ if (previewEmpty) previewEmpty.hidden = !!(tab && tab.created);
if (preview) preview.classList.toggle("loading", !!(tab && tab.loading));
updateBrowserControls();
}
@@ -1470,7 +1494,7 @@
invoke("browser_navigate", { label: label, url: normalised, x: b.x, y: b.y, w: b.w, h: b.h }).then(function () {
tab.created = true; tab.url = normalised;
if (!opts.fromHistory && !opts.preserveHistory) { tab.history = opts.replace ? [] : tab.history.slice(0, tab.historyIndex + 1); tab.history.push(normalised); tab.historyIndex = tab.history.length - 1; }
- if (previewEmpty) previewEmpty.style.display = "none";
+ if (previewEmpty) previewEmpty.hidden = true;
renderBrowserTabs(); syncUrlInput(); saveWorkspaceSoon(); invoke("browser_hide_all_except", { label: label }).catch(function () {});
setTimeout(function () {
if (tab.loading && tab.url === normalised) markBrowserTabLoaded(nativeBrowserLabel(label), normalised, "");
@@ -1540,7 +1564,7 @@
function setSplitFrac(frac) {
var bounds = splitClampBounds();
var next = Math.max(bounds.min, Math.min(bounds.max, frac));
- detail.style.setProperty("--split-frac", next.toFixed(4));
+ setDetailSplitFrac(next.toFixed(4));
splitter.setAttribute("aria-valuenow", String(Math.round(next * 100)));
rememberProjectLayout();
scheduleSplitLayoutSync();
@@ -1594,7 +1618,7 @@
// tracks the side so the splitter feels physical. Shift halves the step.
splitter.addEventListener("keydown", function (e) {
if (currentLayout() !== "split") return;
- var current = parseFloat(detail.style.getPropertyValue("--split-frac")) || 0.6;
+ var current = currentSplitFrac();
var step = e.shiftKey ? 0.01 : 0.04;
var side = currentSide();
var grow, shrink;
@@ -1605,7 +1629,7 @@
if (e.key === shrink) { setSplitFrac(current - step); e.preventDefault(); }
else if (e.key === grow) { setSplitFrac(current + step); e.preventDefault(); }
});
- setSplitFrac(parseFloat(detail.style.getPropertyValue("--split-frac")) || 0.6);
+ setSplitFrac(currentSplitFrac());
}
// ============================================================
diff --git a/native/macos/comux-tauri/web/styles.css b/native/macos/comux-tauri/web/styles.css
--- a/native/macos/comux-tauri/web/styles.css
+++ b/native/macos/comux-tauri/web/styles.css
@@ -195,6 +195,7 @@
--browser-min: 72px;
--terminal-min-y: 160px;
--browser-min-y: 54px;
+ --split-frac: 0.6;
--terminal-col: calc(var(--split-frac, 0.6) * 100%);
transition: grid-template-columns var(--transition-layout),
grid-template-rows var(--transition-layout);
@@ -202,6 +203,13 @@
}
.detail.resizing { transition: none; }
+.boot-error {
+ color: #ff6b6b;
+ padding: 24px;
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
+ white-space: pre-wrap;
+}
+
/* terminal-only & browser-only: hide unused side */
.detail[data-layout="terminal"] {
grid-template-columns: 1fr;You can send follow-ups to the cloud agent here.
053abd7 to
dbc35fd
Compare
dbc35fd to
fdfddc1
Compare
fdfddc1 to
ff2b482
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Agent skills dedup keeps plugins over user/project overrides
- Reversed the tertiary sort key to
b.source.cmp(&a.source)so user/project rows sort before plugin rows anddedup_bykeeps the override.
- Reversed the tertiary sort key to
Or push these changes by commenting:
@cursor push eec81d0986
Preview (eec81d0986)
diff --git a/native/macos/comux-tauri/src-tauri/src/lib.rs b/native/macos/comux-tauri/src-tauri/src/lib.rs
--- a/native/macos/comux-tauri/src-tauri/src/lib.rs
+++ b/native/macos/comux-tauri/src-tauri/src/lib.rs
@@ -535,7 +535,7 @@
a.name
.cmp(&b.name)
.then(a.kind.cmp(&b.kind))
- .then(a.source.cmp(&b.source))
+ .then(b.source.cmp(&a.source))
});
out.dedup_by(|a, b| a.name == b.name && a.kind == b.kind);
outYou can send follow-ups to the cloud agent here.
ff2b482 to
5abc0c5
Compare
5abc0c5 to
f803def
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Redundant
augmented_path()recomputation on every call- The augmented PATH string is now built once on first use via
once_cell::sync::Lazyand reused bypty_startandwhich_on_path.
- The augmented PATH string is now built once on first use via
Or push these changes by commenting:
@cursor push c54f4850d1
Preview (c54f4850d1)
diff --git a/native/macos/comux-tauri/src-tauri/src/lib.rs b/native/macos/comux-tauri/src-tauri/src/lib.rs
--- a/native/macos/comux-tauri/src-tauri/src/lib.rs
+++ b/native/macos/comux-tauri/src-tauri/src/lib.rs
@@ -40,6 +40,7 @@
static SESSIONS: Lazy<Mutex<HashMap<String, PtySession>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
static STARTING_SESSIONS: Lazy<Mutex<HashSet<String>>> = Lazy::new(|| Mutex::new(HashSet::new()));
+static AUGMENTED_PATH: Lazy<String> = Lazy::new(build_augmented_path);
struct PendingPtyStart {
thread_id: String,
@@ -124,8 +125,7 @@
// /opt/homebrew/bin, so comux can't find tmux/git/gh/etc. Augment PATH
// with the conventional locations, and provide reasonable defaults for
// TERM / COLORTERM / LANG so xterm.js renders unicode + truecolor.
- let augmented_path = augmented_path();
- cmd.env("PATH", &augmented_path);
+ cmd.env("PATH", augmented_path());
cmd.env("TERM", "xterm-256color");
cmd.env("COLORTERM", "truecolor");
cmd.env("COMUX_TAURI", "1");
@@ -758,7 +758,11 @@
}
}
-fn augmented_path() -> String {
+fn augmented_path() -> &'static str {
+ AUGMENTED_PATH.as_str()
+}
+
+fn build_augmented_path() -> String {
let existing = std::env::var("PATH").unwrap_or_default();
let extras = [
"/opt/homebrew/bin",You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit f803def. Configure here.
f803def to
ca05550
Compare


Summary
.comux/**from VitestVerification
pnpm vitest --run __tests__/tauriDesktopTabs.test.tsnode --check native/macos/comux-tauri/web/main.js(cd native/macos/comux-tauri/src-tauri && cargo check)pnpm run typecheckpnpm test— 114 passed, 1 skipped; 616 passed, 11 skippedSHA256:Apqee2EOZLXmfVUUy/QX1QLBn9g10SuzHbK5ARks19ENotes
.opencode/was left untracked and excluded.cargo fmt --checkwas not applied because the existinglib.rsformatting differs broadly from rustfmt; avoiding unrelated formatting churn in this PR.Note
Medium Risk
Adds a full new macOS Tauri desktop prototype (Rust backend + web UI + Cargo dependencies) and new keyboard/IPC behavior, so regressions could affect desktop PTY/session handling and embedded browser webviews. Risk is tempered by new targeted tests and a strict CSP, but the large dependency lockfile increases review surface.
Overview
Adds a macOS Tauri desktop prototype (
native/macos/comux-tauri) with a Rust backend for multi-PTY sessions, an embedded browser webview pane, and environment/agent-skill discovery commands exposed viatauri::command.Implements contextual
Command+Tbehavior: the shell routes new-tab to either spawning a terminal thread or opening a blank browser tab based on the last focused surface, and embedded browser webviews emit focus/shortcut events back to the shell.Tightens desktop safety/portability by enforcing a CSP without broad unsafe directives, improving PATH augmentation for Finder-launched apps (incl. dynamic nvm detection), and updates tests to disable git commit signing in temp repos; adds a vitest suite (
tauriDesktopTabs.test.ts) that asserts these invariants and keeps browser tabs thin/collapsible under narrow panes.Reviewed by Cursor Bugbot for commit ca05550. Bugbot is set up for automated code reviews on this repo. Configure here.