Compare commits

..

10 Commits

Author SHA1 Message Date
Max Brunsfeld
35a773c492 Use diagnostic updates in trailing user messages
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-19 12:58:21 +02:00
Agus Zubiaga
ea7fe49fb5 Do not use tag for "Using tool" marker
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-19 12:57:12 +02:00
Agus Zubiaga
7340513eee Wait for diagnostics after saving a buffer from any tool 2025-04-19 12:57:09 +02:00
Agus Zubiaga
4568ed12c3 Do not set edited_since_diagnostics_report unnecessarily 2025-04-19 12:57:05 +02:00
Agus Zubiaga
fd00f0ba73 WIP - Comparing non open path diagnostics 2025-04-19 12:57:01 +02:00
Max Brunsfeld
43c5db9583 WIP - tweak approach to reporting changed diagnostics
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-04-19 12:56:56 +02:00
Antonio Scandurra
cba4effb2d WIP: start reworking diagnostics 2025-04-19 12:56:51 +02:00
Michael Sloan
d88b06a5dc Simplify language model registry + only emit change events on change (#29086)
* Now only does default fallback logic in the registry

* Only emits change events when there is actually a change

Release Notes:

- N/A
2025-04-19 08:26:42 +00:00
Michael Sloan
98ceffe026 Pretty tool inputs in eval output markdown + numbered assistant messages (#29082)
Release Notes:

- N/A
2025-04-19 06:59:22 +00:00
Nathan Sobo
bab28560ef Systematically optimize agentic editing performance (#28961)
Now that we've established a proper eval in tree, this PR is reboots of
our agent loop back to a set of minimal tools and simpler prompts. We
should aim to get this branch feeling subjectively competitive with
what's on main and then merge it, and build from there.

Let's invest in our eval and use it to drive better performance of the
agent loop. How you can help: Pick an example, and then make the outcome
faster or better. It's fine to even use your own subjective judgment, as
our evaluation criteria likely need tuning as well at this point. Focus
on making the agent work better in your own subjective experience first.
Let's focus on simple/practical improvements to make this thing work
better, then determine how we can craft our judgment criteria to lock
those improvements in.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-19 02:47:59 +00:00
27 changed files with 867 additions and 426 deletions

View File

@@ -20,7 +20,6 @@ platforms = [
[traversal-excludes]
workspace-members = [
"remote_server",
"eval",
]
third-party = [
{ name = "reqwest", version = "0.11.27" },

View File

@@ -325,7 +325,7 @@ jobs:
cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/headless && ./script/install-mold 2.34.0
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Configure CI
run: |

View File

@@ -11,8 +11,8 @@ env:
RUST_BACKTRACE: 1
jobs:
build_binary:
name: Build Eval Binary
run_eval:
name: Run Eval
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
@@ -21,72 +21,8 @@ jobs:
with:
clean: false
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
./script/headless && ./script/install-mold 2.34.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Build entire Zed application
run: cargo build --release
- name: Build eval binary separately to ensure it's available
run: cargo build --release -p eval
- name: Upload eval binary
uses: actions/upload-artifact@v4
with:
name: eval-binary
path: target/release/eval
run_eval:
name: Run Eval - ${{ matrix.exercise }}
needs: build_binary
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
exercise:
- find_and_replace_diff_card
- metal_i64_support
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
./script/headless && ./script/install-mold 2.34.0
- name: Create required directories
run: |
mkdir -p /home/runner/.config/zed
touch /home/runner/.config/zed/settings.json
mkdir -p ./crates/eval/repos
mkdir -p ./crates/eval/worktrees
mkdir -p ./crates/eval/runs
- name: Set up Anthropic API key
run: echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> $GITHUB_ENV
- name: Download eval binary
uses: actions/download-artifact@v4
with:
name: eval-binary
path: ./target/release/
- name: Make eval binary executable
run: chmod +x ./target/release/eval
- name: Run eval for ${{ matrix.exercise }}
env:
SHELL: /bin/bash
run: ./target/release/eval --cohort-id dailyrun${{ github.run_id }} ${{ matrix.exercise }}
- name: Run cargo eval
run: cargo run -p eval

2
Cargo.lock generated
View File

@@ -4918,7 +4918,6 @@ dependencies = [
"release_channel",
"reqwest_client",
"serde",
"serde_json",
"settings",
"shellexpand 2.1.2",
"telemetry",
@@ -7652,7 +7651,6 @@ dependencies = [
"http_client",
"icons",
"image",
"log",
"open_ai",
"parking_lot",
"proto",

View File

@@ -984,8 +984,10 @@ mod tests {
)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
// When opening the assistant diff, the cursor is positioned on the first hunk.

View File

@@ -1,6 +1,7 @@
use std::fmt::Write as _;
use std::io::Write;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
@@ -355,7 +356,7 @@ impl Thread {
last_restore_checkpoint: None,
pending_checkpoint: None,
tool_use: ToolUseState::new(tools.clone()),
action_log: cx.new(|_| ActionLog::new(project.clone())),
action_log: cx.new(|cx| ActionLog::new(project.clone(), cx)),
initial_project_snapshot: {
let project_snapshot = Self::project_snapshot(project, cx);
cx.foreground_executor()
@@ -430,7 +431,7 @@ impl Thread {
prompt_builder,
tools,
tool_use,
action_log: cx.new(|_| ActionLog::new(project)),
action_log: cx.new(|cx| ActionLog::new(project, cx)),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
request_token_usage: serialized.request_token_usage,
cumulative_token_usage: serialized.cumulative_token_usage,
@@ -1070,30 +1071,91 @@ impl Thread {
fn attached_tracked_files_state(
&self,
messages: &mut Vec<LanguageModelRequestMessage>,
cx: &App,
cx: &mut App,
) {
const STALE_FILES_HEADER: &str = "These files changed since last read:";
let mut message = String::new();
let mut stale_message = String::new();
let action_log = self.action_log.read(cx);
for stale_file in action_log.stale_buffers(cx) {
let Some(file) = stale_file.read(cx).file() else {
continue;
};
if stale_message.is_empty() {
write!(&mut stale_message, "{}\n", STALE_FILES_HEADER).ok();
self.action_log.update(cx, |action_log, cx| {
let stale_files = action_log
.stale_buffers(cx)
.filter_map(|buffer| buffer.read(cx).file());
for (i, file) in stale_files.enumerate() {
if i == 0 {
writeln!(&mut message, "These files changed since last read:").ok();
}
writeln!(&mut message, "- {}", file.full_path(cx).display()).ok();
}
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
}
if let Some(diagnostic_changes) = action_log.flush_diagnostic_changes(cx) {
let project = self.project.read(cx);
writeln!(
&mut message,
"Diagnostics have changed in the following files:",
)
.ok();
for change in diagnostic_changes {
let path = change.project_path;
let Some(worktree) = project.worktree_for_id(path.worktree_id, cx) else {
continue;
};
let path = PathBuf::from(worktree.read(cx).root_name()).join(path.path);
write!(&mut message, "- {} (", path.display()).ok();
if change.fixed_diagnostic_count > 0 {
write!(&mut message, "{} fixed", change.fixed_diagnostic_count).ok();
if change.introduced_diagnostic_count > 0 {
write!(
&mut message,
", {} introduced",
change.introduced_diagnostic_count
)
.ok();
}
} else if change.introduced_diagnostic_count > 0 {
write!(
&mut message,
"{} introduced",
change.introduced_diagnostic_count
)
.ok();
}
write!(&mut message, ") ").ok();
if change.diagnostics.is_empty() {
writeln!(&mut message, "No diagnostics remaining.").ok();
} else {
writeln!(&mut message, "Remaining diagnostics:").ok();
}
for entry in change.diagnostics {
let mut lines = entry.diagnostic.message.split('\n');
writeln!(
&mut message,
" - line {}: {}",
entry.range.start.0.row + 1,
lines.next().unwrap()
)
.ok();
for line in lines {
writeln!(&mut message, " {}", line).ok();
}
}
if action_log
.last_edited_buffer()
.map_or(false, |last_edited| last_edited.consecutive_edit_count > 2)
{
writeln!(&mut message, "Because you've failed repeatedly, give up. Don't attempt to fix the diagnostics. Wait user input or continue with the next task.").ok();
writeln!(&mut message, "Don't keep trying to fix the diagnostics. Stop and wait for the user to help you fix them.").ok();
}
}
}
});
let mut content = Vec::with_capacity(2);
if !stale_message.is_empty() {
content.push(stale_message.into());
if !message.is_empty() {
content.push(message.into());
}
if !content.is_empty() {

View File

@@ -27,7 +27,7 @@ pub struct ToolUse {
pub needs_confirmation: bool,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
pub const USING_TOOL_MARKER: &str = "Using tool:";
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,

View File

@@ -8,7 +8,7 @@ mod terminal_inline_assistant;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_settings::{AssistantSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
@@ -161,71 +161,38 @@ fn init_language_model_settings(cx: &mut App) {
fn update_active_language_model_from_settings(cx: &mut App) {
let settings = AssistantSettings::get_global(cx);
// Default model - used as fallback
let active_model_provider_name =
LanguageModelProviderId::from(settings.default_model.provider.clone());
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
// Inline assistant model
let inline_assistant_model = settings
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
language_model::SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.clone()),
model: LanguageModelId::from(selection.model.clone()),
}
}
let default = to_selected_model(&settings.default_model);
let inline_assistant = settings
.inline_assistant_model
.as_ref()
.unwrap_or(&settings.default_model);
let inline_assistant_provider_name =
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
// Commit message model
let commit_message_model = settings
.map(to_selected_model);
let commit_message = settings
.commit_message_model
.as_ref()
.unwrap_or(&settings.default_model);
let commit_message_provider_name =
LanguageModelProviderId::from(commit_message_model.provider.clone());
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
// Thread summary model
let thread_summary_model = settings
.map(to_selected_model);
let thread_summary = settings
.thread_summary_model
.as_ref()
.unwrap_or(&settings.default_model);
let thread_summary_provider_name =
LanguageModelProviderId::from(thread_summary_model.provider.clone());
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
.map(to_selected_model);
let inline_alternatives = settings
.inline_alternatives
.iter()
.map(|alternative| {
(
LanguageModelProviderId::from(alternative.provider.clone()),
LanguageModelId::from(alternative.model.clone()),
)
})
.map(to_selected_model)
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
// Set the default model
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
// Set the specific models
registry.select_inline_assistant_model(
&inline_assistant_provider_name,
&inline_assistant_model_id,
cx,
);
registry.select_commit_message_model(
&commit_message_provider_name,
&commit_message_model_id,
cx,
);
registry.select_thread_summary_model(
&thread_summary_provider_name,
&thread_summary_model_id,
cx,
);
// Set the alternatives
registry.select_default_model(Some(&default), cx);
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
registry.select_inline_alternative_models(inline_alternatives, cx);
});
}

View File

@@ -22,6 +22,7 @@ gpui.workspace = true
icons.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
parking_lot.workspace = true
project.workspace = true
serde.workspace = true

View File

@@ -1,42 +1,123 @@
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
use collections::BTreeMap;
use futures::{StreamExt, channel::mpsc};
use collections::{BTreeMap, HashMap, HashSet};
use futures::{
FutureExt as _, StreamExt,
channel::{mpsc, oneshot},
};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
use language::{Anchor, Buffer, BufferEvent, DiagnosticEntry, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, ProjectPath, lsp_store::OpenLspBufferHandle};
use std::{
cmp::{self, Ordering},
ops::Range,
sync::Arc,
time::Duration,
};
use text::{BufferId, Edit, Patch, PointUtf16, Rope, Unclipped};
use util::RangeExt;
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
/// Buffers that we want to notify the model about when they change.
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
/// Has the model edited a file since it last checked diagnostics?
edited_since_project_diagnostics_check: bool,
paths_with_pre_existing_diagnostics: HashSet<ProjectPath>,
edited_since_diagnostics_report: bool,
diagnostic_state: DiagnosticState,
last_edited_buffer: Option<LastEditedBuffer>,
/// The project this action log is associated with
project: Entity<Project>,
_project_subscription: Subscription,
}
#[derive(Clone)]
pub struct LastEditedBuffer {
pub buffer_id: BufferId,
pub consecutive_edit_count: usize,
}
impl ActionLog {
/// Creates a new, empty action log associated with the given project.
pub fn new(project: Entity<Project>) -> Self {
pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
let pre_existing_diagnostics = project.update(cx, |project, cx| {
project
.lsp_store()
.read(cx)
.diagnostic_summaries(true, cx)
.map(|(path, _, _)| path.clone())
.collect()
});
let _project_subscription = cx.subscribe(&project, |this, _, event, cx| {
if let project::Event::BufferEdited(buffer) = event {
if let Some(project_path) = buffer.read(cx).project_path(cx) {
this.paths_with_pre_existing_diagnostics
.remove(&project_path);
}
}
});
Self {
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
paths_with_pre_existing_diagnostics: pre_existing_diagnostics,
edited_since_diagnostics_report: false,
diagnostic_state: Default::default(),
project,
last_edited_buffer: None,
_project_subscription,
}
}
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
pub fn flush_diagnostic_changes(&mut self, cx: &App) -> Option<Vec<DiagnosticChange>> {
if !self.edited_since_diagnostics_report {
return None;
}
let new_state = self.diagnostic_state(cx);
let changes = new_state.compare(&self.diagnostic_state, cx);
self.diagnostic_state = new_state;
self.edited_since_diagnostics_report = false;
Some(changes).filter(|changes| !changes.is_empty())
}
/// Returns true if any files have been edited since the last project diagnostics check
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
self.edited_since_project_diagnostics_check
pub fn last_edited_buffer(&self) -> Option<LastEditedBuffer> {
self.last_edited_buffer.clone()
}
fn diagnostic_state(&self, cx: &App) -> DiagnosticState {
let mut diagnostics_for_open_buffers = HashMap::default();
let mut diagnostics_for_non_open_buffers = HashMap::default();
let project = self.project.read(cx);
let all_diagnostics = project.lsp_store().read(cx).all_diagnostics();
for (project_path, diagnostics) in all_diagnostics {
if self
.paths_with_pre_existing_diagnostics
.contains(&project_path)
{
continue;
}
match project.get_open_buffer(&project_path, cx) {
Some(buffer) => {
let diagnostics = buffer
.read(cx)
.snapshot()
.diagnostics_in_range(Anchor::MIN..Anchor::MAX, false)
.filter(|entry| entry.diagnostic.is_primary)
.collect();
diagnostics_for_open_buffers.insert(buffer, diagnostics);
}
None => {
diagnostics_for_non_open_buffers.insert(project_path.clone(), diagnostics);
}
}
}
DiagnosticState {
diagnostics_for_open_buffers,
diagnostics_for_non_open_paths: diagnostics_for_non_open_buffers,
}
}
fn track_buffer(
@@ -71,6 +152,12 @@ impl ActionLog {
status = TrackedBufferStatus::Modified;
unreviewed_changes = Patch::default();
}
if let Some(project_path) = buffer.read(cx).project_path(cx) {
self.paths_with_pre_existing_diagnostics
.remove(&project_path);
}
TrackedBuffer {
buffer: buffer.clone(),
base_text,
@@ -269,21 +356,88 @@ impl ActionLog {
self.track_buffer(buffer, false, cx);
}
/// Track a buffer as read, so we can notify the model about user edits.
pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
/// Save and track a new buffer
pub fn save_new_buffer(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.track_buffer(buffer.clone(), true, cx);
self.buffer_edited(buffer, cx)
self.save_edited_buffer(buffer, cx)
}
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
/// Save and track an edited buffer
pub fn save_edited_buffer(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.edited_since_diagnostics_report = true;
let saved_buffer_id = buffer.read(cx).remote_id();
match &mut self.last_edited_buffer {
Some(LastEditedBuffer {
buffer_id,
consecutive_edit_count,
}) if *buffer_id == saved_buffer_id => *consecutive_edit_count += 1,
_ => {
self.last_edited_buffer = Some(LastEditedBuffer {
buffer_id: saved_buffer_id,
consecutive_edit_count: 1,
})
}
}
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;
}
tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx);
let project = self.project.clone();
cx.spawn(async move |_this, cx| {
let (tx, mut rx) = oneshot::channel();
let mut tx = Some(tx);
let _subscription = cx.subscribe(&project, move |_, event, _| match event {
project::Event::DiskBasedDiagnosticsFinished { .. } => {
if let Some(tx) = tx.take() {
tx.send(()).ok();
}
}
_ => {}
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
let has_lang_server = project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
buffer.update(cx, |buffer, cx| {
lsp_store
.language_servers_for_local_buffer(buffer, cx)
.next()
.is_some()
})
})
})?;
if has_lang_server {
let timeout = cx.background_executor().timer(Duration::from_secs(30));
futures::select! {
_ = rx => Ok(()),
_ = timeout.fuse() => {
log::info!("Did not receive diagnostics update 30s after agent edit");
// We don't want to fail the tool here
Ok(())
}
}
} else {
Ok(())
}
})
}
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
@@ -497,6 +651,149 @@ impl ActionLog {
}
}
#[derive(Default)]
struct DiagnosticState {
diagnostics_for_open_buffers: HashMap<Entity<Buffer>, Vec<DiagnosticEntry<Anchor>>>,
diagnostics_for_non_open_paths:
HashMap<ProjectPath, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct DiagnosticChange {
pub project_path: ProjectPath,
pub fixed_diagnostic_count: usize,
pub introduced_diagnostic_count: usize,
pub diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
}
impl DiagnosticState {
fn compare(&self, old_state: &Self, cx: &App) -> Vec<DiagnosticChange> {
let mut changes = Vec::new();
let empty = Vec::new();
for (buffer, new) in &self.diagnostics_for_open_buffers {
let old = old_state
.diagnostics_for_open_buffers
.get(&buffer)
.unwrap_or(&empty);
let buffer = buffer.read(cx);
let Some(project_path) = buffer.project_path(cx) else {
continue;
};
let (introduced, fixed) = Self::compare_diagnostics(new, old, |a, b| a.cmp(b, buffer));
if introduced > 0 || fixed > 0 {
changes.push(DiagnosticChange {
project_path,
fixed_diagnostic_count: fixed,
introduced_diagnostic_count: introduced,
diagnostics: new.into_iter().map(|entry| entry.resolve(buffer)).collect(),
});
}
}
for (buffer, old) in &old_state.diagnostics_for_open_buffers {
if !self.diagnostics_for_open_buffers.contains_key(&buffer) && old.len() > 0 {
let buffer = buffer.read(cx);
let Some(project_path) = buffer.project_path(cx) else {
continue;
};
changes.push(DiagnosticChange {
project_path,
fixed_diagnostic_count: old.len(),
introduced_diagnostic_count: 0,
diagnostics: vec![],
});
}
}
let empty = Vec::new();
for (project_path, new) in &self.diagnostics_for_non_open_paths {
let old = old_state
.diagnostics_for_non_open_paths
.get(&project_path)
.unwrap_or(&empty);
let (introduced, fixed) = Self::compare_diagnostics(new, old, |a, b| a.cmp(b));
if introduced > 0 || fixed > 0 {
changes.push(DiagnosticChange {
project_path: project_path.clone(),
fixed_diagnostic_count: fixed,
introduced_diagnostic_count: introduced,
diagnostics: new.clone(),
});
}
}
for (project_path, old) in &old_state.diagnostics_for_non_open_paths {
if !self
.diagnostics_for_non_open_paths
.contains_key(&project_path)
&& old.len() > 0
{
changes.push(DiagnosticChange {
project_path: project_path.clone(),
fixed_diagnostic_count: old.len(),
introduced_diagnostic_count: 0,
diagnostics: vec![],
});
}
}
changes
}
fn compare_diagnostics<T>(
new: &[DiagnosticEntry<T>],
old: &[DiagnosticEntry<T>],
cmp: impl Fn(&DiagnosticEntry<T>, &DiagnosticEntry<T>) -> Ordering,
) -> (usize, usize) {
let mut introduced = 0;
let mut fixed = 0;
let mut old_iter = old.iter().peekable();
let mut new_iter = new.iter().peekable();
loop {
match (old_iter.peek(), new_iter.peek()) {
(Some(old_entry), Some(new_entry)) => {
match cmp(old_entry, new_entry) {
Ordering::Less => {
// Old entry comes first and isn't in new - it's fixed
fixed += 1;
old_iter.next();
}
Ordering::Greater => {
// New entry comes first and isn't in old - it's introduced
introduced += 1;
new_iter.next();
}
Ordering::Equal => {
// They're the same - just advance both iterators
old_iter.next();
new_iter.next();
}
}
}
(Some(_), None) => {
// Only old entries left - they're all fixed
old_iter.next();
fixed += 1;
}
(None, Some(_)) => {
// Only new entries left - they're all introduced
new_iter.next();
introduced += 1;
}
(None, None) => break,
}
}
(introduced, fixed)
}
}
fn apply_non_conflicting_edits(
patch: &Patch<u32>,
edits: Vec<Edit<u32>>,
@@ -667,7 +964,7 @@ mod tests {
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
use language::Point;
use language::{Diagnostic, LanguageServerId, Point};
use project::{FakeFs, Fs, Project, RemoveOptions};
use rand::prelude::*;
use serde_json::json;
@@ -696,7 +993,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
cx.update(|cx| {
@@ -711,8 +1008,10 @@ mod tests {
.edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -766,7 +1065,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
cx.update(|cx| {
@@ -783,8 +1082,10 @@ mod tests {
.unwrap();
buffer.finalize_last_transaction();
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -840,7 +1141,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
cx.update(|cx| {
@@ -850,8 +1151,10 @@ mod tests {
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -929,7 +1232,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({})).await;
@@ -946,12 +1249,10 @@ mod tests {
.unwrap();
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
action_log.update(cx, |log, cx| log.save_new_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
@@ -1005,7 +1306,7 @@ mod tests {
.read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
.unwrap();
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer1 = project
.update(cx, |project, cx| {
project.open_buffer(file1_path.clone(), cx)
@@ -1068,9 +1369,8 @@ mod tests {
.await
.unwrap();
buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
project
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
action_log
.update(cx, |log, cx| log.save_new_buffer(buffer2.clone(), cx))
.await
.unwrap();
@@ -1103,7 +1403,7 @@ mod tests {
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1124,8 +1424,11 @@ mod tests {
.edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1238,7 +1541,7 @@ mod tests {
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1259,8 +1562,10 @@ mod tests {
.edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1314,7 +1619,7 @@ mod tests {
fs.insert_tree(path!("/dir"), json!({"file": "content"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1369,7 +1674,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| {
project.find_project_path("dir/new_file", cx)
@@ -1382,8 +1687,10 @@ mod tests {
.unwrap();
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
});
action_log.update(cx, |log, cx| log.save_new_buffer(buffer.clone(), cx))
})
.await
.unwrap();
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
@@ -1417,6 +1724,177 @@ mod tests {
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test]
async fn test_track_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
json!({
"src": {
"one.rs": "fn one(a: B) -> C { d }",
"two.rs": "fn two(e: F) { G::H }",
"three.rs": "fn three() -> { i(); }",
}
}),
)
.await;
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let diagnostics_1 = vec![language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 11)),
diagnostic: Diagnostic {
message: "pre-existing error 1".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
}];
let diagnostics_2 = vec![language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 18))..Unclipped(PointUtf16::new(0, 19)),
diagnostic: Diagnostic {
message: "pre-existing error 2".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
}];
project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/one.rs".into(),
None,
diagnostics_1.clone(),
cx,
)
.unwrap();
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/two.rs".into(),
None,
diagnostics_2.clone(),
cx,
)
.unwrap();
});
});
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/src/one.rs", cx)
})
.await
.unwrap();
let worktree_id = buffer.read_with(cx, |buffer, cx| buffer.file().unwrap().worktree_id(cx));
action_log.update(cx, |action_log, cx| {
action_log.track_buffer(buffer.clone(), false, cx);
});
let diagnostic_changes = action_log
.update(cx, |action_log, cx| action_log.flush_diagnostic_changes(cx))
.unwrap();
assert_eq!(
diagnostic_changes,
vec![DiagnosticChange {
project_path: (worktree_id, "src/one.rs").into(),
fixed_diagnostic_count: 0,
introduced_diagnostic_count: 1,
diagnostics: diagnostics_1
},]
);
let save_task = action_log.update(cx, |action_log, cx| {
action_log.save_edited_buffer(buffer.clone(), cx)
});
let diagnostics_1 = vec![
language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 11)),
diagnostic: Diagnostic {
message: "pre-existing error 1".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
},
language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 20))..Unclipped(PointUtf16::new(0, 21)),
diagnostic: Diagnostic {
message: "new error".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
},
];
let diagnostics_3 = vec![language::DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 0)),
diagnostic: Diagnostic {
message: "new error 3".into(),
group_id: 0,
is_primary: true,
..Default::default()
},
}];
project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/one.rs".into(),
None,
diagnostics_1.clone(),
cx,
)
.unwrap();
lsp_store
.update_diagnostic_entries(
language_server_id,
"/dir/src/three.rs".into(),
None,
diagnostics_3.clone(),
cx,
)
.unwrap();
lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
});
});
save_task.await.unwrap();
// The diagnostics in the file `two.rs` existed are pre-existing, and
// that file has not been edited, so they are not included.
let diagnostic_changes = action_log
.update(cx, |action_log, cx| action_log.flush_diagnostic_changes(cx))
.unwrap();
assert_eq!(
diagnostic_changes,
vec![
DiagnosticChange {
project_path: (worktree_id, "src/one.rs").into(),
fixed_diagnostic_count: 0,
introduced_diagnostic_count: 1,
diagnostics: diagnostics_1
},
DiagnosticChange {
project_path: (worktree_id, "src/three.rs").into(),
fixed_diagnostic_count: 0,
introduced_diagnostic_count: 1,
diagnostics: diagnostics_3
}
]
);
}
#[gpui::test(iterations = 100)]
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
init_test(cx);
@@ -1429,7 +1907,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({"file": text})).await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
@@ -1469,9 +1947,14 @@ mod tests {
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
if is_agent_change {
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
action_log
.update(cx, |log, cx| log.save_edited_buffer(buffer.clone(), cx))
} else {
Task::ready(Ok(()))
}
});
})
.await
.unwrap();
}
}

View File

@@ -241,13 +241,10 @@ impl Tool for CodeActionTool {
format!("Completed code action: {}", title)
};
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
log.save_edited_buffer(buffer.clone(), cx)
})?.await?;
Ok(response)
} else {

View File

@@ -97,14 +97,11 @@ impl Tool for CreateFileTool {
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
action_log.update(cx, |action_log, cx| {
action_log.will_create_buffer(buffer.clone(), cx)
});
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
action_log.save_new_buffer(buffer.clone(), cx)
})
})?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}"))
})

