mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
* feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations
411 lines
16 KiB
Rust
411 lines
16 KiB
Rust
use crate::api::schema::{EventData, PluginInvocationContext};
|
|
use crate::app::App;
|
|
|
|
impl App {
|
|
pub(super) fn merge_plugin_context(
|
|
&self,
|
|
provided: Option<PluginInvocationContext>,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
let mut context = self.current_plugin_context(correlation_id);
|
|
if let Some(provided) = provided {
|
|
context.workspace_id = provided.workspace_id.or(context.workspace_id);
|
|
context.workspace_label = provided.workspace_label.or(context.workspace_label);
|
|
context.workspace_cwd = provided.workspace_cwd.or(context.workspace_cwd);
|
|
context.worktree = provided.worktree.or(context.worktree);
|
|
context.tab_id = provided.tab_id.or(context.tab_id);
|
|
context.tab_label = provided.tab_label.or(context.tab_label);
|
|
context.focused_pane_id = provided.focused_pane_id.or(context.focused_pane_id);
|
|
context.focused_pane_cwd = provided.focused_pane_cwd.or(context.focused_pane_cwd);
|
|
context.focused_pane_agent = provided.focused_pane_agent.or(context.focused_pane_agent);
|
|
context.focused_pane_status =
|
|
provided.focused_pane_status.or(context.focused_pane_status);
|
|
context.selected_text = provided.selected_text.or(context.selected_text);
|
|
context.invocation_source = provided.invocation_source.or(context.invocation_source);
|
|
context.correlation_id = provided.correlation_id.or(context.correlation_id);
|
|
context.clicked_url = provided.clicked_url.or(context.clicked_url);
|
|
context.link_handler_id = provided.link_handler_id.or(context.link_handler_id);
|
|
}
|
|
context
|
|
}
|
|
|
|
pub(super) fn current_plugin_context(&self, correlation_id: &str) -> PluginInvocationContext {
|
|
let Some(ws_idx) = self.state.active else {
|
|
return empty_plugin_context(correlation_id);
|
|
};
|
|
self.plugin_context_for_workspace(ws_idx, correlation_id)
|
|
}
|
|
|
|
pub(super) fn plugin_context_for_event(
|
|
&self,
|
|
event: &crate::api::schema::EventEnvelope,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
match &event.data {
|
|
EventData::WorkspaceCreated { workspace }
|
|
| EventData::WorkspaceUpdated { workspace }
|
|
| EventData::WorkspaceMetadataUpdated { workspace }
|
|
| EventData::WorktreeCreated { workspace, .. }
|
|
| EventData::WorktreeOpened { workspace, .. } => {
|
|
self.plugin_context_for_workspace_info(workspace, correlation_id)
|
|
}
|
|
EventData::WorkspaceClosed {
|
|
workspace_id,
|
|
workspace,
|
|
} => workspace
|
|
.as_ref()
|
|
.map(|workspace| self.plugin_context_for_workspace_info(workspace, correlation_id))
|
|
.unwrap_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
|
|
})
|
|
}),
|
|
EventData::WorkspaceReordered { workspace_ids, .. } => workspace_ids
|
|
.first()
|
|
.and_then(|workspace_id| {
|
|
self.plugin_context_for_workspace_id(workspace_id, correlation_id)
|
|
})
|
|
.unwrap_or_else(|| empty_plugin_context(correlation_id)),
|
|
EventData::WorkspaceRenamed { workspace_id, .. }
|
|
| EventData::WorkspaceMoved { workspace_id, .. }
|
|
| 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,
|
|
workspace_id,
|
|
} => {
|
|
let mut context = empty_plugin_context(correlation_id);
|
|
context.workspace_id = Some(workspace_id.clone());
|
|
context.tab_id = Some(tab_id.clone());
|
|
context
|
|
}
|
|
EventData::TabRenamed {
|
|
tab_id,
|
|
workspace_id,
|
|
..
|
|
}
|
|
| EventData::TabMoved {
|
|
tab_id,
|
|
workspace_id,
|
|
..
|
|
}
|
|
| EventData::TabFocused {
|
|
tab_id,
|
|
workspace_id,
|
|
} => self
|
|
.plugin_context_for_tab_id(tab_id, 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.tab_id = Some(tab_id.clone());
|
|
context
|
|
}),
|
|
EventData::LayoutUpdated { layout } => self
|
|
.plugin_context_for_tab_id(&layout.tab_id, correlation_id)
|
|
.or_else(|| {
|
|
self.plugin_context_for_workspace_id(&layout.workspace_id, correlation_id)
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let mut context = empty_plugin_context(correlation_id);
|
|
context.workspace_id = Some(layout.workspace_id.clone());
|
|
context.tab_id = Some(layout.tab_id.clone());
|
|
context
|
|
}),
|
|
EventData::PaneCreated { pane } | EventData::PaneUpdated { pane } => {
|
|
self.plugin_context_for_pane_info(pane, correlation_id)
|
|
}
|
|
EventData::PaneMoved { pane, .. } => {
|
|
self.plugin_context_for_pane_info(pane.as_ref(), correlation_id)
|
|
}
|
|
EventData::PaneClosed {
|
|
pane_id,
|
|
workspace_id,
|
|
} => {
|
|
let mut context = empty_plugin_context(correlation_id);
|
|
context.workspace_id = Some(workspace_id.clone());
|
|
context.focused_pane_id = Some(pane_id.clone());
|
|
context
|
|
}
|
|
EventData::PaneFocused {
|
|
pane_id,
|
|
workspace_id,
|
|
}
|
|
| EventData::PaneOutputChanged {
|
|
pane_id,
|
|
workspace_id,
|
|
..
|
|
}
|
|
| EventData::PaneExited {
|
|
pane_id,
|
|
workspace_id,
|
|
}
|
|
| EventData::PaneAgentDetected {
|
|
pane_id,
|
|
workspace_id,
|
|
..
|
|
}
|
|
| EventData::PaneAgentStatusChanged {
|
|
pane_id,
|
|
workspace_id,
|
|
..
|
|
} => self
|
|
.plugin_context_for_public_pane_id(pane_id, 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.focused_pane_id = Some(pane_id.clone());
|
|
context
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn plugin_context_for_workspace_id(
|
|
&self,
|
|
workspace_id: &str,
|
|
correlation_id: &str,
|
|
) -> Option<PluginInvocationContext> {
|
|
let ws_idx = self
|
|
.state
|
|
.workspaces
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(idx, _)| (self.public_workspace_id(idx) == workspace_id).then_some(idx))?;
|
|
Some(self.plugin_context_for_workspace(ws_idx, correlation_id))
|
|
}
|
|
|
|
fn plugin_context_for_workspace_info(
|
|
&self,
|
|
workspace: &crate::api::schema::WorkspaceInfo,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
self.plugin_context_for_workspace_id(&workspace.workspace_id, correlation_id)
|
|
.unwrap_or_else(|| {
|
|
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,
|
|
correlation_id: &str,
|
|
) -> Option<PluginInvocationContext> {
|
|
let (ws_idx, tab_idx) = self.parse_tab_id(tab_id)?;
|
|
let ws = self.state.workspaces.get(ws_idx)?;
|
|
let workspace = self.workspace_info(ws_idx);
|
|
let tab = ws.tabs.get(tab_idx)?;
|
|
let pane_id = tab.layout.focused();
|
|
let focused_pane = self.pane_info(ws_idx, pane_id);
|
|
Some(self.plugin_context_from_parts(
|
|
ws_idx,
|
|
workspace,
|
|
self.public_tab_id(ws_idx, tab_idx),
|
|
ws.tab_display_name(tab_idx),
|
|
focused_pane,
|
|
correlation_id,
|
|
))
|
|
}
|
|
|
|
fn plugin_context_for_tab_info(
|
|
&self,
|
|
tab: &crate::api::schema::TabInfo,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
self.plugin_context_for_tab_id(&tab.tab_id, correlation_id)
|
|
.or_else(|| self.plugin_context_for_workspace_id(&tab.workspace_id, correlation_id))
|
|
.unwrap_or_else(|| {
|
|
let mut context = empty_plugin_context(correlation_id);
|
|
context.workspace_id = Some(tab.workspace_id.clone());
|
|
context.tab_id = Some(tab.tab_id.clone());
|
|
context.tab_label = Some(tab.label.clone());
|
|
context
|
|
})
|
|
}
|
|
|
|
fn plugin_context_for_public_pane_id(
|
|
&self,
|
|
pane_id: &str,
|
|
correlation_id: &str,
|
|
) -> Option<PluginInvocationContext> {
|
|
let (ws_idx, pane_id) = self.parse_pane_id(pane_id)?;
|
|
Some(self.plugin_context_for_pane(ws_idx, pane_id, correlation_id))
|
|
}
|
|
|
|
fn plugin_context_for_pane_info(
|
|
&self,
|
|
pane: &crate::api::schema::PaneInfo,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
self.plugin_context_for_public_pane_id(&pane.pane_id, correlation_id)
|
|
.or_else(|| self.plugin_context_for_workspace_id(&pane.workspace_id, correlation_id))
|
|
.unwrap_or_else(|| {
|
|
let mut context = empty_plugin_context(correlation_id);
|
|
context.workspace_id = Some(pane.workspace_id.clone());
|
|
context.tab_id = Some(pane.tab_id.clone());
|
|
context.focused_pane_id = Some(pane.pane_id.clone());
|
|
context.focused_pane_cwd = pane.cwd.clone();
|
|
context.focused_pane_agent = pane.agent.clone();
|
|
context.focused_pane_status = Some(pane.agent_status);
|
|
context
|
|
})
|
|
}
|
|
|
|
pub(super) fn plugin_context_for_workspace(
|
|
&self,
|
|
ws_idx: usize,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
let Some(ws) = self.state.workspaces.get(ws_idx) else {
|
|
return empty_plugin_context(correlation_id);
|
|
};
|
|
let workspace = self.workspace_info(ws_idx);
|
|
let tab_idx = ws.active_tab_index();
|
|
let tab_id = self.public_tab_id(ws_idx, tab_idx);
|
|
let tab_label = ws.tab_display_name(tab_idx);
|
|
let focused_pane = ws
|
|
.focused_pane_id()
|
|
.and_then(|pane_id| self.pane_info(ws_idx, pane_id));
|
|
self.plugin_context_from_parts(
|
|
ws_idx,
|
|
workspace,
|
|
tab_id,
|
|
tab_label,
|
|
focused_pane,
|
|
correlation_id,
|
|
)
|
|
}
|
|
|
|
pub(super) fn plugin_context_for_pane(
|
|
&self,
|
|
ws_idx: usize,
|
|
pane_id: crate::layout::PaneId,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
let ws = &self.state.workspaces[ws_idx];
|
|
let workspace = self.workspace_info(ws_idx);
|
|
let tab_idx = ws
|
|
.find_tab_index_for_pane(pane_id)
|
|
.unwrap_or_else(|| ws.active_tab_index());
|
|
let tab_id = self.public_tab_id(ws_idx, tab_idx);
|
|
let tab_label = ws.tab_display_name(tab_idx);
|
|
let focused_pane = self.pane_info(ws_idx, pane_id);
|
|
self.plugin_context_from_parts(
|
|
ws_idx,
|
|
workspace,
|
|
tab_id,
|
|
tab_label,
|
|
focused_pane,
|
|
correlation_id,
|
|
)
|
|
}
|
|
|
|
fn plugin_context_from_parts(
|
|
&self,
|
|
ws_idx: usize,
|
|
workspace: crate::api::schema::WorkspaceInfo,
|
|
tab_id: Option<String>,
|
|
tab_label: Option<String>,
|
|
focused_pane: Option<crate::api::schema::PaneInfo>,
|
|
correlation_id: &str,
|
|
) -> PluginInvocationContext {
|
|
let workspace_cwd = focused_pane
|
|
.as_ref()
|
|
.and_then(|pane| pane.cwd.clone())
|
|
.or_else(|| Some(self.default_cwd_for_workspace(ws_idx).display().to_string()));
|
|
PluginInvocationContext {
|
|
workspace_id: Some(workspace.workspace_id),
|
|
workspace_label: Some(workspace.label),
|
|
workspace_cwd,
|
|
worktree: workspace.worktree,
|
|
tab_id,
|
|
tab_label,
|
|
focused_pane_id: focused_pane.as_ref().map(|pane| pane.pane_id.clone()),
|
|
focused_pane_cwd: focused_pane.as_ref().and_then(|pane| pane.cwd.clone()),
|
|
focused_pane_agent: focused_pane.as_ref().and_then(|pane| pane.agent.clone()),
|
|
focused_pane_status: focused_pane.as_ref().map(|pane| pane.agent_status),
|
|
// Selection is client presentation state. Client keybindings provide
|
|
// revision-validated coordinates; API callers can provide explicit context.
|
|
selected_text: None,
|
|
invocation_source: Some("api".to_string()),
|
|
correlation_id: Some(correlation_id.to_string()),
|
|
clicked_url: None,
|
|
link_handler_id: None,
|
|
}
|
|
}
|
|
|
|
fn default_cwd_for_workspace(&self, ws_idx: usize) -> std::path::PathBuf {
|
|
self.state
|
|
.workspaces
|
|
.get(ws_idx)
|
|
.and_then(|ws| {
|
|
ws.resolved_identity_cwd_from(&self.state.terminals, &self.terminal_runtimes)
|
|
})
|
|
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| "/".into()))
|
|
}
|
|
}
|
|
|
|
fn empty_plugin_context(correlation_id: &str) -> PluginInvocationContext {
|
|
PluginInvocationContext {
|
|
workspace_id: None,
|
|
workspace_label: None,
|
|
workspace_cwd: None,
|
|
worktree: None,
|
|
tab_id: None,
|
|
tab_label: None,
|
|
focused_pane_id: None,
|
|
focused_pane_cwd: None,
|
|
focused_pane_agent: None,
|
|
focused_pane_status: None,
|
|
selected_text: None,
|
|
invocation_source: Some("api".to_string()),
|
|
correlation_id: Some(correlation_id.to_string()),
|
|
clicked_url: None,
|
|
link_handler_id: None,
|
|
}
|
|
}
|