generated from nhcarrigan/template
feat: consume worktree field from status line hook events
Parse structured WorktreeInfo (name, path, branch, original_repo_directory) from WorktreeCreate/Remove hook events and emit a dedicated claude:worktree event. Store per-conversation worktree state and display an emerald branch badge in the status bar so users can see at a glance which worktree and branch each session is running on. Closes #206
This commit is contained in:
+118
-15
@@ -17,7 +17,7 @@ use crate::types::{
|
||||
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
|
||||
ConnectionStatus, ContentBlock, MessageCost, OutputEvent, PermissionPromptEvent,
|
||||
PermissionPromptEventItem, QuestionOption, SessionEvent, StateChangeEvent, TodoItem,
|
||||
TodoUpdateEvent, UserQuestionEvent, WorkingDirectoryEvent,
|
||||
TodoUpdateEvent, UserQuestionEvent, WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use std::cell::RefCell;
|
||||
@@ -956,9 +956,10 @@ fn handle_stderr(
|
||||
}
|
||||
|
||||
// Hook events are informational — emit with distinct types instead of error
|
||||
let line_type = if line.contains("[WorktreeCreate Hook]")
|
||||
|| line.contains("[WorktreeRemove Hook]")
|
||||
{
|
||||
let is_worktree_create = line.contains("[WorktreeCreate Hook]");
|
||||
let is_worktree_remove = line.contains("[WorktreeRemove Hook]");
|
||||
|
||||
let line_type = if is_worktree_create || is_worktree_remove {
|
||||
"worktree"
|
||||
} else if line.contains("[ConfigChange Hook]") {
|
||||
"config-change"
|
||||
@@ -966,17 +967,56 @@ fn handle_stderr(
|
||||
"error"
|
||||
};
|
||||
|
||||
let _ = app.emit(
|
||||
"claude:output",
|
||||
OutputEvent {
|
||||
line_type: line_type.to_string(),
|
||||
content: line,
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None,
|
||||
parent_tool_use_id: None,
|
||||
},
|
||||
);
|
||||
// For worktree hooks, parse structured data and emit a dedicated event
|
||||
if is_worktree_create || is_worktree_remove {
|
||||
let worktree_info = parse_worktree_hook(&line);
|
||||
let event_type = if is_worktree_create { "create" } else { "remove" };
|
||||
let friendly_content = if let Some(ref info) = worktree_info {
|
||||
if is_worktree_create {
|
||||
format!(
|
||||
"Worktree created: {} (branch: {}) at {}",
|
||||
info.name, info.branch, info.path
|
||||
)
|
||||
} else {
|
||||
format!("Worktree removed: {} (branch: {})", info.name, info.branch)
|
||||
}
|
||||
} else {
|
||||
line.clone()
|
||||
};
|
||||
|
||||
let _ = app.emit(
|
||||
"claude:worktree",
|
||||
WorktreeEvent {
|
||||
conversation_id: conversation_id.clone(),
|
||||
event_type: event_type.to_string(),
|
||||
worktree: worktree_info,
|
||||
},
|
||||
);
|
||||
|
||||
let _ = app.emit(
|
||||
"claude:output",
|
||||
OutputEvent {
|
||||
line_type: "worktree".to_string(),
|
||||
content: friendly_content,
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None,
|
||||
parent_tool_use_id: None,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let _ = app.emit(
|
||||
"claude:output",
|
||||
OutputEvent {
|
||||
line_type: line_type.to_string(),
|
||||
content: line,
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
cost: None,
|
||||
parent_tool_use_id: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
_ => {}
|
||||
@@ -991,6 +1031,29 @@ struct SubagentStartData {
|
||||
parent_tool_use_id: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_worktree_hook(line: &str) -> Option<WorktreeInfo> {
|
||||
// Parse: [WorktreeCreate/Remove Hook] name=worktree-abc, path=/tmp/worktrees/worktree-abc,
|
||||
// branch=feat/my-feature, original_repo_directory=/home/naomi/code/project, session_id=xxx
|
||||
|
||||
let extract = |key: &str| -> Option<String> {
|
||||
let after_key = line.split(&format!("{}=", key)).nth(1)?;
|
||||
let value = after_key.split(',').next()?.trim().to_string();
|
||||
if value.is_empty() { None } else { Some(value) }
|
||||
};
|
||||
|
||||
let name = extract("name")?;
|
||||
let path = extract("path")?;
|
||||
let branch = extract("branch")?;
|
||||
let original_repo_directory = extract("original_repo_directory")?;
|
||||
|
||||
Some(WorktreeInfo {
|
||||
name,
|
||||
path,
|
||||
branch,
|
||||
original_repo_directory,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_subagent_start_hook(line: &str) -> Option<SubagentStartData> {
|
||||
// Parse: [SubagentStart Hook] agent_id=agent-xxx, agent_type=general-purpose, parent_tool_use_id=Some("toolu_xxx"), ...
|
||||
|
||||
@@ -2913,4 +2976,44 @@ mod tests {
|
||||
*pending_since.lock() = None;
|
||||
assert!(pending_since.lock().is_none(), "pending_since cleared on Result");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_worktree_hook_create_with_all_fields() {
|
||||
let line = r#"[WorktreeCreate Hook] name=worktree-abc, path=/tmp/worktrees/worktree-abc, branch=feat/my-feature, original_repo_directory=/home/naomi/code/project, session_id=123"#;
|
||||
let result = parse_worktree_hook(line);
|
||||
|
||||
assert!(result.is_some());
|
||||
let info = result.unwrap();
|
||||
assert_eq!(info.name, "worktree-abc");
|
||||
assert_eq!(info.path, "/tmp/worktrees/worktree-abc");
|
||||
assert_eq!(info.branch, "feat/my-feature");
|
||||
assert_eq!(info.original_repo_directory, "/home/naomi/code/project");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_worktree_hook_remove_with_all_fields() {
|
||||
let line = r#"[WorktreeRemove Hook] name=worktree-xyz, path=/tmp/worktrees/worktree-xyz, branch=fix/bug-123, original_repo_directory=/home/naomi/code/other, session_id=456"#;
|
||||
let result = parse_worktree_hook(line);
|
||||
|
||||
assert!(result.is_some());
|
||||
let info = result.unwrap();
|
||||
assert_eq!(info.name, "worktree-xyz");
|
||||
assert_eq!(info.branch, "fix/bug-123");
|
||||
assert_eq!(info.original_repo_directory, "/home/naomi/code/other");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_worktree_hook_missing_field_returns_none() {
|
||||
// Missing branch field — should return None
|
||||
let line = r#"[WorktreeCreate Hook] name=worktree-abc, path=/tmp/worktrees/worktree-abc, original_repo_directory=/home/naomi/code/project, session_id=123"#;
|
||||
let result = parse_worktree_hook(line);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_worktree_hook_invalid_returns_none() {
|
||||
let line = "[WorktreeCreate Hook] no structured data here";
|
||||
let result = parse_worktree_hook(line);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user