diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs
index 9ca2af7..1691d5d 100644
--- a/src-tauri/src/config.rs
+++ b/src-tauri/src/config.rs
@@ -70,6 +70,16 @@ pub struct ClaudeStartOptions {
/// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+).
#[serde(default)]
pub custom_model_option: Option,
+
+ /// Passes `--effort ` 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,
+
+ /// 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,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -236,6 +246,16 @@ pub struct HikariConfig {
/// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+).
#[serde(default)]
pub custom_model_option: Option,
+
+ /// Passes `--effort ` 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,
+
+ /// 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,
}
impl Default for HikariConfig {
@@ -291,6 +311,8 @@ impl Default for HikariConfig {
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: None,
+ effort_level: None,
+ prompt_caching_ttl: None,
}
}
}
@@ -508,6 +530,8 @@ mod tests {
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();
diff --git a/src-tauri/src/sessions.rs b/src-tauri/src/sessions.rs
index 42dca83..f325b0a 100644
--- a/src-tauri/src/sessions.rs
+++ b/src-tauri/src/sessions.rs
@@ -81,7 +81,7 @@ pub async fn list_sessions(app: AppHandle) -> Result, Strin
let mut items: Vec = sessions.iter().map(SessionListItem::from).collect();
// 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)
}
@@ -132,7 +132,7 @@ pub async fn search_sessions(app: AppHandle, query: String) -> Result u64 {
match model {
- // Claude 4.6 family
- "claude-opus-4-6" => 200_000,
- "claude-sonnet-4-6" => 1_000_000, // 1M token context window
+ // Claude 4.7 - 1M token context window
+ "claude-opus-4-7" => 1_000_000,
+ // 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-opus-4-5-20251101"
| "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
// Cache pricing: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
pub fn calculate_cost(
@@ -501,14 +502,17 @@ pub fn calculate_cost(
cache_read_tokens: Option,
) -> f64 {
let (input_price_per_million, output_price_per_million) = match model {
- // Current generation (Claude 4.6)
- "claude-opus-4-6" => (5.0, 25.0),
+ // Current generation (Claude 4.7/4.6/4.5)
+ "claude-opus-4-7" => (5.0, 25.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)
"claude-opus-4-5-20251101" => (5.0, 25.0),
"claude-sonnet-4-5-20250929" => (3.0, 15.0),
- "claude-haiku-4-5-20251001" => (1.0, 5.0),
// Previous generation (Claude 4.x)
"claude-opus-4-1-20250805" => (15.0, 75.0),
@@ -681,6 +685,15 @@ mod tests {
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]
fn test_cost_calculation_opus_45() {
let cost = calculate_cost(1000, 2000, "claude-opus-4-5-20251101", None, None);
@@ -1019,6 +1032,16 @@ mod 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]
fn test_context_window_limit_claude_4() {
assert_eq!(get_context_window_limit("claude-opus-4-5-20251101"), 200_000);
diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs
index 44cdf77..0fbdb5e 100644
--- a/src-tauri/src/types.rs
+++ b/src-tauri/src/types.rs
@@ -98,6 +98,10 @@ pub enum ClaudeMessage {
/// Output style hint from Claude Code (v2.1.81+). Informational only.
#[serde(default)]
output_style: Option,
+ /// Plugin errors from Claude Code (v2.1.111+). Populated when plugins are demoted
+ /// due to unsatisfied dependencies.
+ #[serde(default)]
+ plugin_errors: Option,
},
#[serde(rename = "assistant")]
Assistant {
@@ -330,6 +334,14 @@ pub struct PostCompactEvent {
pub conversation_id: Option,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PreCompactEvent {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub session_id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub conversation_id: Option,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CwdChangedEvent {
pub cwd: String,
@@ -790,6 +802,30 @@ mod tests {
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 {
@@ -945,6 +981,44 @@ mod tests {
}
}
+ #[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"}"#;
diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs
index 4197fdf..a348301 100644
--- a/src-tauri/src/wsl_bridge.rs
+++ b/src-tauri/src/wsl_bridge.rs
@@ -17,7 +17,7 @@ use crate::types::{
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
ConnectionStatus, ContentBlock, CwdChangedEvent, ElicitationEvent, ElicitationResultEvent,
FileChangedEvent, MessageCost, OutputEvent, PermissionDeniedEvent, PermissionPromptEvent,
- PermissionPromptEventItem, PostCompactEvent, QuestionOption, SessionEvent, StateChangeEvent,
+ PermissionPromptEventItem, PostCompactEvent, PreCompactEvent, QuestionOption, SessionEvent, StateChangeEvent,
StopFailureEvent, TaskCreatedEvent, TodoItem, TodoUpdateEvent, UserQuestionEvent,
WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo,
};
@@ -308,6 +308,14 @@ impl WslBridge {
cmd.arg("--bare");
}
+ // Add effort level if specified (v2.1.111+)
+ if let Some(ref level) = options.effort_level {
+ if !level.is_empty() {
+ cmd.arg("--effort");
+ cmd.arg(level);
+ }
+ }
+
// Pass combined settings via --settings flag if any settings are specified
{
let has_memory_dir = options
@@ -402,6 +410,15 @@ impl WslBridge {
}
}
+ // Set prompt caching TTL env var if specified (v2.1.108+)
+ if let Some(ref ttl) = options.prompt_caching_ttl {
+ match ttl.as_str() {
+ "1h" => { cmd.env("ENABLE_PROMPT_CACHING_1H", "1"); }
+ "5m" => { cmd.env("FORCE_PROMPT_CACHING_5M", "1"); }
+ _ => {}
+ }
+ }
+
cmd
} else {
// Running on Windows - use wsl with bash login shell to ensure PATH is loaded
@@ -477,6 +494,15 @@ impl WslBridge {
}
}
+ // Set prompt caching TTL env var if specified (v2.1.108+)
+ if let Some(ref ttl) = options.prompt_caching_ttl {
+ match ttl.as_str() {
+ "1h" => { claude_cmd.push_str("ENABLE_PROMPT_CACHING_1H=1 "); }
+ "5m" => { claude_cmd.push_str("FORCE_PROMPT_CACHING_5M=1 "); }
+ _ => {}
+ }
+ }
+
claude_cmd.push_str(
"claude --output-format stream-json --input-format stream-json --verbose",
);
@@ -532,6 +558,13 @@ impl WslBridge {
claude_cmd.push_str(" --bare");
}
+ // Add effort level if specified (v2.1.111+)
+ if let Some(ref level) = options.effort_level {
+ if !level.is_empty() {
+ claude_cmd.push_str(&format!(" --effort {}", level));
+ }
+ }
+
// Pass combined settings via --settings flag if any settings are specified
{
let has_memory_dir = options
@@ -887,10 +920,9 @@ impl WslBridge {
let stats = self.stats.read();
for tool_name in &tools {
if let Some(tool_stats) = stats.session_tools_usage.get(tool_name) {
- if tool_stats.call_count > 0 {
- // Use session average tokens per call for this tool
- let avg_tokens = (tool_stats.estimated_input_tokens + tool_stats.estimated_output_tokens)
- / tool_stats.call_count;
+ if let Some(avg_tokens) = (tool_stats.estimated_input_tokens + tool_stats.estimated_output_tokens)
+ .checked_div(tool_stats.call_count)
+ {
tool_overhead_tokens += avg_tokens;
tracing::info!("[COST ESTIMATION] Tool {} average: {} tokens", tool_name, avg_tokens);
}
@@ -1140,6 +1172,7 @@ fn handle_stderr(
let is_elicitation = line.contains("[Elicitation Hook]");
let is_elicitation_result = line.contains("[ElicitationResult Hook]");
let is_stop_failure = line.contains("[StopFailure Hook]");
+ let is_pre_compact = line.contains("[PreCompact Hook]");
let is_post_compact = line.contains("[PostCompact Hook]");
let is_cwd_changed = line.contains("[CwdChanged Hook]");
let is_file_changed = line.contains("[FileChanged Hook]");
@@ -1154,7 +1187,7 @@ fn handle_stderr(
"elicitation"
} else if is_stop_failure {
"error"
- } else if is_post_compact {
+ } else if is_pre_compact || is_post_compact {
"compact-prompt"
} else if is_cwd_changed {
"cwd-changed"
@@ -1280,6 +1313,28 @@ fn handle_stderr(
parent_tool_use_id: None,
},
);
+ } else if is_pre_compact {
+ let data = parse_pre_compact_hook(&line);
+
+ let _ = app.emit(
+ "claude:pre-compact",
+ PreCompactEvent {
+ session_id: data.session_id,
+ conversation_id: conversation_id.clone(),
+ },
+ );
+
+ let _ = app.emit(
+ "claude:output",
+ OutputEvent {
+ line_type: "compact-prompt".to_string(),
+ content: "Compacting context — conversation history is being summarised to free up space.".to_string(),
+ tool_name: None,
+ conversation_id: conversation_id.clone(),
+ cost: None,
+ parent_tool_use_id: None,
+ },
+ );
} else if is_post_compact {
let data = parse_post_compact_hook(&line);
@@ -1623,6 +1678,16 @@ fn build_stop_failure_message(data: &StopFailureData) -> String {
}
}
+#[derive(Debug)]
+struct PreCompactData {
+ session_id: Option,
+}
+
+fn parse_pre_compact_hook(line: &str) -> PreCompactData {
+ let session_id = extract_debug_string_value(line, "session_id");
+ PreCompactData { session_id }
+}
+
#[derive(Debug)]
struct PostCompactData {
session_id: Option,
@@ -1784,6 +1849,7 @@ fn process_json_line(
subtype,
session_id,
cwd,
+ plugin_errors,
..
} => {
if subtype == "init" {
@@ -1808,6 +1874,31 @@ fn process_json_line(
},
);
}
+
+ // Warn about any plugins that failed to load (v2.1.111+)
+ if let Some(errors) = plugin_errors {
+ if let Some(arr) = errors.as_array() {
+ for error in arr {
+ let msg = if let Some(s) = error.as_str() {
+ s.to_string()
+ } else {
+ error.to_string()
+ };
+ let _ = app.emit(
+ "claude:output",
+ OutputEvent {
+ line_type: "error".to_string(),
+ content: format!("Plugin error: {}", msg),
+ tool_name: None,
+ conversation_id: conversation_id.clone(),
+ cost: None,
+ parent_tool_use_id: None,
+ },
+ );
+ }
+ }
+ }
+
emit_state_change(app, CharacterState::Idle, None, conversation_id.clone());
}
}
@@ -3887,6 +3978,28 @@ mod tests {
assert_eq!(data.session_id, None);
}
+ #[test]
+ fn test_parse_pre_compact_hook_with_session_id() {
+ let line =
+ r#"[PreCompact Hook] session_id=Some("sess-abc123"), conversation_id=Some("conv-xyz")"#;
+ let data = parse_pre_compact_hook(line);
+ assert_eq!(data.session_id, Some("sess-abc123".to_string()));
+ }
+
+ #[test]
+ fn test_parse_pre_compact_hook_without_session_id() {
+ let line = "[PreCompact Hook] session_id=None";
+ let data = parse_pre_compact_hook(line);
+ assert_eq!(data.session_id, None);
+ }
+
+ #[test]
+ fn test_parse_pre_compact_hook_empty_line() {
+ let line = "[PreCompact Hook]";
+ let data = parse_pre_compact_hook(line);
+ assert_eq!(data.session_id, None);
+ }
+
#[test]
fn test_parse_cwd_changed_hook_with_cwd_key() {
let line = r#"[CwdChanged Hook] cwd="/home/naomi/code/my-project""#;
diff --git a/src/lib/commands/slashCommands.ts b/src/lib/commands/slashCommands.ts
index 4bef72e..5d549ee 100644
--- a/src/lib/commands/slashCommands.ts
+++ b/src/lib/commands/slashCommands.ts
@@ -77,6 +77,8 @@ async function changeDirectory(path: string): Promise {
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,
},
});
@@ -164,6 +166,8 @@ async function startNewConversation(): Promise {
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,
},
});
diff --git a/src/lib/components/CliVersion.svelte b/src/lib/components/CliVersion.svelte
index a3120ee..edb82be 100644
--- a/src/lib/components/CliVersion.svelte
+++ b/src/lib/components/CliVersion.svelte
@@ -2,7 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte";
- const SUPPORTED_CLI_VERSION = "2.1.104";
+ const SUPPORTED_CLI_VERSION = "2.1.131";
let installedVersion = $state("Loading...");
let latestNpmVersion = $state(null);
diff --git a/src/lib/components/ConfigSidebar.svelte b/src/lib/components/ConfigSidebar.svelte
index 9b2877e..8ed0d37 100644
--- a/src/lib/components/ConfigSidebar.svelte
+++ b/src/lib/components/ConfigSidebar.svelte
@@ -78,6 +78,8 @@
task_loop_auto_commit: false,
task_loop_commit_prefix: "feat",
task_loop_include_summary: false,
+ effort_level: null,
+ prompt_caching_ttl: null,
});
let showCustomThemeEditor = $state(false);
@@ -144,17 +146,20 @@
const availableModels = [
{ value: "", label: "Default (from ~/.claude)" },
- // Current generation (Claude 4.6)
- { value: "claude-opus-4-6", label: "Claude Opus 4.6 (Most Capable)" },
+ // Current generation (Claude 4.7/4.6/4.5)
+ { 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-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)
{ 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" },
// Previous generation (Claude 4.x)
{ value: "claude-opus-4-1-20250805", label: "Claude Opus 4.1" },
- { value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
- { value: "claude-opus-4-20250514", label: "Claude Opus 4" },
+ // Deprecated — retire June 15, 2026
+ { value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4 (Deprecated)" },
+ { value: "claude-opus-4-20250514", label: "Claude Opus 4 (Deprecated)" },
];
const commonTools = [
@@ -713,6 +718,49 @@
+
+
+
+
+
+ Passes --effort to tune speed vs. intelligence. Requires Claude
+ Code v2.1.111+.
+
+
+
+
+
+
+
+
+ Sets ENABLE_PROMPT_CACHING_1H or
+ FORCE_PROMPT_CACHING_5M. Requires Claude Code v2.1.108+.
+
+
+