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>
This commit was merged in pull request #274.
This commit is contained in:
2026-05-06 16:16:06 -07:00
committed by Naomi Carrigan
parent 7a1ab89ad8
commit 1c4432c4d8
19 changed files with 353 additions and 25 deletions
+24
View File
@@ -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<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)]
@@ -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<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 {
@@ -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();
+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();
// 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<Vec<Sessio
.collect();
// 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)
}
@@ -348,7 +348,7 @@ mod tests {
];
// 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[1].id, "old");
+30 -7
View File
@@ -86,9 +86,10 @@ impl ContextWarning {
/// Get the context window limit (in tokens) for a given model
fn get_context_window_limit(model: &str) -> 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<u64>,
) -> 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);
+74
View File
@@ -98,6 +98,10 @@ pub enum ClaudeMessage {
/// 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")]
Assistant {
@@ -330,6 +334,14 @@ pub struct PostCompactEvent {
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,
@@ -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"}"#;
+119 -6
View File
@@ -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<String>,
}
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<String>,
@@ -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""#;
+4
View File
@@ -77,6 +77,8 @@ async function changeDirectory(path: string): Promise<void> {
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<void> {
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 { onMount } from "svelte";
const SUPPORTED_CLI_VERSION = "2.1.104";
const SUPPORTED_CLI_VERSION = "2.1.131";
let installedVersion = $state("Loading...");
let latestNpmVersion = $state<string | null>(null);
+53 -5
View File
@@ -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 @@
</p>
</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">
@@ -78,6 +78,8 @@
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,
},
});
+2
View File
@@ -412,6 +412,8 @@ User: ${formattedMessage}`;
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,
},
});
@@ -99,6 +99,8 @@
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,
},
});
+6
View File
@@ -97,6 +97,8 @@
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);
@@ -189,6 +191,8 @@
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,
},
});
@@ -357,6 +361,8 @@
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,
},
});
+2
View File
@@ -228,6 +228,8 @@
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) {
@@ -118,6 +118,8 @@
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,
},
});
+6
View File
@@ -229,6 +229,8 @@ describe("config store", () => {
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");
@@ -297,6 +299,8 @@ describe("config store", () => {
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();
@@ -920,6 +924,8 @@ describe("config store", () => {
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);
+6
View File
@@ -99,6 +99,10 @@ export interface HikariConfig {
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 = {
@@ -161,6 +165,8 @@ const defaultConfig: HikariConfig = {
bare_mode: false,
show_clear_context_on_plan_accept: true,
custom_model_option: null,
effort_level: null,
prompt_caching_ttl: null,
};
function createConfigStore() {
+5 -3
View File
@@ -10,13 +10,15 @@ export type BudgetType = "token" | "cost";
// Model pricing (per million tokens) - keep in sync with stats.rs
// Source: https://platform.claude.com/docs/en/about-claude/models/overview
export const MODEL_PRICING: Record<string, { input: number; output: number }> = {
// Current generation (Claude 4.6)
"claude-opus-4-6": { input: 5.0, output: 25.0 },
// Current generation (Claude 4.7/4.6/4.5)
"claude-opus-4-7": { input: 5.0, output: 25.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)
"claude-opus-4-5-20251101": { input: 5.0, output: 25.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)
"claude-opus-4-1-20250805": { input: 15.0, output: 75.0 },
"claude-opus-4-20250514": { input: 15.0, output: 75.0 },
+7
View File
@@ -11,6 +11,7 @@ import type {
ElicitationEvent,
PermissionPromptEvent,
PostCompactEvent,
PreCompactEvent,
StopFailureEvent,
UserQuestionEvent,
} from "$lib/types/messages";
@@ -661,6 +662,12 @@ export async function initializeTauriListeners() {
});
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);
+5
View File
@@ -187,6 +187,11 @@ export interface PostCompactEvent {
conversation_id?: string;
}
export interface PreCompactEvent {
session_id?: string;
conversation_id?: string;
}
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
export interface Attachment {