generated from nhcarrigan/template
feat: Claude Code CLI v2.1.105–v2.1.131 support (#274)
## 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:
+119
-6
@@ -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""#;
|
||||
|
||||
Reference in New Issue
Block a user