Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 124 additions & 39 deletions Cargo.lock

Large diffs are not rendered by default.

35 changes: 21 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@ resolver = "2"
transmutation = { git = "https://github.com/stippi/transmutation", rev = "87f48d18ce3ff61659d7182c1b733eae8ec6f2c9" }
crossterm = { git = "https://github.com/nornagon/crossterm", branch = "nornagon/color-query" }
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }
# gpui-component depends on gpui via an unpinned `git = ".../zed"` dependency.
# The workspace must resolve to the *same* zed commit that gpui-component
# resolves to, otherwise two copies of gpui end up in the graph and cause
# E0053/E0277/E0599 type mismatches. We pin the zed crates to the exact commit
# that the pinned gpui-component revision locks to (see gpui-component's own
# Cargo.lock). At this commit the zed workspace exposes two crates literally
# named `gpui` (a `0.0.0` internal placeholder and the real `0.2.2`), so the
# `gpui` patch must carry `version = "=0.2.2"` to disambiguate.
# When bumping gpui-component, update the `rev` to match gpui-component's
# Cargo.lock, then run `cargo update -p gpui-component -p gpui`.
# gpui-component depends on gpui via a `git = ".../zed"` dependency. On its
# upstream `main` that dependency is UNPINNED, so it floats to zed's branch
# tip. If we pinned our workspace `gpui` to a fixed `?rev=`, that would be a
# different Cargo source than gpui-component's floating one, and two copies of
# gpui would end up in the graph -> E0053/E0277/E0599 type mismatches.
# To get a single copy we consume gpui-component from the `stippi` fork on a
# thin "pin-zed" branch: it is upstream `main` plus one commit that pins the
# zed git deps (gpui/gpui_platform/gpui_web/gpui_macros/reqwest_client) to the
# exact rev below, so both gpui-component and this workspace resolve to the
# identical `git+.../zed?rev=<rev>` source.
# At this zed commit the workspace exposes two crates literally named `gpui`
# (a `0.0.0` internal placeholder and the real `0.2.2`), so the `gpui` patch
# must carry `version = "=0.2.2"` to disambiguate.
# When bumping: rebase the pin-zed commit onto the new gpui-component main,
# set the zed `rev` to whatever that main's Cargo.lock locks gpui to, push the
# branch, then update both `rev`s here and run
# `cargo update -p gpui-component -p gpui`.
# CI builds with `--locked`. See .github/workflows/release.yml.
gpui = { git = "https://github.com/zed-industries/zed", rev = "1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba", version = "=0.2.2" }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" }
gpui_macros = { git = "https://github.com/zed-industries/zed", rev = "1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" }
gpui-component = { git = "https://github.com/stippi/gpui-component", rev = "1a66dc72424ae70300aae2ee02b04c4ce4e71d5a" }
gpui = { git = "https://github.com/zed-industries/zed", rev = "cc053a4a6fa2fd0e8793201ed9099466af1be0b1", version = "=0.2.2" }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "cc053a4a6fa2fd0e8793201ed9099466af1be0b1" }
gpui_macros = { git = "https://github.com/zed-industries/zed", rev = "cc053a4a6fa2fd0e8793201ed9099466af1be0b1" }
gpui-component = { git = "https://github.com/stippi/gpui-component", rev = "abeab54c" }
46 changes: 36 additions & 10 deletions crates/code_assistant_core/src/tools/impls/execute_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,25 @@ mod tests {
}
}

/// Poll the mock UI's live terminal text until it contains `needle` or the
/// timeout elapses, returning the last observed text either way. Session
/// mode spawns a login shell, whose startup latency on a loaded CI runner
/// makes exact wall-clock timing of the first output unreliable.
async fn wait_for_terminal_text(
fixture: &ToolTestFixture,
needle: &str,
timeout: std::time::Duration,
) -> String {
let deadline = tokio::time::Instant::now() + timeout;
loop {
let text = fixture.ui().unwrap().get_terminal_output_text();
if text.contains(needle) || tokio::time::Instant::now() >= deadline {
return text;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}

#[tokio::test(flavor = "multi_thread")]
async fn session_mode_returns_session_id_while_running() -> Result<()> {
let dir = tempfile::tempdir()?;
Expand Down Expand Up @@ -970,11 +989,14 @@ mod tests {
.with_tool_id("tool-bg-1".to_string());

// Short yield: the tool returns while the process is still running,
// before it prints the delayed "LATE" marker.
// before it prints the delayed "LATE" marker. The gap between the
// markers is generous so that login-shell startup latency (session
// mode spawns `$SHELL -l -c ...`) on a loaded CI runner cannot blur
// EARLY into LATE.
let session_id = {
let mut context = fixture.context();
let mut input = session_mode_input(
"printf 'EARLY\\n'; sleep 1; printf 'LATE\\n'; sleep 30",
"printf 'EARLY\\n'; sleep 3; printf 'LATE\\n'; sleep 30",
500,
);
let result = ExecuteCommandTool.execute(&mut context, &mut input).await?;
Expand All @@ -984,20 +1006,24 @@ mod tests {
// The tool call is over (context dropped). The agent would now be
// doing other work — no tool is polling the session.

let streamed_at_return = fixture.ui().unwrap().get_terminal_output_text();
// The login shell may still be starting up when the tool returns, so
// don't demand EARLY at that exact instant; poll for it (comfortably
// within the 3s gap before LATE). This is the "output keeps streaming
// with no tool call polling the session" guarantee.
let streamed_early =
wait_for_terminal_text(&fixture, "EARLY", std::time::Duration::from_secs(2)).await;
assert!(
streamed_at_return.contains("EARLY"),
"early output should have streamed: {streamed_at_return:?}"
streamed_early.contains("EARLY"),
"early output should have streamed: {streamed_early:?}"
);
assert!(
!streamed_at_return.contains("LATE"),
"the delayed output cannot have streamed yet: {streamed_at_return:?}"
!streamed_early.contains("LATE"),
"the delayed output cannot have streamed yet: {streamed_early:?}"
);

// Wait past the delay without any tool call touching the session.
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;

let streamed_later = fixture.ui().unwrap().get_terminal_output_text();
let streamed_later =
wait_for_terminal_text(&fixture, "LATE", std::time::Duration::from_secs(5)).await;
assert!(
streamed_later.contains("LATE"),
"output produced between turns should keep streaming to the card: {streamed_later:?}"
Expand Down
Loading
Loading