Skip to content

Commit 9abbc75

Browse files
committed
Fix remote session context working directory
1 parent cf92d18 commit 9abbc75

3 files changed

Lines changed: 135 additions & 0 deletions

File tree

src/agent/provider.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ impl Agent {
120120
return;
121121
}
122122
self.session.working_dir = Some(dir.to_string());
123+
self.session.refresh_initial_session_context_message();
123124
self.log_env_snapshot("working_dir");
124125
}
125126

src/session.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,44 @@ impl Session {
808808
true
809809
}
810810

811+
/// Refresh the initial immutable session-context message if the session has
812+
/// not started a real conversation yet. This covers remote/client-server
813+
/// startup where the server creates an Agent before the subscribing client
814+
/// sends the terminal working directory that tools will use.
815+
pub fn refresh_initial_session_context_message(&mut self) -> bool {
816+
if self.messages.iter().any(is_visible_conversation_message) {
817+
return false;
818+
}
819+
820+
let Some(message) = self.messages.iter_mut().find(|message| {
821+
message.content.iter().any(|block| match block {
822+
ContentBlock::Text { text, .. } => text.starts_with(SESSION_CONTEXT_PREFIX),
823+
_ => false,
824+
})
825+
}) else {
826+
return false;
827+
};
828+
829+
let context =
830+
crate::prompt::build_session_context(self.working_dir.as_deref().map(Path::new));
831+
let wrapped = format!("<system-reminder>\n{}\n</system-reminder>", context.trim());
832+
for block in &mut message.content {
833+
if let ContentBlock::Text { text, .. } = block
834+
&& text.starts_with(SESSION_CONTEXT_PREFIX)
835+
{
836+
if *text == wrapped {
837+
return false;
838+
}
839+
*text = wrapped;
840+
self.mark_memory_profile_dirty();
841+
self.mark_messages_full_dirty();
842+
return true;
843+
}
844+
}
845+
846+
false
847+
}
848+
811849
/// Get the display name for this session (short memorable name if available)
812850
pub fn display_name(&self) -> &str {
813851
self.short_name

src/session_tests/cases.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,102 @@ fn initial_session_context_uses_current_cwd_when_inserted() -> Result<()> {
167167
Ok(())
168168
}
169169

170+
#[test]
171+
fn initial_session_context_can_refresh_before_real_conversation() -> Result<()> {
172+
let _env_lock = lock_env();
173+
let original_cwd = std::env::current_dir().map_err(|e| anyhow!(e))?;
174+
let first_dir = tempfile::Builder::new()
175+
.prefix("jcode-session-context-stale-")
176+
.tempdir()
177+
.map_err(|e| anyhow!(e))?;
178+
let second_dir = tempfile::Builder::new()
179+
.prefix("jcode-session-context-real-")
180+
.tempdir()
181+
.map_err(|e| anyhow!(e))?;
182+
183+
std::env::set_current_dir(first_dir.path()).map_err(|e| anyhow!(e))?;
184+
let result: std::result::Result<(), anyhow::Error> = (|| {
185+
let mut session = Session::create_with_id(
186+
"session_context_remote_cwd_refresh_test".to_string(),
187+
None,
188+
Some("Remote cwd refresh".to_string()),
189+
);
190+
assert!(session.ensure_initial_session_context_message());
191+
assert!(session.messages[0].content_preview().contains(&format!(
192+
"Working directory: {}",
193+
first_dir.path().display()
194+
)));
195+
196+
session.working_dir = Some(second_dir.path().display().to_string());
197+
assert!(session.refresh_initial_session_context_message());
198+
let refreshed = session.messages[0].content_preview();
199+
assert!(
200+
refreshed.contains(&format!(
201+
"Working directory: {}",
202+
second_dir.path().display()
203+
)),
204+
"session context should refresh to subscribed cwd, got: {refreshed}"
205+
);
206+
assert!(!refreshed.contains(&format!(
207+
"Working directory: {}",
208+
first_dir.path().display()
209+
)));
210+
Ok(())
211+
})();
212+
std::env::set_current_dir(original_cwd).map_err(|e| anyhow!(e))?;
213+
result?;
214+
215+
Ok(())
216+
}
217+
218+
#[test]
219+
fn initial_session_context_does_not_refresh_after_real_conversation() -> Result<()> {
220+
let _env_lock = lock_env();
221+
let original_cwd = std::env::current_dir().map_err(|e| anyhow!(e))?;
222+
let first_dir = tempfile::Builder::new()
223+
.prefix("jcode-session-context-original-")
224+
.tempdir()
225+
.map_err(|e| anyhow!(e))?;
226+
let second_dir = tempfile::Builder::new()
227+
.prefix("jcode-session-context-late-")
228+
.tempdir()
229+
.map_err(|e| anyhow!(e))?;
230+
231+
std::env::set_current_dir(first_dir.path()).map_err(|e| anyhow!(e))?;
232+
let result: std::result::Result<(), anyhow::Error> = (|| {
233+
let mut session = Session::create_with_id(
234+
"session_context_late_cwd_refresh_test".to_string(),
235+
None,
236+
Some("Late cwd refresh".to_string()),
237+
);
238+
assert!(session.ensure_initial_session_context_message());
239+
session.add_message(
240+
Role::User,
241+
vec![ContentBlock::Text {
242+
text: "hello".to_string(),
243+
cache_control: None,
244+
}],
245+
);
246+
247+
session.working_dir = Some(second_dir.path().display().to_string());
248+
assert!(!session.refresh_initial_session_context_message());
249+
let original = session.messages[0].content_preview();
250+
assert!(original.contains(&format!(
251+
"Working directory: {}",
252+
first_dir.path().display()
253+
)));
254+
assert!(!original.contains(&format!(
255+
"Working directory: {}",
256+
second_dir.path().display()
257+
)));
258+
Ok(())
259+
})();
260+
std::env::set_current_dir(original_cwd).map_err(|e| anyhow!(e))?;
261+
result?;
262+
263+
Ok(())
264+
}
265+
170266
#[test]
171267
fn existing_non_empty_session_does_not_get_retroactive_session_context() {
172268
let mut session = Session::create_with_id(

0 commit comments

Comments
 (0)