Compare commits

...

5 Commits

Author SHA1 Message Date
Pascal André
35b171764e fix(desktop): align Electron package and runtime app ids (#342)
Follow-up from #334

## Summary
- align the Electron package `build.appId` with the runtime identifier
already used in `app.setAppUserModelId(...)`
- remove the mismatch between packaged desktop identity and runtime
desktop identity
- keep the change narrowly scoped to identifier consistency only

## Validation
- verified the previous mismatch in `packages/electron-app/package.json`
vs `packages/electron-app/electron/main/main.ts`
- updated the packaging id to match the runtime id exactly
2026-04-18 23:56:58 +01:00
Pascal André
6b53ab2d73 fix(ui): prevent session status labels from being retranslated (#339)
Fixes #273

## Summary
- mark the session list header label as non-translatable
- mark compact session status badges as non-translatable
- prevent browser/page translation from duplicating already localized
labels like the repeated idle badge shown in #273

## Validation
- `npm run build --workspace @codenomad/ui`
2026-04-18 23:49:38 +01:00
Pascal André
1b829094ef fix(desktop): improve Linux desktop icon integration (#334)
Refs #330

## Summary
- add standard Linux hicolor icon sizes to the Tauri package outputs
- enable the GTK app id on Linux and ship a matching reverse-DNS desktop
entry alias for shell association
- mark the alias desktop entry `NoDisplay=true` so it does not surface
as a duplicate launcher in desktop menus
- include the same alias desktop entry for AppImage so the fix is not
limited to deb/rpm packages

## Validation
- confirmed in the Linux VM that the desktop-integrated launch no longer
shows the generic taskbar icon
- verified the alias desktop entry is now hidden from app menus via
`NoDisplay=true`
- attempted a fresh `tauri build --bundles deb`; the build still hits
the known optional `@tauri-apps/cli` native-binding issue in this
workspace after prebuild, not a code/config error from this PR
2026-04-18 23:46:03 +01:00
Pascal André
e28e9f5879 fix(desktop): show explicit missing Node errors (#336)
Fixes #294

## Summary
- detect missing desktop Node runtimes before spawning the bundled CLI
- return a clear error message that tells users to install Node.js or
set `NODE_BINARY`
- handle both direct spawns and desktop-shell launches consistently

## Validation
- `npm run bundle:server --workspace @codenomad/tauri-app && cargo build
--manifest-path packages/tauri-app/src-tauri/Cargo.toml`
- exercised the missing-runtime path in the Linux VM by launching with
an invalid `NODE_BINARY`
2026-04-18 23:39:39 +01:00
Pascal André
cb84547c88 fix(desktop): source shell rc before launching CLI (#332)
Fixes #326

## Summary
- source the user's bash or zsh rc before launching the bundled CLI from
Tauri
- use `-l -i -c` for zsh so shell-managed Node runtimes are available in
launcher-started sessions
- fixes the reproduced Linux launcher case where the app exits with `CLI
exited early: exit status: 127` while terminal launches work

## Validation
- reproduced the failure with the released Tauri `v0.14.0` Linux binary
- verified the patched binary succeeds under the same launcher-like
environment
- ran `cargo build` on the dev-based PR branch
2026-04-18 23:34:49 +01:00
13 changed files with 87 additions and 33 deletions

View File

@@ -20,24 +20,10 @@ function getDefaultShellPath(): string {
return "/bin/bash" return "/bin/bash"
} }
function wrapCommandForShell(command: string, shellPath: string): string {
const shellName = path.basename(shellPath)
if (shellName.includes("bash")) {
return 'if [ -f ~/.bashrc ]; then source ~/.bashrc >/dev/null 2>&1; fi; ' + command
}
if (shellName.includes("zsh")) {
return 'if [ -f ~/.zshrc ]; then source ~/.zshrc >/dev/null 2>&1; fi; ' + command
}
return command
}
function buildShellArgs(shellPath: string): string[] { function buildShellArgs(shellPath: string): string[] {
const shellName = path.basename(shellPath) const shellName = path.basename(shellPath)
if (shellName.includes("zsh")) { if (shellName.includes("zsh") || shellName.includes("bash")) {
return ["-l", "-i", "-c"] return ["-i", "-l", "-c"]
} }
return ["-l", "-c"] return ["-l", "-c"]
} }
@@ -59,12 +45,11 @@ export function buildUserShellCommand(userCommand: string): ShellCommand {
} }
const shellPath = getDefaultShellPath() const shellPath = getDefaultShellPath()
const script = wrapCommandForShell(userCommand, shellPath)
const args = buildShellArgs(shellPath) const args = buildShellArgs(shellPath)
return { return {
command: shellPath, command: shellPath,
args: [...args, script], args: [...args, userCommand],
} }
} }

View File

@@ -62,7 +62,7 @@
"vite-plugin-solid": "^2.10.0" "vite-plugin-solid": "^2.10.0"
}, },
"build": { "build": {
"appId": "ai.opencode.client", "appId": "ai.neuralnomads.codenomad.client",
"productName": "CodeNomad", "productName": "CodeNomad",
"directories": { "directories": {
"output": "release", "output": "release",

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Categories=
Exec=codenomad-tauri
StartupWMClass=codenomad-tauri
Icon=codenomad-tauri
Name=CodeNomad
NoDisplay=true
Terminal=false
Type=Application

View File

@@ -38,6 +38,7 @@ use windows_sys::Win32::System::JobObjects::{
#[cfg(windows)] #[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
const MISSING_NODE_PREFIX: &str = "CODENOMAD_MISSING_NODE:";
#[cfg(windows)] #[cfg(windows)]
#[derive(Debug)] #[derive(Debug)]
@@ -630,6 +631,13 @@ impl CliProcessManager {
let use_user_shell = supports_user_shell(); let use_user_shell = supports_user_shell();
if !use_user_shell && which::which(&resolution.node_binary).is_err() {
return Err(anyhow::anyhow!(
"Node binary '{}' not found. CodeNomad desktop currently requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
resolution.node_binary
));
}
let command_info = if use_user_shell { let command_info = if use_user_shell {
log_line("spawning via user shell"); log_line("spawning via user shell");
ShellCommandType::UserShell(build_shell_command_string(&resolution, &args)?) ShellCommandType::UserShell(build_shell_command_string(&resolution, &args)?)
@@ -641,14 +649,6 @@ impl CliProcessManager {
}) })
}; };
if !use_user_shell {
if which::which(&resolution.node_binary).is_err() {
return Err(anyhow::anyhow!(
"Node binary not found. Make sure Node.js is installed."
));
}
}
let child = match &command_info { let child = match &command_info {
ShellCommandType::UserShell(cmd) => { ShellCommandType::UserShell(cmd) => {
log_line(&format!("spawn command: {} {:?}", cmd.shell, cmd.args)); log_line(&format!("spawn command: {} {:?}", cmd.shell, cmd.args));
@@ -920,6 +920,17 @@ impl CliProcessManager {
continue; continue;
} }
if let Some(node_binary) = line.strip_prefix(MISSING_NODE_PREFIX) {
let mut locked = status.lock();
if locked.error.is_none() {
locked.error = Some(format!(
"Node binary '{}' not found in the desktop shell environment. CodeNomad desktop currently requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
node_binary.trim()
));
}
continue;
}
if let Some(url) = local_url_regex if let Some(url) = local_url_regex
.as_ref() .as_ref()
.and_then(|re| re.captures(line).and_then(|c| c.get(1))) .and_then(|re| re.captures(line).and_then(|c| c.get(1)))
@@ -1248,7 +1259,13 @@ fn build_shell_command_string(
for arg in entry.runner_args(cli_args) { for arg in entry.runner_args(cli_args) {
quoted.push(shell_escape(&arg)); quoted.push(shell_escape(&arg));
} }
let command = format!("ELECTRON_RUN_AS_NODE=1 exec {}", quoted.join(" ")); let command = format!(
"if command -v {} >/dev/null 2>&1; then ELECTRON_RUN_AS_NODE=1 exec {}; else printf '%s%s\\n' '{}' {} >&2; exit 127; fi",
shell_escape(&entry.node_binary),
quoted.join(" "),
MISSING_NODE_PREFIX,
shell_escape(&entry.node_binary),
);
let args = build_shell_args(&shell, &command); let args = build_shell_args(&shell, &command);
log_line(&format!("user shell command: {} {:?}", shell, args)); log_line(&format!("user shell command: {} {:?}", shell, args));
Ok(ShellCommand { shell, args }) Ok(ShellCommand { shell, args })
@@ -1288,8 +1305,11 @@ fn build_shell_args(shell: &str, command: &str) -> Vec<String> {
.unwrap_or("") .unwrap_or("")
.to_lowercase(); .to_lowercase();
let _ = shell_name; if shell_name.contains("zsh") || shell_name.contains("bash") {
vec!["-l".into(), "-c".into(), command.into()] vec!["-i".into(), "-l".into(), "-c".into(), command.into()]
} else {
vec!["-l".into(), "-c".into(), command.into()]
}
} }
fn first_existing(paths: Vec<Option<PathBuf>>) -> Option<String> { fn first_existing(paths: Vec<Option<PathBuf>>) -> Option<String> {

View File

@@ -9,6 +9,7 @@
"frontendDist": "resources/ui-loading" "frontendDist": "resources/ui-loading"
}, },
"app": { "app": {
"enableGTKAppId": true,
"withGlobalTauri": true, "withGlobalTauri": true,
"windows": [ "windows": [
{ {
@@ -41,6 +42,35 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"linux": {
"appimage": {
"files": {
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop"
}
},
"deb": {
"files": {
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop",
"/usr/share/icons/hicolor/32x32/apps/codenomad-tauri.png": "icons/linux/32x32.png",
"/usr/share/icons/hicolor/48x48/apps/codenomad-tauri.png": "icons/linux/48x48.png",
"/usr/share/icons/hicolor/64x64/apps/codenomad-tauri.png": "icons/linux/64x64.png",
"/usr/share/icons/hicolor/128x128/apps/codenomad-tauri.png": "icons/linux/128x128.png",
"/usr/share/icons/hicolor/256x256/apps/codenomad-tauri.png": "icons/linux/256x256.png",
"/usr/share/icons/hicolor/512x512/apps/codenomad-tauri.png": "icons/linux/512x512.png"
}
},
"rpm": {
"files": {
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop",
"/usr/share/icons/hicolor/32x32/apps/codenomad-tauri.png": "icons/linux/32x32.png",
"/usr/share/icons/hicolor/48x48/apps/codenomad-tauri.png": "icons/linux/48x48.png",
"/usr/share/icons/hicolor/64x64/apps/codenomad-tauri.png": "icons/linux/64x64.png",
"/usr/share/icons/hicolor/128x128/apps/codenomad-tauri.png": "icons/linux/128x128.png",
"/usr/share/icons/hicolor/256x256/apps/codenomad-tauri.png": "icons/linux/256x256.png",
"/usr/share/icons/hicolor/512x512/apps/codenomad-tauri.png": "icons/linux/512x512.png"
}
}
},
"resources": [ "resources": [
"resources/server", "resources/server",
"resources/ui-loading" "resources/ui-loading"

View File

@@ -357,7 +357,11 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
const pill = activeSessionStatusPill() const pill = activeSessionStatusPill()
if (!pill) return null if (!pill) return null
return ( return (
<span class={`status-indicator session-status session-status-list ${pill.className}`} title={pill.title}> <span
class={`status-indicator session-status session-status-list ${pill.className} notranslate`}
title={pill.title}
translate="no"
>
{pill.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />} {pill.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
{pill.text} {pill.text}
</span> </span>

View File

@@ -520,7 +520,11 @@ const SessionList: Component<SessionListProps> = (props) => {
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} /> <ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
</span> </span>
</Show> </Show>
<span class={`status-indicator session-status session-status-list ${statusClassName()}`} title={statusTooltip()}> <span
class={`status-indicator session-status session-status-list ${statusClassName()} notranslate`}
title={statusTooltip()}
translate="no"
>
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />} {needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
{statusText()} {statusText()}
</span> </span>
@@ -736,7 +740,9 @@ const SessionList: Component<SessionListProps> = (props) => {
<div class="session-list-header p-3 border-b border-base"> <div class="session-list-header p-3 border-b border-base">
{props.headerContent ?? ( {props.headerContent ?? (
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<h3 class="text-sm font-semibold text-primary">{t("sessionList.header.title")}</h3> <h3 class="text-sm font-semibold text-primary notranslate" translate="no">
{t("sessionList.header.title")}
</h3>
<KeyboardHint <KeyboardHint
shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)} shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)}
/> />