feat: add PreCompact hook support (#267)

Parses [PreCompact Hook] events from the CLI stream and emits a claude:pre-compact Tauri event. Shows a "Compacting context..." toast and thinking state on the frontend, complementing the existing PostCompact handling.
This commit is contained in:
2026-05-06 14:18:57 -07:00
committed by Naomi Carrigan
parent 38692391e0
commit f3a2f8a491
4 changed files with 101 additions and 2 deletions
+32
View File
@@ -330,6 +330,14 @@ pub struct PostCompactEvent {
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreCompactEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CwdChangedEvent {
pub cwd: String,
@@ -790,6 +798,30 @@ mod tests {
assert!(!serialized.contains("conversation_id"));
}
#[test]
fn test_pre_compact_event_serialization() {
let event = PreCompactEvent {
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_pre_compact_event_omits_none_fields() {
let event = PreCompactEvent {
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_cwd_changed_event_serialization() {
let event = CwdChangedEvent {
+57 -2
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,
};
@@ -1173,6 +1173,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]");
@@ -1187,7 +1188,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"
@@ -1313,6 +1314,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);
@@ -1656,6 +1679,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>,
@@ -3920,6 +3953,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""#;