generated from nhcarrigan/template
Compare commits
2 Commits
v1.8.0
..
e8cbe9f647
| Author | SHA1 | Date | |
|---|---|---|---|
| e8cbe9f647 | |||
| 9890b83313 |
+63
-63
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hikari-desktop",
|
||||
"version": "1.8.0",
|
||||
"version": "1.7.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -27,69 +27,69 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/lang-angular": "0.1.4",
|
||||
"@codemirror/lang-cpp": "6.0.3",
|
||||
"@codemirror/lang-css": "6.3.1",
|
||||
"@codemirror/lang-go": "6.0.1",
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/lang-java": "6.0.2",
|
||||
"@codemirror/lang-javascript": "6.2.4",
|
||||
"@codemirror/lang-json": "6.0.2",
|
||||
"@codemirror/lang-less": "6.0.2",
|
||||
"@codemirror/lang-markdown": "6.5.0",
|
||||
"@codemirror/lang-php": "6.0.2",
|
||||
"@codemirror/lang-python": "6.2.1",
|
||||
"@codemirror/lang-rust": "6.0.2",
|
||||
"@codemirror/lang-sass": "6.0.2",
|
||||
"@codemirror/lang-sql": "6.10.0",
|
||||
"@codemirror/lang-vue": "0.1.3",
|
||||
"@codemirror/lang-wast": "6.0.2",
|
||||
"@codemirror/lang-xml": "6.1.0",
|
||||
"@codemirror/lang-yaml": "6.1.2",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/theme-one-dark": "6.1.3",
|
||||
"@codemirror/view": "6.39.15",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "2.6.0",
|
||||
"@tauri-apps/plugin-fs": "2.4.5",
|
||||
"@tauri-apps/plugin-notification": "2.3.3",
|
||||
"@tauri-apps/plugin-opener": "2.5.3",
|
||||
"@tauri-apps/plugin-os": "2.3.2",
|
||||
"@tauri-apps/plugin-shell": "2.3.5",
|
||||
"@tauri-apps/plugin-store": "2.4.2",
|
||||
"codemirror": "6.0.2",
|
||||
"highlight.js": "11.11.1",
|
||||
"lucide-svelte": "0.575.0",
|
||||
"marked": "17.0.3"
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-java": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/lang-sass": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-vue": "^0.1.3",
|
||||
"@codemirror/lang-wast": "^6.0.2",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.11",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
"@tauri-apps/plugin-notification": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2.3.4",
|
||||
"@tauri-apps/plugin-store": "^2",
|
||||
"codemirror": "^6.0.2",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide-svelte": "^0.563.0",
|
||||
"marked": "^17.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.3",
|
||||
"@sveltejs/adapter-static": "3.0.10",
|
||||
"@sveltejs/kit": "2.53.2",
|
||||
"@sveltejs/vite-plugin-svelte": "5.1.1",
|
||||
"@tailwindcss/vite": "4.2.1",
|
||||
"@tauri-apps/cli": "2.10.0",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/svelte": "5.3.1",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
"eslint": "9.39.3",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-svelte": "3.15.0",
|
||||
"globals": "17.3.0",
|
||||
"jsdom": "28.1.0",
|
||||
"prettier": "3.8.1",
|
||||
"prettier-plugin-svelte": "3.5.0",
|
||||
"svelte": "5.53.5",
|
||||
"svelte-check": "4.4.3",
|
||||
"tailwindcss": "4.2.1",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.56.1",
|
||||
"vite": "6.4.1",
|
||||
"vitest": "4.0.18"
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/svelte": "^5.3.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"globals": "^17.0.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"prettier": "^3.8.0",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.53.0",
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+682
-695
File diff suppressed because it is too large
Load Diff
Generated
+297
-492
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hikari-desktop"
|
||||
version = "1.8.0"
|
||||
version = "1.7.0"
|
||||
description = "Hikari - Claude Code Visual Assistant"
|
||||
authors = ["Naomi Carrigan"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -7,7 +7,6 @@ use tauri_plugin_store::StoreExt;
|
||||
use crate::achievements::{get_achievement_info, load_achievements, AchievementUnlockedEvent};
|
||||
use crate::bridge_manager::SharedBridgeManager;
|
||||
use crate::config::{ClaudeStartOptions, HikariConfig};
|
||||
use crate::process_ext::HideWindow;
|
||||
use crate::stats::UsageStats;
|
||||
use crate::temp_manager::SharedTempFileManager;
|
||||
|
||||
@@ -60,7 +59,6 @@ fn create_claude_command() -> std::process::Command {
|
||||
// Non-login shells launched by `wsl` don't inherit the full user PATH,
|
||||
// so we need to use a login shell to get the correct PATH
|
||||
let which_output = std::process::Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "bash", "-l", "-c", "which claude"])
|
||||
.output();
|
||||
|
||||
@@ -68,7 +66,6 @@ fn create_claude_command() -> std::process::Command {
|
||||
Ok(output) if output.status.success() => {
|
||||
let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let mut cmd = std::process::Command::new("wsl");
|
||||
cmd.hide_window();
|
||||
cmd.arg(claude_path);
|
||||
cmd
|
||||
}
|
||||
@@ -76,7 +73,6 @@ fn create_claude_command() -> std::process::Command {
|
||||
// Fallback to just "claude" if which fails
|
||||
// This maintains backwards compatibility
|
||||
let mut cmd = std::process::Command::new("wsl");
|
||||
cmd.hide_window();
|
||||
cmd.arg("claude");
|
||||
cmd
|
||||
}
|
||||
@@ -89,23 +85,18 @@ fn create_claude_command() -> std::process::Command {
|
||||
// This works regardless of how Claude Code was installed (standalone, npm, etc.)
|
||||
// and avoids hardcoding paths
|
||||
let which_output = std::process::Command::new("which")
|
||||
.hide_window()
|
||||
.arg("claude")
|
||||
.output();
|
||||
|
||||
match which_output {
|
||||
Ok(output) if output.status.success() => {
|
||||
let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let mut cmd = std::process::Command::new(claude_path);
|
||||
cmd.hide_window();
|
||||
cmd
|
||||
std::process::Command::new(claude_path)
|
||||
}
|
||||
_ => {
|
||||
// Fallback to just "claude" if which fails
|
||||
// This maintains backwards compatibility
|
||||
let mut cmd = std::process::Command::new("claude");
|
||||
cmd.hide_window();
|
||||
cmd
|
||||
std::process::Command::new("claude")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +412,6 @@ async fn list_workspace_commands(working_dir: &str, use_wsl: bool) -> Vec<String
|
||||
commands_dir
|
||||
);
|
||||
let Ok(output) = std::process::Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "sh", "-c", &script])
|
||||
.output()
|
||||
else {
|
||||
@@ -505,7 +495,6 @@ async fn list_skills_via_wsl() -> Result<Vec<String>, String> {
|
||||
|
||||
// Use WSL to list directories in ~/.claude/skills that contain SKILL.md
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args([
|
||||
"-e",
|
||||
"sh",
|
||||
@@ -805,7 +794,6 @@ async fn list_directory_via_wsl(path: &str) -> Result<Vec<FileEntry>, String> {
|
||||
);
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "sh", "-c", &script])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -868,7 +856,6 @@ async fn read_file_via_wsl(path: &str) -> Result<String, String> {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "cat", path])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -900,7 +887,6 @@ async fn write_file_via_wsl(path: &str, content: &str) -> Result<(), String> {
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let mut child = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "sh", "-c", &format!("cat > '{}'", path)])
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
@@ -949,7 +935,6 @@ async fn create_file_via_wsl(path: &str) -> Result<(), String> {
|
||||
|
||||
// Check if file exists first
|
||||
let check = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-e", path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -959,7 +944,6 @@ async fn create_file_via_wsl(path: &str) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "touch", path])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1000,7 +984,6 @@ async fn create_directory_via_wsl(path: &str) -> Result<(), String> {
|
||||
|
||||
// Check if directory exists first
|
||||
let check = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-e", path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1010,7 +993,6 @@ async fn create_directory_via_wsl(path: &str) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "mkdir", "-p", path])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1055,7 +1037,6 @@ async fn delete_file_via_wsl(path: &str) -> Result<(), String> {
|
||||
|
||||
// Check if path exists
|
||||
let check_exists = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-e", path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1066,7 +1047,6 @@ async fn delete_file_via_wsl(path: &str) -> Result<(), String> {
|
||||
|
||||
// Check if path is a directory
|
||||
let check_dir = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-d", path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1076,7 +1056,6 @@ async fn delete_file_via_wsl(path: &str) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "rm", path])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1121,7 +1100,6 @@ async fn delete_directory_via_wsl(path: &str) -> Result<(), String> {
|
||||
|
||||
// Check if path exists
|
||||
let check_exists = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-e", path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1132,7 +1110,6 @@ async fn delete_directory_via_wsl(path: &str) -> Result<(), String> {
|
||||
|
||||
// Check if path is a directory
|
||||
let check_dir = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-d", path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1142,7 +1119,6 @@ async fn delete_directory_via_wsl(path: &str) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "rm", "-rf", path])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1188,7 +1164,6 @@ async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), Strin
|
||||
|
||||
// Check if old path exists
|
||||
let check_old = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-e", old_path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1199,7 +1174,6 @@ async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), Strin
|
||||
|
||||
// Check if new path already exists
|
||||
let check_new = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-e", new_path])
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1209,7 +1183,6 @@ async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), Strin
|
||||
}
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "mv", old_path, new_path])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
@@ -1387,7 +1360,6 @@ async fn list_memory_files_via_wsl() -> Result<MemoryFilesResponse, String> {
|
||||
"#;
|
||||
|
||||
let output = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "bash", "-l", "-c", script])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
||||
|
||||
@@ -128,13 +128,6 @@ pub struct HikariConfig {
|
||||
|
||||
#[serde(default)]
|
||||
pub trusted_workspaces: Vec<String>,
|
||||
|
||||
// Background image settings
|
||||
#[serde(default)]
|
||||
pub background_image_path: Option<String>,
|
||||
|
||||
#[serde(default = "default_background_image_opacity")]
|
||||
pub background_image_opacity: f32,
|
||||
}
|
||||
|
||||
impl Default for HikariConfig {
|
||||
@@ -170,8 +163,6 @@ impl Default for HikariConfig {
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: Vec::new(),
|
||||
background_image_path: None,
|
||||
background_image_opacity: 0.3,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,10 +199,6 @@ fn default_discord_rpc_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_background_image_opacity() -> f32 {
|
||||
0.3
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BudgetAction {
|
||||
@@ -321,8 +308,6 @@ mod tests {
|
||||
use_worktree: true,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: vec!["/home/naomi/projects/trusted".to_string()],
|
||||
background_image_path: Some("/home/naomi/bg.png".to_string()),
|
||||
background_image_opacity: 0.25,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::process_ext::HideWindow;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitStatus {
|
||||
pub is_repo: bool,
|
||||
@@ -39,7 +37,6 @@ pub struct GitLogEntry {
|
||||
|
||||
fn run_git_command(working_dir: &str, args: &[&str]) -> Result<String, String> {
|
||||
let output = Command::new("git")
|
||||
.hide_window()
|
||||
.args(args)
|
||||
.current_dir(working_dir)
|
||||
.output()
|
||||
|
||||
@@ -8,7 +8,6 @@ mod debug_logger;
|
||||
mod discord_rpc;
|
||||
mod git;
|
||||
mod notifications;
|
||||
mod process_ext;
|
||||
mod quick_actions;
|
||||
mod sessions;
|
||||
mod snippets;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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!(
|
||||
@@ -84,7 +82,6 @@ fn build_simple_notification_command(title: &str, body: &str) -> (String, Vec<St
|
||||
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")
|
||||
@@ -112,7 +109,6 @@ pub async fn send_windows_notification(title: String, body: String) -> Result<()
|
||||
|
||||
// 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")
|
||||
@@ -121,7 +117,6 @@ pub async fn send_windows_notification(title: String, body: String) -> Result<()
|
||||
.output()
|
||||
.or_else(|_| {
|
||||
Command::new("powershell.exe")
|
||||
.hide_window()
|
||||
.arg("-NoProfile")
|
||||
.arg("-WindowStyle")
|
||||
.arg("Hidden")
|
||||
@@ -145,7 +140,6 @@ pub async fn send_simple_notification(title: String, body: String) -> Result<(),
|
||||
let message = format_simple_notification(&title, &body);
|
||||
|
||||
Command::new("cmd.exe")
|
||||
.hide_window()
|
||||
.arg("/c")
|
||||
.arg("msg")
|
||||
.arg("*")
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
use std::process::Command;
|
||||
|
||||
/// Extension trait for `Command` that hides the console window on Windows.
|
||||
///
|
||||
/// On non-Windows platforms this is a no-op, so callers can unconditionally
|
||||
/// chain `.hide_window()` without any `#[cfg]` guards at the call sites.
|
||||
pub trait HideWindow {
|
||||
fn hide_window(&mut self) -> &mut Self;
|
||||
}
|
||||
|
||||
impl HideWindow for Command {
|
||||
fn hide_window(&mut self) -> &mut Self {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
self.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ use std::process::Command;
|
||||
use tauri::command;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::process_ext::HideWindow;
|
||||
|
||||
#[command]
|
||||
pub async fn send_vbs_notification(title: String, body: String) -> Result<(), String> {
|
||||
// Create a VBScript that shows a Windows notification
|
||||
@@ -42,7 +40,7 @@ objShell.Popup "{}" & vbCrLf & vbCrLf & "{}", 5, "{}", 64
|
||||
} else if temp_path.starts_with("/tmp/") {
|
||||
// WSL temp files might be in a different location
|
||||
// Try to use wslpath to convert
|
||||
let output = Command::new("wslpath").hide_window().arg("-w").arg(&temp_path).output();
|
||||
let output = Command::new("wslpath").arg("-w").arg(&temp_path).output();
|
||||
|
||||
if let Ok(result) = output {
|
||||
if result.status.success() {
|
||||
@@ -59,7 +57,6 @@ objShell.Popup "{}" & vbCrLf & vbCrLf & "{}", 5, "{}", 64
|
||||
|
||||
// Execute the VBScript using wscript.exe
|
||||
let output = Command::new("/mnt/c/Windows/System32/wscript.exe")
|
||||
.hide_window()
|
||||
.arg("//NoLogo")
|
||||
.arg(&windows_path)
|
||||
.output()
|
||||
|
||||
+23
-105
@@ -1,17 +1,17 @@
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use parking_lot::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use crate::achievements::{get_achievement_info, AchievementUnlockedEvent};
|
||||
use crate::commands::record_cost;
|
||||
use crate::config::ClaudeStartOptions;
|
||||
use crate::process_ext::HideWindow;
|
||||
use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
|
||||
use crate::types::{
|
||||
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
|
||||
@@ -89,7 +89,7 @@ fn find_claude_binary() -> Option<String> {
|
||||
|
||||
// Use a login shell to resolve claude via the user's PATH - GUI apps don't
|
||||
// inherit shell PATH, so bare `which` may miss ~/.local/bin entries
|
||||
if let Ok(output) = Command::new("bash").hide_window().args(["-lc", "which claude"]).output() {
|
||||
if let Ok(output) = Command::new("bash").args(["-lc", "which claude"]).output() {
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !path.is_empty() {
|
||||
@@ -102,59 +102,53 @@ fn find_claude_binary() -> Option<String> {
|
||||
}
|
||||
|
||||
pub struct WslBridge {
|
||||
process: Arc<Mutex<Option<Child>>>,
|
||||
process: Option<Child>,
|
||||
stdin: Option<ChildStdin>,
|
||||
working_directory: String,
|
||||
session_id: Option<String>,
|
||||
mcp_config_file: Option<NamedTempFile>,
|
||||
stats: Arc<RwLock<UsageStats>>,
|
||||
conversation_id: Option<String>,
|
||||
/// Set to true once the `system:init` message arrives, false at the start of every new session.
|
||||
received_init: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl WslBridge {
|
||||
pub fn new() -> Self {
|
||||
WslBridge {
|
||||
process: Arc::new(Mutex::new(None)),
|
||||
process: None,
|
||||
stdin: None,
|
||||
working_directory: String::new(),
|
||||
session_id: None,
|
||||
mcp_config_file: None,
|
||||
stats: Arc::new(RwLock::new(UsageStats::new())),
|
||||
conversation_id: None,
|
||||
received_init: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_conversation_id(conversation_id: String) -> Self {
|
||||
WslBridge {
|
||||
process: Arc::new(Mutex::new(None)),
|
||||
process: None,
|
||||
stdin: None,
|
||||
working_directory: String::new(),
|
||||
session_id: None,
|
||||
mcp_config_file: None,
|
||||
stats: Arc::new(RwLock::new(UsageStats::new())),
|
||||
conversation_id: Some(conversation_id),
|
||||
received_init: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, app: AppHandle, options: ClaudeStartOptions) -> Result<(), String> {
|
||||
// If a process handle exists but the process has already exited (e.g. due to a
|
||||
// failed working directory), clean up the stale handle so we can restart cleanly.
|
||||
{
|
||||
let mut proc_guard = self.process.lock();
|
||||
if let Some(ref mut proc) = *proc_guard {
|
||||
if proc.try_wait().map(|s| s.is_some()).unwrap_or(false) {
|
||||
*proc_guard = None;
|
||||
if let Some(ref mut process) = self.process {
|
||||
if process.try_wait().map(|s| s.is_some()).unwrap_or(false) {
|
||||
self.process = None;
|
||||
self.stdin = None;
|
||||
}
|
||||
}
|
||||
if proc_guard.is_some() {
|
||||
|
||||
if self.process.is_some() {
|
||||
return Err("Process already running".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved achievements and stats when starting a new session
|
||||
let app_clone = app.clone();
|
||||
@@ -230,7 +224,6 @@ impl WslBridge {
|
||||
tracing::debug!("Working dir: {}", working_dir);
|
||||
|
||||
let mut cmd = Command::new(&claude_path);
|
||||
cmd.hide_window();
|
||||
cmd.args([
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
@@ -298,7 +291,6 @@ impl WslBridge {
|
||||
|
||||
// Check if Claude binary is installed inside WSL
|
||||
let binary_check = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "bash", "-lc", "which claude"])
|
||||
.output();
|
||||
if let Ok(output) = binary_check {
|
||||
@@ -309,7 +301,6 @@ impl WslBridge {
|
||||
|
||||
// Validate the working directory exists inside WSL before spawning
|
||||
let dir_check = Command::new("wsl")
|
||||
.hide_window()
|
||||
.args(["-e", "test", "-d", working_dir])
|
||||
.output();
|
||||
if let Ok(output) = dir_check {
|
||||
@@ -384,7 +375,8 @@ impl WslBridge {
|
||||
cmd.args(["-e", "bash", "-lc", &claude_cmd]);
|
||||
|
||||
// Hide the console window on Windows
|
||||
cmd.hide_window();
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||
|
||||
cmd
|
||||
};
|
||||
@@ -404,10 +396,7 @@ impl WslBridge {
|
||||
let stderr = child.stderr.take();
|
||||
|
||||
self.stdin = stdin;
|
||||
*self.process.lock() = Some(child);
|
||||
|
||||
// Reset the init flag so the watchdog and stdout handler start fresh.
|
||||
self.received_init.store(false, Ordering::SeqCst);
|
||||
self.process = Some(child);
|
||||
|
||||
// Note: We no longer reset stats here - stats persist across reconnects
|
||||
// Stats are only reset when explicitly disconnecting via stop()
|
||||
@@ -424,9 +413,8 @@ impl WslBridge {
|
||||
let app_clone = app.clone();
|
||||
let stats_clone = self.stats.clone();
|
||||
let conv_id = self.conversation_id.clone();
|
||||
let received_init_clone = self.received_init.clone();
|
||||
thread::spawn(move || {
|
||||
handle_stdout(stdout, app_clone, stats_clone, conv_id, received_init_clone);
|
||||
handle_stdout(stdout, app_clone, stats_clone, conv_id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -438,31 +426,12 @@ impl WslBridge {
|
||||
});
|
||||
}
|
||||
|
||||
// Emit Connected immediately so the frontend can send the greeting message.
|
||||
// This is intentionally optimistic — Claude Code buffers stdout until stdin receives
|
||||
// data on Windows/WSL, so we must send something to stdin first or system:init never
|
||||
// arrives. The received_init flag below tracks whether init actually arrived.
|
||||
emit_connection_status(
|
||||
&app,
|
||||
ConnectionStatus::Connected,
|
||||
self.conversation_id.clone(),
|
||||
);
|
||||
|
||||
// Watchdog: if system:init never arrives the process is truly hung (e.g. a silent crash
|
||||
// after spawning). After 5 minutes we kill it so the user isn't stuck forever.
|
||||
// handle_stdout will surface the error when stdout closes after the kill.
|
||||
let process_watchdog = self.process.clone();
|
||||
let received_init_watchdog = self.received_init.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(60));
|
||||
if !received_init_watchdog.load(Ordering::SeqCst) {
|
||||
if let Some(mut proc) = process_watchdog.lock().take() {
|
||||
let _ = proc.kill();
|
||||
let _ = proc.wait();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -541,10 +510,7 @@ impl WslBridge {
|
||||
// Due to persistent bug in Claude Code where ESC/Ctrl+C doesn't work,
|
||||
// we have to kill the process. This is the only reliable way to stop it.
|
||||
// See: https://github.com/anthropics/claude-code/issues/3455
|
||||
// Extract the process first so the MutexGuard is dropped before we mutably
|
||||
// borrow `self` again via estimate_interrupted_request_cost.
|
||||
let maybe_process = self.process.lock().take();
|
||||
if let Some(mut process) = maybe_process {
|
||||
if let Some(mut process) = self.process.take() {
|
||||
// Estimate cost for interrupted request before killing
|
||||
self.estimate_interrupted_request_cost(app);
|
||||
|
||||
@@ -674,7 +640,7 @@ impl WslBridge {
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, app: &AppHandle) {
|
||||
if let Some(mut process) = self.process.lock().take() {
|
||||
if let Some(mut process) = self.process.take() {
|
||||
let _ = process.kill();
|
||||
let _ = process.wait();
|
||||
}
|
||||
@@ -705,7 +671,7 @@ impl WslBridge {
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.process.lock().is_some()
|
||||
self.process.is_some()
|
||||
}
|
||||
|
||||
pub fn get_working_directory(&self) -> &str {
|
||||
@@ -728,16 +694,13 @@ fn handle_stdout(
|
||||
app: AppHandle,
|
||||
stats: Arc<RwLock<UsageStats>>,
|
||||
conversation_id: Option<String>,
|
||||
received_init: Arc<AtomicBool>,
|
||||
) {
|
||||
let reader = BufReader::new(stdout);
|
||||
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) if !line.is_empty() => {
|
||||
if let Err(e) =
|
||||
process_json_line(&line, &app, &stats, &conversation_id, &received_init)
|
||||
{
|
||||
if let Err(e) = process_json_line(&line, &app, &stats, &conversation_id) {
|
||||
tracing::error!("Error processing line: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -749,22 +712,6 @@ fn handle_stdout(
|
||||
}
|
||||
}
|
||||
|
||||
// If stdout closed before system:init arrived the process exited without initialising.
|
||||
// Emit an error line so the user understands why the connection failed.
|
||||
if !received_init.load(Ordering::SeqCst) {
|
||||
let _ = app.emit(
|
||||
"claude:output",
|
||||
OutputEvent {
|
||||
line_type: "error".to_string(),
|
||||
content: "Claude Code exited before initialising. Check the working directory and Claude Code installation, then try connecting again.".to_string(),
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None,
|
||||
parent_tool_use_id: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
emit_connection_status(&app, ConnectionStatus::Disconnected, conversation_id);
|
||||
}
|
||||
|
||||
@@ -969,7 +916,6 @@ fn process_json_line(
|
||||
app: &AppHandle,
|
||||
stats: &Arc<RwLock<UsageStats>>,
|
||||
conversation_id: &Option<String>,
|
||||
received_init: &Arc<AtomicBool>,
|
||||
) -> Result<(), String> {
|
||||
let message: ClaudeMessage = serde_json::from_str(line)
|
||||
.map_err(|e| format!("Failed to parse JSON: {} - Line: {}", e, line))?;
|
||||
@@ -982,9 +928,6 @@ fn process_json_line(
|
||||
..
|
||||
} => {
|
||||
if subtype == "init" {
|
||||
// Mark as initialised so the watchdog knows the process is healthy.
|
||||
received_init.store(true, Ordering::SeqCst);
|
||||
|
||||
if let Some(id) = session_id {
|
||||
let _ = app.emit(
|
||||
"claude:session",
|
||||
@@ -2116,7 +2059,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_stale_process_detection_with_try_wait() {
|
||||
// Spawn a real process that exits immediately so we can verify try_wait detects it
|
||||
let mut child = Command::new("true").hide_window().spawn().expect("Failed to spawn 'true'");
|
||||
let mut child = Command::new("true").spawn().expect("Failed to spawn 'true'");
|
||||
|
||||
// Wait for it to exit
|
||||
let _ = child.wait();
|
||||
@@ -2135,7 +2078,7 @@ mod tests {
|
||||
fn test_stale_process_is_some_after_exit() {
|
||||
// Verify the logic used in start(): a process that has exited is detected
|
||||
// and the handle is cleaned up so start() can proceed
|
||||
let mut child = Command::new("true").hide_window().spawn().expect("Failed to spawn 'true'");
|
||||
let mut child = Command::new("true").spawn().expect("Failed to spawn 'true'");
|
||||
|
||||
// Let it exit
|
||||
let _ = child.wait();
|
||||
@@ -2412,29 +2355,4 @@ mod tests {
|
||||
let content = serde_json::json!([]);
|
||||
assert_eq!(extract_tool_result_text(&content), None);
|
||||
}
|
||||
|
||||
// Verify the 50K tool result persistence threshold (CLI v2.1.51+).
|
||||
// Results > 50K chars are now persisted to disk; the stream may send null
|
||||
// or a large inline string. Both must be handled without panicking.
|
||||
#[test]
|
||||
fn test_extract_tool_result_text_large_content_above_50k_threshold() {
|
||||
let large_text = "x".repeat(60_000);
|
||||
let content = serde_json::Value::String(large_text.clone());
|
||||
assert_eq!(extract_tool_result_text(&content), Some(large_text));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_result_deserializes_with_null_content() {
|
||||
let json = r#"{"type":"tool_result","tool_use_id":"toolu_abc","content":null}"#;
|
||||
let block: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
if let ContentBlock::ToolResult { tool_use_id, content, is_error } = block {
|
||||
assert_eq!(tool_use_id, "toolu_abc");
|
||||
assert!(content.is_null());
|
||||
assert_eq!(is_error, None);
|
||||
// Persisted-to-disk results produce null content → no preview shown
|
||||
assert_eq!(extract_tool_result_text(&content), None);
|
||||
} else {
|
||||
panic!("Expected ToolResult variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::process::Command;
|
||||
use tauri::command;
|
||||
|
||||
use crate::process_ext::HideWindow;
|
||||
|
||||
#[command]
|
||||
pub async fn send_wsl_notification(title: String, body: String) -> Result<(), String> {
|
||||
// Method 1: Try Windows 10/11 toast notification using PowerShell
|
||||
@@ -38,7 +36,6 @@ $notifier.Show($toast)
|
||||
|
||||
// Try PowerShell.exe through WSL
|
||||
let output = Command::new("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe")
|
||||
.hide_window()
|
||||
.arg("-NoProfile")
|
||||
.arg("-ExecutionPolicy")
|
||||
.arg("Bypass")
|
||||
@@ -68,7 +65,6 @@ $notifier.Show($toast)
|
||||
|
||||
// Method 3: Try wsl-notify-send if available
|
||||
let notify_result = Command::new("wsl-notify-send")
|
||||
.hide_window()
|
||||
.arg("--appId")
|
||||
.arg("HikariDesktop")
|
||||
.arg("--category")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "hikari-desktop",
|
||||
"version": "1.8.0",
|
||||
"version": "1.7.0",
|
||||
"identifier": "com.naomi.hikari-desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const SUPPORTED_CLI_VERSION = "2.1.53";
|
||||
const SUPPORTED_CLI_VERSION = "2.1.50";
|
||||
|
||||
let installedVersion = $state("Loading...");
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { get } from "svelte/store";
|
||||
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
||||
import { characterState, characterInfo } from "$lib/stores/character";
|
||||
import { isStreamerMode, configStore } from "$lib/stores/config";
|
||||
import { isStreamerMode } from "$lib/stores/config";
|
||||
import { handleNewUserMessage } from "$lib/notifications/rules";
|
||||
import { setSkipNextGreeting } from "$lib/tauri";
|
||||
import type { CharacterState, CharacterStateInfo } from "$lib/types/states";
|
||||
@@ -14,9 +14,6 @@
|
||||
|
||||
let { onExpand }: Props = $props();
|
||||
|
||||
const configValues = configStore.config;
|
||||
const hasBackgroundImage = $derived($configValues.background_image_path !== null);
|
||||
|
||||
let inputValue = $state("");
|
||||
let isSubmitting = $state(false);
|
||||
let isConnected = $state(false);
|
||||
@@ -153,10 +150,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="compact-container {getStateGlow()}"
|
||||
style={hasBackgroundImage ? "background: transparent !important;" : ""}
|
||||
>
|
||||
<div class="compact-container {getStateGlow()}">
|
||||
<!-- Character sprite (smaller) -->
|
||||
<div class="compact-character">
|
||||
<div class="sprite-wrapper {getAnimationClass()}">
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
import { claudeStore } from "$lib/stores/claude";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import CostSummary from "./CostSummary.svelte";
|
||||
|
||||
let config: HikariConfig = $state({
|
||||
@@ -57,8 +56,6 @@
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: [],
|
||||
background_image_path: null,
|
||||
background_image_opacity: 0.3,
|
||||
});
|
||||
|
||||
let showCustomThemeEditor = $state(false);
|
||||
@@ -245,20 +242,6 @@
|
||||
await window.setAlwaysOnTop(enabled);
|
||||
await configStore.updateConfig({ always_on_top: enabled });
|
||||
}
|
||||
|
||||
async function pickBackgroundImage() {
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif"] }],
|
||||
});
|
||||
if (selected) {
|
||||
config.background_image_path = selected;
|
||||
}
|
||||
}
|
||||
|
||||
function clearBackgroundImage() {
|
||||
config.background_image_path = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Backdrop -->
|
||||
@@ -932,52 +915,6 @@
|
||||
expanded/collapsed to see reasoning details.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Background Image -->
|
||||
<div class="mb-4">
|
||||
<span class="block text-sm text-[var(--text-secondary)] mb-2">Background Image</span>
|
||||
{#if config.background_image_path}
|
||||
<p class="text-xs text-[var(--text-tertiary)] font-mono mb-2 truncate">
|
||||
{config.background_image_path.split("/").pop()}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={pickBackgroundImage}
|
||||
class="flex-1 px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)] transition-colors"
|
||||
>
|
||||
{config.background_image_path ? "Change Image" : "Choose Image"}
|
||||
</button>
|
||||
{#if config.background_image_path}
|
||||
<button
|
||||
onclick={clearBackgroundImage}
|
||||
class="px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-red-400 hover:text-red-400 transition-colors"
|
||||
title="Remove background image"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if config.background_image_path}
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label for="bg-opacity" class="text-xs text-[var(--text-secondary)]"> Opacity </label>
|
||||
<span class="text-xs text-[var(--text-tertiary)]">
|
||||
{Math.round(config.background_image_opacity * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
id="bg-opacity"
|
||||
type="range"
|
||||
bind:value={config.background_image_opacity}
|
||||
min="0.05"
|
||||
max="1"
|
||||
step="0.05"
|
||||
class="w-full h-2 bg-[var(--bg-primary)] rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Window Section -->
|
||||
|
||||
@@ -12,25 +12,6 @@
|
||||
let editingTabId = $state<string | null>(null);
|
||||
let editingName = $state("");
|
||||
|
||||
// Tab order for pointer-drag reordering
|
||||
let tabOrder = $state<string[]>([]);
|
||||
let draggedId = $state<string | null>(null);
|
||||
let dragOverId = $state<string | null>(null);
|
||||
let dragStartX = 0;
|
||||
let isDragging = false;
|
||||
let wasDragged = false;
|
||||
let tabsRef = $state<HTMLElement | null>(null);
|
||||
|
||||
// Keep tabOrder in sync with conversations map (add new, remove deleted)
|
||||
$effect(() => {
|
||||
const currentIds = Array.from($conversations.keys());
|
||||
const validIds = tabOrder.filter((id) => currentIds.includes(id));
|
||||
const newIds = currentIds.filter((id) => !tabOrder.includes(id));
|
||||
if (validIds.length !== tabOrder.length || newIds.length > 0) {
|
||||
tabOrder = [...validIds, ...newIds];
|
||||
}
|
||||
});
|
||||
|
||||
// Track last seen message count for each conversation
|
||||
let lastSeenMessageCount = new SvelteMap<string, number>();
|
||||
|
||||
@@ -157,73 +138,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTabClick(id: string) {
|
||||
if (wasDragged) {
|
||||
wasDragged = false;
|
||||
return;
|
||||
}
|
||||
await switchTab(id);
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent, id: string) {
|
||||
if (editingTabId === id) return;
|
||||
draggedId = id;
|
||||
dragStartX = event.clientX;
|
||||
isDragging = false;
|
||||
wasDragged = false;
|
||||
|
||||
function onMove(e: PointerEvent) {
|
||||
if (!isDragging && Math.abs(e.clientX - dragStartX) > 5) {
|
||||
isDragging = true;
|
||||
}
|
||||
if (!isDragging || !tabsRef) return;
|
||||
const tabs = tabsRef.querySelectorAll<HTMLElement>("[data-tab-id]");
|
||||
dragOverId = null;
|
||||
for (const tab of tabs) {
|
||||
const rect = tab.getBoundingClientRect();
|
||||
if (e.clientX >= rect.left && e.clientX <= rect.right) {
|
||||
const tabId = tab.dataset.tabId;
|
||||
if (tabId && tabId !== id) {
|
||||
dragOverId = tabId;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
if (isDragging && draggedId && dragOverId && draggedId !== dragOverId) {
|
||||
const order = [...tabOrder];
|
||||
const fromIndex = order.indexOf(draggedId);
|
||||
const toIndex = order.indexOf(dragOverId);
|
||||
order.splice(fromIndex, 1);
|
||||
order.splice(toIndex, 0, draggedId);
|
||||
tabOrder = order;
|
||||
wasDragged = true;
|
||||
}
|
||||
draggedId = null;
|
||||
dragOverId = null;
|
||||
isDragging = false;
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
window.removeEventListener("pointerup", onUp);
|
||||
window.removeEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
window.addEventListener("pointermove", onMove);
|
||||
window.addEventListener("pointerup", onUp);
|
||||
window.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
onMount(() => {
|
||||
// Initialise all conversations as seen on mount so that remounting
|
||||
// this component (e.g. after closing the file editor) doesn't falsely
|
||||
// mark existing messages as unread.
|
||||
for (const [id, conversation] of $conversations) {
|
||||
lastSeenMessageCount.set(id, conversation.terminalLines.length);
|
||||
}
|
||||
lastSeenMessageCount = lastSeenMessageCount;
|
||||
|
||||
function handleGlobalKeydown(event: KeyboardEvent) {
|
||||
// Ctrl/Cmd + T: New tab
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "t") {
|
||||
@@ -249,19 +165,21 @@
|
||||
// Ctrl/Cmd + Tab: Next tab
|
||||
else if ((event.ctrlKey || event.metaKey) && event.key === "Tab" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const currentIndex = tabOrder.findIndex((id) => id === $activeConversationId);
|
||||
const tabs = Array.from($conversations.keys());
|
||||
const currentIndex = tabs.findIndex((id) => id === $activeConversationId);
|
||||
if (currentIndex !== -1) {
|
||||
const nextIndex = (currentIndex + 1) % tabOrder.length;
|
||||
claudeStore.switchConversation(tabOrder[nextIndex]);
|
||||
const nextIndex = (currentIndex + 1) % tabs.length;
|
||||
claudeStore.switchConversation(tabs[nextIndex]);
|
||||
}
|
||||
}
|
||||
// Ctrl/Cmd + Shift + Tab: Previous tab
|
||||
else if ((event.ctrlKey || event.metaKey) && event.key === "Tab" && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const currentIndex = tabOrder.findIndex((id) => id === $activeConversationId);
|
||||
const tabs = Array.from($conversations.keys());
|
||||
const currentIndex = tabs.findIndex((id) => id === $activeConversationId);
|
||||
if (currentIndex !== -1) {
|
||||
const prevIndex = (currentIndex - 1 + tabOrder.length) % tabOrder.length;
|
||||
claudeStore.switchConversation(tabOrder[prevIndex]);
|
||||
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
||||
claudeStore.switchConversation(tabs[prevIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,22 +190,15 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={tabsRef}
|
||||
class="terminal-tabs flex items-center gap-1 px-2 py-1 bg-[var(--bg-secondary)] border-b border-[var(--border-color)]"
|
||||
>
|
||||
{#each tabOrder
|
||||
.filter((id) => $conversations.has(id))
|
||||
.map((id) => ({ id, conversation: $conversations.get(id)! })) as { id, conversation } (id)}
|
||||
{#each Array.from($conversations.entries()) as [id, conversation] (id)}
|
||||
<div
|
||||
data-tab-id={id}
|
||||
class="tab-item group relative flex items-center px-3 py-1.5 rounded-t transition-all
|
||||
class="tab-item group relative flex items-center px-3 py-1.5 rounded-t cursor-pointer transition-all
|
||||
{id === $activeConversationId
|
||||
? 'bg-[var(--bg-terminal)] text-[var(--text-primary)] border-t border-l border-r border-[var(--border-color)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-terminal)]/50'}
|
||||
{dragOverId === id && draggedId !== id ? 'drag-over' : ''}
|
||||
{draggedId === id ? 'dragging' : ''}"
|
||||
onpointerdown={(e) => handlePointerDown(e, id)}
|
||||
onclick={() => handleTabClick(id)}
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-terminal)]/50'}"
|
||||
onclick={() => switchTab(id)}
|
||||
onkeydown={(e) => handleTabKeydown(id, e)}
|
||||
role="tab"
|
||||
tabindex={0}
|
||||
@@ -300,7 +211,7 @@
|
||||
onblur={saveTabName}
|
||||
onkeydown={handleKeydown}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
class="bg-transparent border-b border-[var(--border-color)] outline-none px-0 py-0 text-sm w-32 select-text"
|
||||
class="bg-transparent border-b border-[var(--border-color)] outline-none px-0 py-0 text-sm w-32"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -385,20 +296,5 @@
|
||||
|
||||
.tab-item {
|
||||
min-width: 100px;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
border-left: 2px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,12 +35,7 @@
|
||||
};
|
||||
|
||||
renderer.codespan = ({ text }) => {
|
||||
const escaped = text.replace(/</g, "<").replace(/>/g, ">");
|
||||
return `<code class="hljs-inline">${escaped}</code>`;
|
||||
};
|
||||
|
||||
renderer.html = ({ text }) => {
|
||||
return text.replace(/</g, "<").replace(/>/g, ">");
|
||||
return `<code class="hljs-inline">${text}</code>`;
|
||||
};
|
||||
|
||||
marked.setOptions({
|
||||
|
||||
@@ -190,13 +190,10 @@
|
||||
<h3 class="text-sm font-medium text-[var(--text-primary)] mb-3">Add MCP Server</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label
|
||||
for="mcp-new-name"
|
||||
class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
||||
>Server Name</label
|
||||
>
|
||||
<input
|
||||
id="mcp-new-name"
|
||||
type="text"
|
||||
bind:value={newServerName}
|
||||
placeholder="my-server"
|
||||
@@ -204,13 +201,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="mcp-new-transport"
|
||||
class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
||||
>Transport</label
|
||||
>
|
||||
<select
|
||||
id="mcp-new-transport"
|
||||
bind:value={newServerTransport}
|
||||
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
||||
>
|
||||
@@ -220,14 +214,10 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="mcp-new-url"
|
||||
class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
||||
>
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1">
|
||||
{newServerTransport === "stdio" ? "Command" : "URL"}
|
||||
</label>
|
||||
<input
|
||||
id="mcp-new-url"
|
||||
type="text"
|
||||
bind:value={newServerUrl}
|
||||
placeholder={newServerTransport === "stdio"
|
||||
@@ -276,7 +266,6 @@
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each servers as server (server.name)}
|
||||
{@const TransportIcon = getTransportIcon(server.transport)}
|
||||
<button
|
||||
onclick={() => loadServerDetails(server.name)}
|
||||
class="w-full bg-[var(--bg-secondary)]/50 rounded-lg p-3 border border-[var(--border-color)] hover:border-[var(--accent-primary)]/50 transition-all text-left"
|
||||
@@ -285,7 +274,10 @@
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-[var(--text-primary)] flex items-center gap-2">
|
||||
<TransportIcon class="w-4 h-4 {getTransportColor(server.transport)}" />
|
||||
<svelte:component
|
||||
this={getTransportIcon(server.transport)}
|
||||
class="w-4 h-4 {getTransportColor(server.transport)}"
|
||||
/>
|
||||
{server.name}
|
||||
{#if server.status}
|
||||
{#if server.status.includes("Connected")}
|
||||
@@ -331,19 +323,25 @@
|
||||
<RefreshCw class="w-6 h-6 animate-spin text-[var(--text-secondary)]" />
|
||||
</div>
|
||||
{:else}
|
||||
{@const TransportIcon = getTransportIcon(selectedServer.transport)}
|
||||
<div class="space-y-4">
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<p class="text-xs text-[var(--text-secondary)] uppercase tracking-wider">Name</p>
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||
>Name</label
|
||||
>
|
||||
<p class="text-sm text-[var(--text-primary)] mt-1">{selectedServer.name}</p>
|
||||
</div>
|
||||
|
||||
<!-- Transport -->
|
||||
<div>
|
||||
<p class="text-xs text-[var(--text-secondary)] uppercase tracking-wider">Transport</p>
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||
>Transport</label
|
||||
>
|
||||
<p class="text-sm text-[var(--text-primary)] mt-1 flex items-center gap-2">
|
||||
<TransportIcon class="w-4 h-4 {getTransportColor(selectedServer.transport)}" />
|
||||
<svelte:component
|
||||
this={getTransportIcon(selectedServer.transport)}
|
||||
class="w-4 h-4 {getTransportColor(selectedServer.transport)}"
|
||||
/>
|
||||
{selectedServer.transport.toUpperCase()}
|
||||
</p>
|
||||
</div>
|
||||
@@ -351,7 +349,9 @@
|
||||
<!-- URL or Command -->
|
||||
{#if selectedServer.url}
|
||||
<div>
|
||||
<p class="text-xs text-[var(--text-secondary)] uppercase tracking-wider">URL</p>
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||
>URL</label
|
||||
>
|
||||
<p
|
||||
class="text-sm text-[var(--text-primary)] mt-1 break-all font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)]"
|
||||
>
|
||||
@@ -362,7 +362,9 @@
|
||||
|
||||
{#if selectedServer.command}
|
||||
<div>
|
||||
<p class="text-xs text-[var(--text-secondary)] uppercase tracking-wider">Command</p>
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||
>Command</label
|
||||
>
|
||||
<p
|
||||
class="text-sm text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)]"
|
||||
>
|
||||
@@ -374,9 +376,9 @@
|
||||
<!-- Environment Variables -->
|
||||
{#if selectedServer.env}
|
||||
<div>
|
||||
<p class="text-xs text-[var(--text-secondary)] uppercase tracking-wider">
|
||||
Environment
|
||||
</p>
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||
>Environment</label
|
||||
>
|
||||
<pre
|
||||
class="text-xs text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)] overflow-x-auto">{JSON.stringify(
|
||||
selectedServer.env,
|
||||
@@ -389,9 +391,9 @@
|
||||
<!-- Full Server Details -->
|
||||
{#if serverDetails}
|
||||
<div>
|
||||
<p class="text-xs text-[var(--text-secondary)] uppercase tracking-wider">
|
||||
Full Details
|
||||
</p>
|
||||
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
||||
>Full Details</label
|
||||
>
|
||||
<pre
|
||||
class="text-xs text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)] overflow-x-auto whitespace-pre-wrap">{serverDetails}</pre>
|
||||
</div>
|
||||
@@ -414,3 +416,18 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -430,3 +430,18 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -471,7 +471,6 @@
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === "Escape" && (showClearAllConfirm = false)}
|
||||
>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="bg-[var(--bg-primary)] border border-red-500/30 rounded-lg shadow-xl max-w-md w-full p-6"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -108,8 +108,6 @@
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: [],
|
||||
background_image_path: null,
|
||||
background_image_opacity: 0.3,
|
||||
});
|
||||
|
||||
let streamerModeActive = $state(false);
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={menuElement}
|
||||
class="menu-content"
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="dialog-overlay" onclick={onCancel}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="dialog-content" onclick={(e) => e.stopPropagation()}>
|
||||
<h2 class="dialog-title">{title}</h2>
|
||||
<p class="dialog-message">{message}</p>
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={menuElement}
|
||||
class="menu-content"
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu-overlay"
|
||||
onclick={onClose}
|
||||
@@ -87,7 +87,7 @@
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={menuElement}
|
||||
class="menu-content"
|
||||
|
||||
@@ -50,9 +50,9 @@
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="dialog-overlay" onclick={onCancel}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="dialog-content" onclick={(e) => e.stopPropagation()}>
|
||||
<h2 class="dialog-title">{title}</h2>
|
||||
|
||||
|
||||
@@ -197,8 +197,6 @@ describe("config store", () => {
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: [],
|
||||
background_image_path: null,
|
||||
background_image_opacity: 0.3,
|
||||
};
|
||||
|
||||
expect(config.model).toBe("claude-sonnet-4");
|
||||
@@ -248,8 +246,6 @@ describe("config store", () => {
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: [],
|
||||
background_image_path: null,
|
||||
background_image_opacity: 0.3,
|
||||
};
|
||||
|
||||
expect(config.model).toBeNull();
|
||||
@@ -798,8 +794,6 @@ describe("config store", () => {
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: [],
|
||||
background_image_path: null,
|
||||
background_image_opacity: 0.3,
|
||||
};
|
||||
|
||||
const mockInvokeImpl = vi.mocked(invoke);
|
||||
|
||||
@@ -53,9 +53,6 @@ export interface HikariConfig {
|
||||
disable_1m_context: boolean;
|
||||
// Workspaces the user has explicitly trusted
|
||||
trusted_workspaces: string[];
|
||||
// Background image settings
|
||||
background_image_path: string | null;
|
||||
background_image_opacity: number;
|
||||
}
|
||||
|
||||
const defaultConfig: HikariConfig = {
|
||||
@@ -99,8 +96,6 @@ const defaultConfig: HikariConfig = {
|
||||
use_worktree: false,
|
||||
disable_1m_context: false,
|
||||
trusted_workspaces: [],
|
||||
background_image_path: null,
|
||||
background_image_opacity: 0.3,
|
||||
};
|
||||
|
||||
function createConfigStore() {
|
||||
|
||||
+3
-58
@@ -12,7 +12,6 @@
|
||||
setSkipNextGreeting,
|
||||
} from "$lib/tauri";
|
||||
import { configStore, applyTheme, applyFontSize, isCompactMode } from "$lib/stores/config";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
||||
import { conversationsStore } from "$lib/stores/conversations";
|
||||
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
||||
@@ -38,45 +37,6 @@
|
||||
import { debugConsoleStore } from "$lib/stores/debugConsole";
|
||||
import { initializeTodoListener, cleanupTodoListener } from "$lib/stores/todos";
|
||||
|
||||
let backgroundDataUrl = $state<string | null>(null);
|
||||
let backgroundOpacity = $state(0.3);
|
||||
|
||||
const configValues = configStore.config;
|
||||
$effect(() => {
|
||||
const cfg = $configValues;
|
||||
backgroundOpacity = cfg.background_image_opacity;
|
||||
if (cfg.background_image_path) {
|
||||
void loadBackgroundImage(cfg.background_image_path);
|
||||
} else {
|
||||
backgroundDataUrl = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadBackgroundImage(path: string) {
|
||||
try {
|
||||
const data = await readFile(path);
|
||||
const chunks: string[] = [];
|
||||
const chunkSize = 8192;
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
chunks.push(String.fromCharCode(...data.slice(i, i + chunkSize)));
|
||||
}
|
||||
const ext = path.split(".").pop()?.toLowerCase() ?? "png";
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
png: "image/png",
|
||||
webp: "image/webp",
|
||||
gif: "image/gif",
|
||||
avif: "image/avif",
|
||||
};
|
||||
const mime = mimeMap[ext] ?? "image/png";
|
||||
backgroundDataUrl = `data:${mime};base64,${btoa(chunks.join(""))}`;
|
||||
} catch (error) {
|
||||
console.error("Failed to load background image:", error);
|
||||
backgroundDataUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
let updateNotification: UpdateNotification | undefined = $state(undefined);
|
||||
let achievementPanelOpen = $state(false);
|
||||
@@ -513,27 +473,16 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if backgroundDataUrl}
|
||||
<div
|
||||
class="fixed inset-0 bg-cover bg-center pointer-events-none"
|
||||
style="background-image: url('{backgroundDataUrl}'); opacity: {backgroundOpacity}; z-index: 0;"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
{#if compactModeActive}
|
||||
<!-- Compact mode: minimal widget interface -->
|
||||
<div
|
||||
class="app-container compact-app h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden"
|
||||
style={backgroundDataUrl ? "background: transparent;" : ""}
|
||||
>
|
||||
<CompactMode onExpand={exitCompactMode} />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Full mode: standard interface -->
|
||||
<div
|
||||
class="app-container h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden"
|
||||
style={backgroundDataUrl ? "background: transparent;" : ""}
|
||||
>
|
||||
<div class="app-container h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden">
|
||||
<StatusBar
|
||||
onToggleAchievements={() => (achievementPanelOpen = !achievementPanelOpen)}
|
||||
onToggleCompact={enterCompactMode}
|
||||
@@ -542,12 +491,8 @@
|
||||
<main class="flex-1 flex overflow-hidden">
|
||||
<!-- Left panel: Character display -->
|
||||
<div
|
||||
class="character-panel {getPanelGlowClass()} flex flex-col items-center justify-center {backgroundDataUrl
|
||||
? ''
|
||||
: 'bg-[var(--bg-secondary)]/50'}"
|
||||
style="width: {panelWidth}px; min-width: {MIN_PANEL_WIDTH}px; max-width: {MAX_PANEL_WIDTH}px;{backgroundDataUrl
|
||||
? ' background: transparent !important;'
|
||||
: ''}"
|
||||
class="character-panel {getPanelGlowClass()} flex flex-col items-center justify-center bg-[var(--bg-secondary)]/50"
|
||||
style="width: {panelWidth}px; min-width: {MIN_PANEL_WIDTH}px; max-width: {MAX_PANEL_WIDTH}px;"
|
||||
>
|
||||
<AnimeGirl />
|
||||
</div>
|
||||
|
||||
@@ -8,20 +8,6 @@ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
// Suppress specific build-time warnings that are intentional patterns:
|
||||
// - a11y_click_events_have_key_events: all overlay/context-menu divs use svelte:window handlers
|
||||
// - state_referenced_locally: InputDialog intentionally captures the initial prop value
|
||||
onwarn: (warning, handler) => {
|
||||
if (
|
||||
warning.code === "a11y_click_events_have_key_events" ||
|
||||
warning.code === "state_referenced_locally" ||
|
||||
// SvelteSet is already reactive; $state wrapping is unnecessary per ESLint,
|
||||
// but vite-plugin-svelte incorrectly fires non_reactive_update on SvelteSet mutations
|
||||
warning.code === "non_reactive_update"
|
||||
)
|
||||
return;
|
||||
handler(warning);
|
||||
},
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
fallback: "index.html",
|
||||
|
||||
+1
-15
@@ -1,27 +1,13 @@
|
||||
import { defineConfig, createLogger } from "vite";
|
||||
import { defineConfig } from "vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// SvelteKit passes codeSplitting to Rollup 4 which no longer recognises it — harmless
|
||||
const logger = createLogger();
|
||||
const baseWarn = logger.warn.bind(logger);
|
||||
logger.warn = (/** @type {string} */ msg, /** @type {any} */ options) => {
|
||||
// SvelteKit passes codeSplitting to Rollup 4 which no longer recognises it
|
||||
if (msg.includes("codeSplitting")) return;
|
||||
// Large chunks are fine for a desktop app — no network penalty
|
||||
if (msg.includes("chunks are larger than")) return;
|
||||
// Dynamic/static import mix in CodeMirror — harmless, module stays in main chunk
|
||||
if (msg.includes("dynamically imported by") && msg.includes("codemirror")) return;
|
||||
baseWarn(msg, options);
|
||||
};
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
customLogger: logger,
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user