aboutsummaryrefslogtreecommitdiff
path: root/src/app/api
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-18 16:05:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-18 16:05:43 +0300
commitd74ba8ca9e49d9d94a4e59a0ee0bdbd1128af76e (patch)
tree13473d5a8c3fd137f40e645e9f9eff3081ce3238 /src/app/api
parentb7a504b35e54e68ca7aceb01d21f066124f7a536 (diff)
fix: emit plugin lifecycle events from ui worktree flows
refs #662
Diffstat (limited to 'src/app/api')
-rw-r--r--src/app/api/plugins/context.rs49
-rw-r--r--src/app/api/plugins/mod.rs70
-rw-r--r--src/app/api/tabs.rs15
-rw-r--r--src/app/api/workspaces.rs24
-rw-r--r--src/app/api/worktrees.rs148
5 files changed, 193 insertions, 113 deletions
diff --git a/src/app/api/plugins/context.rs b/src/app/api/plugins/context.rs
index cd7d1818..92d0d4c6 100644
--- a/src/app/api/plugins/context.rs
+++ b/src/app/api/plugins/context.rs
@@ -63,14 +63,31 @@ impl App {
})
}),
EventData::WorkspaceRenamed { workspace_id, .. }
- | EventData::WorkspaceFocused { workspace_id }
- | EventData::WorktreeRemoved { workspace_id, .. } => self
+ | EventData::WorkspaceFocused { workspace_id } => self
.plugin_context_for_workspace_id(workspace_id, correlation_id)
.unwrap_or_else(|| {
let mut context = empty_plugin_context(correlation_id);
context.workspace_id = Some(workspace_id.clone());
context
}),
+ EventData::WorktreeRemoved {
+ workspace_id,
+ workspace,
+ worktree,
+ ..
+ } => workspace
+ .as_ref()
+ .map(|workspace| {
+ self.plugin_context_for_workspace_snapshot(workspace, correlation_id)
+ })
+ .or_else(|| self.plugin_context_for_workspace_id(workspace_id, correlation_id))
+ .unwrap_or_else(|| {
+ let mut context = empty_plugin_context(correlation_id);
+ context.workspace_id = Some(workspace_id.clone());
+ context.workspace_label = Some(worktree.label.clone());
+ context.workspace_cwd = Some(worktree.path.clone());
+ context
+ }),
EventData::TabCreated { tab } => self.plugin_context_for_tab_info(tab, correlation_id),
EventData::TabClosed {
tab_id,
@@ -168,19 +185,27 @@ impl App {
) -> PluginInvocationContext {
self.plugin_context_for_workspace_id(&workspace.workspace_id, correlation_id)
.unwrap_or_else(|| {
- let mut context = empty_plugin_context(correlation_id);
- context.workspace_id = Some(workspace.workspace_id.clone());
- context.workspace_label = Some(workspace.label.clone());
- context.workspace_cwd = workspace
- .worktree
- .as_ref()
- .map(|worktree| worktree.checkout_path.clone());
- context.worktree = workspace.worktree.clone();
- context.tab_id = Some(workspace.active_tab_id.clone());
- context
+ self.plugin_context_for_workspace_snapshot(workspace, correlation_id)
})
}
+ fn plugin_context_for_workspace_snapshot(
+ &self,
+ workspace: &crate::api::schema::WorkspaceInfo,
+ correlation_id: &str,
+ ) -> PluginInvocationContext {
+ let mut context = empty_plugin_context(correlation_id);
+ context.workspace_id = Some(workspace.workspace_id.clone());
+ context.workspace_label = Some(workspace.label.clone());
+ context.workspace_cwd = workspace
+ .worktree
+ .as_ref()
+ .map(|worktree| worktree.checkout_path.clone());
+ context.worktree = workspace.worktree.clone();
+ context.tab_id = Some(workspace.active_tab_id.clone());
+ context
+ }
+
fn plugin_context_for_tab_id(
&self,
tab_id: &str,
diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs
index 8c0e4212..833134b5 100644
--- a/src/app/api/plugins/mod.rs
+++ b/src/app/api/plugins/mod.rs
@@ -1906,6 +1906,76 @@ command = ["sh", "-c", "printf '%s' \"$HERDR_PLUGIN_CONTEXT_JSON\" > {}"]
pane_context.focused_pane_id.as_deref(),
Some(active_public_pane_id.as_str())
);
+
+ app.state.workspaces[0].worktree_space = Some(crate::workspace::WorktreeSpaceMembership {
+ key: "repo-key".into(),
+ label: "herdr".into(),
+ repo_root: "/repo/herdr".into(),
+ checkout_path: "/repo/herdr-issue".into(),
+ is_linked_worktree: true,
+ });
+ let workspace = app.workspace_info(0);
+ let worktree = crate::api::schema::WorktreeInfo {
+ path: "/repo/herdr-issue".into(),
+ branch: Some("worktree/issue".into()),
+ is_bare: false,
+ is_detached: false,
+ is_prunable: false,
+ is_linked_worktree: true,
+ open_workspace_id: None,
+ label: "herdr".into(),
+ };
+ app.state.workspaces[0].worktree_space = Some(crate::workspace::WorktreeSpaceMembership {
+ key: "repo-key".into(),
+ label: "herdr".into(),
+ repo_root: "/repo/herdr".into(),
+ checkout_path: "/repo/herdr-other".into(),
+ is_linked_worktree: true,
+ });
+ let changed_context = app.plugin_context_for_event(
+ &crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::WorktreeRemoved,
+ data: crate::api::schema::EventData::WorktreeRemoved {
+ workspace_id: workspace_id.clone(),
+ workspace: Some(workspace.clone()),
+ worktree: worktree.clone(),
+ forced: true,
+ },
+ },
+ "worktree.removed",
+ );
+ assert_eq!(
+ changed_context
+ .worktree
+ .as_ref()
+ .map(|worktree| worktree.checkout_path.as_str()),
+ Some("/repo/herdr-issue")
+ );
+
+ app.state.workspaces.clear();
+ let removed_context = app.plugin_context_for_event(
+ &crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::WorktreeRemoved,
+ data: crate::api::schema::EventData::WorktreeRemoved {
+ workspace_id: workspace_id.clone(),
+ workspace: Some(workspace),
+ worktree,
+ forced: true,
+ },
+ },
+ "worktree.removed",
+ );
+ assert_eq!(
+ removed_context.workspace_id.as_deref(),
+ Some(workspace_id.as_str())
+ );
+ assert_eq!(
+ removed_context
+ .worktree
+ .as_ref()
+ .map(|worktree| worktree.checkout_path.as_str()),
+ Some("/repo/herdr-issue")
+ );
}
#[cfg(unix)]
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
index 079c2ea4..6c14a330 100644
--- a/src/app/api/tabs.rs
+++ b/src/app/api/tabs.rs
@@ -122,20 +122,7 @@ impl App {
self.state.mode = Mode::Terminal;
}
self.schedule_session_save();
- let tab = self.tab_info(ws_idx, tab_idx).unwrap();
- let root_pane = self
- .root_pane_info(ws_idx, tab_idx)
- .expect("new tab should have a root pane");
- self.emit_event(EventEnvelope {
- event: EventKind::TabCreated,
- data: EventData::TabCreated { tab: tab.clone() },
- });
- self.emit_event(EventEnvelope {
- event: EventKind::PaneCreated,
- data: EventData::PaneCreated {
- pane: root_pane.clone(),
- },
- });
+ self.emit_tab_created_events(ws_idx, tab_idx);
encode_success(
id,
self.tab_created_result(ws_idx, tab_idx)
diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs
index cdfd4ec5..d1d7bf5c 100644
--- a/src/app/api/workspaces.rs
+++ b/src/app/api/workspaces.rs
@@ -63,29 +63,7 @@ impl App {
crate::logging::workspace_renamed(&workspace.id);
}
}
- let workspace = self.workspace_info(index);
- let tab = self
- .tab_info(index, 0)
- .expect("new workspace should have an initial tab");
- let root_pane = self
- .root_pane_info(index, 0)
- .expect("new workspace should have an initial root pane");
- self.emit_event(EventEnvelope {
- event: EventKind::WorkspaceCreated,
- data: EventData::WorkspaceCreated {
- workspace: workspace.clone(),
- },
- });
- self.emit_event(EventEnvelope {
- event: EventKind::TabCreated,
- data: EventData::TabCreated { tab: tab.clone() },
- });
- self.emit_event(EventEnvelope {
- event: EventKind::PaneCreated,
- data: EventData::PaneCreated {
- pane: root_pane.clone(),
- },
- });
+ self.emit_workspace_open_events(index);
encode_success(
id,
self.workspace_created_result(index)
diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs
index 648b571a..abfa0c1e 100644
--- a/src/app/api/worktrees.rs
+++ b/src/app/api/worktrees.rs
@@ -146,15 +146,9 @@ impl App {
self.emit_workspace_open_events(ws_idx);
let worktree = self
- .worktree_info_for_checkout(&source, ws_idx)
+ .worktree_info_for_workspace(ws_idx)
.expect("created worktree workspace should have worktree info");
- self.emit_event(EventEnvelope {
- event: EventKind::WorktreeCreated,
- data: EventData::WorktreeCreated {
- workspace: self.workspace_info(ws_idx),
- worktree: worktree.clone(),
- },
- });
+ self.emit_worktree_created_event(ws_idx, worktree.clone());
encode_success(
id,
ResponseResult::WorktreeCreated {
@@ -252,14 +246,7 @@ impl App {
let tab_idx = self.state.workspaces[ws_idx].active_tab;
let worktree = self.worktree_info_for_entry(&source, entry);
- self.emit_event(EventEnvelope {
- event: EventKind::WorktreeOpened,
- data: EventData::WorktreeOpened {
- workspace: self.workspace_info(ws_idx),
- worktree: worktree.clone(),
- already_open: already_open.is_some(),
- },
- });
+ self.emit_worktree_opened_event(ws_idx, worktree.clone(), already_open.is_some());
encode_success(
id,
ResponseResult::WorktreeOpened {
@@ -355,18 +342,16 @@ impl App {
event: EventKind::WorkspaceClosed,
data: EventData::WorkspaceClosed {
workspace_id: workspace_id.clone(),
- workspace: Some(workspace_snapshot),
+ workspace: Some(workspace_snapshot.clone()),
},
});
}
- self.emit_event(EventEnvelope {
- event: EventKind::WorktreeRemoved,
- data: EventData::WorktreeRemoved {
- workspace_id: workspace_id.clone(),
- worktree,
- forced: params.force,
- },
- });
+ self.emit_worktree_removed_event(
+ workspace_id.clone(),
+ Some(workspace_snapshot),
+ worktree,
+ params.force,
+ );
encode_success(
id,
@@ -653,7 +638,7 @@ impl App {
self.set_worktree_membership(target_ws_idx, membership, emit_update);
}
- fn set_worktree_membership(
+ pub(crate) fn set_worktree_membership(
&mut self,
ws_idx: usize,
membership: crate::workspace::WorktreeSpaceMembership,
@@ -754,27 +739,7 @@ impl App {
}
}
- fn worktree_info_for_checkout(
- &self,
- source: &WorktreeSource,
- ws_idx: usize,
- ) -> Option<WorktreeInfo> {
- let membership = self.state.workspaces.get(ws_idx)?.worktree_space()?;
- let branch = crate::workspace::git_branch(&membership.checkout_path);
- let is_detached = branch.is_none();
- Some(WorktreeInfo {
- path: membership.checkout_path.display().to_string(),
- branch,
- is_bare: false,
- is_detached,
- is_prunable: false,
- is_linked_worktree: membership.is_linked_worktree,
- open_workspace_id: Some(self.public_workspace_id(ws_idx)),
- label: source.repo_name.clone(),
- })
- }
-
- fn worktree_info_for_membership(
+ pub(crate) fn worktree_info_for_membership(
&self,
membership: &crate::workspace::WorktreeSpaceMembership,
open_workspace_id: Option<String>,
@@ -793,7 +758,7 @@ impl App {
}
}
- fn open_workspace_idx_for_checkout(&self, checkout_path: &Path) -> Option<usize> {
+ pub(crate) fn open_workspace_idx_for_checkout(&self, checkout_path: &Path) -> Option<usize> {
let canonical_checkout = crate::worktree::canonical_or_original(checkout_path);
let checkout_key = canonical_checkout.display().to_string();
self.state.workspaces.iter().position(|ws| {
@@ -823,27 +788,59 @@ impl App {
})
}
- fn emit_workspace_open_events(&mut self, ws_idx: usize) {
- let workspace_info = self.workspace_info(ws_idx);
- let Some(tab) = self.tab_info(ws_idx, 0) else {
- return;
- };
- let Some(root_pane) = self.root_pane_info(ws_idx, 0) else {
- return;
- };
+ pub(crate) fn worktree_info_for_workspace(&self, ws_idx: usize) -> Option<WorktreeInfo> {
+ let membership = self.state.workspaces.get(ws_idx)?.worktree_space()?;
+ Some(self.worktree_info_for_membership(membership, Some(self.public_workspace_id(ws_idx))))
+ }
+
+ pub(crate) fn emit_worktree_created_event(&mut self, ws_idx: usize, worktree: WorktreeInfo) {
self.emit_event(EventEnvelope {
- event: EventKind::WorkspaceCreated,
- data: EventData::WorkspaceCreated {
- workspace: workspace_info,
+ event: EventKind::WorktreeCreated,
+ data: EventData::WorktreeCreated {
+ workspace: self.workspace_info(ws_idx),
+ worktree,
},
});
+ }
+
+ pub(crate) fn emit_worktree_opened_for_workspace(&mut self, ws_idx: usize, already_open: bool) {
+ let Some(worktree) = self.worktree_info_for_workspace(ws_idx) else {
+ return;
+ };
+ self.emit_worktree_opened_event(ws_idx, worktree, already_open);
+ }
+
+ fn emit_worktree_opened_event(
+ &mut self,
+ ws_idx: usize,
+ worktree: WorktreeInfo,
+ already_open: bool,
+ ) {
self.emit_event(EventEnvelope {
- event: EventKind::TabCreated,
- data: EventData::TabCreated { tab },
+ event: EventKind::WorktreeOpened,
+ data: EventData::WorktreeOpened {
+ workspace: self.workspace_info(ws_idx),
+ worktree,
+ already_open,
+ },
});
+ }
+
+ pub(crate) fn emit_worktree_removed_event(
+ &mut self,
+ workspace_id: String,
+ workspace: Option<crate::api::schema::WorkspaceInfo>,
+ worktree: WorktreeInfo,
+ forced: bool,
+ ) {
self.emit_event(EventEnvelope {
- event: EventKind::PaneCreated,
- data: EventData::PaneCreated { pane: root_pane },
+ event: EventKind::WorktreeRemoved,
+ data: EventData::WorktreeRemoved {
+ workspace_id,
+ workspace,
+ worktree,
+ forced,
+ },
});
}
@@ -1033,7 +1030,8 @@ mod tests {
.is_linked_worktree
);
assert!(workspace.worktree.unwrap().is_linked_worktree);
- assert!(event_hub.events_after(0).iter().any(|(_, event)| {
+ let events = event_hub.events_after(0);
+ assert!(events.iter().any(|(_, event)| {
matches!(
&event.data,
EventData::WorktreeCreated {
@@ -1044,6 +1042,26 @@ mod tests {
&& event_worktree.is_linked_worktree
)
}));
+ let kinds = events
+ .iter()
+ .map(|(_, event)| event.event)
+ .collect::<Vec<_>>();
+ assert_eq!(
+ kinds
+ .iter()
+ .filter(|event| **event == EventKind::WorktreeCreated)
+ .count(),
+ 1
+ );
+ assert_eq!(
+ &kinds[kinds.len() - 4..],
+ &[
+ EventKind::WorkspaceCreated,
+ EventKind::TabCreated,
+ EventKind::PaneCreated,
+ EventKind::WorktreeCreated,
+ ]
+ );
for (_, runtime) in app.terminal_runtimes.drain() {
runtime.shutdown();
@@ -1674,9 +1692,11 @@ mod tests {
&event.data,
EventData::WorktreeRemoved {
workspace_id,
+ workspace: Some(workspace),
worktree,
forced,
} if workspace_id == &child_id
+ && workspace.workspace_id == child_id
&& worktree.branch.as_deref() == Some("worktree/api-remove-event")
&& worktree.is_linked_worktree
&& worktree.open_workspace_id.is_none()