Compare commits

..

9 Commits

Author SHA1 Message Date
minori 294ea44e5d deps: update @codemirror/legacy-modes to 6.5.3
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m40s
CI / Lint & Test (pull_request) Successful in 19m0s
CI / Build Linux (pull_request) Successful in 20m27s
CI / Build Windows (cross-compile) (pull_request) Successful in 30m24s
2026-05-24 07:03:19 -07:00
naomi b5693d7b7e release: v1.15.0
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint & Test (push) Successful in 16m30s
CI / Build Linux (push) Successful in 20m47s
CI / Build Windows (cross-compile) (push) Successful in 30m45s
2026-05-06 17:06:51 -07:00
hikari 1c4432c4d8 feat: Claude Code CLI v2.1.105–v2.1.131 support (#274)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m3s
CI / Lint & Test (push) Successful in 16m43s
CI / Build Linux (push) Successful in 21m5s
CI / Build Windows (cross-compile) (push) Successful in 31m3s
## Overview

Full audit and implementation of Claude Code CLI changelog entries from v2.1.105 through v2.1.131.

## Changes

### Implemented

- **#268** — Add `claude-opus-4-7` to the model picker, update all model pricing and context window sizes (also fixes a bug where `claude-opus-4-6` was coded as 200K context instead of 1M)
- **#269** — Expose effort level setting in UI via `--effort <level>` flag (`low`, `medium`, `high`, `xhigh`, `max`)
- **#273** — Expose prompt caching TTL env vars in UI (`ENABLE_PROMPT_CACHING_1H` / `FORCE_PROMPT_CACHING_5M`)
- **#267** — Add `PreCompact` hook support — emits `claude:pre-compact` event with "Compacting context..." toast and thinking state
- **#270** — Parse `plugin_errors` from stream-json init event and surface them as error output lines
- **#266** — Bump supported CLI version constant to `2.1.131`

### Closed as Not Applicable

- **#271** (`autoScrollEnabled`) — TUI-only setting; we manage our own scroll behaviour
- **#272** (`tui` fullscreen mode) — TUI-only setting; we use stream-json and never activate the TUI

## Testing

All checks pass (`./check-all.sh`) including frontend lint, format, type check, Vitest coverage, Clippy, and Rust test coverage.

✨ This PR was created with help from Hikari~ 🌸

Reviewed-on: #274
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-05-06 16:16:06 -07:00
naomi 7a1ab89ad8 release: v1.14.0
CI / Lint & Test (push) Successful in 17m53s
CI / Build Linux (push) Successful in 24m6s
CI / Build Windows (cross-compile) (push) Successful in 38m42s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 2m29s
2026-04-13 15:59:52 -07:00
hikari c4af551375 fix: read/write global CLAUDE.md via WSL on Windows (#264)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m0s
CI / Lint & Test (push) Successful in 16m37s
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
## Summary

- `get_global_claude_md` and `save_global_claude_md` were using `dirs::home_dir()` which resolves to the Windows home directory (`C:\Users\accou`) on Windows builds
- The actual `~/.claude/CLAUDE.md` lives in the WSL home directory, so the editor was always showing an empty text box and would have written to the wrong location on save
- Added `get_global_claude_md_via_wsl()` and `save_global_claude_md_via_wsl()` helpers that shell out to WSL (matching the existing pattern used by `list_skills` and `list_memory_files`)
- Both Tauri commands now branch on `cfg!(target_os = "windows")` to use the appropriate path

## Test plan

- [ ] Open Settings sidebar on a Windows build — Global Instructions textarea should load the contents of `~/.claude/CLAUDE.md` from WSL
- [ ] Edit the content and click Save — verify the WSL file is updated
- [ ] Verify Linux/macOS builds are unaffected (native filesystem path unchanged)

✨ This issue was created with help from Hikari~ 🌸

Reviewed-on: #264
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-04-13 15:56:53 -07:00
hikari b88f25a61b feat: CLI v2.1.81 features + global CLAUDE.md editor (#263)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m9s
CI / Lint & Test (push) Successful in 18m55s
CI / Build Linux (push) Successful in 22m9s
CI / Build Windows (cross-compile) (push) Successful in 31m38s
## Summary

Implements support for Claude Code CLI v2.1.81 features and adds a global CLAUDE.md editor, closing issues #237, #239, #244, #245, #246, #247, #248, and #262.

### Stream-JSON forward-compatibility (#245, #246, #247, #248)

- **#248** — `output_style` field added to `System` init message; silently accepted for forward-compat
- **#245** — `fast_mode_state` field added to `Result` message; logged at debug level
- **#246** — `model_usage` field added to `Result` message; per-model breakdown logged at debug level
- **#247** — `total_cost_usd` field added to `Result` message; authoritative cost logged at debug level

### New config options (#237, #239, #244)

- **#237** — `bare_mode` config toggle: passes `--bare` to Claude Code, suppressing UI chrome for scripted headless `-p` calls
- **#239** — `show_clear_context_on_plan_accept` toggle: passes `showClearContextOnPlanAccept: false` in `--settings` when disabled
- **#244** — `custom_model_option` text field: sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers

### Global CLAUDE.md editor (#262)

- New Tauri commands `get_global_claude_md` / `save_global_claude_md` read/write `~/.claude/CLAUDE.md` (creates file + directory if absent)
- New "Global Instructions" section in the Config Sidebar with a textarea and Save button

### Bug fix (pre-existing)

`disable_cron` and `disable_skill_shell_execution` were saved to `HikariConfig` but never passed to `start_claude` invocations — fixed in all 9 call sites. All 3 new config fields are also wired through all 9 call sites.

All changes pass `check-all.sh` (ESLint → Prettier → svelte-check → Vitest → Clippy → cargo test with llvm-cov).

✨ This PR was created with help from Hikari~ 🌸

Reviewed-on: #263
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-04-13 13:32:03 -07:00
hikari 5663b1c09a feat: CLI v2.1.81–v2.1.104 support (#261)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m33s
CI / Lint & Test (push) Successful in 20m29s
CI / Build Linux (push) Successful in 24m9s
CI / Build Windows (cross-compile) (push) Successful in 40m28s
## Summary

Implements support for all Claude Code CLI changes from v2.1.81 through v2.1.104, closing issues #253–#260.

### Changes

- **#253** — New `CwdChanged` hook event: parse and emit `claude:cwd-changed` Tauri event; new `CwdChangedEvent` type
- **#254** — New `FileChanged` hook event: parse and emit `claude:file-changed` Tauri event; new `FileChangedEvent` type
- **#255** — Idle-return prompt: TUI-only feature, not present in `--output-format stream-json` mode — closed as not applicable
- **#256** — New `TaskCreated` and `PermissionDenied` hook events: parse and emit `claude:task-created` / `claude:permission-denied` Tauri events; `PermissionDenied` also triggers `CharacterState::Permission` character animation
- **#257** — Defer permission request: no PreToolUse hook response mechanism in Hikari Desktop — closed as not applicable
- **#258** — `Monitor` tool: added `"Monitor"` to `SEARCH_TOOLS` constant so it maps to `CharacterState::Searching`
- **#259** — `disableSkillShellExecution` setting: wired through `ClaudeStartOptions`, `HikariConfig`, `--settings` JSON, TypeScript interface, and exposed in the Config Sidebar UI
- **#260** — Updated `SUPPORTED_CLI_VERSION` constant from `"2.1.80"` to `"2.1.104"`

All changes pass `check-all.sh` (ESLint → Prettier → svelte-check → Vitest → Clippy → cargo test with llvm-cov).

✨ This PR was created with help from Hikari~ 🌸

Reviewed-on: #261
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-04-13 12:05:57 -07:00
naomi 542d2eb315 release: v1.13.0
CI / Lint & Test (push) Successful in 16m44s
CI / Build Linux (push) Successful in 21m21s
CI / Build Windows (cross-compile) (push) Successful in 31m55s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 2m19s
2026-03-23 14:41:21 -07:00
hikari 4134e11c88 chore: CLI v2.1.75–v2.1.80 audit and support (#223–#232) (#233)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m8s
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled
## Summary

This PR implements all tickets filed from the CLI v2.1.74 → v2.1.80 changelog audit (issues #223–#232).

### Changes by Issue

- **#223** — `feat: handle Elicitation and ElicitationResult hook events`
  New `ElicitationModal.svelte` component, Rust parsing for `[Elicitation Hook]` and `[ElicitationResult Hook]`, new store methods, and TypeScript event types.

- **#224** — `feat: handle StopFailure hook event for API error turns`
  Rust parsing for `[StopFailure Hook]`; frontend shows error toast + error character state.

- **#225** — `feat: handle PostCompact hook event`
  Rust parsing for `[PostCompact Hook]`; frontend shows info toast + success character state.

- **#226** — `feat: expose --name CLI flag as session name at startup`
  Added `session_name` field to `ClaudeStartOptions`; `StatusBar.doConnect()` passes the conversation name.

- **#227** — `fix: tighten startup watchdog and correct misleading comment`
  Startup watchdog tightened from 60 s → 30 s; corrected a comment that said "5 minutes" whilst the code used 60 seconds.

- **#228** — `fix: document cost estimation review and update default model fallback`
  Default model fallback updated from `claude-sonnet-4-5-20250929` → `claude-sonnet-4-6`; added doc comment explaining why char-based estimation is unaffected by v2.1.75 token overcounting fix.

- **#229** — `chore: update supported CLI version constant to 2.1.80`
  `SUPPORTED_CLI_VERSION` bumped in `CliVersion.svelte`.

- **#230** — `feat: surface memory file last-modified timestamps in MemoryBrowserPanel`
  Backend populates `last_modified` Unix timestamp; frontend formats and displays it per file.

- **#231** — `feat: update max_output_tokens upper bound and helper text for 128k`
  Input max raised to 128 000; placeholder and helper text updated to reflect model-dependent defaults and 128 k ceiling for Opus/Sonnet 4.6.

- **#232** — `fix: document non-streaming fallback compatibility with mid-session watchdog`
  Added doc comment above `STUCK_TIMEOUT` explaining the 5-minute watchdog is intentionally larger than the CLI's 2-minute non-streaming API fallback.

---

✨ This PR was created with help from Hikari~ 🌸

Reviewed-on: #233
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-23 14:28:08 -07:00
32 changed files with 2496 additions and 63 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "hikari-desktop", "name": "hikari-desktop",
"version": "1.12.0", "version": "1.15.0",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -48,7 +48,7 @@
"@codemirror/lang-xml": "6.1.0", "@codemirror/lang-xml": "6.1.0",
"@codemirror/lang-yaml": "6.1.2", "@codemirror/lang-yaml": "6.1.2",
"@codemirror/language": "6.12.2", "@codemirror/language": "6.12.2",
"@codemirror/legacy-modes": "6.5.2", "@codemirror/legacy-modes": "6.5.3",
"@codemirror/state": "6.5.4", "@codemirror/state": "6.5.4",
"@codemirror/theme-one-dark": "6.1.3", "@codemirror/theme-one-dark": "6.1.3",
"@codemirror/view": "6.39.15", "@codemirror/view": "6.39.15",
@@ -83,7 +83,7 @@
"globals": "17.3.0", "globals": "17.3.0",
"jsdom": "28.1.0", "jsdom": "28.1.0",
"prettier": "3.8.1", "prettier": "3.8.1",
"prettier-plugin-svelte": "3.5.1", "prettier-plugin-svelte": "3.5.0",
"svelte": "5.53.5", "svelte": "5.53.5",
"svelte-check": "4.4.3", "svelte-check": "4.4.3",
"tailwindcss": "4.2.1", "tailwindcss": "4.2.1",
+10 -10
View File
@@ -72,8 +72,8 @@ importers:
specifier: 6.12.2 specifier: 6.12.2
version: 6.12.2 version: 6.12.2
'@codemirror/legacy-modes': '@codemirror/legacy-modes':
specifier: 6.5.2 specifier: 6.5.3
version: 6.5.2 version: 6.5.3
'@codemirror/state': '@codemirror/state':
specifier: 6.5.4 specifier: 6.5.4
version: 6.5.4 version: 6.5.4
@@ -172,8 +172,8 @@ importers:
specifier: 3.8.1 specifier: 3.8.1
version: 3.8.1 version: 3.8.1
prettier-plugin-svelte: prettier-plugin-svelte:
specifier: 3.5.1 specifier: 3.5.0
version: 3.5.1(prettier@3.8.1)(svelte@5.53.5) version: 3.5.0(prettier@3.8.1)(svelte@5.53.5)
svelte: svelte:
specifier: 5.53.5 specifier: 5.53.5
version: 5.53.5 version: 5.53.5
@@ -313,8 +313,8 @@ packages:
'@codemirror/language@6.12.2': '@codemirror/language@6.12.2':
resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==} resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==}
'@codemirror/legacy-modes@6.5.2': '@codemirror/legacy-modes@6.5.3':
resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==} resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==}
'@codemirror/lint@6.9.3': '@codemirror/lint@6.9.3':
resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==} resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==}
@@ -1818,8 +1818,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier-plugin-svelte@3.5.1: prettier-plugin-svelte@3.5.0:
resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==}
peerDependencies: peerDependencies:
prettier: ^3.0.0 prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
@@ -2374,7 +2374,7 @@ snapshots:
'@lezer/lr': 1.4.8 '@lezer/lr': 1.4.8
style-mod: 4.1.3 style-mod: 4.1.3
'@codemirror/legacy-modes@6.5.2': '@codemirror/legacy-modes@6.5.3':
dependencies: dependencies:
'@codemirror/language': 6.12.2 '@codemirror/language': 6.12.2
@@ -3776,7 +3776,7 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.5): prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.5):
dependencies: dependencies:
prettier: 3.8.1 prettier: 3.8.1
svelte: 5.53.5 svelte: 5.53.5
+1 -1
View File
@@ -1648,7 +1648,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hikari-desktop" name = "hikari-desktop"
version = "1.12.0" version = "1.15.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs 5.0.1", "dirs 5.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "hikari-desktop" name = "hikari-desktop"
version = "1.12.0" version = "1.15.0"
description = "Hikari - Claude Code Visual Assistant" description = "Hikari - Claude Code Visual Assistant"
authors = ["Naomi Carrigan"] authors = ["Naomi Carrigan"]
edition = "2021" edition = "2021"
+132 -3
View File
@@ -1464,6 +1464,7 @@ pub async fn close_application(app_handle: AppHandle) -> Result<(), String> {
pub struct MemoryFileInfo { pub struct MemoryFileInfo {
pub path: String, pub path: String,
pub heading: Option<String>, pub heading: Option<String>,
pub last_modified: Option<String>, // Unix timestamp in seconds as a string
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
@@ -1535,7 +1536,11 @@ async fn list_memory_files_via_wsl() -> Result<MemoryFilesResponse, String> {
let mut files = Vec::new(); let mut files = Vec::new();
for path in paths { for path in paths {
let heading = read_wsl_file_first_heading(&path); let heading = read_wsl_file_first_heading(&path);
files.push(MemoryFileInfo { path, heading }); files.push(MemoryFileInfo {
path,
heading,
last_modified: None,
});
} }
Ok(MemoryFilesResponse { files }) Ok(MemoryFilesResponse { files })
@@ -1605,14 +1610,23 @@ async fn list_memory_files_native() -> Result<MemoryFilesResponse, String> {
// Sort files alphabetically // Sort files alphabetically
memory_paths.sort(); memory_paths.sort();
// Read first heading from each file // Read first heading and modification time from each file
let files = memory_paths let files = memory_paths
.into_iter() .into_iter()
.map(|path| { .map(|path| {
let heading = fs::read_to_string(&path) let heading = fs::read_to_string(&path)
.ok() .ok()
.and_then(|content| extract_first_heading(&content)); .and_then(|content| extract_first_heading(&content));
MemoryFileInfo { path, heading } let last_modified = fs::metadata(&path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs().to_string());
MemoryFileInfo {
path,
heading,
last_modified,
}
}) })
.collect(); .collect();
@@ -2604,6 +2618,103 @@ pub async fn open_binary_file(app: AppHandle, path: String) -> Result<(), String
} }
} }
/// Read `~/.claude/CLAUDE.md` via WSL (for Windows).
/// Returns an empty string if the file does not exist.
#[cfg(target_os = "windows")]
async fn get_global_claude_md_via_wsl() -> Result<String, String> {
use std::process::Command;
let output = Command::new("wsl")
.hide_window()
.args(["-e", "bash", "-l", "-c", "cat ~/.claude/CLAUDE.md 2>/dev/null || true"])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
/// Write content to `~/.claude/CLAUDE.md` via WSL (for Windows).
/// Creates the file (and `~/.claude/` directory) if they do not exist.
#[cfg(target_os = "windows")]
async fn save_global_claude_md_via_wsl(content: String) -> Result<(), String> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("wsl")
.hide_window()
.args([
"-e",
"bash",
"-l",
"-c",
"mkdir -p ~/.claude && cat > ~/.claude/CLAUDE.md",
])
.stdin(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if let Some(stdin) = child.stdin.as_mut() {
stdin
.write_all(content.as_bytes())
.map_err(|e| format!("Failed to write content to WSL stdin: {}", e))?;
}
let status = child
.wait()
.map_err(|e| format!("Failed to wait for WSL command: {}", e))?;
if !status.success() {
return Err("Failed to save CLAUDE.md via WSL".to_string());
}
Ok(())
}
/// Read the contents of `~/.claude/CLAUDE.md`.
/// Returns an empty string if the file does not exist.
#[tauri::command]
pub async fn get_global_claude_md() -> Result<String, String> {
#[cfg(target_os = "windows")]
return get_global_claude_md_via_wsl().await;
#[cfg(not(target_os = "windows"))]
{
let path = dirs::home_dir()
.ok_or_else(|| "Could not determine home directory".to_string())?
.join(".claude")
.join("CLAUDE.md");
if !path.exists() {
return Ok(String::new());
}
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read CLAUDE.md: {}", e))
}
}
/// Write content to `~/.claude/CLAUDE.md`.
/// Creates the file (and `~/.claude/` directory) if they do not exist.
#[tauri::command]
pub async fn save_global_claude_md(content: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
return save_global_claude_md_via_wsl(content).await;
#[cfg(not(target_os = "windows"))]
{
let claude_dir = dirs::home_dir()
.ok_or_else(|| "Could not determine home directory".to_string())?
.join(".claude");
if !claude_dir.exists() {
std::fs::create_dir_all(&claude_dir)
.map_err(|e| format!("Failed to create ~/.claude directory: {}", e))?;
}
let path = claude_dir.join("CLAUDE.md");
std::fs::write(&path, content).map_err(|e| format!("Failed to write CLAUDE.md: {}", e))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -3353,4 +3464,22 @@ gitea: gitea-mcp -t stdio (STDIO) - âś“ Connected"#;
let (_, args) = build_wslpath_command(path); let (_, args) = build_wslpath_command(path);
assert_eq!(args[2], path); assert_eq!(args[2], path);
} }
#[test]
fn test_get_global_claude_md_path_construction() {
// Verify that home_dir() resolves successfully on the test platform
let home = dirs::home_dir();
assert!(home.is_some(), "home_dir() should be available in test environment");
let expected = home.unwrap().join(".claude").join("CLAUDE.md");
assert!(expected.to_string_lossy().contains(".claude"));
assert!(expected.to_string_lossy().ends_with("CLAUDE.md"));
}
#[test]
fn test_save_global_claude_md_dir_path_construction() {
let home = dirs::home_dir();
assert!(home.is_some());
let dir = home.unwrap().join(".claude");
assert!(dir.to_string_lossy().contains(".claude"));
}
} }
+76
View File
@@ -51,6 +51,35 @@ pub struct ClaudeStartOptions {
#[serde(default)] #[serde(default)]
pub model_overrides: Option<HashMap<String, String>>, pub model_overrides: Option<HashMap<String, String>>,
#[serde(default)]
pub session_name: Option<String>,
#[serde(default)]
pub disable_skill_shell_execution: bool,
/// Pass `--bare` flag to suppress UI chrome, useful for scripted headless `-p` calls (v2.1.81+).
#[serde(default)]
pub bare_mode: bool,
/// Controls `showClearContextOnPlanAccept` in `--settings` (v2.1.81+).
/// Defaults to true (matching CLI default). Set to false to suppress the dialog.
#[serde(default = "default_show_clear_context")]
pub show_clear_context_on_plan_accept: bool,
/// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+).
#[serde(default)]
pub custom_model_option: Option<String>,
/// Passes `--effort <level>` to set the effort level (v2.1.111+).
/// Valid values: "low", "medium", "high", "xhigh" (Opus 4.7 only), "max". None uses CLI default.
#[serde(default)]
pub effort_level: Option<String>,
/// Sets `ENABLE_PROMPT_CACHING_1H=1` or `FORCE_PROMPT_CACHING_5M=1` env var (v2.1.108+).
/// Values: "1h" → 1-hour TTL, "5m" → force 5-minute TTL. None uses CLI default.
#[serde(default)]
pub prompt_caching_ttl: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -200,6 +229,33 @@ pub struct HikariConfig {
#[serde(default)] #[serde(default)]
pub model_overrides: Option<HashMap<String, String>>, pub model_overrides: Option<HashMap<String, String>>,
/// Prevents skill scripts from executing shell commands (Claude Code v2.1.91+).
/// Passes `"disableSkillShellExecution": true` via the `--settings` flag.
#[serde(default)]
pub disable_skill_shell_execution: bool,
/// Pass `--bare` flag to suppress UI chrome, useful for scripted headless `-p` calls (v2.1.81+).
#[serde(default)]
pub bare_mode: bool,
/// Controls `showClearContextOnPlanAccept` in `--settings` (v2.1.81+).
#[serde(default = "default_show_clear_context")]
pub show_clear_context_on_plan_accept: bool,
/// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+).
#[serde(default)]
pub custom_model_option: Option<String>,
/// Passes `--effort <level>` to set the effort level (v2.1.111+).
/// Valid values: "low", "medium", "high", "xhigh" (Opus 4.7 only), "max". None uses CLI default.
#[serde(default)]
pub effort_level: Option<String>,
/// Sets `ENABLE_PROMPT_CACHING_1H=1` or `FORCE_PROMPT_CACHING_5M=1` env var (v2.1.108+).
/// Values: "1h" → 1-hour TTL, "5m" → force 5-minute TTL. None uses CLI default.
#[serde(default)]
pub prompt_caching_ttl: Option<String>,
} }
impl Default for HikariConfig { impl Default for HikariConfig {
@@ -251,6 +307,12 @@ impl Default for HikariConfig {
enable_claudeai_mcp_servers: true, enable_claudeai_mcp_servers: true,
auto_memory_directory: None, auto_memory_directory: None,
model_overrides: None, model_overrides: None,
disable_skill_shell_execution: false,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: None,
effort_level: None,
prompt_caching_ttl: None,
} }
} }
} }
@@ -303,6 +365,10 @@ fn default_enable_claudeai_mcp_servers() -> bool {
true true
} }
fn default_show_clear_context() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum BudgetAction { pub enum BudgetAction {
@@ -402,6 +468,10 @@ mod tests {
assert!(config.enable_claudeai_mcp_servers); assert!(config.enable_claudeai_mcp_servers);
assert!(config.auto_memory_directory.is_none()); assert!(config.auto_memory_directory.is_none());
assert!(config.model_overrides.is_none()); assert!(config.model_overrides.is_none());
assert!(!config.disable_skill_shell_execution);
assert!(!config.bare_mode);
assert!(config.show_clear_context_on_plan_accept);
assert!(config.custom_model_option.is_none());
} }
#[test] #[test]
@@ -456,6 +526,12 @@ mod tests {
"claude-opus-4-6".to_string(), "claude-opus-4-6".to_string(),
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(), "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(),
)])), )])),
disable_skill_shell_execution: true,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: None,
effort_level: None,
prompt_caching_ttl: None,
}; };
let json = serde_json::to_string(&config).unwrap(); let json = serde_json::to_string(&config).unwrap();
+2
View File
@@ -224,6 +224,8 @@ pub fn run() {
delete_all_drafts, delete_all_drafts,
scan_project, scan_project,
open_binary_file, open_binary_file,
get_global_claude_md,
save_global_claude_md,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
+3 -3
View File
@@ -81,7 +81,7 @@ pub async fn list_sessions(app: AppHandle) -> Result<Vec<SessionListItem>, Strin
let mut items: Vec<SessionListItem> = sessions.iter().map(SessionListItem::from).collect(); let mut items: Vec<SessionListItem> = sessions.iter().map(SessionListItem::from).collect();
// Sort by last activity, most recent first // Sort by last activity, most recent first
items.sort_by(|a, b| b.last_activity_at.cmp(&a.last_activity_at)); items.sort_by_key(|b| std::cmp::Reverse(b.last_activity_at));
Ok(items) Ok(items)
} }
@@ -132,7 +132,7 @@ pub async fn search_sessions(app: AppHandle, query: String) -> Result<Vec<Sessio
.collect(); .collect();
// Sort by last activity, most recent first // Sort by last activity, most recent first
matching.sort_by(|a, b| b.last_activity_at.cmp(&a.last_activity_at)); matching.sort_by_key(|b| std::cmp::Reverse(b.last_activity_at));
Ok(matching) Ok(matching)
} }
@@ -348,7 +348,7 @@ mod tests {
]; ];
// Sort by last activity, most recent first (mimics list_sessions behavior) // Sort by last activity, most recent first (mimics list_sessions behavior)
sessions.sort_by(|a, b| b.last_activity_at.cmp(&a.last_activity_at)); sessions.sort_by_key(|b| std::cmp::Reverse(b.last_activity_at));
assert_eq!(sessions[0].id, "new"); assert_eq!(sessions[0].id, "new");
assert_eq!(sessions[1].id, "old"); assert_eq!(sessions[1].id, "old");
+30 -7
View File
@@ -86,9 +86,10 @@ impl ContextWarning {
/// Get the context window limit (in tokens) for a given model /// Get the context window limit (in tokens) for a given model
fn get_context_window_limit(model: &str) -> u64 { fn get_context_window_limit(model: &str) -> u64 {
match model { match model {
// Claude 4.6 family // Claude 4.7 - 1M token context window
"claude-opus-4-6" => 200_000, "claude-opus-4-7" => 1_000_000,
"claude-sonnet-4-6" => 1_000_000, // 1M token context window // Claude 4.6 family - 1M token context window
"claude-opus-4-6" | "claude-sonnet-4-6" => 1_000_000,
// Claude 4.5 family - 200K standard context // Claude 4.5 family - 200K standard context
"claude-opus-4-5-20251101" "claude-opus-4-5-20251101"
| "claude-sonnet-4-5-20250929" | "claude-sonnet-4-5-20250929"
@@ -490,7 +491,7 @@ fn is_consecutive_day(prev_date: &str, current_date: &str) -> bool {
} }
} }
// Pricing as of February 2026 // Pricing as of May 2026
// https://platform.claude.com/docs/en/about-claude/models/overview // https://platform.claude.com/docs/en/about-claude/models/overview
// Cache pricing: https://platform.claude.com/docs/en/build-with-claude/prompt-caching // Cache pricing: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
pub fn calculate_cost( pub fn calculate_cost(
@@ -501,14 +502,17 @@ pub fn calculate_cost(
cache_read_tokens: Option<u64>, cache_read_tokens: Option<u64>,
) -> f64 { ) -> f64 {
let (input_price_per_million, output_price_per_million) = match model { let (input_price_per_million, output_price_per_million) = match model {
// Current generation (Claude 4.6) // Current generation (Claude 4.7/4.6/4.5)
"claude-opus-4-6" => (5.0, 25.0), "claude-opus-4-7" => (5.0, 25.0),
"claude-sonnet-4-6" => (3.0, 15.0), "claude-sonnet-4-6" => (3.0, 15.0),
"claude-haiku-4-5-20251001" => (1.0, 5.0),
// Previous generation (Claude 4.6)
"claude-opus-4-6" => (5.0, 25.0),
// Previous generation (Claude 4.5) // Previous generation (Claude 4.5)
"claude-opus-4-5-20251101" => (5.0, 25.0), "claude-opus-4-5-20251101" => (5.0, 25.0),
"claude-sonnet-4-5-20250929" => (3.0, 15.0), "claude-sonnet-4-5-20250929" => (3.0, 15.0),
"claude-haiku-4-5-20251001" => (1.0, 5.0),
// Previous generation (Claude 4.x) // Previous generation (Claude 4.x)
"claude-opus-4-1-20250805" => (15.0, 75.0), "claude-opus-4-1-20250805" => (15.0, 75.0),
@@ -681,6 +685,15 @@ mod tests {
assert!((cost - 0.165).abs() < 0.0001); assert!((cost - 0.165).abs() < 0.0001);
} }
#[test]
fn test_cost_calculation_opus_47() {
let cost = calculate_cost(1000, 2000, "claude-opus-4-7", None, None);
// Opus 4.7 pricing: $5/MTok input, $25/MTok output
// 1000 input tokens = $0.005, 2000 output tokens = $0.05
// Total = $0.055
assert!((cost - 0.055).abs() < 0.0001);
}
#[test] #[test]
fn test_cost_calculation_opus_45() { fn test_cost_calculation_opus_45() {
let cost = calculate_cost(1000, 2000, "claude-opus-4-5-20251101", None, None); let cost = calculate_cost(1000, 2000, "claude-opus-4-5-20251101", None, None);
@@ -1019,6 +1032,16 @@ mod tests {
// Context Window Tracking tests // Context Window Tracking tests
// ===================== // =====================
#[test]
fn test_context_window_limit_opus_47() {
assert_eq!(get_context_window_limit("claude-opus-4-7"), 1_000_000);
}
#[test]
fn test_context_window_limit_opus_46() {
assert_eq!(get_context_window_limit("claude-opus-4-6"), 1_000_000);
}
#[test] #[test]
fn test_context_window_limit_claude_4() { fn test_context_window_limit_claude_4() {
assert_eq!(get_context_window_limit("claude-opus-4-5-20251101"), 200_000); assert_eq!(get_context_window_limit("claude-opus-4-5-20251101"), 200_000);
+493
View File
@@ -95,6 +95,13 @@ pub enum ClaudeMessage {
cwd: Option<String>, cwd: Option<String>,
#[serde(default)] #[serde(default)]
tools: Option<Vec<String>>, tools: Option<Vec<String>>,
/// Output style hint from Claude Code (v2.1.81+). Informational only.
#[serde(default)]
output_style: Option<String>,
/// Plugin errors from Claude Code (v2.1.111+). Populated when plugins are demoted
/// due to unsatisfied dependencies.
#[serde(default)]
plugin_errors: Option<serde_json::Value>,
}, },
#[serde(rename = "assistant")] #[serde(rename = "assistant")]
Assistant { Assistant {
@@ -119,6 +126,15 @@ pub enum ClaudeMessage {
permission_denials: Option<Vec<PermissionDenial>>, permission_denials: Option<Vec<PermissionDenial>>,
#[serde(default)] #[serde(default)]
usage: Option<UsageInfo>, usage: Option<UsageInfo>,
/// Fast mode state from Claude Code v2.1.81+. Values: "default" | "enabled" | "disabled".
#[serde(default)]
fast_mode_state: Option<String>,
/// Per-model usage breakdown from Claude Code v2.1.81+.
#[serde(default)]
model_usage: Option<serde_json::Value>,
/// Authoritative total cost in USD reported by Claude Code v2.1.81+.
#[serde(default)]
total_cost_usd: Option<f64>,
}, },
#[serde(rename = "rate_limit_event")] #[serde(rename = "rate_limit_event")]
RateLimitEvent { RateLimitEvent {
@@ -280,6 +296,88 @@ pub struct UserQuestionEvent {
pub conversation_id: Option<String>, pub conversation_id: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitationEvent {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitationResultEvent {
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopFailureEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostCompactEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreCompactEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CwdChangedEvent {
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileChangedEvent {
pub file: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskCreatedEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_tool_use_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionDeniedEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStartEvent { pub struct AgentStartEvent {
pub tool_use_id: String, pub tool_use_id: String,
@@ -566,4 +664,399 @@ mod tests {
panic!("Expected RateLimitEvent variant"); panic!("Expected RateLimitEvent variant");
} }
} }
#[test]
fn test_elicitation_event_serialization() {
let event = ElicitationEvent {
message: "Please provide the API endpoint".to_string(),
server_name: Some("my-server".to_string()),
request_id: Some("req-123".to_string()),
conversation_id: Some("conv-abc".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"message\":\"Please provide the API endpoint\""));
assert!(serialized.contains("\"server_name\":\"my-server\""));
assert!(serialized.contains("\"request_id\":\"req-123\""));
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
}
#[test]
fn test_elicitation_event_omits_none_fields() {
let event = ElicitationEvent {
message: "Enter your token".to_string(),
server_name: None,
request_id: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"message\":\"Enter your token\""));
assert!(!serialized.contains("server_name"));
assert!(!serialized.contains("request_id"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_elicitation_result_event_serialization() {
let event = ElicitationResultEvent {
action: "accept".to_string(),
request_id: Some("req-123".to_string()),
conversation_id: Some("conv-abc".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"action\":\"accept\""));
assert!(serialized.contains("\"request_id\":\"req-123\""));
}
#[test]
fn test_elicitation_result_event_cancel_omits_none_fields() {
let event = ElicitationResultEvent {
action: "cancel".to_string(),
request_id: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"action\":\"cancel\""));
assert!(!serialized.contains("request_id"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_stop_failure_event_serialization() {
let event = StopFailureEvent {
stop_reason: Some("api_error".to_string()),
error_type: Some("rate_limit".to_string()),
conversation_id: Some("conv-abc".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"stop_reason\":\"api_error\""));
assert!(serialized.contains("\"error_type\":\"rate_limit\""));
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
}
#[test]
fn test_stop_failure_event_omits_none_fields() {
let event = StopFailureEvent {
stop_reason: None,
error_type: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(!serialized.contains("stop_reason"));
assert!(!serialized.contains("error_type"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_stop_failure_event_partial_fields() {
let event = StopFailureEvent {
stop_reason: Some("api_error".to_string()),
error_type: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"stop_reason\":\"api_error\""));
assert!(!serialized.contains("error_type"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_post_compact_event_serialization() {
let event = PostCompactEvent {
session_id: Some("sess-abc".to_string()),
conversation_id: Some("conv-123".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"session_id\":\"sess-abc\""));
assert!(serialized.contains("\"conversation_id\":\"conv-123\""));
}
#[test]
fn test_post_compact_event_omits_none_fields() {
let event = PostCompactEvent {
session_id: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(!serialized.contains("session_id"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_post_compact_event_partial_fields() {
let event = PostCompactEvent {
session_id: Some("sess-xyz".to_string()),
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"session_id\":\"sess-xyz\""));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_pre_compact_event_serialization() {
let event = PreCompactEvent {
session_id: Some("sess-abc".to_string()),
conversation_id: Some("conv-123".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"session_id\":\"sess-abc\""));
assert!(serialized.contains("\"conversation_id\":\"conv-123\""));
}
#[test]
fn test_pre_compact_event_omits_none_fields() {
let event = PreCompactEvent {
session_id: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(!serialized.contains("session_id"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_cwd_changed_event_serialization() {
let event = CwdChangedEvent {
cwd: "/home/naomi/code/my-project".to_string(),
conversation_id: Some("conv-abc".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"cwd\":\"/home/naomi/code/my-project\""));
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
}
#[test]
fn test_cwd_changed_event_omits_none_fields() {
let event = CwdChangedEvent {
cwd: "/tmp/workspace".to_string(),
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"cwd\":\"/tmp/workspace\""));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_file_changed_event_serialization() {
let event = FileChangedEvent {
file: "/home/naomi/code/my-project/src/main.rs".to_string(),
conversation_id: Some("conv-abc".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"file\":\"/home/naomi/code/my-project/src/main.rs\""));
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
}
#[test]
fn test_file_changed_event_omits_none_fields() {
let event = FileChangedEvent {
file: "/tmp/test.txt".to_string(),
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"file\":\"/tmp/test.txt\""));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_task_created_event_serialization() {
let event = TaskCreatedEvent {
task_id: Some("task-abc123".to_string()),
description: Some("Explore the codebase".to_string()),
parent_tool_use_id: Some("toolu_xyz".to_string()),
conversation_id: Some("conv-abc".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"task_id\":\"task-abc123\""));
assert!(serialized.contains("\"description\":\"Explore the codebase\""));
assert!(serialized.contains("\"parent_tool_use_id\":\"toolu_xyz\""));
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
}
#[test]
fn test_task_created_event_omits_none_fields() {
let event = TaskCreatedEvent {
task_id: None,
description: None,
parent_tool_use_id: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert_eq!(serialized, "{}");
}
#[test]
fn test_task_created_event_partial_fields() {
let event = TaskCreatedEvent {
task_id: Some("task-001".to_string()),
description: None,
parent_tool_use_id: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"task_id\":\"task-001\""));
assert!(!serialized.contains("description"));
assert!(!serialized.contains("parent_tool_use_id"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_permission_denied_event_serialization() {
let event = PermissionDeniedEvent {
tool_name: Some("Bash".to_string()),
reason: Some("Tool not in allow list".to_string()),
conversation_id: Some("conv-abc".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"tool_name\":\"Bash\""));
assert!(serialized.contains("\"reason\":\"Tool not in allow list\""));
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
}
#[test]
fn test_permission_denied_event_omits_none_fields() {
let event = PermissionDeniedEvent {
tool_name: None,
reason: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert_eq!(serialized, "{}");
}
#[test]
fn test_permission_denied_event_partial_fields() {
let event = PermissionDeniedEvent {
tool_name: Some("Edit".to_string()),
reason: None,
conversation_id: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"tool_name\":\"Edit\""));
assert!(!serialized.contains("reason"));
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_system_init_with_output_style() {
let json = r#"{"type":"system","subtype":"init","session_id":"sess-1","output_style":"auto"}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::System { output_style, .. } = msg {
assert_eq!(output_style, Some("auto".to_string()));
} else {
panic!("Expected System variant");
}
}
#[test]
fn test_system_init_without_output_style() {
let json = r#"{"type":"system","subtype":"init","session_id":"sess-1"}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::System { output_style, .. } = msg {
assert!(output_style.is_none());
} else {
panic!("Expected System variant");
}
}
#[test]
fn test_system_init_with_plugin_errors() {
let json = r#"{"type":"system","subtype":"init","session_id":"sess-1","plugin_errors":["Plugin 'foo' requires 'bar' which is not installed","Plugin 'baz' failed to load"]}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::System { plugin_errors, .. } = msg {
let errors = plugin_errors.expect("plugin_errors should be present");
let arr = errors.as_array().expect("plugin_errors should be an array");
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_str(), Some("Plugin 'foo' requires 'bar' which is not installed"));
} else {
panic!("Expected System variant");
}
}
#[test]
fn test_system_init_without_plugin_errors() {
let json = r#"{"type":"system","subtype":"init","session_id":"sess-1"}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::System { plugin_errors, .. } = msg {
assert!(plugin_errors.is_none());
} else {
panic!("Expected System variant");
}
}
#[test]
fn test_system_init_with_empty_plugin_errors() {
let json = r#"{"type":"system","subtype":"init","session_id":"sess-1","plugin_errors":[]}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::System { plugin_errors, .. } = msg {
let errors = plugin_errors.expect("plugin_errors should be present");
let arr = errors.as_array().expect("plugin_errors should be an array");
assert!(arr.is_empty());
} else {
panic!("Expected System variant");
}
}
#[test]
fn test_result_message_with_fast_mode_state() {
let json = r#"{"type":"result","subtype":"success","fast_mode_state":"enabled"}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::Result { fast_mode_state, .. } = msg {
assert_eq!(fast_mode_state, Some("enabled".to_string()));
} else {
panic!("Expected Result variant");
}
}
#[test]
fn test_result_message_with_total_cost_usd() {
let json = r#"{"type":"result","subtype":"success","total_cost_usd":0.05}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::Result { total_cost_usd, .. } = msg {
assert!((total_cost_usd.unwrap() - 0.05).abs() < f64::EPSILON);
} else {
panic!("Expected Result variant");
}
}
#[test]
fn test_result_message_without_new_fields() {
let json = r#"{"type":"result","subtype":"success"}"#;
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
if let ClaudeMessage::Result {
fast_mode_state,
model_usage,
total_cost_usd,
..
} = msg
{
assert!(fast_mode_state.is_none());
assert!(model_usage.is_none());
assert!(total_cost_usd.is_none());
} else {
panic!("Expected Result variant");
}
}
} }
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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.12.0", "version": "1.15.0",
"identifier": "com.naomi.hikari-desktop", "identifier": "com.naomi.hikari-desktop",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
+16
View File
@@ -67,10 +67,18 @@ async function changeDirectory(path: string): Promise<void> {
use_worktree: config.use_worktree ?? false, use_worktree: config.use_worktree ?? false,
disable_1m_context: config.disable_1m_context ?? false, disable_1m_context: config.disable_1m_context ?? false,
max_output_tokens: config.max_output_tokens ?? null, max_output_tokens: config.max_output_tokens ?? null,
disable_cron: config.disable_cron ?? false,
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
include_git_instructions: config.include_git_instructions ?? true, include_git_instructions: config.include_git_instructions ?? true,
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: config.auto_memory_directory || null, auto_memory_directory: config.auto_memory_directory || null,
model_overrides: config.model_overrides || null, model_overrides: config.model_overrides || null,
session_name: null,
bare_mode: config.bare_mode ?? false,
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
custom_model_option: config.custom_model_option || null,
effort_level: config.effort_level || null,
prompt_caching_ttl: config.prompt_caching_ttl || null,
}, },
}); });
@@ -148,10 +156,18 @@ async function startNewConversation(): Promise<void> {
use_worktree: config.use_worktree ?? false, use_worktree: config.use_worktree ?? false,
disable_1m_context: config.disable_1m_context ?? false, disable_1m_context: config.disable_1m_context ?? false,
max_output_tokens: config.max_output_tokens ?? null, max_output_tokens: config.max_output_tokens ?? null,
disable_cron: config.disable_cron ?? false,
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
include_git_instructions: config.include_git_instructions ?? true, include_git_instructions: config.include_git_instructions ?? true,
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: config.auto_memory_directory || null, auto_memory_directory: config.auto_memory_directory || null,
model_overrides: config.model_overrides || null, model_overrides: config.model_overrides || null,
session_name: null,
bare_mode: config.bare_mode ?? false,
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
custom_model_option: config.custom_model_option || null,
effort_level: config.effort_level || null,
prompt_caching_ttl: config.prompt_caching_ttl || null,
}, },
}); });
+1 -1
View File
@@ -2,7 +2,7 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte"; import { onMount } from "svelte";
const SUPPORTED_CLI_VERSION = "2.1.74"; const SUPPORTED_CLI_VERSION = "2.1.131";
let installedVersion = $state("Loading..."); let installedVersion = $state("Loading...");
let latestNpmVersion = $state<string | null>(null); let latestNpmVersion = $state<string | null>(null);
+202 -8
View File
@@ -63,6 +63,10 @@
enable_claudeai_mcp_servers: true, enable_claudeai_mcp_servers: true,
auto_memory_directory: null, auto_memory_directory: null,
model_overrides: null, model_overrides: null,
disable_skill_shell_execution: false,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: null,
max_output_tokens: null, max_output_tokens: null,
trusted_workspaces: [], trusted_workspaces: [],
background_image_path: null, background_image_path: null,
@@ -74,6 +78,8 @@
task_loop_auto_commit: false, task_loop_auto_commit: false,
task_loop_commit_prefix: "feat", task_loop_commit_prefix: "feat",
task_loop_include_summary: false, task_loop_include_summary: false,
effort_level: null,
prompt_caching_ttl: null,
}); });
let showCustomThemeEditor = $state(false); let showCustomThemeEditor = $state(false);
@@ -85,6 +91,9 @@
let customUiFontStatus: string | null = $state(null); let customUiFontStatus: string | null = $state(null);
let modelOverridesJson = $state(""); let modelOverridesJson = $state("");
let modelOverridesError: string | null = $state(null); let modelOverridesError: string | null = $state(null);
let globalClaudeMd = $state("");
let globalClaudeMdSaving = $state(false);
let globalClaudeMdSaveStatus: string | null = $state(null);
interface AuthStatus { interface AuthStatus {
is_logged_in: boolean; is_logged_in: boolean;
@@ -122,6 +131,9 @@
if (open && authStatus === null) { if (open && authStatus === null) {
void refreshAuthStatus(); void refreshAuthStatus();
} }
if (open) {
void loadGlobalClaudeMd();
}
}); });
configStore.saveError.subscribe((error) => { configStore.saveError.subscribe((error) => {
@@ -134,17 +146,20 @@
const availableModels = [ const availableModels = [
{ value: "", label: "Default (from ~/.claude)" }, { value: "", label: "Default (from ~/.claude)" },
// Current generation (Claude 4.6) // Current generation (Claude 4.7/4.6/4.5)
{ value: "claude-opus-4-6", label: "Claude Opus 4.6 (Most Capable)" }, { value: "claude-opus-4-7", label: "Claude Opus 4.7 (Most Capable)" },
{ value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (Recommended)" }, { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (Recommended)" },
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (Fast & Cheap)" },
// Previous generation (Claude 4.6)
{ value: "claude-opus-4-6", label: "Claude Opus 4.6" },
// Previous generation (Claude 4.5) // Previous generation (Claude 4.5)
{ value: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" }, { value: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (Fast & Cheap)" },
{ value: "claude-opus-4-5-20251101", label: "Claude Opus 4.5" }, { value: "claude-opus-4-5-20251101", label: "Claude Opus 4.5" },
// Previous generation (Claude 4.x) // Previous generation (Claude 4.x)
{ value: "claude-opus-4-1-20250805", label: "Claude Opus 4.1" }, { value: "claude-opus-4-1-20250805", label: "Claude Opus 4.1" },
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" }, // Deprecated — retire June 15, 2026
{ value: "claude-opus-4-20250514", label: "Claude Opus 4" }, { value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4 (Deprecated)" },
{ value: "claude-opus-4-20250514", label: "Claude Opus 4 (Deprecated)" },
]; ];
const commonTools = [ const commonTools = [
@@ -197,6 +212,30 @@
} }
} }
async function loadGlobalClaudeMd() {
try {
globalClaudeMd = await invoke<string>("get_global_claude_md");
} catch (error) {
console.error("Failed to load global CLAUDE.md:", error);
}
}
async function saveGlobalClaudeMd() {
globalClaudeMdSaving = true;
globalClaudeMdSaveStatus = null;
try {
await invoke("save_global_claude_md", { content: globalClaudeMd });
globalClaudeMdSaveStatus = "Saved!";
setTimeout(() => {
globalClaudeMdSaveStatus = null;
}, 2000);
} catch (error) {
globalClaudeMdSaveStatus = `Error: ${error}`;
} finally {
globalClaudeMdSaving = false;
}
}
async function handleSave() { async function handleSave() {
isSaving = true; isSaving = true;
saveError = null; saveError = null;
@@ -585,6 +624,22 @@
</p> </p>
</div> </div>
<!-- Disable Skill Shell Execution -->
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
bind:checked={config.disable_skill_shell_execution}
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
/>
<span class="text-sm text-[var(--text-primary)]">Disable skill shell execution</span>
</label>
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
Passes <code class="font-mono">disableSkillShellExecution: true</code> to prevent skill scripts
from executing shell commands (requires Claude Code v2.1.91+)
</p>
</div>
<!-- Include Git Instructions --> <!-- Include Git Instructions -->
<div class="mb-4"> <div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer"> <label class="flex items-center gap-3 cursor-pointer">
@@ -610,13 +665,15 @@
id="max-output-tokens" id="max-output-tokens"
type="number" type="number"
min="1" min="1"
placeholder="Default (32000)" max="128000"
placeholder="Default (model-dependent)"
bind:value={config.max_output_tokens} bind:value={config.max_output_tokens}
class="w-full px-3 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)]" class="w-full px-3 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)]"
/> />
<p class="text-xs text-[var(--text-tertiary)] mt-1"> <p class="text-xs text-[var(--text-tertiary)] mt-1">
Sets <code class="font-mono">CLAUDE_CODE_MAX_OUTPUT_TOKENS</code> — increase if responses are Sets <code class="font-mono">CLAUDE_CODE_MAX_OUTPUT_TOKENS</code>. Maximum: 128k tokens
being cut off mid-reply for Opus 4.6 and Sonnet 4.6 (64k default for Opus 4.6, 32k for other models). Increase if
responses are being cut off.
</p> </p>
</div> </div>
@@ -660,6 +717,102 @@
defaults. defaults.
</p> </p>
</div> </div>
<!-- Effort Level -->
<div class="mb-4">
<label class="block text-sm text-[var(--text-primary)] mb-1" for="effort-level"
>Effort level</label
>
<select
id="effort-level"
bind:value={config.effort_level}
class="w-full px-3 py-2 rounded border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm focus:outline-none focus:ring-1 focus:ring-[var(--accent-primary)]"
>
<option value={null}>Default (CLI decides)</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="xhigh">Extra High (Opus 4.7 only)</option>
<option value="max">Max</option>
</select>
<p class="text-xs text-[var(--text-tertiary)] mt-1">
Passes <code class="font-mono">--effort</code> to tune speed vs. intelligence. Requires Claude
Code v2.1.111+.
</p>
</div>
<!-- Prompt Caching TTL -->
<div class="mb-4">
<label class="block text-sm text-[var(--text-primary)] mb-1" for="prompt-caching-ttl"
>Prompt caching TTL</label
>
<select
id="prompt-caching-ttl"
bind:value={config.prompt_caching_ttl}
class="w-full px-3 py-2 rounded border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm focus:outline-none focus:ring-1 focus:ring-[var(--accent-primary)]"
>
<option value={null}>Default (CLI decides)</option>
<option value="1h">1-hour TTL (reduces cost for long sessions)</option>
<option value="5m">Force 5-minute TTL</option>
</select>
<p class="text-xs text-[var(--text-tertiary)] mt-1">
Sets <code class="font-mono">ENABLE_PROMPT_CACHING_1H</code> or
<code class="font-mono">FORCE_PROMPT_CACHING_5M</code>. Requires Claude Code v2.1.108+.
</p>
</div>
<!-- Bare Mode -->
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
bind:checked={config.bare_mode}
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
/>
<span class="text-sm text-[var(--text-primary)]">Bare mode</span>
</label>
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
Passes <code class="font-mono">--bare</code> to suppress UI chrome — useful for scripted
headless <code class="font-mono">-p</code> calls (requires Claude Code v2.1.81+)
</p>
</div>
<!-- Show Clear Context on Plan Accept -->
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
bind:checked={config.show_clear_context_on_plan_accept}
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
/>
<span class="text-sm text-[var(--text-primary)]"
>Show clear context prompt on plan accept</span
>
</label>
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
When enabled, prompts to clear context when accepting a plan. Passes
<code class="font-mono">showClearContextOnPlanAccept: false</code> in
<code class="font-mono">--settings</code> when disabled (requires Claude Code v2.1.81+)
</p>
</div>
<!-- Custom Model Option -->
<div class="mb-4">
<label for="custom-model-option" class="block text-sm text-[var(--text-primary)] mb-1">
Custom model option <span class="text-[var(--text-tertiary)]">(optional)</span>
</label>
<input
id="custom-model-option"
type="text"
placeholder="Leave blank to use default"
bind:value={config.custom_model_option}
class="w-full px-3 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)]"
/>
<p class="text-xs text-[var(--text-tertiary)] mt-1">
Sets <code class="font-mono">ANTHROPIC_CUSTOM_MODEL_OPTION</code> environment variable for custom
model providers (requires Claude Code v2.1.81+)
</p>
</div>
</section> </section>
<!-- Greeting Section --> <!-- Greeting Section -->
@@ -700,6 +853,47 @@
{/if} {/if}
</section> </section>
<!-- Global Instructions Section -->
<section class="mb-6">
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
Global Instructions
</h3>
<div class="mb-2">
<label for="global-claude-md" class="block text-sm text-[var(--text-secondary)] mb-1">
~/.claude/CLAUDE.md
</label>
<textarea
id="global-claude-md"
bind:value={globalClaudeMd}
rows="12"
placeholder="Add persistent instructions for all Claude Code sessions..."
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] font-mono text-sm focus:outline-none focus:border-[var(--accent-primary)] resize-y"
></textarea>
</div>
<div class="flex items-center gap-2">
<button
onclick={saveGlobalClaudeMd}
disabled={globalClaudeMdSaving}
class="px-3 py-1.5 text-sm bg-[var(--accent-primary)] text-white rounded hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
>
{globalClaudeMdSaving ? "Saving..." : "Save"}
</button>
{#if globalClaudeMdSaveStatus}
<span
class="text-xs {globalClaudeMdSaveStatus.startsWith('Error')
? 'text-red-500'
: 'text-green-500'}"
>
{globalClaudeMdSaveStatus}
</span>
{/if}
</div>
<p class="text-xs text-[var(--text-tertiary)] mt-2">
Persistent instructions applied to all Claude Code sessions. Changes take effect on the next
session start.
</p>
</section>
<!-- MCP Servers Section --> <!-- MCP Servers Section -->
<section class="mb-6"> <section class="mb-6">
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3"> <h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
+195
View File
@@ -0,0 +1,195 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { get } from "svelte/store";
import { claudeStore, hasElicitationPending } from "$lib/stores/claude";
import { characterState } from "$lib/stores/character";
import type { ElicitationEvent } from "$lib/types/messages";
import { updateDiscordRpc, setSkipNextGreeting } from "$lib/tauri";
import { conversationsStore } from "$lib/stores/conversations";
import { configStore } from "$lib/stores/config";
let isVisible = $state(false);
let elicitation: ElicitationEvent | null = $state(null);
let response = $state("");
let grantedToolsList: string[] = $state([]);
let workingDirectory = $state("");
hasElicitationPending.subscribe((pending) => {
isVisible = pending;
if (!pending) {
response = "";
}
});
claudeStore.pendingElicitation.subscribe((e) => {
elicitation = e;
if (e) {
characterState.setState("permission");
}
});
claudeStore.grantedTools.subscribe((tools) => {
grantedToolsList = Array.from(tools);
});
claudeStore.currentWorkingDirectory.subscribe((dir) => {
workingDirectory = dir;
});
async function handleSubmitAndReconnect() {
if (!elicitation || !response.trim()) return;
const conversationId = get(claudeStore.activeConversationId);
if (!conversationId) return;
const responseText = response.trim();
const elicitationMessage = elicitation.message;
const conversationHistory = claudeStore.getConversationHistory();
claudeStore.addLine("system", `MCP response submitted. Reconnecting with context...`);
claudeStore.clearElicitation();
try {
setSkipNextGreeting(true);
await invoke("stop_claude", { conversationId });
await new Promise((resolve) => setTimeout(resolve, 500));
const config = configStore.getConfig();
await invoke("start_claude", {
conversationId,
options: {
working_dir: workingDirectory || "/home/naomi",
model: config.model || null,
api_key: config.api_key || null,
custom_instructions: config.custom_instructions || null,
mcp_servers_json: config.mcp_servers_json || null,
allowed_tools: grantedToolsList,
use_worktree: config.use_worktree ?? false,
disable_1m_context: config.disable_1m_context ?? false,
disable_cron: config.disable_cron ?? false,
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
include_git_instructions: config.include_git_instructions ?? true,
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: config.auto_memory_directory || null,
model_overrides: config.model_overrides || null,
session_name: null,
bare_mode: config.bare_mode ?? false,
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
custom_model_option: config.custom_model_option || null,
effort_level: config.effort_level || null,
prompt_caching_ttl: config.prompt_caching_ttl || null,
},
});
const activeConversation = get(conversationsStore.activeConversation);
if (activeConversation) {
await updateDiscordRpc(
activeConversation.name,
config.model || "claude",
activeConversation.startedAt
);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
if (conversationHistory) {
const contextMessage = `[CONTEXT RESTORATION]
I just responded to an MCP server elicitation request. Here's our conversation so far:
${conversationHistory}
The MCP server asked: "${elicitationMessage}"
My response: "${responseText}"
Please continue where we left off, taking my response into account.`;
await invoke("send_prompt", {
conversationId,
message: contextMessage,
});
}
characterState.setTemporaryState("success", 2000);
} catch (error) {
console.error("Failed to reconnect:", error);
claudeStore.addLine("error", `Reconnect failed: ${error}`);
characterState.setTemporaryState("error", 3000);
}
}
function handleDismiss() {
claudeStore.clearElicitation();
claudeStore.addLine("system", "MCP elicitation dismissed");
characterState.setTemporaryState("idle", 1000);
}
function handleKeydown(event: KeyboardEvent) {
if (!isVisible || !elicitation) return;
if (event.key === "Escape") {
event.preventDefault();
handleDismiss();
}
}
function canSubmit(): boolean {
return response.trim().length > 0;
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if isVisible && elicitation}
<div
class="elicitation-overlay fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm"
>
<div
class="elicitation-modal bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl"
>
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-blue-500/20 flex items-center justify-center">
<span class="text-xl">đź’¬</span>
</div>
<div>
<h2 class="text-lg font-semibold text-[var(--text-primary)]">MCP Server Request</h2>
{#if elicitation.server_name}
<p class="text-sm text-[var(--text-secondary)]">from: {elicitation.server_name}</p>
{:else}
<p class="text-sm text-[var(--text-secondary)]">Input required from MCP server</p>
{/if}
</div>
</div>
<div class="mb-4">
<p class="text-[var(--text-primary)]">{elicitation.message}</p>
</div>
<div class="mb-4">
<textarea
bind:value={response}
placeholder="Type your response here..."
class="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-secondary)] resize-none focus:outline-none focus:border-[var(--accent-primary)]"
rows="4"
></textarea>
</div>
<div class="flex gap-3">
<button
onclick={handleDismiss}
class="flex-1 px-4 py-2 bg-gray-500/20 hover:bg-gray-500/30 text-[var(--text-secondary)] rounded-lg transition-colors font-medium"
>
Dismiss
</button>
<button
onclick={handleSubmitAndReconnect}
disabled={!canSubmit()}
class="flex-1 px-4 py-2 bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Submit & Reconnect
</button>
</div>
</div>
</div>
{/if}
+8
View File
@@ -402,10 +402,18 @@ User: ${formattedMessage}`;
allowed_tools: allAllowedTools, allowed_tools: allAllowedTools,
use_worktree: config.use_worktree ?? false, use_worktree: config.use_worktree ?? false,
disable_1m_context: config.disable_1m_context ?? false, disable_1m_context: config.disable_1m_context ?? false,
disable_cron: config.disable_cron ?? false,
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
include_git_instructions: config.include_git_instructions ?? true, include_git_instructions: config.include_git_instructions ?? true,
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: config.auto_memory_directory || null, auto_memory_directory: config.auto_memory_directory || null,
model_overrides: config.model_overrides || null, model_overrides: config.model_overrides || null,
session_name: null,
bare_mode: config.bare_mode ?? false,
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
custom_model_option: config.custom_model_option || null,
effort_level: config.effort_level || null,
prompt_caching_ttl: config.prompt_caching_ttl || null,
}, },
}); });
+33 -1
View File
@@ -14,6 +14,7 @@
interface MemoryFileInfo { interface MemoryFileInfo {
path: string; path: string;
heading: string | null; heading: string | null;
last_modified?: string; // Unix timestamp in seconds as a string, optional for backwards compat
} }
interface MemoryFilesResponse { interface MemoryFilesResponse {
@@ -65,6 +66,16 @@
return file.heading ?? getFileName(file.path); return file.heading ?? getFileName(file.path);
} }
function formatLastModified(ts: string | undefined): string {
if (!ts) return "";
const date = new Date(Number(ts) * 1000);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
async function sendMemoryCommand() { async function sendMemoryCommand() {
const conversationId = get(claudeStore.activeConversationId); const conversationId = get(claudeStore.activeConversationId);
if (!conversationId) return; if (!conversationId) return;
@@ -205,7 +216,12 @@
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/> />
</svg> </svg>
<div class="file-info">
<span class="file-name">{getDisplayName(file)}</span> <span class="file-name">{getDisplayName(file)}</span>
{#if file.last_modified}
<span class="file-date">{formatLastModified(file.last_modified)}</span>
{/if}
</div>
</button> </button>
{/each} {/each}
</div> </div>
@@ -416,7 +432,7 @@
.file-item { .file-item {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 0.75rem; gap: 0.75rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
background: var(--bg-secondary); background: var(--bg-secondary);
@@ -445,6 +461,13 @@
flex-shrink: 0; flex-shrink: 0;
} }
.file-info {
display: flex;
flex-direction: column;
gap: 0.125rem;
overflow: hidden;
}
.file-name { .file-name {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
@@ -453,6 +476,15 @@
white-space: nowrap; white-space: nowrap;
} }
.file-date {
font-size: 0.75rem;
color: var(--text-secondary);
}
.file-item.active .file-date {
color: rgba(255, 255, 255, 0.75);
}
.file-viewer { .file-viewer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -89,10 +89,18 @@
allowed_tools: [...new Set([...newGrantedTools, ...config.auto_granted_tools])], allowed_tools: [...new Set([...newGrantedTools, ...config.auto_granted_tools])],
use_worktree: config.use_worktree ?? false, use_worktree: config.use_worktree ?? false,
disable_1m_context: config.disable_1m_context ?? false, disable_1m_context: config.disable_1m_context ?? false,
disable_cron: config.disable_cron ?? false,
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
include_git_instructions: config.include_git_instructions ?? true, include_git_instructions: config.include_git_instructions ?? true,
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: config.auto_memory_directory || null, auto_memory_directory: config.auto_memory_directory || null,
model_overrides: config.model_overrides || null, model_overrides: config.model_overrides || null,
session_name: null,
bare_mode: config.bare_mode ?? false,
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
custom_model_option: config.custom_model_option || null,
effort_level: config.effort_level || null,
prompt_caching_ttl: config.prompt_caching_ttl || null,
}, },
}); });
+25
View File
@@ -93,6 +93,12 @@
enable_claudeai_mcp_servers: true, enable_claudeai_mcp_servers: true,
auto_memory_directory: null, auto_memory_directory: null,
model_overrides: null, model_overrides: null,
disable_skill_shell_execution: false,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: null,
effort_level: null,
prompt_caching_ttl: null,
}); });
let streamerModeActive = $state(false); let streamerModeActive = $state(false);
@@ -160,6 +166,7 @@
if (!conversationId) { if (!conversationId) {
throw new Error("No active conversation"); throw new Error("No active conversation");
} }
const activeConversationForName = get(conversationsStore.activeConversation);
await invoke("start_claude", { await invoke("start_claude", {
conversationId, conversationId,
options: { options: {
@@ -173,10 +180,19 @@
use_worktree: currentConfig.use_worktree ?? false, use_worktree: currentConfig.use_worktree ?? false,
disable_1m_context: currentConfig.disable_1m_context ?? false, disable_1m_context: currentConfig.disable_1m_context ?? false,
max_output_tokens: currentConfig.max_output_tokens ?? null, max_output_tokens: currentConfig.max_output_tokens ?? null,
disable_cron: currentConfig.disable_cron ?? false,
disable_skill_shell_execution: currentConfig.disable_skill_shell_execution ?? false,
include_git_instructions: currentConfig.include_git_instructions ?? true, include_git_instructions: currentConfig.include_git_instructions ?? true,
enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: currentConfig.auto_memory_directory || null, auto_memory_directory: currentConfig.auto_memory_directory || null,
model_overrides: currentConfig.model_overrides || null, model_overrides: currentConfig.model_overrides || null,
session_name: activeConversationForName?.name || null,
bare_mode: currentConfig.bare_mode ?? false,
show_clear_context_on_plan_accept:
currentConfig.show_clear_context_on_plan_accept ?? true,
custom_model_option: currentConfig.custom_model_option || null,
effort_level: currentConfig.effort_level || null,
prompt_caching_ttl: currentConfig.prompt_caching_ttl || null,
}, },
}); });
@@ -334,10 +350,19 @@
use_worktree: currentConfig.use_worktree ?? false, use_worktree: currentConfig.use_worktree ?? false,
disable_1m_context: currentConfig.disable_1m_context ?? false, disable_1m_context: currentConfig.disable_1m_context ?? false,
max_output_tokens: currentConfig.max_output_tokens ?? null, max_output_tokens: currentConfig.max_output_tokens ?? null,
disable_cron: currentConfig.disable_cron ?? false,
disable_skill_shell_execution: currentConfig.disable_skill_shell_execution ?? false,
include_git_instructions: currentConfig.include_git_instructions ?? true, include_git_instructions: currentConfig.include_git_instructions ?? true,
enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: currentConfig.auto_memory_directory || null, auto_memory_directory: currentConfig.auto_memory_directory || null,
model_overrides: currentConfig.model_overrides || null, model_overrides: currentConfig.model_overrides || null,
session_name: null,
bare_mode: currentConfig.bare_mode ?? false,
show_clear_context_on_plan_accept:
currentConfig.show_clear_context_on_plan_accept ?? true,
custom_model_option: currentConfig.custom_model_option || null,
effort_level: currentConfig.effort_level || null,
prompt_caching_ttl: currentConfig.prompt_caching_ttl || null,
}, },
}); });
+8
View File
@@ -218,10 +218,18 @@
use_worktree: cfg.use_worktree ?? false, use_worktree: cfg.use_worktree ?? false,
disable_1m_context: cfg.disable_1m_context ?? false, disable_1m_context: cfg.disable_1m_context ?? false,
max_output_tokens: cfg.max_output_tokens ?? null, max_output_tokens: cfg.max_output_tokens ?? null,
disable_cron: cfg.disable_cron ?? false,
disable_skill_shell_execution: cfg.disable_skill_shell_execution ?? false,
include_git_instructions: cfg.include_git_instructions ?? true, include_git_instructions: cfg.include_git_instructions ?? true,
enable_claudeai_mcp_servers: cfg.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: cfg.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: cfg.auto_memory_directory || null, auto_memory_directory: cfg.auto_memory_directory || null,
model_overrides: cfg.model_overrides || null, model_overrides: cfg.model_overrides || null,
session_name: null,
bare_mode: cfg.bare_mode ?? false,
show_clear_context_on_plan_accept: cfg.show_clear_context_on_plan_accept ?? true,
custom_model_option: cfg.custom_model_option || null,
effort_level: cfg.effort_level || null,
prompt_caching_ttl: cfg.prompt_caching_ttl || null,
}, },
}); });
} catch (error) { } catch (error) {
@@ -108,10 +108,18 @@
allowed_tools: grantedToolsList, allowed_tools: grantedToolsList,
use_worktree: config.use_worktree ?? false, use_worktree: config.use_worktree ?? false,
disable_1m_context: config.disable_1m_context ?? false, disable_1m_context: config.disable_1m_context ?? false,
disable_cron: config.disable_cron ?? false,
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
include_git_instructions: config.include_git_instructions ?? true, include_git_instructions: config.include_git_instructions ?? true,
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true, enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
auto_memory_directory: config.auto_memory_directory || null, auto_memory_directory: config.auto_memory_directory || null,
model_overrides: config.model_overrides || null, model_overrides: config.model_overrides || null,
session_name: null,
bare_mode: config.bare_mode ?? false,
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
custom_model_option: config.custom_model_option || null,
effort_level: config.effort_level || null,
prompt_caching_ttl: config.prompt_caching_ttl || null,
}, },
}); });
+11
View File
@@ -22,6 +22,7 @@ export const claudeStore = {
terminalLines: conversationsStore.terminalLines, terminalLines: conversationsStore.terminalLines,
pendingPermission: conversationsStore.pendingPermission, pendingPermission: conversationsStore.pendingPermission,
pendingQuestion: conversationsStore.pendingQuestion, pendingQuestion: conversationsStore.pendingQuestion,
pendingElicitation: conversationsStore.pendingElicitation,
isProcessing: conversationsStore.isProcessing, isProcessing: conversationsStore.isProcessing,
grantedTools: conversationsStore.grantedTools, grantedTools: conversationsStore.grantedTools,
pendingRetryMessage: conversationsStore.pendingRetryMessage, pendingRetryMessage: conversationsStore.pendingRetryMessage,
@@ -57,6 +58,10 @@ export const claudeStore = {
clearQuestion: conversationsStore.clearQuestion, clearQuestion: conversationsStore.clearQuestion,
requestQuestionForConversation: conversationsStore.requestQuestionForConversation, requestQuestionForConversation: conversationsStore.requestQuestionForConversation,
clearQuestionForConversation: conversationsStore.clearQuestionForConversation, clearQuestionForConversation: conversationsStore.clearQuestionForConversation,
requestElicitation: conversationsStore.requestElicitation,
clearElicitation: conversationsStore.clearElicitation,
requestElicitationForConversation: conversationsStore.requestElicitationForConversation,
clearElicitationForConversation: conversationsStore.clearElicitationForConversation,
grantTool: conversationsStore.grantTool, grantTool: conversationsStore.grantTool,
revokeAllTools: conversationsStore.revokeAllTools, revokeAllTools: conversationsStore.revokeAllTools,
isToolGranted: conversationsStore.isToolGranted, isToolGranted: conversationsStore.isToolGranted,
@@ -126,6 +131,12 @@ export const hasQuestionPending = derived(
($conversation) => $conversation?.pendingQuestion !== null ($conversation) => $conversation?.pendingQuestion !== null
); );
export const hasElicitationPending = derived(
claudeStore.activeConversation,
($conversation) =>
$conversation?.pendingElicitation !== null && $conversation?.pendingElicitation !== undefined
);
// Derived store to check if Claude is currently processing (can be interrupted) // Derived store to check if Claude is currently processing (can be interrupted)
export const isClaudeProcessing = derived( export const isClaudeProcessing = derived(
[claudeStore.connectionStatus, characterState], [claudeStore.connectionStatus, characterState],
+18
View File
@@ -225,6 +225,12 @@ describe("config store", () => {
enable_claudeai_mcp_servers: true, enable_claudeai_mcp_servers: true,
auto_memory_directory: null, auto_memory_directory: null,
model_overrides: null, model_overrides: null,
disable_skill_shell_execution: false,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: null,
effort_level: null,
prompt_caching_ttl: null,
}; };
expect(config.model).toBe("claude-sonnet-4"); expect(config.model).toBe("claude-sonnet-4");
@@ -289,6 +295,12 @@ describe("config store", () => {
enable_claudeai_mcp_servers: true, enable_claudeai_mcp_servers: true,
auto_memory_directory: null, auto_memory_directory: null,
model_overrides: null, model_overrides: null,
disable_skill_shell_execution: false,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: null,
effort_level: null,
prompt_caching_ttl: null,
}; };
expect(config.model).toBeNull(); expect(config.model).toBeNull();
@@ -908,6 +920,12 @@ describe("config store", () => {
enable_claudeai_mcp_servers: true, enable_claudeai_mcp_servers: true,
auto_memory_directory: null, auto_memory_directory: null,
model_overrides: null, model_overrides: null,
disable_skill_shell_execution: false,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: null,
effort_level: null,
prompt_caching_ttl: null,
}; };
const mockInvokeImpl = vi.mocked(invoke); const mockInvokeImpl = vi.mocked(invoke);
+18
View File
@@ -91,6 +91,18 @@ export interface HikariConfig {
auto_memory_directory: string | null; auto_memory_directory: string | null;
// Model overrides for provider-specific model IDs (AWS Bedrock, Google Vertex, etc.) // Model overrides for provider-specific model IDs (AWS Bedrock, Google Vertex, etc.)
model_overrides: Record<string, string> | null; model_overrides: Record<string, string> | null;
// Prevents skill scripts from executing shell commands (Claude Code v2.1.91+)
disable_skill_shell_execution: boolean;
// Bare mode — suppress UI chrome for scripted headless -p calls (v2.1.81+)
bare_mode: boolean;
// Show clear context dialog when accepting a plan (v2.1.81+)
show_clear_context_on_plan_accept: boolean;
// Custom model option env var (v2.1.81+)
custom_model_option: string | null;
// Effort level for Claude Code (v2.1.111+) — null means use CLI default
effort_level: string | null;
// Prompt caching TTL override (v2.1.108+) — null means use CLI default
prompt_caching_ttl: string | null;
} }
const defaultConfig: HikariConfig = { const defaultConfig: HikariConfig = {
@@ -149,6 +161,12 @@ const defaultConfig: HikariConfig = {
enable_claudeai_mcp_servers: true, enable_claudeai_mcp_servers: true,
auto_memory_directory: null, auto_memory_directory: null,
model_overrides: null, model_overrides: null,
disable_skill_shell_execution: false,
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: null,
effort_level: null,
prompt_caching_ttl: null,
}; };
function createConfigStore() { function createConfigStore() {
+54
View File
@@ -2,6 +2,7 @@ import { writable, derived, get } from "svelte/store";
import type { import type {
TerminalLine, TerminalLine,
ConnectionStatus, ConnectionStatus,
ElicitationEvent,
PermissionRequest, PermissionRequest,
UserQuestionEvent, UserQuestionEvent,
Attachment, Attachment,
@@ -32,6 +33,7 @@ export interface Conversation {
grantedTools: Set<string>; grantedTools: Set<string>;
pendingPermissions: PermissionRequest[]; pendingPermissions: PermissionRequest[];
pendingQuestion: UserQuestionEvent | null; pendingQuestion: UserQuestionEvent | null;
pendingElicitation: ElicitationEvent | null;
scrollPosition: number; scrollPosition: number;
createdAt: Date; createdAt: Date;
lastActivityAt: Date; lastActivityAt: Date;
@@ -157,6 +159,7 @@ function createConversationsStore() {
grantedTools: new Set(), grantedTools: new Set(),
pendingPermissions: [], pendingPermissions: [],
pendingQuestion: null, pendingQuestion: null,
pendingElicitation: null,
scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll) scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll)
createdAt: new Date(), createdAt: new Date(),
lastActivityAt: new Date(), lastActivityAt: new Date(),
@@ -221,6 +224,10 @@ function createConversationsStore() {
($conv) => $conv?.pendingPermissions || [] ($conv) => $conv?.pendingPermissions || []
); );
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null); const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
const pendingElicitation = derived(
activeConversation,
($conv) => $conv?.pendingElicitation ?? null
);
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1); const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
const attachments = derived(activeConversation, ($conv) => $conv?.attachments || []); const attachments = derived(activeConversation, ($conv) => $conv?.attachments || []);
const worktreeInfo = derived(activeConversation, ($conv) => $conv?.worktreeInfo ?? null); const worktreeInfo = derived(activeConversation, ($conv) => $conv?.worktreeInfo ?? null);
@@ -234,6 +241,7 @@ function createConversationsStore() {
pendingPermission: { subscribe: pendingPermission.subscribe }, pendingPermission: { subscribe: pendingPermission.subscribe },
pendingPermissions: { subscribe: pendingPermissions.subscribe }, pendingPermissions: { subscribe: pendingPermissions.subscribe },
pendingQuestion: { subscribe: pendingQuestion.subscribe }, pendingQuestion: { subscribe: pendingQuestion.subscribe },
pendingElicitation: { subscribe: pendingElicitation.subscribe },
isProcessing: { subscribe: isProcessing.subscribe }, isProcessing: { subscribe: isProcessing.subscribe },
grantedTools: { subscribe: grantedTools.subscribe }, grantedTools: { subscribe: grantedTools.subscribe },
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe }, pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
@@ -399,6 +407,52 @@ function createConversationsStore() {
return convs; return convs;
}); });
}, },
requestElicitation: (elicitation: ElicitationEvent) => {
const activeId = get(activeConversationId);
if (!activeId) return;
conversations.update((convs) => {
const conv = convs.get(activeId);
if (conv) {
conv.pendingElicitation = elicitation;
conv.lastActivityAt = new Date();
}
return convs;
});
},
clearElicitation: () => {
const activeId = get(activeConversationId);
if (!activeId) return;
conversations.update((convs) => {
const conv = convs.get(activeId);
if (conv) {
conv.pendingElicitation = null;
conv.lastActivityAt = new Date();
}
return convs;
});
},
requestElicitationForConversation: (conversationId: string, elicitation: ElicitationEvent) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.pendingElicitation = elicitation;
conv.lastActivityAt = new Date();
}
return convs;
});
},
clearElicitationForConversation: (conversationId: string) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.pendingElicitation = null;
conv.lastActivityAt = new Date();
}
return convs;
});
},
setPendingRetryMessage: (message: string | null) => pendingRetryMessage.set(message), setPendingRetryMessage: (message: string | null) => pendingRetryMessage.set(message),
// Conversation management // Conversation management
+5 -3
View File
@@ -10,13 +10,15 @@ export type BudgetType = "token" | "cost";
// Model pricing (per million tokens) - keep in sync with stats.rs // Model pricing (per million tokens) - keep in sync with stats.rs
// Source: https://platform.claude.com/docs/en/about-claude/models/overview // Source: https://platform.claude.com/docs/en/about-claude/models/overview
export const MODEL_PRICING: Record<string, { input: number; output: number }> = { export const MODEL_PRICING: Record<string, { input: number; output: number }> = {
// Current generation (Claude 4.6) // Current generation (Claude 4.7/4.6/4.5)
"claude-opus-4-6": { input: 5.0, output: 25.0 }, "claude-opus-4-7": { input: 5.0, output: 25.0 },
"claude-sonnet-4-6": { input: 3.0, output: 15.0 }, "claude-sonnet-4-6": { input: 3.0, output: 15.0 },
"claude-haiku-4-5-20251001": { input: 1.0, output: 5.0 },
// Previous generation (Claude 4.6)
"claude-opus-4-6": { input: 5.0, output: 25.0 },
// Previous generation (Claude 4.5) // Previous generation (Claude 4.5)
"claude-opus-4-5-20251101": { input: 5.0, output: 25.0 }, "claude-opus-4-5-20251101": { input: 5.0, output: 25.0 },
"claude-sonnet-4-5-20250929": { input: 3.0, output: 15.0 }, "claude-sonnet-4-5-20250929": { input: 3.0, output: 15.0 },
"claude-haiku-4-5-20251001": { input: 1.0, output: 5.0 },
// Previous generation (Claude 4.x) // Previous generation (Claude 4.x)
"claude-opus-4-1-20250805": { input: 15.0, output: 75.0 }, "claude-opus-4-1-20250805": { input: 15.0, output: 75.0 },
"claude-opus-4-20250514": { input: 15.0, output: 75.0 }, "claude-opus-4-20250514": { input: 15.0, output: 75.0 },
+27
View File
@@ -187,6 +187,33 @@ describe("toastStore", () => {
}); });
}); });
describe("addError", () => {
it("adds an error toast with the warning icon", () => {
toastStore.addError("Something went wrong");
const toasts = get(toastStore);
expect(toasts).toHaveLength(1);
const toast = toasts[0];
expect(toast.kind).toBe("info");
if (toast.kind === "info") {
expect(toast.message).toBe("Something went wrong");
expect(toast.icon).toBe("⚠️");
expect(typeof toast.id).toBe("string");
expect(toast.id.length).toBeGreaterThan(0);
}
});
it("auto-dismisses after 6000ms", () => {
toastStore.addError("Rate limit reached");
expect(get(toastStore)).toHaveLength(1);
vi.advanceTimersByTime(5999);
expect(get(toastStore)).toHaveLength(1);
vi.advanceTimersByTime(1);
expect(get(toastStore)).toHaveLength(0);
});
});
describe("addUpdate", () => { describe("addUpdate", () => {
it("adds a persistent update toast with the correct fields", () => { it("adds a persistent update toast with the correct fields", () => {
toastStore.addUpdate("2.0.0", "1.9.0", "https://example.com/release"); toastStore.addUpdate("2.0.0", "1.9.0", "https://example.com/release");
+8 -1
View File
@@ -68,6 +68,13 @@ function createToastStore() {
setTimeout(() => remove(id), 4000); setTimeout(() => remove(id), 4000);
} }
function addError(message: string) {
const id = crypto.randomUUID();
const toast: InfoToast = { id, kind: "info", message, icon: "⚠️" };
update((toasts) => [...toasts, toast]);
setTimeout(() => remove(id), 6000);
}
function addAchievement(achievement: AchievementUnlockedEvent["achievement"]) { function addAchievement(achievement: AchievementUnlockedEvent["achievement"]) {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const toast: AchievementToast = { id, kind: "achievement", achievement }; const toast: AchievementToast = { id, kind: "achievement", achievement };
@@ -82,7 +89,7 @@ function createToastStore() {
// Update toasts are persistent — no auto-dismiss // Update toasts are persistent — no auto-dismiss
} }
return { subscribe, addInfo, addAchievement, addUpdate, remove }; return { subscribe, addInfo, addError, addAchievement, addUpdate, remove };
} }
export const toastStore = createToastStore(); export const toastStore = createToastStore();
+64 -2
View File
@@ -8,7 +8,11 @@ import { initStatsListener, resetSessionStats } from "$lib/stores/stats";
import { initAchievementsListener } from "$lib/stores/achievements"; import { initAchievementsListener } from "$lib/stores/achievements";
import type { import type {
ConnectionStatus, ConnectionStatus,
ElicitationEvent,
PermissionPromptEvent, PermissionPromptEvent,
PostCompactEvent,
PreCompactEvent,
StopFailureEvent,
UserQuestionEvent, UserQuestionEvent,
} from "$lib/types/messages"; } from "$lib/types/messages";
import type { CharacterState } from "$lib/types/states"; import type { CharacterState } from "$lib/types/states";
@@ -406,7 +410,8 @@ export async function initializeTauriListeners() {
| "rate-limit" | "rate-limit"
| "compact-prompt" | "compact-prompt"
| "worktree" | "worktree"
| "config-change", | "config-change"
| "elicitation",
content, content,
tool_name || undefined, tool_name || undefined,
costData, costData,
@@ -425,7 +430,8 @@ export async function initializeTauriListeners() {
| "rate-limit" | "rate-limit"
| "compact-prompt" | "compact-prompt"
| "worktree" | "worktree"
| "config-change", | "config-change"
| "elicitation",
content, content,
tool_name || undefined, tool_name || undefined,
costData, costData,
@@ -611,6 +617,62 @@ export async function initializeTauriListeners() {
} }
}); });
unlisteners.push(questionUnlisten); unlisteners.push(questionUnlisten);
const elicitationUnlisten = await listen<ElicitationEvent>("claude:elicitation", (event) => {
const elicitationEvent = event.payload;
if (elicitationEvent.conversation_id) {
claudeStore.requestElicitationForConversation(
elicitationEvent.conversation_id,
elicitationEvent
);
} else {
claudeStore.requestElicitation(elicitationEvent);
}
});
unlisteners.push(elicitationUnlisten);
const elicitationResultUnlisten = await listen<{ conversation_id?: string }>(
"claude:elicitation-result",
(event) => {
const { conversation_id } = event.payload;
if (conversation_id) {
claudeStore.clearElicitationForConversation(conversation_id);
} else {
claudeStore.clearElicitation();
}
}
);
unlisteners.push(elicitationResultUnlisten);
const stopFailureUnlisten = await listen<StopFailureEvent>("claude:stop-failure", (event) => {
const { stop_reason, error_type } = event.payload;
characterState.setTemporaryState("error", 3000);
let message: string;
if (stop_reason === "rate_limit") {
message = "Rate limit reached";
} else if (stop_reason === "auth_failure" || stop_reason === "authentication") {
message = "Authentication failed";
} else {
message = `API error: ${stop_reason ?? error_type ?? "unknown"}`;
}
toastStore.addError(message);
});
unlisteners.push(stopFailureUnlisten);
const preCompactUnlisten = await listen<PreCompactEvent>("claude:pre-compact", () => {
toastStore.addInfo("Compacting context...", "🗜️");
characterState.setTemporaryState("thinking", 3000);
});
unlisteners.push(preCompactUnlisten);
const postCompactUnlisten = await listen<PostCompactEvent>("claude:post-compact", () => {
toastStore.addInfo("Context compacted", "🗜️");
characterState.setTemporaryState("success", 2000);
});
unlisteners.push(postCompactUnlisten);
} }
export function cleanupTauriListeners() { export function cleanupTauriListeners() {
+31 -1
View File
@@ -10,7 +10,8 @@ export interface TerminalLine {
| "rate-limit" | "rate-limit"
| "compact-prompt" | "compact-prompt"
| "worktree" | "worktree"
| "config-change"; | "config-change"
| "elicitation";
content: string; content: string;
timestamp: Date; timestamp: Date;
toolName?: string; toolName?: string;
@@ -162,6 +163,35 @@ export interface UserQuestionEvent {
conversation_id?: string; conversation_id?: string;
} }
export interface ElicitationEvent {
message: string;
server_name?: string;
request_id?: string;
conversation_id?: string;
}
export interface ElicitationResultEvent {
action: string;
request_id?: string;
conversation_id?: string;
}
export interface StopFailureEvent {
stop_reason?: string;
error_type?: string;
conversation_id?: string;
}
export interface PostCompactEvent {
session_id?: string;
conversation_id?: string;
}
export interface PreCompactEvent {
session_id?: string;
conversation_id?: string;
}
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error"; export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
export interface Attachment { export interface Attachment {
+2
View File
@@ -36,6 +36,7 @@
import type { CharacterState } from "$lib/types/states"; import type { CharacterState } from "$lib/types/states";
import PermissionModal from "$lib/components/PermissionModal.svelte"; import PermissionModal from "$lib/components/PermissionModal.svelte";
import UserQuestionModal from "$lib/components/UserQuestionModal.svelte"; import UserQuestionModal from "$lib/components/UserQuestionModal.svelte";
import ElicitationModal from "$lib/components/ElicitationModal.svelte";
import ConfigSidebar from "$lib/components/ConfigSidebar.svelte"; import ConfigSidebar from "$lib/components/ConfigSidebar.svelte";
import AchievementsPanel from "$lib/components/AchievementsPanel.svelte"; import AchievementsPanel from "$lib/components/AchievementsPanel.svelte";
import ToastContainer from "$lib/components/ToastContainer.svelte"; import ToastContainer from "$lib/components/ToastContainer.svelte";
@@ -593,6 +594,7 @@
<PermissionModal /> <PermissionModal />
<UserQuestionModal /> <UserQuestionModal />
<ElicitationModal />
<ConfigSidebar /> <ConfigSidebar />
<AchievementsPanel <AchievementsPanel
bind:isOpen={achievementPanelOpen} bind:isOpen={achievementPanelOpen}