Skip to content

Add desktop Command-T tab shortcuts#2

Merged
BunsDev merged 2 commits intomainfrom
cody/desktop-command-t-tabs
May 5, 2026
Merged

Add desktop Command-T tab shortcuts#2
BunsDev merged 2 commits intomainfrom
cody/desktop-command-t-tabs

Conversation

@BunsDev
Copy link
Copy Markdown
Owner

@BunsDev BunsDev commented May 4, 2026

Summary

  • add the Tauri desktop prototype to the repo so desktop app behavior is reviewable/testable
  • route Command+T by focused surface: terminal side opens a new terminal thread, browser side opens a new browser tab
  • let embedded browser webviews emit Command+T/focus events back to the app shell
  • make browser tabs thin, shrinkable, and collapsible for narrow panes
  • keep prior test/doc generation fixes: disable commit signing in temp git test repos, stable generated hook-doc version, exclude .comux/** from Vitest

Verification

  • pnpm vitest --run __tests__/tauriDesktopTabs.test.ts
  • node --check native/macos/comux-tauri/web/main.js
  • (cd native/macos/comux-tauri/src-tauri && cargo check)
  • pnpm run typecheck
  • pnpm test — 114 passed, 1 skipped; 616 passed, 11 skipped
  • signed commit verified with SSH signing key SHA256:Apqee2EOZLXmfVUUy/QX1QLBn9g10SuzHbK5ARks19E

Notes

  • .opencode/ was left untracked and excluded.
  • cargo fmt --check was not applied because the existing lib.rs formatting 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 via tauri::command.

Implements contextual Command+T behavior: 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.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comux-docs Ready Ready Preview May 4, 2026 7:09am

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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.

Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs Outdated
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs Outdated
Comment thread __tests__/tauriDesktopTabs.test.ts
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Comment thread native/macos/comux-tauri/src-tauri/tauri.conf.json Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_skills to sort by (name, kind, source) so dedup_by on (name, kind) compares consecutive duplicates.
  • ✅ Fixed: Global session lock held during PTY write I/O
    • pty_write now clones the writer Arc under SESSIONS then drops the map lock before write_all/flush on the per-session writer mutex.

Create PR

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.

Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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.

Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs Outdated
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs Outdated
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 and dedup_by keeps the override.

Create PR

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);
     out

You can send follow-ups to the cloud agent here.

Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs Outdated
Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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::Lazy and reused by pty_start and which_on_path.

Create PR

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.

Comment thread native/macos/comux-tauri/src-tauri/src/lib.rs
@BunsDev BunsDev force-pushed the cody/desktop-command-t-tabs branch from f803def to ca05550 Compare May 4, 2026 07:09
@BunsDev BunsDev merged commit b46453c into main May 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant