feat(desktop): add consent-aware repository discovery

This commit is contained in:
Carlos Florêncio
2026-09-06 11:32:14 +01:00
committed by GitHub
parent 9b9f347148
commit d796cf131d
136 changed files with 6654 additions and 542 deletions
+77 -10
View File
@@ -774,7 +774,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -949,7 +949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -1856,6 +1856,7 @@ dependencies = [
"serde_with",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-single-instance",
@@ -2045,7 +2046,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.18",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2835,6 +2836,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -2874,7 +2899,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2930,7 +2955,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -3315,7 +3340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -3659,6 +3684,48 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-notification"
version = "2.3.3"
@@ -3870,7 +3937,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4220,7 +4287,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.18",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4249,7 +4316,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4627,7 +4694,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
+2
View File
@@ -25,6 +25,7 @@ desktop-runtime = [
"dep:tauri-plugin-opener",
"dep:tauri-plugin-updater",
"dep:tauri-plugin-notification",
"dep:tauri-plugin-dialog",
"dep:tauri-plugin-single-instance",
"dep:tauri-runtime",
]
@@ -49,4 +50,5 @@ tauri-plugin-single-instance = { version = "=2.4.3", optional = true }
tauri-plugin-updater = { version = "=2.10.1", optional = true }
tauri-plugin-notification = { version = "=2.3.3", optional = true }
tauri-plugin-opener = { version = "=2.5.4", optional = true }
tauri-plugin-dialog = { version = "=2.7.2", optional = true }
url = "2"
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSDesktopFolderUsageDescription</key>
<string>Kandev needs access to folders you choose to discover local repositories.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>Kandev needs access to folders you choose to discover local repositories.</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>Kandev needs access to folders you choose to discover local repositories.</string>
</dict>
</plist>
+1
View File
@@ -8,6 +8,7 @@ fn main() {
"get_native_notification_permission",
"request_native_notification_permission",
"open_external_url",
"pick_directory",
]);
tauri_build::try_build(tauri_build::Attributes::new().app_manifest(manifest))
.expect("failed to build Tauri desktop application");
@@ -16,6 +16,7 @@
"allow-show-native-notification",
"allow-get-native-notification-permission",
"allow-request-native-notification-permission",
"allow-open-external-url"
"allow-open-external-url",
"allow-pick-directory"
]
}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"default":{"identifier":"default","description":"Desktop bridge; privileged commands verify the exact owned backend origin","remote":{"urls":["http://127.0.0.1:*"]},"local":false,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","allow-get-update-state","allow-check-for-updates","allow-install-update","allow-show-native-notification","allow-get-native-notification-permission","allow-request-native-notification-permission","allow-open-external-url"]}}
{"default":{"identifier":"default","description":"Desktop bridge; privileged commands verify the exact owned backend origin","remote":{"urls":["http://127.0.0.1:*"]},"local":false,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","allow-get-update-state","allow-check-for-updates","allow-install-update","allow-show-native-notification","allow-get-native-notification-permission","allow-request-native-notification-permission","allow-open-external-url","allow-pick-directory"]}}
@@ -374,6 +374,12 @@
"const": "allow-open-external-url",
"markdownDescription": "Enables the open_external_url command without any pre-configured scope."
},
{
"description": "Enables the pick_directory command without any pre-configured scope.",
"type": "string",
"const": "allow-pick-directory",
"markdownDescription": "Enables the pick_directory command without any pre-configured scope."
},
{
"description": "Enables the request_native_notification_permission command without any pre-configured scope.",
"type": "string",
@@ -416,6 +422,12 @@
"const": "deny-open-external-url",
"markdownDescription": "Denies the open_external_url command without any pre-configured scope."
},
{
"description": "Denies the pick_directory command without any pre-configured scope.",
"type": "string",
"const": "deny-pick-directory",
"markdownDescription": "Denies the pick_directory command without any pre-configured scope."
},
{
"description": "Denies the request_native_notification_permission command without any pre-configured scope.",
"type": "string",
@@ -2444,6 +2456,72 @@
"const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-message",
"markdownDescription": "Enables the message command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-save",
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-message",
"markdownDescription": "Denies the message command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-save",
"markdownDescription": "Denies the save command without any pre-configured scope."
},
{
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
"type": "string",
@@ -374,6 +374,12 @@
"const": "allow-open-external-url",
"markdownDescription": "Enables the open_external_url command without any pre-configured scope."
},
{
"description": "Enables the pick_directory command without any pre-configured scope.",
"type": "string",
"const": "allow-pick-directory",
"markdownDescription": "Enables the pick_directory command without any pre-configured scope."
},
{
"description": "Enables the request_native_notification_permission command without any pre-configured scope.",
"type": "string",
@@ -416,6 +422,12 @@
"const": "deny-open-external-url",
"markdownDescription": "Denies the open_external_url command without any pre-configured scope."
},
{
"description": "Denies the pick_directory command without any pre-configured scope.",
"type": "string",
"const": "deny-pick-directory",
"markdownDescription": "Denies the pick_directory command without any pre-configured scope."
},
{
"description": "Denies the request_native_notification_permission command without any pre-configured scope.",
"type": "string",
@@ -2444,6 +2456,72 @@
"const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-message",
"markdownDescription": "Enables the message command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-save",
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-message",
"markdownDescription": "Denies the message command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-save",
"markdownDescription": "Denies the save command without any pre-configured scope."
},
{
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
"type": "string",
@@ -0,0 +1,9 @@
[[permission]]
identifier = "allow-pick-directory"
description = "Enables the Kandev-owned directory picker command without exposing generic dialog commands."
commands.allow = ["pick_directory"]
[[permission]]
identifier = "deny-pick-directory"
description = "Denies the Kandev-owned directory picker command."
commands.deny = ["pick_directory"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-pick-directory"
description = "Enables the pick_directory command without any pre-configured scope."
commands.allow = ["pick_directory"]
[[permission]]
identifier = "deny-pick-directory"
description = "Denies the pick_directory command without any pre-configured scope."
commands.deny = ["pick_directory"]
+13 -2
View File
@@ -24,6 +24,7 @@ const DEFAULT_DESKTOP_PORT: u16 = 38430;
const DESKTOP_PORT_ENV: &str = "KANDEV_DESKTOP_PORT";
const DESKTOP_HEALTH_TOKEN_ENV: &str = "KANDEV_DESKTOP_HEALTH_TOKEN";
const DESKTOP_NATIVE_NOTIFICATIONS_ENV: &str = "KANDEV_DESKTOP_NATIVE_NOTIFICATIONS";
const DESKTOP_RUNTIME_ENV: &str = "KANDEV_DESKTOP_RUNTIME";
const LAUNCHER_PARENT_PID_ENV: &str = "KANDEV_LAUNCHER_PARENT_PID";
const DESKTOP_HEALTH_TOKEN_HEADER: &str = "x-kandev-desktop-health-token";
const STARTUP_OUTPUT_LIMIT: usize = 12 * 1024;
@@ -369,8 +370,9 @@ pub fn desktop_environment(
) -> BTreeMap<OsString, OsString> {
let path = normalized_path(env.get(OsStr::new("PATH")), home_dir);
env.retain(|key, _| {
!key.to_string_lossy()
.eq_ignore_ascii_case(DESKTOP_NATIVE_NOTIFICATIONS_ENV)
let key = key.to_string_lossy();
!key.eq_ignore_ascii_case(DESKTOP_NATIVE_NOTIFICATIONS_ENV)
&& !key.eq_ignore_ascii_case(DESKTOP_RUNTIME_ENV)
});
env.insert(
OsString::from("KANDEV_SERVER_HOST"),
@@ -384,6 +386,7 @@ pub fn desktop_environment(
OsString::from(DESKTOP_NATIVE_NOTIFICATIONS_ENV),
OsString::from("true"),
);
env.insert(OsString::from(DESKTOP_RUNTIME_ENV), OsString::from("true"));
env.insert(OsString::from("PATH"), path);
env
}
@@ -730,6 +733,10 @@ fn current_home_dir() -> Option<PathBuf> {
.map(PathBuf::from)
}
pub(crate) fn picker_home_dir() -> Option<PathBuf> {
current_home_dir()
}
fn executable_name(name: &str) -> OsString {
if cfg!(windows) {
OsString::from(format!("{name}.exe"))
@@ -870,6 +877,10 @@ mod tests {
.get(OsStr::new("KANDEV_DESKTOP_NATIVE_NOTIFICATIONS")),
Some(&OsString::from("true"))
);
assert_eq!(
spec.env.get(OsStr::new("KANDEV_DESKTOP_RUNTIME")),
Some(&OsString::from("true"))
);
}
#[test]
@@ -0,0 +1,75 @@
use serde::Serialize;
use std::path::PathBuf;
/// The result of the deliberately narrow native folder-selection boundary.
/// The command never accepts a path from the WebView. The selected path is
/// returned only after the user chooses it in the operating-system picker.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum FolderPickerOutcome {
Selected { path: String },
Cancelled,
Failed { message: String },
}
fn outcome_from_path(path: Option<Result<PathBuf, String>>) -> FolderPickerOutcome {
match path {
Some(Ok(path)) => FolderPickerOutcome::Selected {
path: path.to_string_lossy().into_owned(),
},
Some(Err(message)) => FolderPickerOutcome::Failed { message },
None => FolderPickerOutcome::Cancelled,
}
}
#[cfg(feature = "desktop-runtime")]
#[tauri::command]
pub fn pick_directory(
app: tauri::AppHandle,
backend: tauri::State<'_, crate::backend::BackendState>,
webview: tauri::WebviewWindow,
) -> Result<FolderPickerOutcome, String> {
use tauri_plugin_dialog::{DialogExt, FilePath};
backend.require_owned_origin(&webview)?;
let home = crate::backend::picker_home_dir()
.ok_or_else(|| "could not determine the desktop user's home directory".to_string())?;
let picked = app
.dialog()
.file()
.set_directory(home)
.set_parent(&webview)
.blocking_pick_folder();
let result = picked.map(|path: FilePath| path.into_path().map_err(|err| err.to_string()));
Ok(outcome_from_path(result))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_a_selected_directory() {
assert_eq!(
outcome_from_path(Some(Ok(PathBuf::from("/Users/example/Code")))),
FolderPickerOutcome::Selected {
path: "/Users/example/Code".to_string()
}
);
}
#[test]
fn maps_cancellation_without_a_path() {
assert_eq!(outcome_from_path(None), FolderPickerOutcome::Cancelled);
}
#[test]
fn maps_picker_failures_without_exposing_a_path_argument() {
assert_eq!(
outcome_from_path(Some(Err("picker unavailable".to_string()))),
FolderPickerOutcome::Failed {
message: "picker unavailable".to_string()
}
);
}
}
+1
View File
@@ -1,5 +1,6 @@
pub mod backend;
pub mod external_links;
pub mod folder_picker;
pub mod native_notifications;
pub mod shell;
pub mod updater;
+3 -1
View File
@@ -1,5 +1,5 @@
use kandev_desktop::{
backend, external_links,
backend, external_links, folder_picker,
native_notifications::{self, NativeNotificationState},
shell::{self, MenuAction, ZoomState},
updater::{self, UpdaterState},
@@ -22,6 +22,7 @@ fn main() {
let app = tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_dialog::init())
.plugin(
tauri_plugin_opener::Builder::new()
.open_js_links_on_click(false)
@@ -42,6 +43,7 @@ fn main() {
native_notifications::get_native_notification_permission,
native_notifications::request_native_notification_permission,
external_links::open_external_url,
folder_picker::pick_directory,
])
.menu(build_menu)
.on_menu_event(handle_menu_event)
+3
View File
@@ -58,6 +58,9 @@
"digestAlgorithm": "sha256",
"timestampUrl": "https://timestamp.digicert.com",
"signCommand": "powershell -NoProfile -ExecutionPolicy Bypass -File ./windows-sign.ps1 %1"
},
"macOS": {
"infoPlist": "Info.plist"
}
}
}