优化上下文事件更新的处理逻辑
This commit is contained in:
@@ -58,6 +58,19 @@ fn is_meta_message(value: &serde_json::Value) -> bool {
|
||||
.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> {
|
||||
let captures = model_capacity_suffix_regex().captures(model.trim())?;
|
||||
let value = captures.get(1)?.as_str().parse::<f64>().ok()?;
|
||||
@@ -236,6 +249,11 @@ impl ClaudeParser {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Extract model from assistant messages
|
||||
@@ -449,6 +467,10 @@ impl ClaudeParser {
|
||||
}
|
||||
|
||||
match msg_type {
|
||||
"assistant" if is_synthetic_assistant(&value) => {
|
||||
// Skip synthetic assistant placeholders for local commands
|
||||
continue;
|
||||
}
|
||||
"user" => {
|
||||
let content = extract_user_content(&value);
|
||||
|
||||
@@ -1036,6 +1058,103 @@ mod tests {
|
||||
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]
|
||||
fn extract_user_content_parses_claude_base64_image_block() {
|
||||
let value = json!({
|
||||
|
||||
@@ -65,8 +65,12 @@ export function StatusBarTokens() {
|
||||
getConnSnapshot
|
||||
)
|
||||
|
||||
const liveContextUsed = activeConn?.usage?.used ?? null
|
||||
const liveContextMax = activeConn?.usage?.size ?? null
|
||||
const rawLiveUsed = activeConn?.usage?.used ?? 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 =
|
||||
liveContextUsed ?? sessionStats?.context_window_used_tokens ?? null
|
||||
|
||||
@@ -975,6 +975,12 @@ function connectionsReducer(
|
||||
case "USAGE_UPDATE": {
|
||||
const conn = state.get(action.contextKey)
|
||||
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 (
|
||||
conn.usage?.used === action.usage.used &&
|
||||
conn.usage?.size === action.usage.size
|
||||
|
||||
Reference in New Issue
Block a user