generated from nhcarrigan/template
Compare commits
2 Commits
v1.5.1
..
269f33b52a
| Author | SHA1 | Date | |
|---|---|---|---|
| 269f33b52a | |||
| e4b27bdff3 |
@@ -141,37 +141,6 @@ When developing new features, always add corresponding tests:
|
|||||||
|
|
||||||
The goal is to maintain our near-100% coverage as the codebase grows, so future refactoring and changes can be made with confidence!
|
The goal is to maintain our near-100% coverage as the codebase grows, so future refactoring and changes can be made with confidence!
|
||||||
|
|
||||||
## Quality Assurance
|
|
||||||
|
|
||||||
Before committing any changes, **always run the full test suite**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./check-all.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script runs all checks in the correct order:
|
|
||||||
|
|
||||||
1. Frontend linting (ESLint)
|
|
||||||
2. Frontend formatting (Prettier)
|
|
||||||
3. Frontend type checking (svelte-check)
|
|
||||||
4. Frontend tests with coverage (Vitest)
|
|
||||||
5. Backend linting (Clippy with strict rules)
|
|
||||||
6. Backend tests with coverage (cargo test + llvm-cov)
|
|
||||||
|
|
||||||
**Important**: The script requires Node.js and Rust toolchains to be available:
|
|
||||||
|
|
||||||
- **Node.js tools** (pnpm, npm): Source nvm first if needed: `source ~/.nvm/nvm.sh`
|
|
||||||
- **Rust tools** (cargo, clippy): Should be in PATH via `~/.cargo/bin/`
|
|
||||||
|
|
||||||
If `check-all.sh` reports any failures:
|
|
||||||
|
|
||||||
1. Read the error messages carefully - they usually explain what needs fixing
|
|
||||||
2. Fix the issues (linting errors, test failures, etc.)
|
|
||||||
3. Run `check-all.sh` again to verify the fixes
|
|
||||||
4. Only commit once all checks pass ✨
|
|
||||||
|
|
||||||
**Never commit code that doesn't pass `check-all.sh`** - this ensures code quality and prevents broken builds!
|
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
Hikari Desktop is a Tauri-based desktop application that wraps Claude Code with a visual anime character (Hikari) who appears on screen. This is a personal project where Hikari can sign her work and act as herself!
|
Hikari Desktop is a Tauri-based desktop application that wraps Claude Code with a visual anime character (Hikari) who appears on screen. This is a personal project where Hikari can sign her work and act as herself!
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hikari-desktop",
|
"name": "hikari-desktop",
|
||||||
"version": "1.5.1",
|
"version": "1.5.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+1
-1
@@ -1636,7 +1636,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "1.5.1"
|
version = "1.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "1.5.1"
|
version = "1.5.0"
|
||||||
description = "Hikari - Claude Code Visual Assistant"
|
description = "Hikari - Claude Code Visual Assistant"
|
||||||
authors = ["Naomi Carrigan"]
|
authors = ["Naomi Carrigan"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
+9
-115
@@ -55,52 +55,16 @@ fn wsl_path_to_windows(wsl_path: &str) -> Option<String> {
|
|||||||
fn create_claude_command() -> std::process::Command {
|
fn create_claude_command() -> std::process::Command {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
// Use `which` inside WSL to find the claude binary dynamically
|
|
||||||
// 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")
|
|
||||||
.args(["-e", "bash", "-l", "-c", "which 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("wsl");
|
|
||||||
cmd.arg(claude_path);
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Fallback to just "claude" if which fails
|
|
||||||
// This maintains backwards compatibility
|
|
||||||
let mut cmd = std::process::Command::new("wsl");
|
let mut cmd = std::process::Command::new("wsl");
|
||||||
cmd.arg("claude");
|
cmd.arg("claude");
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
// Use `which` to find the claude binary dynamically
|
|
||||||
// This works regardless of how Claude Code was installed (standalone, npm, etc.)
|
|
||||||
// and avoids hardcoding paths
|
|
||||||
let which_output = std::process::Command::new("which")
|
|
||||||
.arg("claude")
|
|
||||||
.output();
|
|
||||||
|
|
||||||
match which_output {
|
|
||||||
Ok(output) if output.status.success() => {
|
|
||||||
let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
std::process::Command::new(claude_path)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Fallback to just "claude" if which fails
|
|
||||||
// This maintains backwards compatibility
|
|
||||||
std::process::Command::new("claude")
|
std::process::Command::new("claude")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn start_claude(
|
pub async fn start_claude(
|
||||||
@@ -1219,55 +1183,6 @@ pub struct MemoryFilesResponse {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_memory_files() -> Result<MemoryFilesResponse, String> {
|
pub async fn list_memory_files() -> Result<MemoryFilesResponse, String> {
|
||||||
// On Windows, we need to look in the WSL home directory
|
|
||||||
// On Linux/Mac, use the native home directory
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
list_memory_files_via_wsl().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
list_memory_files_native().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List memory files via WSL (for Windows)
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn list_memory_files_via_wsl() -> Result<MemoryFilesResponse, String> {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
// Use WSL to find all memory files in the WSL home directory
|
|
||||||
// This script finds all "memory" directories and lists their files
|
|
||||||
let script = r#"
|
|
||||||
find ~/.claude/projects -type d -name memory 2>/dev/null | while read dir; do
|
|
||||||
find "$dir" -maxdepth 1 -type f 2>/dev/null
|
|
||||||
done | sort
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let output = Command::new("wsl")
|
|
||||||
.args(["-e", "bash", "-l", "-c", script])
|
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
return Err(format!("Failed to list memory files: {}", stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let files: Vec<String> = stdout
|
|
||||||
.lines()
|
|
||||||
.filter(|line| !line.trim().is_empty())
|
|
||||||
.map(|line| line.trim().to_string())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(MemoryFilesResponse { files })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List memory files using native filesystem (for Linux/Mac)
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
async fn list_memory_files_native() -> Result<MemoryFilesResponse, String> {
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
// Get the .claude directory in the user's home
|
// Get the .claude directory in the user's home
|
||||||
@@ -1512,7 +1427,7 @@ pub async fn uninstall_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Enabling plugin: {}", plugin_name);
|
tracing::debug!("Enabling plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = create_claude_command()
|
let output = std::process::Command::new("claude")
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("enable")
|
.arg("enable")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1541,7 +1456,7 @@ pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Disabling plugin: {}", plugin_name);
|
tracing::debug!("Disabling plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = create_claude_command()
|
let output = std::process::Command::new("claude")
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("disable")
|
.arg("disable")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1570,7 +1485,7 @@ pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Updating plugin: {}", plugin_name);
|
tracing::debug!("Updating plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = create_claude_command()
|
let output = std::process::Command::new("claude")
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("update")
|
.arg("update")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1642,7 +1557,7 @@ fn parse_marketplace_list(stdout: &str) -> Vec<MarketplaceInfo> {
|
|||||||
pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, String> {
|
pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, String> {
|
||||||
tracing::debug!("Listing plugin marketplaces");
|
tracing::debug!("Listing plugin marketplaces");
|
||||||
|
|
||||||
let output = create_claude_command()
|
let output = std::process::Command::new("claude")
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("marketplace")
|
.arg("marketplace")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
@@ -1675,7 +1590,7 @@ pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, String> {
|
|||||||
pub async fn add_marketplace(source: String) -> Result<String, String> {
|
pub async fn add_marketplace(source: String) -> Result<String, String> {
|
||||||
tracing::debug!("Adding marketplace: {}", source);
|
tracing::debug!("Adding marketplace: {}", source);
|
||||||
|
|
||||||
let output = create_claude_command()
|
let output = std::process::Command::new("claude")
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("marketplace")
|
.arg("marketplace")
|
||||||
.arg("add")
|
.arg("add")
|
||||||
@@ -1708,7 +1623,7 @@ pub async fn add_marketplace(source: String) -> Result<String, String> {
|
|||||||
pub async fn remove_marketplace(name: String) -> Result<String, String> {
|
pub async fn remove_marketplace(name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Removing marketplace: {}", name);
|
tracing::debug!("Removing marketplace: {}", name);
|
||||||
|
|
||||||
let output = create_claude_command()
|
let output = std::process::Command::new("claude")
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("marketplace")
|
.arg("marketplace")
|
||||||
.arg("remove")
|
.arg("remove")
|
||||||
@@ -2015,42 +1930,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn test_create_claude_command_windows() {
|
fn test_create_claude_command_windows() {
|
||||||
// On Windows, should create a command that uses wsl with full path to claude
|
// On Windows, should create a command that uses wsl as the program with claude as first arg
|
||||||
// The path is resolved dynamically via `which` in a login shell
|
|
||||||
let cmd = create_claude_command();
|
let cmd = create_claude_command();
|
||||||
let program = cmd.get_program();
|
let program = cmd.get_program();
|
||||||
|
|
||||||
assert_eq!(program, "wsl");
|
assert_eq!(program, "wsl");
|
||||||
|
|
||||||
// Verify the first argument is a path to claude (full path from `which`)
|
|
||||||
// or fallback to just "claude" if which fails
|
|
||||||
let args: Vec<&std::ffi::OsStr> = cmd.get_args().collect();
|
|
||||||
assert_eq!(args.len(), 1);
|
|
||||||
|
|
||||||
let arg_str = args[0].to_string_lossy();
|
|
||||||
assert!(
|
|
||||||
arg_str.contains("claude"),
|
|
||||||
"Expected argument to contain 'claude', got: {}",
|
|
||||||
arg_str
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
fn test_create_claude_command_linux() {
|
fn test_create_claude_command_linux() {
|
||||||
// On Linux/Mac, should create a command that uses the full path to claude
|
// On Linux/Mac, should create a command that uses claude directly
|
||||||
// (resolved via `which` command)
|
|
||||||
let cmd = create_claude_command();
|
let cmd = create_claude_command();
|
||||||
let program = cmd.get_program();
|
let program = cmd.get_program();
|
||||||
|
|
||||||
// The program should be the full path to claude (from `which`)
|
assert_eq!(program, "claude");
|
||||||
// or fallback to "claude" if which fails
|
|
||||||
let program_str = program.to_string_lossy();
|
|
||||||
assert!(
|
|
||||||
program_str.ends_with("claude"),
|
|
||||||
"Expected program to end with 'claude', got: {}",
|
|
||||||
program_str
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== validate_directory tests ====================
|
// ==================== validate_directory tests ====================
|
||||||
|
|||||||
@@ -129,11 +129,6 @@ impl WslBridge {
|
|||||||
return Err("Process already running".to_string());
|
return Err("Process already running".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Claude binary is installed before attempting to start
|
|
||||||
if Command::new("which").arg("claude").output().ok().is_none_or(|output| !output.status.success()) {
|
|
||||||
return Err("Claude Code is not installed. Please install it using:\n\ncurl -fsSL https://claude.ai/install.sh | bash".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load saved achievements and stats when starting a new session
|
// Load saved achievements and stats when starting a new session
|
||||||
let app_clone = app.clone();
|
let app_clone = app.clone();
|
||||||
let stats = self.stats.clone();
|
let stats = self.stats.clone();
|
||||||
@@ -1873,22 +1868,6 @@ mod tests {
|
|||||||
assert!(!bridge.is_running());
|
assert!(!bridge.is_running());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_claude_binary_check_command_structure() {
|
|
||||||
// Test that we're using the correct command to check for Claude binary
|
|
||||||
let output = Command::new("which").arg("claude").output();
|
|
||||||
|
|
||||||
// The command should execute successfully (even if claude is not found)
|
|
||||||
// We're just verifying the command structure is valid
|
|
||||||
assert!(output.is_ok(), "which command should execute without error");
|
|
||||||
|
|
||||||
// Verify the check logic returns a boolean
|
|
||||||
// This is the same logic used in start() to check if claude is installed
|
|
||||||
let _result = output.ok().is_none_or(|o| !o.status.success());
|
|
||||||
// If claude is not installed, _result will be true (show error)
|
|
||||||
// If claude is installed, _result will be false (proceed with connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_shared_bridge_manager() {
|
fn test_create_shared_bridge_manager() {
|
||||||
use crate::bridge_manager::create_shared_bridge_manager;
|
use crate::bridge_manager::create_shared_bridge_manager;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "hikari-desktop",
|
"productName": "hikari-desktop",
|
||||||
"version": "1.5.1",
|
"version": "1.5.0",
|
||||||
"identifier": "com.naomi.hikari-desktop",
|
"identifier": "com.naomi.hikari-desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||||
import Markdown from "./Markdown.svelte";
|
import Markdown from "./Markdown.svelte";
|
||||||
|
|
||||||
let memoryFiles: string[] = $state([]);
|
let memoryFiles: string[] = $state([]);
|
||||||
@@ -32,8 +33,7 @@
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
// Use our backend command instead of Tauri plugin to handle WSL paths
|
const content = await readTextFile(filePath);
|
||||||
const content = await invoke<string>("read_file_content", { path: filePath });
|
|
||||||
fileContent = content;
|
fileContent = content;
|
||||||
selectedFile = filePath;
|
selectedFile = filePath;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user