优化上下文事件更新的处理逻辑
This commit is contained in:
@@ -58,6 +58,19 @@ fn is_meta_message(value: &serde_json::Value) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if an assistant message is a synthetic placeholder (e.g. generated by
|
||||||
|
/// Claude Code for local commands like `/context` or `/model`).
|
||||||
|
/// These carry `model: "<synthetic>"` and all-zero usage, so they should be
|
||||||
|
/// excluded from conversation turns and stats.
|
||||||
|
fn is_synthetic_assistant(value: &serde_json::Value) -> bool {
|
||||||
|
value
|
||||||
|
.get("message")
|
||||||
|
.and_then(|m| m.get("model"))
|
||||||
|
.and_then(|m| m.as_str())
|
||||||
|
.map(|s| s == "<synthetic>")
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_model_capacity_suffix(model: &str) -> Option<u64> {
|
fn parse_model_capacity_suffix(model: &str) -> Option<u64> {
|
||||||
let captures = model_capacity_suffix_regex().captures(model.trim())?;
|
let captures = model_capacity_suffix_regex().captures(model.trim())?;
|
||||||
let value = captures.get(1)?.as_str().parse::<f64>().ok()?;
|
let value = captures.get(1)?.as_str().parse::<f64>().ok()?;
|
||||||
@@ -236,6 +249,11 @@ impl ClaudeParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if msg_type == "user" || msg_type == "assistant" {
|
if msg_type == "user" || msg_type == "assistant" {
|
||||||
|
// Skip synthetic assistant placeholders for local commands
|
||||||
|
if msg_type == "assistant" && is_synthetic_assistant(&value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
message_count += 1;
|
message_count += 1;
|
||||||
|
|
||||||
// Extract model from assistant messages
|
// Extract model from assistant messages
|
||||||
@@ -449,6 +467,10 @@ impl ClaudeParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match msg_type {
|
match msg_type {
|
||||||
|
"assistant" if is_synthetic_assistant(&value) => {
|
||||||
|
// Skip synthetic assistant placeholders for local commands
|
||||||
|
continue;
|
||||||
|
}
|
||||||
"user" => {
|
"user" => {
|
||||||
let content = extract_user_content(&value);
|
let content = extract_user_content(&value);
|
||||||
|
|
||||||
@@ -1036,6 +1058,103 @@ mod tests {
|
|||||||
assert_eq!(resolved, PathBuf::from("/Users/default/.claude"));
|
assert_eq!(resolved, PathBuf::from("/Users/default/.claude"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthetic_assistant_excluded_from_detail() {
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"codeg-claude-synthetic-{}.jsonl",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
let mut file = fs::File::create(&path).expect("create temp jsonl");
|
||||||
|
// Normal user message
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
"{}",
|
||||||
|
json!({
|
||||||
|
"type": "user",
|
||||||
|
"sessionId": "synth-test",
|
||||||
|
"timestamp": "2026-03-01T10:00:00Z",
|
||||||
|
"uuid": "u1",
|
||||||
|
"cwd": "/tmp/demo",
|
||||||
|
"message": {
|
||||||
|
"content": [{"type": "text", "text": "hello"}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Normal assistant message with real usage
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
"{}",
|
||||||
|
json!({
|
||||||
|
"type": "assistant",
|
||||||
|
"sessionId": "synth-test",
|
||||||
|
"timestamp": "2026-03-01T10:00:02Z",
|
||||||
|
"uuid": "a1",
|
||||||
|
"message": {
|
||||||
|
"model": "claude-sonnet-4-6",
|
||||||
|
"content": [{"type": "text", "text": "world"}],
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 1000,
|
||||||
|
"output_tokens": 200,
|
||||||
|
"cache_creation_input_tokens": 300,
|
||||||
|
"cache_read_input_tokens": 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Synthetic assistant from a local command like /context
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
"{}",
|
||||||
|
json!({
|
||||||
|
"type": "assistant",
|
||||||
|
"sessionId": "synth-test",
|
||||||
|
"timestamp": "2026-03-01T10:01:00Z",
|
||||||
|
"uuid": "a2",
|
||||||
|
"message": {
|
||||||
|
"model": "<synthetic>",
|
||||||
|
"content": [{"type": "text", "text": "No response requested."}],
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 0,
|
||||||
|
"output_tokens": 0,
|
||||||
|
"cache_creation_input_tokens": 0,
|
||||||
|
"cache_read_input_tokens": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let parser = ClaudeParser {
|
||||||
|
base_dir: PathBuf::new(),
|
||||||
|
};
|
||||||
|
let detail = parser
|
||||||
|
.parse_conversation_detail(&path, "synth-test")
|
||||||
|
.expect("parse detail");
|
||||||
|
fs::remove_file(&path).unwrap();
|
||||||
|
|
||||||
|
// Should have 2 turns (user + real assistant), synthetic is excluded
|
||||||
|
assert_eq!(detail.turns.len(), 2);
|
||||||
|
assert!(
|
||||||
|
!detail
|
||||||
|
.turns
|
||||||
|
.iter()
|
||||||
|
.any(|t| t.blocks.iter().any(|b| matches!(
|
||||||
|
b,
|
||||||
|
ContentBlock::Text { text } if text == "No response requested."
|
||||||
|
))),
|
||||||
|
"synthetic assistant content should not appear in turns"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stats should reflect only the real assistant usage
|
||||||
|
let stats = detail.session_stats.expect("session stats");
|
||||||
|
assert_eq!(stats.context_window_used_tokens, Some(1700));
|
||||||
|
assert_eq!(stats.context_window_max_tokens, Some(200_000));
|
||||||
|
let total = stats.total_tokens.expect("total tokens");
|
||||||
|
assert_eq!(total, 1900); // 1000 + 200 + 300 + 400
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extract_user_content_parses_claude_base64_image_block() {
|
fn extract_user_content_parses_claude_base64_image_block() {
|
||||||
let value = json!({
|
let value = json!({
|
||||||
|
|||||||
@@ -65,8 +65,12 @@ export function StatusBarTokens() {
|
|||||||
getConnSnapshot
|
getConnSnapshot
|
||||||
)
|
)
|
||||||
|
|
||||||
const liveContextUsed = activeConn?.usage?.used ?? null
|
const rawLiveUsed = activeConn?.usage?.used ?? null
|
||||||
const liveContextMax = activeConn?.usage?.size ?? null
|
const rawLiveSize = activeConn?.usage?.size ?? null
|
||||||
|
// Treat live used=0 as "no data" so we fall back to sessionStats —
|
||||||
|
// Claude Code sends used=0 for synthetic local commands (/context etc.)
|
||||||
|
const liveContextUsed = rawLiveUsed != null && rawLiveUsed > 0 ? rawLiveUsed : null
|
||||||
|
const liveContextMax = rawLiveSize != null && rawLiveSize > 0 ? rawLiveSize : null
|
||||||
|
|
||||||
const contextUsed =
|
const contextUsed =
|
||||||
liveContextUsed ?? sessionStats?.context_window_used_tokens ?? null
|
liveContextUsed ?? sessionStats?.context_window_used_tokens ?? null
|
||||||
|
|||||||
@@ -975,6 +975,12 @@ function connectionsReducer(
|
|||||||
case "USAGE_UPDATE": {
|
case "USAGE_UPDATE": {
|
||||||
const conn = state.get(action.contextKey)
|
const conn = state.get(action.contextKey)
|
||||||
if (!conn) return state
|
if (!conn) return state
|
||||||
|
// Ignore usage updates that reset used to 0 when we already have
|
||||||
|
// valid data — these come from synthetic responses for local commands
|
||||||
|
// like /context and would overwrite the real context window usage.
|
||||||
|
if (action.usage.used === 0 && conn.usage && conn.usage.used > 0) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
conn.usage?.used === action.usage.used &&
|
conn.usage?.used === action.usage.used &&
|
||||||
conn.usage?.size === action.usage.size
|
conn.usage?.size === action.usage.size
|
||||||
|
|||||||
Reference in New Issue
Block a user