View File

@@ -81,7 +81,7 @@ impl Tool for DiagnosticsTool {
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
match serde_json::from_value::<DiagnosticsToolInput>(input)
@@ -152,10 +152,6 @@ impl Tool for DiagnosticsTool {
}
}
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
});
if has_diagnostics {
Task::ready(Ok(output)).into()
} else {

View File

@@ -1,11 +1,14 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
@@ -160,24 +163,22 @@ impl Tool for EditFileTool {
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
snapshot
})?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
action_log.update(cx, |log, cx| {
log.save_edited_buffer(buffer.clone(), cx)
})?.await?;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
let diff_str = cx.background_spawn({
let snapshot = snapshot.clone();
async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}
}).await;
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
}).into()
}
}

View File

@@ -186,7 +186,7 @@ mod test {
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
let input = json!({
@@ -216,7 +216,7 @@ mod test {
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
let input = json!({
@@ -245,7 +245,7 @@ mod test {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
@@ -314,7 +314,7 @@ mod test {
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let action_log = cx.new(|cx| ActionLog::new(project.clone(), cx));
let result = cx
.update(|cx| {
let input = json!({

View File

@@ -129,13 +129,9 @@ impl Tool for RenameTool {
})?
.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
log.save_edited_buffer(buffer.clone(), cx)
})?.await?;
Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
}).into()

View File

@@ -36,7 +36,6 @@ prompt_store.workspace = true
release_channel.workspace = true
reqwest_client.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
shellexpand.workspace = true
telemetry.workspace = true

View File

@@ -11,12 +11,10 @@ use clap::Parser;
use extension::ExtensionHostProxy;
use futures::{StreamExt, future};
use gpui::http_client::{Uri, read_proxy_from_env};
use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, Task, UpdateGlobal};
use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, UpdateGlobal};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
use language_model::{
AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
};
use language_model::{ConfiguredModel, LanguageModel, LanguageModelRegistry};
use node_runtime::{NodeBinaryOptions, NodeRuntime};
use project::Project;
use project::project_settings::ProjectSettings;
@@ -53,9 +51,6 @@ struct Args {
/// Maximum number of examples to run concurrently.
#[arg(long, default_value = "10")]
concurrency: usize,
/// Optional cohort ID to group runs together (useful for GitHub Actions)
#[arg(long)]
cohort_id: Option<String>,
}
fn main() {
@@ -97,18 +92,25 @@ fn main() {
.telemetry()
.start(system_id, installation_id, session_id, cx);
let model = find_model("claude-3-7-sonnet-latest", cx).unwrap();
let model_registry = LanguageModelRegistry::read_global(cx);
let model = find_model("claude-3-7-sonnet-latest", model_registry, cx).unwrap();
let model_provider_id = model.provider_id();
let model_provider = model_registry.provider(&model_provider_id).unwrap();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(Some(model.clone()), cx);
registry.set_default_model(
Some(ConfiguredModel {
provider: model_provider.clone(),
model: model.clone(),
}),
cx,
);
});
let model_provider_id = model.provider_id();
let authenticate = authenticate_model_provider(model_provider_id.clone(), cx);
let authenticate_task = model_provider.authenticate(cx);
cx.spawn(async move |cx| {
authenticate.await.unwrap();
authenticate_task.await.unwrap();
std::fs::create_dir_all(REPOS_DIR)?;
std::fs::create_dir_all(WORKTREES_DIR)?;
@@ -236,17 +238,14 @@ fn main() {
let judge_repetitions = args.judge_repetitions;
let concurrency = args.concurrency;
let cohort_id = args.cohort_id.clone();
let tasks = examples.iter().map(|example| {
let app_state = app_state.clone();
let model = model.clone();
let example = example.clone();
let cohort_id = cohort_id.clone();
cx.spawn(async move |cx| {
let result =
run_example(&example, model, app_state, judge_repetitions, cohort_id, cx)
.await;
run_example(&example, model, app_state, judge_repetitions, cx).await;
(result, example)
})
});
@@ -357,23 +356,14 @@ async fn run_example(
model: Arc<dyn LanguageModel>,
app_state: Arc<AgentAppState>,
judge_repetitions: u32,
optional_cohort_id: Option<String>,
cx: &mut AsyncApp,
) -> Result<Vec<Result<JudgeOutput>>> {
let run_output = cx
.update(|cx| example.run(model.clone(), app_state.clone(), cx))?
.await?;
let judge_tasks = (0..judge_repetitions).map(|round| {
run_judge_repetition(
example.clone(),
model.clone(),
&run_output,
round,
optional_cohort_id.clone(),
cx,
)
});
let judge_tasks = (0..judge_repetitions)
.map(|round| run_judge_repetition(example.clone(), model.clone(), &run_output, round, cx));
let results = future::join_all(judge_tasks).await;
@@ -513,8 +503,11 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
})
}
pub fn find_model(model_name: &str, cx: &App) -> anyhow::Result<Arc<dyn LanguageModel>> {
let model_registry = LanguageModelRegistry::read_global(cx);
pub fn find_model(
model_name: &str,
model_registry: &LanguageModelRegistry,
cx: &App,
) -> anyhow::Result<Arc<dyn LanguageModel>> {
let model = model_registry
.available_models(cx)
.find(|model| model.id().0 == model_name);
@@ -534,15 +527,6 @@ pub fn find_model(model_name: &str, cx: &App) -> anyhow::Result<Arc<dyn Language
Ok(model)
}
pub fn authenticate_model_provider(
provider_id: LanguageModelProviderId,
cx: &mut App,
) -> Task<std::result::Result<(), AuthenticateError>> {
let model_registry = LanguageModelRegistry::read_global(cx);
let model_provider = model_registry.provider(&provider_id).unwrap();
model_provider.authenticate(cx)
}
pub async fn get_current_commit_id(repo_path: &Path) -> Option<String> {
(run_git(repo_path, &["rev-parse", "HEAD"]).await).ok()
}
@@ -558,7 +542,6 @@ async fn run_judge_repetition(
model: Arc<dyn LanguageModel>,
run_output: &RunOutput,
round: u32,
optional_cohort_id: Option<String>,
cx: &AsyncApp,
) -> Result<JudgeOutput> {
let judge_result = example.judge(model.clone(), &run_output, round, cx).await;
@@ -574,10 +557,6 @@ async fn run_judge_repetition(
let commit_id = get_current_commit_id(path).await.unwrap_or_default();
if let Some(thread) = &judge_output.thread {
let cohort_id = optional_cohort_id.clone().unwrap_or_else(|| cohort_id);
let path = std::path::Path::new(".");
let commit_id = get_current_commit_id(path).await.unwrap_or_default();
telemetry::event!(
"Agent Eval Completed",
cohort_id = cohort_id,

View File

@@ -29,6 +29,7 @@ use std::{
use unindent::Unindent as _;
use util::ResultExt as _;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
use util::serde::default_true;
use crate::AgentAppState;
@@ -879,6 +880,7 @@ impl RequestMarkdown {
fn new(request: &LanguageModelRequest) -> Self {
let mut tools = String::new();
let mut messages = String::new();
let mut assistant_message_number: u32 = 1;
// Print the tools
if !request.tools.is_empty() {
@@ -887,8 +889,8 @@ impl RequestMarkdown {
write!(&mut tools, "{}\n\n", tool.description).unwrap();
write!(
&mut tools,
"```json\n{}\n```\n\n",
serde_json::to_string_pretty(&tool.input_schema).unwrap_or_default()
"{}\n",
MarkdownString::code_block("json", &format!("{:#}", tool.input_schema))
)
.unwrap();
}
@@ -896,14 +898,15 @@ impl RequestMarkdown {
// Print the messages
for message in &request.messages {
let role_str = match message.role {
Role::User => "👤 USER",
Role::Assistant => "🤖 ASSISTANT",
Role::System => "⚙️ SYSTEM",
match message.role {
Role::System => messages.push_str("# ⚙️ SYSTEM\n\n"),
Role::User => messages.push_str("# 👤 USER\n\n"),
Role::Assistant => {
messages.push_str(&format!("# 🤖 ASSISTANT {assistant_message_number}\n\n"));
assistant_message_number += 1;
}
};
messages.push_str(&format!("# {}\n\n", role_str));
for content in &message.content {
match content {
MessageContent::Text(text) => {
@@ -918,7 +921,10 @@ impl RequestMarkdown {
"**Tool Use**: {} (ID: {})\n",
tool_use.name, tool_use.id
));
messages.push_str(&format!("```json\n{}\n```\n\n", tool_use.input));
messages.push_str(&format!(
"{}\n",
MarkdownString::code_block("json", &format!("{:#}", tool_use.input))
));
}
MessageContent::ToolResult(tool_result) => {
messages.push_str(&format!(
@@ -977,7 +983,10 @@ fn response_events_to_markdown(
"**Tool Use**: {} (ID: {})\n",
tool_use.name, tool_use.id
));
response.push_str(&format!("```json\n{}\n```\n\n", tool_use.input));
response.push_str(&format!(
"{}\n",
MarkdownString::code_block("json", &format!("{:#}", tool_use.input))
));
}
Ok(
LanguageModelCompletionEvent::UsageUpdate(_)

View File

@@ -230,6 +230,23 @@ pub struct Diagnostic {
pub data: Option<Value>,
}
impl Ord for Diagnostic {
fn cmp(&self, other: &Self) -> Ordering {
other
.is_primary
.cmp(&self.is_primary)
.then_with(|| self.is_disk_based.cmp(&other.is_disk_based))
.then_with(|| self.severity.cmp(&other.severity))
.then_with(|| self.message.cmp(&other.message))
}
}
impl PartialOrd for Diagnostic {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// An operation used to synchronize this buffer with its other replicas.
#[derive(Clone, Debug, PartialEq)]
pub enum Operation {

View File

@@ -9,7 +9,7 @@ use std::{
ops::Range,
};
use sum_tree::{self, Bias, SumTree};
use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
use text::{Anchor, AnchorRangeExt, FromAnchor, PointUtf16, ToOffset, Unclipped};
/// A set of diagnostics associated with a given buffer, provided
/// by a single language server.
@@ -246,6 +246,12 @@ impl sum_tree::Item for DiagnosticEntry<Anchor> {
}
impl DiagnosticEntry<Anchor> {
pub fn cmp(&self, other: &Self, buffer: &text::BufferSnapshot) -> Ordering {
self.range
.cmp(&other.range, buffer)
.then_with(|| self.diagnostic.cmp(&other.diagnostic))
}
/// Converts the [DiagnosticEntry] to a different buffer coordinate type.
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticEntry<O> {
DiagnosticEntry {
@@ -256,6 +262,20 @@ impl DiagnosticEntry<Anchor> {
}
}
impl DiagnosticEntry<Unclipped<PointUtf16>> {
pub fn cmp(&self, other: &Self) -> Ordering {
self.range
.start
.0
.row
.cmp(&other.range.start.0.row)
.then_with(|| self.range.start.0.column.cmp(&other.range.start.0.column))
.then_with(|| self.range.end.0.row.cmp(&other.range.end.0.row))
.then_with(|| self.range.end.0.column.cmp(&other.range.end.0.column))
.then_with(|| self.diagnostic.cmp(&other.diagnostic))
}
}
impl Default for Summary {
fn default() -> Self {
Self {

View File

@@ -27,7 +27,6 @@ gpui.workspace = true
http_client.workspace = true
icons.workspace = true
image.workspace = true
log.workspace = true
open_ai = { workspace = true, features = ["schemars"] }
parking_lot.workspace = true
proto.workspace = true

View File

@@ -25,12 +25,23 @@ pub struct LanguageModelRegistry {
inline_alternatives: Vec<Arc<dyn LanguageModel>>,
}
pub struct SelectedModel {
pub provider: LanguageModelProviderId,
pub model: LanguageModelId,
}
#[derive(Clone)]
pub struct ConfiguredModel {
pub provider: Arc<dyn LanguageModelProvider>,
pub model: Arc<dyn LanguageModel>,
}
impl ConfiguredModel {
pub fn is_same_as(&self, other: &ConfiguredModel) -> bool {
self.model.id() == other.model.id() && self.provider.id() == other.provider.id()
}
}
pub enum Event {
DefaultModelChanged,
InlineAssistantModelChanged,
@@ -59,7 +70,11 @@ impl LanguageModelRegistry {
let mut registry = Self::default();
registry.register_provider(fake_provider.clone(), cx);
let model = fake_provider.provided_models(cx)[0].clone();
registry.set_default_model(Some(model), cx);
let configured_model = ConfiguredModel {
provider: Arc::new(fake_provider.clone()),
model,
};
registry.set_default_model(Some(configured_model), cx);
registry
});
cx.set_global(GlobalLanguageModelRegistry(registry));
@@ -119,144 +134,114 @@ impl LanguageModelRegistry {
self.providers.get(id).cloned()
}
pub fn select_default_model(
&mut self,
provider: &LanguageModelProviderId,
model_id: &LanguageModelId,
cx: &mut Context<Self>,
) {
let Some(provider) = self.provider(provider) else {
return;
};
let models = provider.provided_models(cx);
if let Some(model) = models.iter().find(|model| &model.id() == model_id).cloned() {
self.set_default_model(Some(model), cx);
}
pub fn select_default_model(&mut self, model: Option<&SelectedModel>, cx: &mut Context<Self>) {
let configured_model = model.and_then(|model| self.select_model(model, cx));
self.set_default_model(configured_model, cx);
}
pub fn select_inline_assistant_model(
&mut self,
provider: &LanguageModelProviderId,
model_id: &LanguageModelId,
model: Option<&SelectedModel>,
cx: &mut Context<Self>,
) {
let Some(provider) = self.provider(provider) else {
return;
};
let models = provider.provided_models(cx);
if let Some(model) = models.iter().find(|model| &model.id() == model_id).cloned() {
self.set_inline_assistant_model(Some(model), cx);
}
let configured_model = model.and_then(|model| self.select_model(model, cx));
self.set_inline_assistant_model(configured_model, cx);
}
pub fn select_commit_message_model(
&mut self,
provider: &LanguageModelProviderId,
model_id: &LanguageModelId,
model: Option<&SelectedModel>,
cx: &mut Context<Self>,
) {
let Some(provider) = self.provider(provider) else {
return;
};
let models = provider.provided_models(cx);
if let Some(model) = models.iter().find(|model| &model.id() == model_id).cloned() {
self.set_commit_message_model(Some(model), cx);
}
let configured_model = model.and_then(|model| self.select_model(model, cx));
self.set_commit_message_model(configured_model, cx);
}
pub fn select_thread_summary_model(
&mut self,
provider: &LanguageModelProviderId,
model_id: &LanguageModelId,
model: Option<&SelectedModel>,
cx: &mut Context<Self>,
) {
let Some(provider) = self.provider(provider) else {
return;
};
let models = provider.provided_models(cx);
if let Some(model) = models.iter().find(|model| &model.id() == model_id).cloned() {
self.set_thread_summary_model(Some(model), cx);
}
let configured_model = model.and_then(|model| self.select_model(model, cx));
self.set_thread_summary_model(configured_model, cx);
}
pub fn set_default_model(
/// Selects and sets the inline alternatives for language models based on
/// provider name and id.
pub fn select_inline_alternative_models(
&mut self,
model: Option<Arc<dyn LanguageModel>>,
alternatives: impl IntoIterator<Item = SelectedModel>,
cx: &mut Context<Self>,
) {
if let Some(model) = model {
let provider_id = model.provider_id();
if let Some(provider) = self.providers.get(&provider_id).cloned() {
self.default_model = Some(ConfiguredModel { provider, model });
cx.emit(Event::DefaultModelChanged);
} else {
log::warn!("Active model's provider not found in registry");
}
} else {
self.default_model = None;
cx.emit(Event::DefaultModelChanged);
self.inline_alternatives = alternatives
.into_iter()
.flat_map(|alternative| {
self.select_model(&alternative, cx)
.map(|configured_model| configured_model.model)
})
.collect::<Vec<_>>();
}
fn select_model(
&mut self,
selected_model: &SelectedModel,
cx: &mut Context<Self>,
) -> Option<ConfiguredModel> {
let provider = self.provider(&selected_model.provider)?;
let model = provider
.provided_models(cx)
.iter()
.find(|model| model.id() == selected_model.model)?
.clone();
Some(ConfiguredModel { provider, model })
}
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
match (self.default_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {}
_ => cx.emit(Event::DefaultModelChanged),
}
self.default_model = model;
}
pub fn set_inline_assistant_model(
&mut self,
model: Option<Arc<dyn LanguageModel>>,
model: Option<ConfiguredModel>,
cx: &mut Context<Self>,
) {
if let Some(model) = model {
let provider_id = model.provider_id();
if let Some(provider) = self.providers.get(&provider_id).cloned() {
self.inline_assistant_model = Some(ConfiguredModel { provider, model });
cx.emit(Event::InlineAssistantModelChanged);
} else {
log::warn!("Inline assistant model's provider not found in registry");
}
} else {
self.inline_assistant_model = None;
cx.emit(Event::InlineAssistantModelChanged);
match (self.inline_assistant_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {}
_ => cx.emit(Event::InlineAssistantModelChanged),
}
self.inline_assistant_model = model;
}
pub fn set_commit_message_model(
&mut self,
model: Option<Arc<dyn LanguageModel>>,
model: Option<ConfiguredModel>,
cx: &mut Context<Self>,
) {
if let Some(model) = model {
let provider_id = model.provider_id();
if let Some(provider) = self.providers.get(&provider_id).cloned() {
self.commit_message_model = Some(ConfiguredModel { provider, model });
cx.emit(Event::CommitMessageModelChanged);
} else {
log::warn!("Commit message model's provider not found in registry");
}
} else {
self.commit_message_model = None;
cx.emit(Event::CommitMessageModelChanged);
match (self.commit_message_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {}
_ => cx.emit(Event::CommitMessageModelChanged),
}
self.commit_message_model = model;
}
pub fn set_thread_summary_model(
&mut self,
model: Option<Arc<dyn LanguageModel>>,
model: Option<ConfiguredModel>,
cx: &mut Context<Self>,
) {
if let Some(model) = model {
let provider_id = model.provider_id();
if let Some(provider) = self.providers.get(&provider_id).cloned() {
self.thread_summary_model = Some(ConfiguredModel { provider, model });
cx.emit(Event::ThreadSummaryModelChanged);
} else {
log::warn!("Thread summary model's provider not found in registry");
}
} else {
self.thread_summary_model = None;
cx.emit(Event::ThreadSummaryModelChanged);
match (self.thread_summary_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {}
_ => cx.emit(Event::ThreadSummaryModelChanged),
}
self.thread_summary_model = model;
}
pub fn default_model(&self) -> Option<ConfiguredModel> {
@@ -286,30 +271,6 @@ impl LanguageModelRegistry {
.or_else(|| self.default_model())
}
/// Selects and sets the inline alternatives for language models based on
/// provider name and id.
pub fn select_inline_alternative_models(
&mut self,
alternatives: impl IntoIterator<Item = (LanguageModelProviderId, LanguageModelId)>,
cx: &mut Context<Self>,
) {
let mut selected_alternatives = Vec::new();
for (provider_id, model_id) in alternatives {
if let Some(provider) = self.providers.get(&provider_id) {
if let Some(model) = provider
.provided_models(cx)
.iter()
.find(|m| m.id() == model_id)
{
selected_alternatives.push(model.clone());
}
}
}
self.inline_alternatives = selected_alternatives;
}
/// The models to use for inline assists. Returns the union of the active
/// model and all inline alternatives. When there are multiple models, the
/// user will be able to cycle through results.

View File

@@ -2169,19 +2169,11 @@ impl LocalLspStore {
mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
cx: &mut Context<LspStore>,
) -> Result<()> {
fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering {
Ordering::Equal
.then_with(|| b.is_primary.cmp(&a.is_primary))
.then_with(|| a.is_disk_based.cmp(&b.is_disk_based))
.then_with(|| a.severity.cmp(&b.severity))
.then_with(|| a.message.cmp(&b.message))
}
diagnostics.sort_unstable_by(|a, b| {
Ordering::Equal
.then_with(|| a.range.start.cmp(&b.range.start))
.then_with(|| b.range.end.cmp(&a.range.end))
.then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic))
.then_with(|| a.diagnostic.cmp(&b.diagnostic))
});
let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx)?;
@@ -5938,6 +5930,29 @@ impl LspStore {
}
}
pub fn all_diagnostics(
&self,
) -> HashMap<ProjectPath, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>> {
let mut result = HashMap::default();
if let Some(this) = self.as_local() {
for (worktree_id, diagnostics_by_path) in &this.diagnostics {
for (path, diagnostics_by_server_id) in diagnostics_by_path {
let project_path = ProjectPath {
worktree_id: *worktree_id,
path: path.clone(),
};
let mut all_diagnostics = Vec::new();
for (_, diagnostics) in diagnostics_by_server_id {
all_diagnostics.extend_from_slice(&diagnostics);
}
all_diagnostics.sort_unstable_by_key(|entry| entry.range.start);
result.insert(project_path, all_diagnostics);
}
}
}
result
}
pub fn diagnostic_summary(&self, include_ignored: bool, cx: &App) -> DiagnosticSummary {
let mut summary = DiagnosticSummary::default();
for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) {

View File

@@ -303,6 +303,7 @@ pub enum Event {
RevealInProjectPanel(ProjectEntryId),
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
BufferEdited(Entity<Buffer>),
}
pub enum DebugAdapterClientState {
@@ -2905,7 +2906,7 @@ impl Project {
})
.ok();
}
cx.emit(Event::BufferEdited(buffer.clone()));
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Operation {
buffer_id,
operation,

View File

@@ -3125,6 +3125,12 @@ impl FromAnchor for usize {
}
}
impl<T: FromAnchor> FromAnchor for Unclipped<T> {
fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self {
Unclipped(T::from_anchor(anchor, snapshot))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LineEnding {
Unix,