fix: route osc 8 file links to plugin handlers (#2942)

refs #2941

Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
This commit is contained in:
akbash
2026-08-25 22:02:37 +03:00
committed by GitHub
co-authored by akbash-bot
parent 6e8b138d0f
commit 9ff467bda4
4 changed files with 65 additions and 6 deletions
+1
View File
@@ -23,6 +23,7 @@
- Claude Code panes now remain working while MCP tasks continue in the background after a turn ends. (#3090)
- Tab bar status commands now remove ESC-prefixed terminal control sequences instead of displaying their sequence bodies as text. (#3001)
- Unix plugin pane commands now default `PWD` to their resolved working directory, so direct popup tools open at explicit `--cwd` paths while preserving caller-provided `PWD` values. (#2984)
- Plugin link handlers now receive matching OSC 8 `file://` clicks while unmatched file links remain excluded from the system URL opener. (#2941)
## [0.8.2] - 2026-08-19
+1 -1
View File
@@ -2245,7 +2245,7 @@ impl AppState {
.into_iter()
.find(|((x, y), _, _)| *x == screen_col && *y == screen_row)
{
return safe_web_url(&uri).map(str::to_owned);
return Some(uri);
}
let metrics = self.pane_scroll_metrics(terminal_runtimes, pane_id);
+12 -5
View File
@@ -595,14 +595,21 @@ impl App {
return false;
};
self.last_pane_click = None;
self.pending_url_click_sources.insert(source_id);
match self.invoke_plugin_link_handler_for_url(&url, info.id) {
Ok(true) => return true,
Ok(false) => {}
let plugin_handled = match self.invoke_plugin_link_handler_for_url(&url, info.id) {
Ok(handled) => handled,
Err(err) => {
tracing::warn!(err = %err, url = %url, "failed to invoke plugin link handler");
false
}
};
if !plugin_handled && crate::app::actions::safe_web_url(&url).is_none() {
return false;
}
self.last_pane_click = None;
self.pending_url_click_sources.insert(source_id);
if plugin_handled {
return true;
}
match open_url(&url) {
Ok(Some(child)) => self.detached_process_children.push(child),
+51
View File
@@ -1042,6 +1042,57 @@ mod tests {
);
}
#[cfg(unix)]
#[tokio::test]
async fn ctrl_click_osc8_file_url_invokes_plugin_link_handler() {
let uri = "file:///tmp/herdr-file-repro.txt";
let screen = format!("\x1b]8;;{uri}\x1b\\FILE\x1b]8;;\x1b\\");
let (mut app, info) = app_with_screen_bytes(screen.as_bytes());
install_test_link_handler(&mut app);
app.state
.installed_plugins
.get_mut("example.links")
.expect("test plugin")
.link_handlers[0]
.pattern = r"^file:///tmp/herdr-file-repro\.txt$".into();
let handled = app.handle_modified_url_click_with(
41,
modified_mouse(
MouseEventKind::Down(MouseButton::Left),
info.inner_rect.x + 1,
info.inner_rect.y,
KeyModifiers::CONTROL,
),
|_| panic!("matched file link should not use the system URL opener"),
);
assert!(handled);
let log = app
.state
.plugin_command_logs
.last()
.expect("ctrl-click should start plugin link handler");
assert_eq!(log.plugin_id, "example.links");
assert_eq!(log.action_id.as_deref(), Some("open"));
let (mut unmatched_app, unmatched_info) = app_with_screen_bytes(screen.as_bytes());
install_test_link_handler(&mut unmatched_app);
let unmatched_handled = unmatched_app.handle_modified_url_click_with(
42,
modified_mouse(
MouseEventKind::Down(MouseButton::Left),
unmatched_info.inner_rect.x + 1,
unmatched_info.inner_rect.y,
KeyModifiers::CONTROL,
),
|_| panic!("unmatched file link should not use the system URL opener"),
);
assert!(!unmatched_handled);
assert!(unmatched_app.state.plugin_command_logs.is_empty());
}
#[cfg(unix)]
#[tokio::test]
async fn ctrl_click_url_invokes_plugin_link_handler_but_super_click_does_not() {