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
+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""#;