Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ jobs:
packages/ui/src/lib/hooks/use-foreground-refresh.test.ts
packages/ui/src/lib/launch-errors.test.ts
packages/ui/src/lib/message-selection-position.test.ts
packages/ui/src/lib/runtime-env.test.ts
packages/ui/src/lib/trailing-resync.test.ts
packages/ui/src/stores/abort-created-workspace-cleanup.test.ts
packages/ui/src/stores/app-session-reconciliation.test.ts
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://schema.tauri.app/capabilities.json",
"identifier": "remote-window-notifications",
"description": "Grant remote CodeNomad windows access only to native OS notifications.",
"local": false,
"remote": {
"urls": ["http://*:*", "https://*:*"]
},
"windows": ["remote-*"],
"permissions": [
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify"
]
}
2 changes: 1 addition & 1 deletion packages/tauri-app/src-tauri/gen/schemas/capabilities.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*","http://tauri.localhost/*","https://tauri.localhost/*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","opener:allow-open-url","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-show","core:webview:allow-set-webview-zoom"]}}
{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*","http://tauri.localhost/*","https://tauri.localhost/*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","opener:allow-open-url","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-show","core:webview:allow-set-webview-zoom"]},"remote-window-notifications":{"identifier":"remote-window-notifications","description":"Grant remote CodeNomad windows access only to native OS notifications.","remote":{"urls":["http://*:*","https://*:*"]},"local":false,"windows":["remote-*"],"permissions":["notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify"]}}
76 changes: 66 additions & 10 deletions packages/tauri-app/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
const ZOOM_STEP: f64 = 0.1;
const RELEASES_URL: &str = "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest";
const LOCAL_WINDOW_CONTEXT_SCRIPT: &str = "window.__CODENOMAD_WINDOW_CONTEXT__ = 'local';";
const REMOTE_WINDOW_CONTEXT_SCRIPT: &str = "window.__CODENOMAD_WINDOW_CONTEXT__ = 'remote';";
const REMOTE_WINDOW_CONTEXT_SCRIPT: &str =
"window.__CODENOMAD_RUNTIME_HOST__ = 'tauri'; window.__CODENOMAD_WINDOW_CONTEXT__ = 'remote';";

#[cfg(windows)]
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
Expand Down Expand Up @@ -218,19 +219,20 @@ fn should_allow_window_origin<R: Runtime>(
window_label: &str,
url: &Url,
) -> bool {
if should_allow_internal(url) {
return true;
}

let state = app_handle.state::<AppState>();
let Ok(allowed) = state.remote_origins.lock() else {
return false;
};
if let Some(origin) = allowed.get(window_label) {
return origin == &url.origin().ascii_serialization();
}
should_allow_registered_origin(allowed.get(window_label).map(String::as_str), url)
}

false
fn should_allow_registered_origin(registered_origin: Option<&str>, url: &Url) -> bool {
if let Some(origin) = registered_origin {
if matches!(url.scheme(), "http" | "https") {
return origin == url.origin().ascii_serialization();
}
}
should_allow_internal(url)
}

fn intercept_navigation<R: Runtime>(webview: &Webview<R>, url: &Url) -> bool {
Expand Down Expand Up @@ -1059,8 +1061,13 @@ fn build_about_metadata(version: &str, include_update_link: bool) -> AboutMetada

#[cfg(test)]
mod menu_tests {
use super::{build_about_metadata, run_update_with_fallback, RELEASES_URL};
use super::{
build_about_metadata, run_update_with_fallback, should_allow_registered_origin,
RELEASES_URL, REMOTE_WINDOW_CONTEXT_SCRIPT,
};
use serde_json::json;
use std::sync::atomic::{AtomicBool, Ordering};
use url::Url;

#[test]
fn failed_update_uses_release_fallback() {
Expand Down Expand Up @@ -1091,4 +1098,53 @@ mod menu_tests {
assert_eq!(metadata.website, None);
assert_eq!(metadata.website_label, None);
}

#[test]
fn remote_windows_identify_as_remote_tauri_windows() {
assert!(REMOTE_WINDOW_CONTEXT_SCRIPT.contains("__CODENOMAD_RUNTIME_HOST__ = 'tauri'"));
assert!(REMOTE_WINDOW_CONTEXT_SCRIPT.contains("__CODENOMAD_WINDOW_CONTEXT__ = 'remote'"));

let capability: serde_json::Value = serde_json::from_str(include_str!(
"../capabilities/remote-window-notifications.json"
))
.unwrap();
assert_eq!(capability["local"], false);
assert_eq!(
capability["remote"]["urls"],
json!(["http://*:*", "https://*:*"])
);
assert_eq!(capability["windows"], json!(["remote-*"]));
assert_eq!(
capability["permissions"],
json!([
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify"
])
);

let config: serde_json::Value =
serde_json::from_str(include_str!("../tauri.conf.json")).unwrap();
assert!(config["app"]["security"]["capabilities"]
.as_array()
.unwrap()
.contains(&json!("remote-window-notifications")));
}

#[test]
fn remote_windows_stay_on_their_registered_http_origin() {
let origin = "https://remote.example:9898";
assert!(should_allow_registered_origin(
Some(origin),
&Url::parse("https://remote.example:9898/settings").unwrap()
));
assert!(!should_allow_registered_origin(
Some(origin),
&Url::parse("http://localhost:9898/").unwrap()
));
assert!(should_allow_registered_origin(
Some(origin),
&Url::parse("about:blank").unwrap()
));
}
}
3 changes: 2 additions & 1 deletion packages/tauri-app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
]
},
"capabilities": [
"main-window-native-dialogs"
"main-window-native-dialogs",
"remote-window-notifications"
]
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
import { openExternalUrl } from "../../lib/external-url"
import { useI18n } from "../../lib/i18n"
import { requestData } from "../../lib/opencode-api"
import { isTauriHost } from "../../lib/runtime-env"
import { isLocalTauriHost } from "../../lib/runtime-env"
import {
extractProviderAuthErrorMessage,
genericApiMethod,
Expand Down Expand Up @@ -186,7 +186,7 @@ export const ProviderManagerModal: Component<ProviderManagerModalProps> = (props
}

function isBrowserHostForOAuth(): boolean {
return !isTauriHost() && typeof window !== "undefined"
return !isLocalTauriHost() && typeof window !== "undefined"
}

function prepareOAuthPopupWindow(): Window | null {
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/lib/external-url.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { isTauriHost } from "./runtime-env"
import { isLocalTauriHost } from "./runtime-env"

export async function openExternalUrl(url: string, context = "ui"): Promise<boolean> {
if (typeof window === "undefined") {
return false
}

if (isTauriHost()) {
if (isLocalTauriHost()) {
try {
const { openUrl } = await import("@tauri-apps/plugin-opener")
await openUrl(url)
Expand Down
23 changes: 23 additions & 0 deletions packages/ui/src/lib/runtime-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { isLocalTauriHost, type RuntimeEnvironment } from "./runtime-env.ts"

const environment = (host: RuntimeEnvironment["host"], windowContext: RuntimeEnvironment["windowContext"]) => ({
host,
windowContext,
})

describe("isLocalTauriHost", () => {
it("enables native-only features in the local Tauri window", () => {
assert.equal(isLocalTauriHost(environment("tauri", "local")), true)
})

it("keeps native-only features disabled in remote Tauri windows", () => {
assert.equal(isLocalTauriHost(environment("tauri", "remote")), false)
})

it("does not classify web or Electron windows as local Tauri", () => {
assert.equal(isLocalTauriHost(environment("web", "remote")), false)
assert.equal(isLocalTauriHost(environment("electron", "local")), false)
})
})
3 changes: 3 additions & 0 deletions packages/ui/src/lib/runtime-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ export const runtimeEnv = detectRuntimeEnvironment()
export const isElectronHost = () => detectHost() === "electron"
export const isTauriHost = () => detectHost() === "tauri"
export const isWebHost = () => detectHost() === "web"
export const isLocalTauriHost = (
environment: Pick<RuntimeEnvironment, "host" | "windowContext"> = detectRuntimeEnvironment(),
) => environment.host === "tauri" && environment.windowContext === "local"
export const isDesktopHost = () => isElectronHost() || isTauriHost()
export const isMobilePlatform = () => detectPlatform() === "mobile"
export const isLocalWindow = () => detectWindowContext() === "local"
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/lib/settings/behavior-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
} from "../../stores/preferences"
import type { Command } from "../commands"
import { tGlobal } from "../i18n"
import { isTauriHost, isWebHost } from "../runtime-env"
import { isLocalTauriHost, isWebHost } from "../runtime-env"

export type BehaviorSettingKind = "toggle" | "enum"

Expand Down Expand Up @@ -302,7 +302,7 @@ export function getBehaviorSettings(actions: BehaviorRegistryActions): BehaviorS
}
},
},
...(isTauriHost()
...(isLocalTauriHost()
? [
{
kind: "toggle" as const,
Expand Down
Loading