generated from nhcarrigan/template
b745100bd5
## Summary This PR covers the full audit of Claude CLI changes from 2.1.50 to 2.1.53, plus a batch of bug fixes, new features, and maintenance work identified during that review. ### New Features - **Workspace trust gate** — detects hooks, MCP servers, and custom commands in a workspace before connecting; persists trust decisions so users aren't prompted repeatedly - **Custom background image** — users can set a background image with configurable opacity; character panel and compact mode go transparent when active - **Draggable tab reordering** — conversation tabs can be reordered via pointer-event drag-and-drop (HTML5 drag is intercepted by Tauri/WebView2, so pointer events are used instead) - **Org UUID in account info** — exposes the org UUID from Claude auth status ### Bug Fixes - **Unread dot false positives** — initialise unread counts on mount to prevent all tabs showing the blue dot after toggling the file editor (Closes #164) - **Watchdog for hung WSL bridge** — detects connections that never receive `system:init` and kills the stale process after 1 minute (Closes #166) - **Suppress terminal window flash on Windows** — applies `CREATE_NO_WINDOW` to all subprocesses via a `HideWindow` trait extension (Closes #165) - **HTML escaping in markdown renderer** — escape `<` and `>` in `codespan` and `html` renderer callbacks to prevent raw HTML injection (Closes #169) ### Maintenance - Verify stream-JSON handles tool results above the 50K threshold correctly (Closes #162) - Reviewed hook security fixes from CLI 2.1.51 — not applicable to our setup (Closes #163) - Expose org UUID from `claude auth status` (Closes #160) - Clean up Svelte and Vite build warnings (`a11y_click_events_have_key_events`, `state_referenced_locally`, `non_reactive_update`, `codeSplitting`, chunk size, CodeMirror dynamic import) - Update all npm dependencies to latest compatible versions with exact pinning (Closes #81, Closes #82, Closes #83, Closes #84, Closes #85, Closes #86, Closes #87, Closes #90, Closes #91, Closes #93, Closes #94, Closes #95, Closes #96, Closes #97, Closes #98, Closes #99, Closes #101, Closes #141, Closes #142, Closes #143, Closes #145, Closes #146, Closes #147) - Run `cargo update` to bring Cargo.lock up to date ### Closes Closes #160 Closes #162 Closes #163 Closes #164 Closes #165 Closes #166 Closes #167 Closes #168 Closes #169 Closes #81 Closes #82 Closes #83 Closes #84 Closes #85 Closes #86 Closes #87 Closes #90 Closes #91 Closes #93 Closes #94 Closes #95 Closes #96 Closes #97 Closes #98 Closes #99 Closes #101 Closes #141 Closes #142 Closes #143 Closes #145 Closes #146 Closes #147 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #171 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
397 lines
13 KiB
Rust
397 lines
13 KiB
Rust
use std::process::Command;
|
|
use tauri::command;
|
|
|
|
use crate::process_ext::HideWindow;
|
|
|
|
/// Generate PowerShell script for Windows Toast Notification
|
|
fn generate_powershell_toast_script(title: &str, body: &str) -> String {
|
|
format!(
|
|
r#"
|
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
|
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null
|
|
|
|
$APP_ID = 'Hikari Desktop'
|
|
|
|
$template = @"
|
|
<toast>
|
|
<visual>
|
|
<binding template="ToastText02">
|
|
<text id="1">{}</text>
|
|
<text id="2">{}</text>
|
|
</binding>
|
|
</visual>
|
|
<audio src="ms-winsoundevent:Notification.Default" />
|
|
</toast>
|
|
"@
|
|
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml($template)
|
|
|
|
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)
|
|
"#,
|
|
title.replace("\"", "`\""),
|
|
body.replace("\"", "`\"")
|
|
)
|
|
}
|
|
|
|
/// Format simple notification message
|
|
fn format_simple_notification(title: &str, body: &str) -> String {
|
|
format!("{}\n\n{}", title, body)
|
|
}
|
|
|
|
/// Build notify-send command for testing (doesn't execute)
|
|
#[cfg(test)]
|
|
fn build_notify_send_command(title: &str, body: &str) -> (String, Vec<String>) {
|
|
(
|
|
"notify-send".to_string(),
|
|
vec![
|
|
title.to_string(),
|
|
body.to_string(),
|
|
"--urgency=normal".to_string(),
|
|
"--app-name=Hikari Desktop".to_string(),
|
|
],
|
|
)
|
|
}
|
|
|
|
/// Build Windows PowerShell command for testing (doesn't execute)
|
|
#[cfg(test)]
|
|
fn build_windows_powershell_command(title: &str, body: &str) -> (String, Vec<String>) {
|
|
let script = generate_powershell_toast_script(title, body);
|
|
(
|
|
"pwsh.exe".to_string(),
|
|
vec![
|
|
"-NoProfile".to_string(),
|
|
"-WindowStyle".to_string(),
|
|
"Hidden".to_string(),
|
|
"-Command".to_string(),
|
|
script,
|
|
],
|
|
)
|
|
}
|
|
|
|
/// Build simple notification command for testing (doesn't execute)
|
|
#[cfg(test)]
|
|
fn build_simple_notification_command(title: &str, body: &str) -> (String, Vec<String>) {
|
|
let message = format_simple_notification(title, body);
|
|
(
|
|
"cmd.exe".to_string(),
|
|
vec!["/c".to_string(), "msg".to_string(), "*".to_string(), message],
|
|
)
|
|
}
|
|
|
|
#[command]
|
|
pub async fn send_notify_send(title: String, body: String) -> Result<(), String> {
|
|
// Use notify-send for Linux/WSL
|
|
let output = Command::new("notify-send")
|
|
.hide_window()
|
|
.arg(&title)
|
|
.arg(&body)
|
|
.arg("--urgency=normal")
|
|
.arg("--app-name=Hikari Desktop")
|
|
.output()
|
|
.map_err(|e| {
|
|
format!(
|
|
"Failed to execute notify-send: {}. Make sure libnotify-bin is installed.",
|
|
e
|
|
)
|
|
})?;
|
|
|
|
if !output.status.success() {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!("notify-send failed: {}", error));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
pub async fn send_windows_notification(title: String, body: String) -> Result<(), String> {
|
|
// Create PowerShell script for Windows Toast Notification
|
|
let ps_script = generate_powershell_toast_script(&title, &body);
|
|
|
|
// Try PowerShell Core first (pwsh), then fall back to Windows PowerShell
|
|
let output = Command::new("pwsh.exe")
|
|
.hide_window()
|
|
.arg("-NoProfile")
|
|
.arg("-WindowStyle")
|
|
.arg("Hidden")
|
|
.arg("-Command")
|
|
.arg(&ps_script)
|
|
.output()
|
|
.or_else(|_| {
|
|
Command::new("powershell.exe")
|
|
.hide_window()
|
|
.arg("-NoProfile")
|
|
.arg("-WindowStyle")
|
|
.arg("Hidden")
|
|
.arg("-Command")
|
|
.arg(&ps_script)
|
|
.output()
|
|
})
|
|
.map_err(|e| format!("Failed to execute PowerShell: {}", e))?;
|
|
|
|
if !output.status.success() {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!("PowerShell script failed: {}", error));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Alternative: Use Windows built-in MSG command for simple notifications
|
|
#[command]
|
|
pub async fn send_simple_notification(title: String, body: String) -> Result<(), String> {
|
|
let message = format_simple_notification(&title, &body);
|
|
|
|
Command::new("cmd.exe")
|
|
.hide_window()
|
|
.arg("/c")
|
|
.arg("msg")
|
|
.arg("*")
|
|
.arg(&message)
|
|
.output()
|
|
.map_err(|e| format!("Failed to send message: {}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_generate_powershell_toast_script_basic() {
|
|
let script = generate_powershell_toast_script("Title", "Body");
|
|
|
|
assert!(script.contains("Hikari Desktop"));
|
|
assert!(script.contains("Title"));
|
|
assert!(script.contains("Body"));
|
|
assert!(script.contains("ToastNotification"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_powershell_toast_script_escapes_quotes() {
|
|
let script = generate_powershell_toast_script("Title with \"quotes\"", "Body with \"quotes\"");
|
|
|
|
// Quotes should be escaped as `" in PowerShell
|
|
assert!(script.contains("Title with `\"quotes`\""));
|
|
assert!(script.contains("Body with `\"quotes`\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_powershell_toast_script_with_special_chars() {
|
|
let script = generate_powershell_toast_script("Title: Test", "Body\nwith\nnewlines");
|
|
|
|
assert!(script.contains("Title: Test"));
|
|
assert!(script.contains("Body\nwith\nnewlines"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_powershell_toast_script_unicode() {
|
|
let script = generate_powershell_toast_script("日本語 Title", "Unicode: 🎉");
|
|
|
|
assert!(script.contains("日本語 Title"));
|
|
assert!(script.contains("Unicode: 🎉"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_powershell_toast_script_empty() {
|
|
let script = generate_powershell_toast_script("", "");
|
|
|
|
// Should still contain the structure
|
|
assert!(script.contains("Hikari Desktop"));
|
|
assert!(script.contains("ToastNotification"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_simple_notification_basic() {
|
|
let message = format_simple_notification("Title", "Body");
|
|
|
|
assert_eq!(message, "Title\n\nBody");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_simple_notification_with_newlines() {
|
|
let message = format_simple_notification("Multi\nLine\nTitle", "Multi\nLine\nBody");
|
|
|
|
assert!(message.contains("Multi\nLine\nTitle"));
|
|
assert!(message.contains("\n\n"));
|
|
assert!(message.contains("Multi\nLine\nBody"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_simple_notification_unicode() {
|
|
let message = format_simple_notification("日本語", "🎉 Unicode");
|
|
|
|
assert_eq!(message, "日本語\n\n🎉 Unicode");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_simple_notification_empty() {
|
|
let message = format_simple_notification("", "");
|
|
|
|
assert_eq!(message, "\n\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_simple_notification_long_text() {
|
|
let long_title = "A".repeat(1000);
|
|
let long_body = "B".repeat(1000);
|
|
let message = format_simple_notification(&long_title, &long_body);
|
|
|
|
assert!(message.starts_with(&long_title));
|
|
assert!(message.ends_with(&long_body));
|
|
assert!(message.contains("\n\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_powershell_toast_script_multiple_quotes() {
|
|
let script = generate_powershell_toast_script(
|
|
"\"Quoted\" \"Multiple\" \"Times\"",
|
|
"\"More\" \"Quotes\" \"Here\""
|
|
);
|
|
|
|
// Each quote should be escaped
|
|
assert!(script.contains("`\"Quoted`\" `\"Multiple`\" `\"Times`\""));
|
|
assert!(script.contains("`\"More`\" `\"Quotes`\" `\"Here`\""));
|
|
}
|
|
|
|
// E2E Integration Tests - Command Structure Verification
|
|
|
|
#[test]
|
|
fn test_e2e_notify_send_command_structure() {
|
|
let (command, args) = build_notify_send_command("Test Title", "Test Body");
|
|
|
|
assert_eq!(command, "notify-send");
|
|
assert_eq!(args.len(), 4);
|
|
assert_eq!(args[0], "Test Title");
|
|
assert_eq!(args[1], "Test Body");
|
|
assert_eq!(args[2], "--urgency=normal");
|
|
assert_eq!(args[3], "--app-name=Hikari Desktop");
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_notify_send_with_special_chars() {
|
|
let (command, args) =
|
|
build_notify_send_command("Title with \"quotes\"", "Body\nwith\nnewlines");
|
|
|
|
assert_eq!(command, "notify-send");
|
|
assert_eq!(args[0], "Title with \"quotes\"");
|
|
assert_eq!(args[1], "Body\nwith\nnewlines");
|
|
// notify-send handles these directly
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_windows_powershell_command_structure() {
|
|
let (command, args) = build_windows_powershell_command("Test Title", "Test Body");
|
|
|
|
assert_eq!(command, "pwsh.exe");
|
|
assert_eq!(args.len(), 5);
|
|
assert_eq!(args[0], "-NoProfile");
|
|
assert_eq!(args[1], "-WindowStyle");
|
|
assert_eq!(args[2], "Hidden");
|
|
assert_eq!(args[3], "-Command");
|
|
|
|
// Verify the script in args[4] contains expected elements
|
|
let script = &args[4];
|
|
assert!(script.contains("Test Title"));
|
|
assert!(script.contains("Test Body"));
|
|
assert!(script.contains("Hikari Desktop"));
|
|
assert!(script.contains("ToastNotification"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_windows_powershell_quote_escaping() {
|
|
let (_, args) =
|
|
build_windows_powershell_command("Title with \"quotes\"", "Body with \"quotes\"");
|
|
|
|
let script = &args[4];
|
|
// Verify quotes are properly escaped in the PowerShell script
|
|
assert!(script.contains("Title with `\"quotes`\""));
|
|
assert!(script.contains("Body with `\"quotes`\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_simple_notification_command_structure() {
|
|
let (command, args) = build_simple_notification_command("Test Title", "Test Body");
|
|
|
|
assert_eq!(command, "cmd.exe");
|
|
assert_eq!(args.len(), 4);
|
|
assert_eq!(args[0], "/c");
|
|
assert_eq!(args[1], "msg");
|
|
assert_eq!(args[2], "*");
|
|
assert_eq!(args[3], "Test Title\n\nTest Body");
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_simple_notification_multiline() {
|
|
let (_, args) =
|
|
build_simple_notification_command("Multi\nLine\nTitle", "Multi\nLine\nBody");
|
|
|
|
let message = &args[3];
|
|
assert!(message.contains("Multi\nLine\nTitle"));
|
|
assert!(message.contains("\n\n"));
|
|
assert!(message.contains("Multi\nLine\nBody"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_command_consistency_across_platforms() {
|
|
// Test that different platforms use consistent parameters
|
|
let title = "Consistency Test";
|
|
let body = "Testing cross-platform consistency";
|
|
|
|
// Linux command
|
|
let (notify_cmd, notify_args) = build_notify_send_command(title, body);
|
|
assert!(notify_cmd.contains("notify"));
|
|
assert!(notify_args.iter().any(|arg| arg.contains("Hikari Desktop")));
|
|
|
|
// Windows PowerShell command
|
|
let (ps_cmd, ps_args) = build_windows_powershell_command(title, body);
|
|
assert!(ps_cmd.contains("pwsh") || ps_cmd.contains("powershell"));
|
|
let ps_script = &ps_args[4];
|
|
assert!(ps_script.contains("Hikari Desktop"));
|
|
|
|
// Windows simple command
|
|
let (msg_cmd, msg_args) = build_simple_notification_command(title, body);
|
|
assert!(msg_cmd.contains("cmd"));
|
|
assert!(msg_args[3].contains(title));
|
|
assert!(msg_args[3].contains(body));
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_unicode_support_across_platforms() {
|
|
let title = "日本語 Title";
|
|
let body = "Unicode: 🎉";
|
|
|
|
// Verify all platforms preserve unicode
|
|
let (_, notify_args) = build_notify_send_command(title, body);
|
|
assert_eq!(notify_args[0], title);
|
|
assert_eq!(notify_args[1], body);
|
|
|
|
let (_, ps_args) = build_windows_powershell_command(title, body);
|
|
let ps_script = &ps_args[4];
|
|
assert!(ps_script.contains(title));
|
|
assert!(ps_script.contains(body));
|
|
|
|
let (_, msg_args) = build_simple_notification_command(title, body);
|
|
assert!(msg_args[3].contains(title));
|
|
assert!(msg_args[3].contains(body));
|
|
}
|
|
|
|
#[test]
|
|
fn test_e2e_empty_input_handling() {
|
|
// Test that empty inputs are handled gracefully
|
|
let (_, notify_args) = build_notify_send_command("", "");
|
|
assert_eq!(notify_args[0], "");
|
|
assert_eq!(notify_args[1], "");
|
|
|
|
let (_, ps_args) = build_windows_powershell_command("", "");
|
|
let ps_script = &ps_args[4];
|
|
assert!(ps_script.contains("Hikari Desktop")); // Still has app name
|
|
|
|
let (_, msg_args) = build_simple_notification_command("", "");
|
|
assert_eq!(msg_args[3], "\n\n");
|
|
}
|
|
}
|