diff --git a/bin/tact/src/tui/mod.rs b/bin/tact/src/tui/mod.rs index 6945a72..d924c75 100644 --- a/bin/tact/src/tui/mod.rs +++ b/bin/tact/src/tui/mod.rs @@ -502,7 +502,11 @@ pub(crate) async fn run( 1, ) }; + let workspace = config.agent().workspace().to_path_buf(); let mut terminal = TerminalSession::enter().map_err(RuntimeError::Terminal)?; + terminal + .report_working_directory(&workspace) + .map_err(RuntimeError::Terminal)?; let ConfiguredAgent { agent, events, @@ -541,7 +545,6 @@ pub(crate) async fn run( worker::MemoryReviewState::fresh(memory_enabled) }; let (commands, mut worker_updates) = worker::spawn(agent, memory_review, shutdown.clone()); - let workspace = config.agent().workspace().to_path_buf(); let (agent_event_sender, mut agent_events) = mpsc::unbounded_channel(); agent_events::forward(PaneId::Main, 0, events, agent_event_sender.clone()); let (subagent_sender, mut subagent_events) = mpsc::unbounded_channel(); @@ -775,7 +778,14 @@ pub(crate) async fn run( continue; } let record = runtime.journal_mut()?.append_agent(event)?; + // `tool.result` is the canonical completion event for every agent tool. + let tool_finished = record.kind() == "tool.result"; apply_app_update!(app.update(AppEvent::Transcript { pane, record })); + if tool_finished { + terminal + .report_working_directory(&workspace) + .map_err(RuntimeError::Terminal)?; + } } ForwardedAgentEvent::Closed { pane, session_id, generation } => { let mut stream_closed = false; @@ -1173,6 +1183,9 @@ pub(crate) async fn run( }, if editor_task.is_some() && !stopping => { editor_task = None; terminal.resume().map_err(RuntimeError::Terminal)?; + terminal + .report_working_directory(&workspace) + .map_err(RuntimeError::Terminal)?; app.refresh_terminal_images(); input = Some(EventStream::new()); match result.map_err(RuntimeError::ExternalEditorTask)?? { diff --git a/bin/tact/src/tui/terminal.rs b/bin/tact/src/tui/terminal.rs index 88f1e1d..b5b1c15 100644 --- a/bin/tact/src/tui/terminal.rs +++ b/bin/tact/src/tui/terminal.rs @@ -23,6 +23,7 @@ use ratatui::{ use std::{ io::{self, IsTerminal, Stdout, Write, stdin, stdout}, panic, + path::Path, sync::{ Once, atomic::{AtomicBool, Ordering}, @@ -251,6 +252,10 @@ impl TerminalSession { copy_to_clipboard(self.terminal.backend_mut(), text) } + pub(crate) fn report_working_directory(&mut self, path: &Path) -> io::Result<()> { + write_osc7_working_directory(self.terminal.backend_mut(), path) + } + pub(crate) fn suspend(&mut self) -> io::Result<()> { if !self.active { return Ok(()); @@ -288,6 +293,14 @@ fn copy_to_clipboard(output: &mut impl Write, text: &str) -> io::Result<()> { execute!(output, CopyToClipboard::to_clipboard_from(text)) } +fn write_osc7_working_directory(output: &mut impl Write, path: &Path) -> io::Result<()> { + let uri = url::Url::from_directory_path(path).map_err(|()| { + io::Error::new(io::ErrorKind::InvalidInput, "working directory is relative") + })?; + write!(output, "\x1b]7;{uri}\x1b\\")?; + output.flush() +} + impl Drop for TerminalSession { fn drop(&mut self) { if !self.active { @@ -371,6 +384,7 @@ mod tests { use super::{ MeasuredBackend, StableCursorBackend, activate_commands, begin_synchronized_update, copy_to_clipboard, end_synchronized_update, reset_after_resume, restore_commands, + write_osc7_working_directory, }; use crate::{ app::config::ReasoningEffort, @@ -424,6 +438,15 @@ mod tests { assert_eq!(output, b"\x1b]52;c;Y29weSBtZQ==\x1b\\"); } + #[test] + fn osc7_working_directory_uses_an_encoded_file_uri() { + let mut output = Vec::new(); + + write_osc7_working_directory(&mut output, Path::new("/work/with space")).unwrap(); + + assert_eq!(output, b"\x1b]7;file:///work/with%20space/\x1b\\"); + } + #[test] fn unchanged_frames_produce_zero_ratatui_diff_cells() { let backend = MeasuredBackend {