diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index aa9223d..73a1f57 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -310,6 +310,14 @@ pub struct StopFailureEvent { pub conversation_id: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostCompactEvent { + #[serde(skip_serializing_if = "Option::is_none")] + pub session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub conversation_id: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentStartEvent { pub tool_use_id: String, @@ -697,4 +705,40 @@ mod tests { assert!(!serialized.contains("error_type")); assert!(!serialized.contains("conversation_id")); } + + #[test] + fn test_post_compact_event_serialization() { + let event = PostCompactEvent { + 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_post_compact_event_omits_none_fields() { + let event = PostCompactEvent { + 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_post_compact_event_partial_fields() { + let event = PostCompactEvent { + session_id: Some("sess-xyz".to_string()), + conversation_id: None, + }; + + let serialized = serde_json::to_string(&event).unwrap(); + assert!(serialized.contains("\"session_id\":\"sess-xyz\"")); + assert!(!serialized.contains("conversation_id")); + } } diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 410725a..f8790f4 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -17,8 +17,8 @@ use crate::types::{ AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent, ConnectionStatus, ContentBlock, ElicitationEvent, ElicitationResultEvent, MessageCost, OutputEvent, PermissionPromptEvent, PermissionPromptEventItem, QuestionOption, SessionEvent, - StateChangeEvent, StopFailureEvent, TodoItem, TodoUpdateEvent, UserQuestionEvent, - WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo, + PostCompactEvent, StateChangeEvent, StopFailureEvent, TodoItem, TodoUpdateEvent, + UserQuestionEvent, WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo, }; use parking_lot::RwLock; use std::cell::RefCell; @@ -1055,6 +1055,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_post_compact = line.contains("[PostCompact Hook]"); let line_type = if is_worktree_create || is_worktree_remove { "worktree" @@ -1064,6 +1065,8 @@ fn handle_stderr( "elicitation" } else if is_stop_failure { "error" + } else if is_post_compact { + "compact-prompt" } else { "error" }; @@ -1180,6 +1183,28 @@ fn handle_stderr( parent_tool_use_id: None, }, ); + } else if is_post_compact { + let data = parse_post_compact_hook(&line); + + let _ = app.emit( + "claude:post-compact", + PostCompactEvent { + session_id: data.session_id, + conversation_id: conversation_id.clone(), + }, + ); + + let _ = app.emit( + "claude:output", + OutputEvent { + line_type: "compact-prompt".to_string(), + content: "Context compacted — conversation history has been summarised to free up space.".to_string(), + tool_name: None, + conversation_id: conversation_id.clone(), + cost: None, + parent_tool_use_id: None, + }, + ); } else { let _ = app.emit( "claude:output", @@ -1383,6 +1408,16 @@ fn build_stop_failure_message(data: &StopFailureData) -> String { } } +#[derive(Debug)] +struct PostCompactData { + session_id: Option, +} + +fn parse_post_compact_hook(line: &str) -> PostCompactData { + let session_id = extract_debug_string_value(line, "session_id"); + PostCompactData { session_id } +} + /// Extracts a double-quoted string value from a `key="value"` pair in a hook line. /// Handles escape sequences within the quoted value. fn extract_quoted_value(line: &str, key: &str) -> Option { @@ -3509,6 +3544,28 @@ mod tests { ); } + #[test] + fn test_parse_post_compact_hook_with_session_id() { + let line = + r#"[PostCompact Hook] session_id=Some("sess-abc123"), conversation_id=Some("conv-xyz")"#; + let data = parse_post_compact_hook(line); + assert_eq!(data.session_id, Some("sess-abc123".to_string())); + } + + #[test] + fn test_parse_post_compact_hook_without_session_id() { + let line = "[PostCompact Hook] session_id=None"; + let data = parse_post_compact_hook(line); + assert_eq!(data.session_id, None); + } + + #[test] + fn test_parse_post_compact_hook_empty_line() { + let line = "[PostCompact Hook]"; + let data = parse_post_compact_hook(line); + assert_eq!(data.session_id, None); + } + #[test] fn test_build_stop_failure_message_no_fields() { let data = StopFailureData { diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 6b2a884..ac33c02 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -10,6 +10,7 @@ import type { ConnectionStatus, ElicitationEvent, PermissionPromptEvent, + PostCompactEvent, StopFailureEvent, UserQuestionEvent, } from "$lib/types/messages"; @@ -659,6 +660,12 @@ export async function initializeTauriListeners() { toastStore.addError(message); }); unlisteners.push(stopFailureUnlisten); + + const postCompactUnlisten = await listen("claude:post-compact", () => { + toastStore.addInfo("Context compacted", "🗜️"); + characterState.setTemporaryState("success", 2000); + }); + unlisteners.push(postCompactUnlisten); } export function cleanupTauriListeners() { diff --git a/src/lib/types/messages.ts b/src/lib/types/messages.ts index 0012cab..754f94c 100644 --- a/src/lib/types/messages.ts +++ b/src/lib/types/messages.ts @@ -182,6 +182,11 @@ export interface StopFailureEvent { conversation_id?: string; } +export interface PostCompactEvent { + session_id?: string; + conversation_id?: string; +} + export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error"; export interface Attachment {