Merge remote-tracking branch 'origin/main' into acp-onb-modal
This commit is contained in:
commit
5c9a4f55d1
29 changed files with 812 additions and 464 deletions
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
|
@ -14,7 +14,7 @@ body:
|
||||||
### Description
|
### Description
|
||||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||||
- Any code must be sufficient to reproduce (include context!)
|
- Any code must be sufficient to reproduce (include context!)
|
||||||
- Code must as text, not just as a screenshot.
|
- Include code as text, not just as a screenshot.
|
||||||
- Issues with insufficient detail may be summarily closed.
|
- Issues with insufficient detail may be summarily closed.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
"shift-f11": "debugger::StepOut",
|
"shift-f11": "debugger::StepOut",
|
||||||
"f11": "zed::ToggleFullScreen",
|
"f11": "zed::ToggleFullScreen",
|
||||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
"alt-g m": "git::OpenModifiedFiles",
|
"alt-g m": "git::OpenModifiedFiles",
|
||||||
"menu": "editor::OpenContextMenu",
|
"menu": "editor::OpenContextMenu",
|
||||||
"shift-f10": "editor::OpenContextMenu",
|
"shift-f10": "editor::OpenContextMenu",
|
||||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
|
||||||
"f9": "editor::ToggleBreakpoint",
|
"f9": "editor::ToggleBreakpoint",
|
||||||
"shift-f9": "editor::EditLogBreakpoint"
|
"shift-f9": "editor::EditLogBreakpoint"
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||||
|
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||||
"ctrl-d": "editor::Delete", // delete-char
|
"ctrl-d": "editor::Delete", // delete-char
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||||
|
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||||
"ctrl-d": "editor::Delete", // delete-char
|
"ctrl-d": "editor::Delete", // delete-char
|
||||||
|
|
|
@ -653,6 +653,8 @@
|
||||||
// "never"
|
// "never"
|
||||||
"show": "always"
|
"show": "always"
|
||||||
},
|
},
|
||||||
|
// Whether to enable drag-and-drop operations in the project panel.
|
||||||
|
"drag_and_drop": true,
|
||||||
// Whether to hide the root entry when only one folder is open in the window.
|
// Whether to hide the root entry when only one folder is open in the window.
|
||||||
"hide_root": false
|
"hide_root": false
|
||||||
},
|
},
|
||||||
|
|
|
@ -43,8 +43,8 @@
|
||||||
// "args": ["--login"]
|
// "args": ["--login"]
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
"shell": "system",
|
"shell": "system"
|
||||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||||
"tags": []
|
// "tags": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -664,7 +664,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
||||||
if self.configured_model.is_none() || self.messages.is_empty() {
|
if self.configured_model.is_none() {
|
||||||
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||||
}
|
}
|
||||||
self.configured_model.clone()
|
self.configured_model.clone()
|
||||||
|
@ -2097,7 +2097,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
||||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
|
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
|
||||||
println!("No thread summary model");
|
println!("No thread summary model");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -2416,7 +2416,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ConfiguredModel { model, provider }) =
|
let Some(ConfiguredModel { model, provider }) =
|
||||||
LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
|
LanguageModelRegistry::read_global(cx).thread_summary_model()
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -5410,10 +5410,13 @@ fn main() {{
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
registry.set_thread_summary_model(Some(ConfiguredModel {
|
registry.set_thread_summary_model(
|
||||||
provider,
|
Some(ConfiguredModel {
|
||||||
model: model.clone(),
|
provider,
|
||||||
}));
|
model: model.clone(),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -228,7 +228,7 @@ impl NativeAgent {
|
||||||
) -> Entity<AcpThread> {
|
) -> Entity<AcpThread> {
|
||||||
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
||||||
let registry = LanguageModelRegistry::read_global(cx);
|
let registry = LanguageModelRegistry::read_global(cx);
|
||||||
let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
|
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
||||||
|
|
||||||
thread_handle.update(cx, |thread, cx| {
|
thread_handle.update(cx, |thread, cx| {
|
||||||
thread.set_summarization_model(summarization_model, cx);
|
thread.set_summarization_model(summarization_model, cx);
|
||||||
|
@ -524,7 +524,7 @@ impl NativeAgent {
|
||||||
|
|
||||||
let registry = LanguageModelRegistry::read_global(cx);
|
let registry = LanguageModelRegistry::read_global(cx);
|
||||||
let default_model = registry.default_model().map(|m| m.model);
|
let default_model = registry.default_model().map(|m| m.model);
|
||||||
let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
|
let summarization_model = registry.thread_summary_model().map(|m| m.model);
|
||||||
|
|
||||||
for session in self.sessions.values_mut() {
|
for session in self.sessions.values_mut() {
|
||||||
session.thread.update(cx, |thread, cx| {
|
session.thread.update(cx, |thread, cx| {
|
||||||
|
|
|
@ -472,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||||
tool_name: ToolRequiringPermission::name().into(),
|
tool_name: ToolRequiringPermission::name().into(),
|
||||||
is_error: true,
|
is_error: true,
|
||||||
content: "Permission to run tool denied by user".into(),
|
content: "Permission to run tool denied by user".into(),
|
||||||
output: None
|
output: Some("Permission to run tool denied by user".into())
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -1822,11 +1822,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||||
let clock = Arc::new(clock::FakeSystemClock::new());
|
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||||
let client = Client::new(clock, http_client, cx);
|
let client = Client::new(clock, http_client, cx);
|
||||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||||
Project::init_settings(cx);
|
|
||||||
agent_settings::init(cx);
|
|
||||||
language_model::init(client.clone(), cx);
|
language_model::init(client.clone(), cx);
|
||||||
language_models::init(user_store, client.clone(), cx);
|
language_models::init(user_store, client.clone(), cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
LanguageModelRegistry::test(cx);
|
LanguageModelRegistry::test(cx);
|
||||||
|
agent_settings::init(cx);
|
||||||
});
|
});
|
||||||
cx.executor().forbid_parking();
|
cx.executor().forbid_parking();
|
||||||
|
|
||||||
|
|
|
@ -732,7 +732,17 @@ impl Thread {
|
||||||
stream.update_tool_call_fields(
|
stream.update_tool_call_fields(
|
||||||
&tool_use.id,
|
&tool_use.id,
|
||||||
acp::ToolCallUpdateFields {
|
acp::ToolCallUpdateFields {
|
||||||
status: Some(acp::ToolCallStatus::Completed),
|
status: Some(
|
||||||
|
tool_result
|
||||||
|
.as_ref()
|
||||||
|
.map_or(acp::ToolCallStatus::Failed, |result| {
|
||||||
|
if result.is_error {
|
||||||
|
acp::ToolCallStatus::Failed
|
||||||
|
} else {
|
||||||
|
acp::ToolCallStatus::Completed
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
raw_output: output,
|
raw_output: output,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -1557,7 +1567,7 @@ impl Thread {
|
||||||
tool_name: tool_use.name,
|
tool_name: tool_use.name,
|
||||||
is_error: true,
|
is_error: true,
|
||||||
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
|
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
|
||||||
output: None,
|
output: Some(error.to_string().into()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -2459,6 +2469,30 @@ impl ToolCallEventStreamReceiver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
|
||||||
|
let event = self.0.next().await;
|
||||||
|
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
|
||||||
|
update,
|
||||||
|
)))) = event
|
||||||
|
{
|
||||||
|
update.fields
|
||||||
|
} else {
|
||||||
|
panic!("Expected update fields but got: {:?}", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn expect_diff(&mut self) -> Entity<acp_thread::Diff> {
|
||||||
|
let event = self.0.next().await;
|
||||||
|
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
|
||||||
|
update,
|
||||||
|
)))) = event
|
||||||
|
{
|
||||||
|
update.diff
|
||||||
|
} else {
|
||||||
|
panic!("Expected diff but got: {:?}", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
|
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
|
||||||
let event = self.0.next().await;
|
let event = self.0.next().await;
|
||||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
|
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
|
||||||
|
|
|
@ -273,6 +273,13 @@ impl AgentTool for EditFileTool {
|
||||||
|
|
||||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||||
event_stream.update_diff(diff.clone());
|
event_stream.update_diff(diff.clone());
|
||||||
|
let _finalize_diff = util::defer({
|
||||||
|
let diff = diff.downgrade();
|
||||||
|
let mut cx = cx.clone();
|
||||||
|
move || {
|
||||||
|
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||||
let old_text = cx
|
let old_text = cx
|
||||||
|
@ -389,8 +396,6 @@ impl AgentTool for EditFileTool {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
|
|
||||||
|
|
||||||
let input_path = input.path.display();
|
let input_path = input.path.display();
|
||||||
if unified_diff.is_empty() {
|
if unified_diff.is_empty() {
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
|
@ -1545,6 +1550,100 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_diff_finalization(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let fs = project::FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/", json!({"main.rs": ""})).await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
|
||||||
|
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||||
|
let context_server_registry =
|
||||||
|
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||||
|
let model = Arc::new(FakeLanguageModel::default());
|
||||||
|
let thread = cx.new(|cx| {
|
||||||
|
Thread::new(
|
||||||
|
project.clone(),
|
||||||
|
cx.new(|_cx| ProjectContext::default()),
|
||||||
|
context_server_registry.clone(),
|
||||||
|
Templates::new(),
|
||||||
|
Some(model.clone()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the diff is finalized after the edit completes.
|
||||||
|
{
|
||||||
|
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||||
|
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||||
|
let edit = cx.update(|cx| {
|
||||||
|
tool.run(
|
||||||
|
EditFileToolInput {
|
||||||
|
display_description: "Edit file".into(),
|
||||||
|
path: path!("/main.rs").into(),
|
||||||
|
mode: EditFileMode::Edit,
|
||||||
|
},
|
||||||
|
stream_tx,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
stream_rx.expect_update_fields().await;
|
||||||
|
let diff = stream_rx.expect_diff().await;
|
||||||
|
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||||
|
cx.run_until_parked();
|
||||||
|
model.end_last_completion_stream();
|
||||||
|
edit.await.unwrap();
|
||||||
|
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the diff is finalized if an error occurs while editing.
|
||||||
|
{
|
||||||
|
model.forbid_requests();
|
||||||
|
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||||
|
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||||
|
let edit = cx.update(|cx| {
|
||||||
|
tool.run(
|
||||||
|
EditFileToolInput {
|
||||||
|
display_description: "Edit file".into(),
|
||||||
|
path: path!("/main.rs").into(),
|
||||||
|
mode: EditFileMode::Edit,
|
||||||
|
},
|
||||||
|
stream_tx,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
stream_rx.expect_update_fields().await;
|
||||||
|
let diff = stream_rx.expect_diff().await;
|
||||||
|
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||||
|
edit.await.unwrap_err();
|
||||||
|
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||||
|
model.allow_requests();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the diff is finalized if the tool call gets dropped.
|
||||||
|
{
|
||||||
|
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||||
|
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||||
|
let edit = cx.update(|cx| {
|
||||||
|
tool.run(
|
||||||
|
EditFileToolInput {
|
||||||
|
display_description: "Edit file".into(),
|
||||||
|
path: path!("/main.rs").into(),
|
||||||
|
mode: EditFileMode::Edit,
|
||||||
|
},
|
||||||
|
stream_tx,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
stream_rx.expect_update_fields().await;
|
||||||
|
let diff = stream_rx.expect_diff().await;
|
||||||
|
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||||
|
drop(edit);
|
||||||
|
cx.run_until_parked();
|
||||||
|
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn init_test(cx: &mut TestAppContext) {
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
|
|
@ -6,7 +6,7 @@ use agent2::HistoryStore;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
|
||||||
TextStyleRefinement, WeakEntity, Window,
|
TextStyleRefinement, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
|
@ -154,10 +154,22 @@ impl EntryViewState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AgentThreadEntry::AssistantMessage(_) => {
|
AgentThreadEntry::AssistantMessage(message) => {
|
||||||
if index == self.entries.len() {
|
let entry = if let Some(Entry::AssistantMessage(entry)) =
|
||||||
self.entries.push(Entry::empty())
|
self.entries.get_mut(index)
|
||||||
}
|
{
|
||||||
|
entry
|
||||||
|
} else {
|
||||||
|
self.set_entry(
|
||||||
|
index,
|
||||||
|
Entry::AssistantMessage(AssistantMessageEntry::default()),
|
||||||
|
);
|
||||||
|
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
entry
|
||||||
|
};
|
||||||
|
entry.sync(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -177,7 +189,7 @@ impl EntryViewState {
|
||||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||||
for entry in self.entries.iter() {
|
for entry in self.entries.iter() {
|
||||||
match entry {
|
match entry {
|
||||||
Entry::UserMessage { .. } => {}
|
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||||
Entry::Content(response_views) => {
|
Entry::Content(response_views) => {
|
||||||
for view in response_views.values() {
|
for view in response_views.values() {
|
||||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||||
|
@ -208,9 +220,29 @@ pub enum ViewEvent {
|
||||||
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct AssistantMessageEntry {
|
||||||
|
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantMessageEntry {
|
||||||
|
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
|
||||||
|
self.scroll_handles_by_chunk_index.get(&ix).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
|
||||||
|
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
|
||||||
|
let ix = message.chunks.len() - 1;
|
||||||
|
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
|
||||||
|
handle.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Entry {
|
pub enum Entry {
|
||||||
UserMessage(Entity<MessageEditor>),
|
UserMessage(Entity<MessageEditor>),
|
||||||
|
AssistantMessage(AssistantMessageEntry),
|
||||||
Content(HashMap<EntityId, AnyEntity>),
|
Content(HashMap<EntityId, AnyEntity>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +250,7 @@ impl Entry {
|
||||||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||||
match self {
|
match self {
|
||||||
Self::UserMessage(editor) => Some(editor),
|
Self::UserMessage(editor) => Some(editor),
|
||||||
Entry::Content(_) => None,
|
Self::AssistantMessage(_) | Self::Content(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +271,16 @@ impl Entry {
|
||||||
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_handle_for_assistant_message_chunk(
|
||||||
|
&self,
|
||||||
|
chunk_ix: usize,
|
||||||
|
) -> Option<ScrollHandle> {
|
||||||
|
match self {
|
||||||
|
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
|
||||||
|
Self::UserMessage(_) | Self::Content(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Content(map) => Some(map),
|
Self::Content(map) => Some(map),
|
||||||
|
@ -254,7 +296,7 @@ impl Entry {
|
||||||
pub fn has_content(&self) -> bool {
|
pub fn has_content(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Content(map) => !map.is_empty(),
|
Self::Content(map) => !map.is_empty(),
|
||||||
Self::UserMessage(_) => false,
|
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ use file_icons::FileIcons;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
|
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
|
||||||
EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
|
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
|
||||||
ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
|
ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
|
||||||
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
|
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||||
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
|
Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
|
||||||
prelude::*, pulsating_between,
|
point, prelude::*, pulsating_between,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ use crate::{
|
||||||
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
|
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
|
||||||
pub const MIN_EDITOR_LINES: usize = 4;
|
pub const MIN_EDITOR_LINES: usize = 4;
|
||||||
pub const MAX_EDITOR_LINES: usize = 8;
|
pub const MAX_EDITOR_LINES: usize = 8;
|
||||||
|
|
||||||
|
@ -1334,6 +1333,10 @@ impl AcpThreadView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let is_generating = self
|
||||||
|
.thread()
|
||||||
|
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
|
||||||
|
|
||||||
let primary = match &entry {
|
let primary = match &entry {
|
||||||
AgentThreadEntry::UserMessage(message) => {
|
AgentThreadEntry::UserMessage(message) => {
|
||||||
let Some(editor) = self
|
let Some(editor) = self
|
||||||
|
@ -1493,6 +1496,20 @@ impl AcpThreadView {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
||||||
|
let is_last = entry_ix + 1 == total_entries;
|
||||||
|
let pending_thinking_chunk_ix = if is_generating && is_last {
|
||||||
|
chunks
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.next_back()
|
||||||
|
.filter(|(_, segment)| {
|
||||||
|
matches!(segment, AssistantMessageChunk::Thought { .. })
|
||||||
|
})
|
||||||
|
.map(|(index, _)| index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let style = default_markdown_style(false, false, window, cx);
|
let style = default_markdown_style(false, false, window, cx);
|
||||||
let message_body = v_flex()
|
let message_body = v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -1511,6 +1528,7 @@ impl AcpThreadView {
|
||||||
entry_ix,
|
entry_ix,
|
||||||
chunk_ix,
|
chunk_ix,
|
||||||
md.clone(),
|
md.clone(),
|
||||||
|
Some(chunk_ix) == pending_thinking_chunk_ix,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -1524,7 +1542,7 @@ impl AcpThreadView {
|
||||||
v_flex()
|
v_flex()
|
||||||
.px_5()
|
.px_5()
|
||||||
.py_1()
|
.py_1()
|
||||||
.when(entry_ix + 1 == total_entries, |this| this.pb_4())
|
.when(is_last, |this| this.pb_4())
|
||||||
.w_full()
|
.w_full()
|
||||||
.text_ui(cx)
|
.text_ui(cx)
|
||||||
.child(message_body)
|
.child(message_body)
|
||||||
|
@ -1533,7 +1551,7 @@ impl AcpThreadView {
|
||||||
AgentThreadEntry::ToolCall(tool_call) => {
|
AgentThreadEntry::ToolCall(tool_call) => {
|
||||||
let has_terminals = tool_call.terminals().next().is_some();
|
let has_terminals = tool_call.terminals().next().is_some();
|
||||||
|
|
||||||
div().w_full().py_1().px_5().map(|this| {
|
div().w_full().map(|this| {
|
||||||
if has_terminals {
|
if has_terminals {
|
||||||
this.children(tool_call.terminals().map(|terminal| {
|
this.children(tool_call.terminals().map(|terminal| {
|
||||||
self.render_terminal_tool_call(
|
self.render_terminal_tool_call(
|
||||||
|
@ -1609,64 +1627,90 @@ impl AcpThreadView {
|
||||||
entry_ix: usize,
|
entry_ix: usize,
|
||||||
chunk_ix: usize,
|
chunk_ix: usize,
|
||||||
chunk: Entity<Markdown>,
|
chunk: Entity<Markdown>,
|
||||||
|
pending: bool,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
|
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
|
||||||
let card_header_id = SharedString::from("inner-card-header");
|
let card_header_id = SharedString::from("inner-card-header");
|
||||||
|
|
||||||
let key = (entry_ix, chunk_ix);
|
let key = (entry_ix, chunk_ix);
|
||||||
|
|
||||||
let is_open = self.expanded_thinking_blocks.contains(&key);
|
let is_open = self.expanded_thinking_blocks.contains(&key);
|
||||||
|
let editor_bg = cx.theme().colors().editor_background;
|
||||||
|
let gradient_overlay = div()
|
||||||
|
.rounded_b_lg()
|
||||||
|
.h_full()
|
||||||
|
.absolute()
|
||||||
|
.w_full()
|
||||||
|
.bottom_0()
|
||||||
|
.left_0()
|
||||||
|
.bg(linear_gradient(
|
||||||
|
180.,
|
||||||
|
linear_color_stop(editor_bg, 1.),
|
||||||
|
linear_color_stop(editor_bg.opacity(0.2), 0.),
|
||||||
|
));
|
||||||
|
|
||||||
|
let scroll_handle = self
|
||||||
|
.entry_view_state
|
||||||
|
.read(cx)
|
||||||
|
.entry(entry_ix)
|
||||||
|
.and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.rounded_md()
|
||||||
|
.border_1()
|
||||||
|
.border_color(self.tool_card_border_color(cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(header_id)
|
.id(header_id)
|
||||||
.group(&card_header_id)
|
.group(&card_header_id)
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1p5()
|
.py_0p5()
|
||||||
|
.px_1p5()
|
||||||
|
.rounded_t_md()
|
||||||
|
.bg(self.tool_card_header_bg(cx))
|
||||||
|
.justify_between()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(self.tool_card_border_color(cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.size_4()
|
.h(window.line_height())
|
||||||
.justify_center()
|
.gap_1p5()
|
||||||
.child(
|
.child(
|
||||||
div()
|
Icon::new(IconName::ToolThink)
|
||||||
.group_hover(&card_header_id, |s| s.invisible().w_0())
|
.size(IconSize::Small)
|
||||||
.child(
|
.color(Color::Muted),
|
||||||
Icon::new(IconName::ToolThink)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
div()
|
||||||
.absolute()
|
.text_size(self.tool_name_font_size())
|
||||||
.inset_0()
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.invisible()
|
.map(|this| {
|
||||||
.justify_center()
|
if pending {
|
||||||
.group_hover(&card_header_id, |s| s.visible())
|
this.child("Thinking")
|
||||||
.child(
|
} else {
|
||||||
Disclosure::new(("expand", entry_ix), is_open)
|
this.child("Thought Process")
|
||||||
.opened_icon(IconName::ChevronUp)
|
}
|
||||||
.closed_icon(IconName::ChevronRight)
|
}),
|
||||||
.on_click(cx.listener({
|
|
||||||
move |this, _event, _window, cx| {
|
|
||||||
if is_open {
|
|
||||||
this.expanded_thinking_blocks.remove(&key);
|
|
||||||
} else {
|
|
||||||
this.expanded_thinking_blocks.insert(key);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
Disclosure::new(("expand", entry_ix), is_open)
|
||||||
.text_size(self.tool_name_font_size())
|
.opened_icon(IconName::ChevronUp)
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.closed_icon(IconName::ChevronDown)
|
||||||
.child("Thinking"),
|
.visible_on_hover(&card_header_id)
|
||||||
|
.on_click(cx.listener({
|
||||||
|
move |this, _event, _window, cx| {
|
||||||
|
if is_open {
|
||||||
|
this.expanded_thinking_blocks.remove(&key);
|
||||||
|
} else {
|
||||||
|
this.expanded_thinking_blocks.insert(key);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
move |this, _event, _window, cx| {
|
move |this, _event, _window, cx| {
|
||||||
|
@ -1679,22 +1723,28 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when(is_open, |this| {
|
.child(
|
||||||
this.child(
|
div()
|
||||||
div()
|
.relative()
|
||||||
.relative()
|
.bg(editor_bg)
|
||||||
.mt_1p5()
|
.rounded_b_lg()
|
||||||
.ml(px(7.))
|
.child(
|
||||||
.pl_4()
|
div()
|
||||||
.border_l_1()
|
.id(("thinking-content", chunk_ix))
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.when_some(scroll_handle, |this, scroll_handle| {
|
||||||
.text_ui_sm(cx)
|
this.track_scroll(&scroll_handle)
|
||||||
.child(self.render_markdown(
|
})
|
||||||
chunk,
|
.p_2()
|
||||||
default_markdown_style(false, false, window, cx),
|
.when(!is_open, |this| this.max_h_20())
|
||||||
)),
|
.text_ui_sm(cx)
|
||||||
)
|
.overflow_hidden()
|
||||||
})
|
.child(self.render_markdown(
|
||||||
|
chunk,
|
||||||
|
default_markdown_style(false, false, window, cx),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.when(!is_open && pending, |this| this.child(gradient_overlay)),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1705,7 +1755,6 @@ impl AcpThreadView {
|
||||||
window: &Window,
|
window: &Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
|
|
||||||
let card_header_id = SharedString::from("inner-tool-call-header");
|
let card_header_id = SharedString::from("inner-tool-call-header");
|
||||||
|
|
||||||
let tool_icon =
|
let tool_icon =
|
||||||
|
@ -1734,11 +1783,7 @@ impl AcpThreadView {
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let failed_tool_call = matches!(
|
let has_location = tool_call.locations.len() == 1;
|
||||||
tool_call.status,
|
|
||||||
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
|
|
||||||
);
|
|
||||||
|
|
||||||
let needs_confirmation = matches!(
|
let needs_confirmation = matches!(
|
||||||
tool_call.status,
|
tool_call.status,
|
||||||
ToolCallStatus::WaitingForConfirmation { .. }
|
ToolCallStatus::WaitingForConfirmation { .. }
|
||||||
|
@ -1751,23 +1796,31 @@ impl AcpThreadView {
|
||||||
|
|
||||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||||
|
|
||||||
let gradient_overlay = |color: Hsla| {
|
let gradient_overlay = {
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_0()
|
.top_0()
|
||||||
.right_0()
|
.right_0()
|
||||||
.w_12()
|
.w_12()
|
||||||
.h_full()
|
.h_full()
|
||||||
.bg(linear_gradient(
|
.map(|this| {
|
||||||
90.,
|
if use_card_layout {
|
||||||
linear_color_stop(color, 1.),
|
this.bg(linear_gradient(
|
||||||
linear_color_stop(color.opacity(0.2), 0.),
|
90.,
|
||||||
))
|
linear_color_stop(self.tool_card_header_bg(cx), 1.),
|
||||||
};
|
linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
|
||||||
let gradient_color = if use_card_layout {
|
))
|
||||||
self.tool_card_header_bg(cx)
|
} else {
|
||||||
} else {
|
this.bg(linear_gradient(
|
||||||
cx.theme().colors().panel_background
|
90.,
|
||||||
|
linear_color_stop(cx.theme().colors().panel_background, 1.),
|
||||||
|
linear_color_stop(
|
||||||
|
cx.theme().colors().panel_background.opacity(0.2),
|
||||||
|
0.,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let tool_output_display = if is_open {
|
let tool_output_display = if is_open {
|
||||||
|
@ -1818,40 +1871,58 @@ impl AcpThreadView {
|
||||||
};
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.when(use_card_layout, |this| {
|
.map(|this| {
|
||||||
this.rounded_md()
|
if use_card_layout {
|
||||||
.border_1()
|
this.my_2()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.rounded_md()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.border_1()
|
||||||
.overflow_hidden()
|
.border_color(self.tool_card_border_color(cx))
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.overflow_hidden()
|
||||||
|
} else {
|
||||||
|
this.my_1()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.map(|this| {
|
||||||
|
if has_location && !use_card_layout {
|
||||||
|
this.ml_4()
|
||||||
|
} else {
|
||||||
|
this.ml_5()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.mr_5()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(header_id)
|
|
||||||
.group(&card_header_id)
|
.group(&card_header_id)
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
.justify_between()
|
||||||
.when(use_card_layout, |this| {
|
.when(use_card_layout, |this| {
|
||||||
this.pl_1p5()
|
this.p_0p5()
|
||||||
.pr_1()
|
|
||||||
.py_0p5()
|
|
||||||
.rounded_t_md()
|
.rounded_t_md()
|
||||||
.when(is_open && !failed_tool_call, |this| {
|
.bg(self.tool_card_header_bg(cx))
|
||||||
|
.when(is_open && !failed_or_canceled, |this| {
|
||||||
this.border_b_1()
|
this.border_b_1()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.border_color(self.tool_card_border_color(cx))
|
||||||
})
|
})
|
||||||
.bg(self.tool_card_header_bg(cx))
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(window.line_height() - px(2.))
|
.h(window.line_height())
|
||||||
.text_size(self.tool_name_font_size())
|
.text_size(self.tool_name_font_size())
|
||||||
|
.gap_1p5()
|
||||||
|
.when(has_location || use_card_layout, |this| this.px_1())
|
||||||
|
.when(has_location, |this| {
|
||||||
|
this.cursor(CursorStyle::PointingHand)
|
||||||
|
.rounded_sm()
|
||||||
|
.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||||
|
})
|
||||||
|
.overflow_hidden()
|
||||||
.child(tool_icon)
|
.child(tool_icon)
|
||||||
.child(if tool_call.locations.len() == 1 {
|
.child(if has_location {
|
||||||
let name = tool_call.locations[0]
|
let name = tool_call.locations[0]
|
||||||
.path
|
.path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -1862,13 +1933,6 @@ impl AcpThreadView {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(("open-tool-call-location", entry_ix))
|
.id(("open-tool-call-location", entry_ix))
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
|
||||||
.px_1p5()
|
|
||||||
.rounded_sm()
|
|
||||||
.overflow_x_scroll()
|
|
||||||
.hover(|label| {
|
|
||||||
label.bg(cx.theme().colors().element_hover.opacity(0.5))
|
|
||||||
})
|
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if use_card_layout {
|
if use_card_layout {
|
||||||
this.text_color(cx.theme().colors().text)
|
this.text_color(cx.theme().colors().text)
|
||||||
|
@ -1878,31 +1942,28 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
.child(name)
|
.child(name)
|
||||||
.tooltip(Tooltip::text("Jump to File"))
|
.tooltip(Tooltip::text("Jump to File"))
|
||||||
.cursor(gpui::CursorStyle::PointingHand)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
h_flex()
|
h_flex()
|
||||||
.relative()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
.child(self.render_markdown(
|
||||||
.ml_1p5()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(h_flex().pr_8().child(self.render_markdown(
|
|
||||||
tool_call.label.clone(),
|
tool_call.label.clone(),
|
||||||
default_markdown_style(false, true, window, cx),
|
default_markdown_style(false, true, window, cx),
|
||||||
)))
|
))
|
||||||
.child(gradient_overlay(gradient_color))
|
|
||||||
.into_any()
|
.into_any()
|
||||||
}),
|
})
|
||||||
|
.when(!has_location, |this| this.child(gradient_overlay)),
|
||||||
)
|
)
|
||||||
.child(
|
.when(is_collapsible || failed_or_canceled, |this| {
|
||||||
h_flex()
|
this.child(
|
||||||
.gap_px()
|
h_flex()
|
||||||
.when(is_collapsible, |this| {
|
.px_1()
|
||||||
this.child(
|
.gap_px()
|
||||||
|
.when(is_collapsible, |this| {
|
||||||
|
this.child(
|
||||||
Disclosure::new(("expand", entry_ix), is_open)
|
Disclosure::new(("expand", entry_ix), is_open)
|
||||||
.opened_icon(IconName::ChevronUp)
|
.opened_icon(IconName::ChevronUp)
|
||||||
.closed_icon(IconName::ChevronDown)
|
.closed_icon(IconName::ChevronDown)
|
||||||
|
@ -1919,15 +1980,16 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(failed_or_canceled, |this| {
|
.when(failed_or_canceled, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Icon::new(IconName::Close)
|
Icon::new(IconName::Close)
|
||||||
.color(Color::Error)
|
.color(Color::Error)
|
||||||
.size(IconSize::Small),
|
.size(IconSize::Small),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.children(tool_output_display)
|
.children(tool_output_display)
|
||||||
}
|
}
|
||||||
|
@ -1968,7 +2030,7 @@ impl AcpThreadView {
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_1p5()
|
.mt_1p5()
|
||||||
.ml(px(7.))
|
.ml(rems(0.4))
|
||||||
.px_3p5()
|
.px_3p5()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
|
@ -2025,7 +2087,7 @@ impl AcpThreadView {
|
||||||
let button_id = SharedString::from(format!("item-{}", uri));
|
let button_id = SharedString::from(format!("item-{}", uri));
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.ml(px(7.))
|
.ml(rems(0.4))
|
||||||
.pl_2p5()
|
.pl_2p5()
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.border_color(self.tool_card_border_color(cx))
|
||||||
|
@ -2213,6 +2275,12 @@ impl AcpThreadView {
|
||||||
started_at.elapsed()
|
started_at.elapsed()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let header_id =
|
||||||
|
SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
|
||||||
|
let header_group = SharedString::from(format!(
|
||||||
|
"terminal-tool-header-group-{}",
|
||||||
|
terminal.entity_id()
|
||||||
|
));
|
||||||
let header_bg = cx
|
let header_bg = cx
|
||||||
.theme()
|
.theme()
|
||||||
.colors()
|
.colors()
|
||||||
|
@ -2228,10 +2296,7 @@ impl AcpThreadView {
|
||||||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||||
|
|
||||||
let header = h_flex()
|
let header = h_flex()
|
||||||
.id(SharedString::from(format!(
|
.id(header_id)
|
||||||
"terminal-tool-header-{}",
|
|
||||||
terminal.entity_id()
|
|
||||||
)))
|
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
@ -2295,23 +2360,6 @@ impl AcpThreadView {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(tool_failed || command_failed, |header| {
|
|
||||||
header.child(
|
|
||||||
div()
|
|
||||||
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Close)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Error),
|
|
||||||
)
|
|
||||||
.when_some(output.and_then(|o| o.exit_status), |this, status| {
|
|
||||||
this.tooltip(Tooltip::text(format!(
|
|
||||||
"Exited with code {}",
|
|
||||||
status.code().unwrap_or(-1),
|
|
||||||
)))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(truncated_output, |header| {
|
.when(truncated_output, |header| {
|
||||||
let tooltip = if let Some(output) = output {
|
let tooltip = if let Some(output) = output {
|
||||||
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
||||||
|
@ -2364,6 +2412,7 @@ impl AcpThreadView {
|
||||||
)
|
)
|
||||||
.opened_icon(IconName::ChevronUp)
|
.opened_icon(IconName::ChevronUp)
|
||||||
.closed_icon(IconName::ChevronDown)
|
.closed_icon(IconName::ChevronDown)
|
||||||
|
.visible_on_hover(&header_group)
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
let id = tool_call.id.clone();
|
let id = tool_call.id.clone();
|
||||||
move |this, _event, _window, _cx| {
|
move |this, _event, _window, _cx| {
|
||||||
|
@ -2372,8 +2421,26 @@ impl AcpThreadView {
|
||||||
} else {
|
} else {
|
||||||
this.expanded_tool_calls.insert(id.clone());
|
this.expanded_tool_calls.insert(id.clone());
|
||||||
}
|
}
|
||||||
}})),
|
}
|
||||||
);
|
})),
|
||||||
|
)
|
||||||
|
.when(tool_failed || command_failed, |header| {
|
||||||
|
header.child(
|
||||||
|
div()
|
||||||
|
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Close)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Error),
|
||||||
|
)
|
||||||
|
.when_some(output.and_then(|o| o.exit_status), |this, status| {
|
||||||
|
this.tooltip(Tooltip::text(format!(
|
||||||
|
"Exited with code {}",
|
||||||
|
status.code().unwrap_or(-1),
|
||||||
|
)))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let terminal_view = self
|
let terminal_view = self
|
||||||
.entry_view_state
|
.entry_view_state
|
||||||
|
@ -2383,7 +2450,8 @@ impl AcpThreadView {
|
||||||
let show_output = is_expanded && terminal_view.is_some();
|
let show_output = is_expanded && terminal_view.is_some();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mb_2()
|
.my_2()
|
||||||
|
.mx_5()
|
||||||
.border_1()
|
.border_1()
|
||||||
.when(tool_failed || command_failed, |card| card.border_dashed())
|
.when(tool_failed || command_failed, |card| card.border_dashed())
|
||||||
.border_color(border_color)
|
.border_color(border_color)
|
||||||
|
@ -2391,9 +2459,10 @@ impl AcpThreadView {
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.group(&header_group)
|
||||||
.py_1p5()
|
.py_1p5()
|
||||||
.pl_2()
|
|
||||||
.pr_1p5()
|
.pr_1p5()
|
||||||
|
.pl_2()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.bg(header_bg)
|
.bg(header_bg)
|
||||||
.text_xs()
|
.text_xs()
|
||||||
|
@ -4152,13 +4221,14 @@ impl AcpThreadView {
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||||
if is_generating {
|
if is_generating {
|
||||||
return h_flex().id("thread-controls-container").ml_1().child(
|
return h_flex().id("thread-controls-container").child(
|
||||||
div()
|
div()
|
||||||
.py_2()
|
.py_2()
|
||||||
.px(rems_from_px(22.))
|
.px_5()
|
||||||
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
@ -4184,12 +4254,10 @@ impl AcpThreadView {
|
||||||
.id("thread-controls-container")
|
.id("thread-controls-container")
|
||||||
.group("thread-controls-container")
|
.group("thread-controls-container")
|
||||||
.w_full()
|
.w_full()
|
||||||
.mr_1()
|
.py_2()
|
||||||
.pt_1()
|
.px_5()
|
||||||
.pb_2()
|
|
||||||
.px(RESPONSE_PADDING_X)
|
|
||||||
.gap_px()
|
.gap_px()
|
||||||
.opacity(0.4)
|
.opacity(0.6)
|
||||||
.hover(|style| style.opacity(1.))
|
.hover(|style| style.opacity(1.))
|
||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.justify_end();
|
.justify_end();
|
||||||
|
@ -4200,56 +4268,50 @@ impl AcpThreadView {
|
||||||
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
|
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
|
||||||
{
|
{
|
||||||
let feedback = self.thread_feedback.feedback;
|
let feedback = self.thread_feedback.feedback;
|
||||||
container = container.child(
|
|
||||||
div().visible_on_hover("thread-controls-container").child(
|
container = container
|
||||||
Label::new(
|
.child(
|
||||||
match feedback {
|
div().visible_on_hover("thread-controls-container").child(
|
||||||
|
Label::new(match feedback {
|
||||||
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
|
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
|
||||||
Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.",
|
Some(ThreadFeedback::Negative) => {
|
||||||
None => "Rating the thread sends all of your current conversation to the Zed team.",
|
"We appreciate your feedback and will use it to improve."
|
||||||
}
|
}
|
||||||
)
|
None => {
|
||||||
.color(Color::Muted)
|
"Rating the thread sends all of your current conversation to the Zed team."
|
||||||
.size(LabelSize::XSmall)
|
}
|
||||||
.truncate(),
|
})
|
||||||
),
|
.color(Color::Muted)
|
||||||
).child(
|
.size(LabelSize::XSmall)
|
||||||
h_flex()
|
.truncate(),
|
||||||
.child(
|
),
|
||||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.child(
|
||||||
.icon_size(IconSize::Small)
|
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||||
.icon_color(match feedback {
|
.shape(ui::IconButtonShape::Square)
|
||||||
Some(ThreadFeedback::Positive) => Color::Accent,
|
.icon_size(IconSize::Small)
|
||||||
_ => Color::Ignored,
|
.icon_color(match feedback {
|
||||||
})
|
Some(ThreadFeedback::Positive) => Color::Accent,
|
||||||
.tooltip(Tooltip::text("Helpful Response"))
|
_ => Color::Ignored,
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
})
|
||||||
this.handle_feedback_click(
|
.tooltip(Tooltip::text("Helpful Response"))
|
||||||
ThreadFeedback::Positive,
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
window,
|
this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
|
||||||
cx,
|
})),
|
||||||
);
|
)
|
||||||
})),
|
.child(
|
||||||
)
|
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
||||||
.child(
|
.shape(ui::IconButtonShape::Square)
|
||||||
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
.icon_size(IconSize::Small)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.icon_color(match feedback {
|
||||||
.icon_size(IconSize::Small)
|
Some(ThreadFeedback::Negative) => Color::Accent,
|
||||||
.icon_color(match feedback {
|
_ => Color::Ignored,
|
||||||
Some(ThreadFeedback::Negative) => Color::Accent,
|
})
|
||||||
_ => Color::Ignored,
|
.tooltip(Tooltip::text("Not Helpful"))
|
||||||
})
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
.tooltip(Tooltip::text("Not Helpful"))
|
this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
})),
|
||||||
this.handle_feedback_click(
|
);
|
||||||
ThreadFeedback::Negative,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
container.child(open_as_markdown).child(scroll_to_top)
|
container.child(open_as_markdown).child(scroll_to_top)
|
||||||
|
|
|
@ -3,20 +3,23 @@ mod configure_context_server_modal;
|
||||||
mod manage_profiles_modal;
|
mod manage_profiles_modal;
|
||||||
mod tool_picker;
|
mod tool_picker;
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{ops::Range, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
|
use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
|
use anyhow::Result;
|
||||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
use cloud_llm_client::Plan;
|
use cloud_llm_client::Plan;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_server::ContextServerId;
|
use context_server::ContextServerId;
|
||||||
|
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||||
use extension::ExtensionManifest;
|
use extension::ExtensionManifest;
|
||||||
use extension_host::ExtensionStore;
|
use extension_host::ExtensionStore;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
|
||||||
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
|
||||||
|
WeakEntity, percentage,
|
||||||
};
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
|
@ -34,7 +37,7 @@ use ui::{
|
||||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::Workspace;
|
use workspace::{Workspace, create_and_open_local_file};
|
||||||
use zed_actions::ExtensionCategoryFilter;
|
use zed_actions::ExtensionCategoryFilter;
|
||||||
|
|
||||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||||
|
@ -1058,7 +1061,36 @@ impl AgentConfiguration {
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.child(Headline::new("External Agents"))
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.justify_between()
|
||||||
|
.child(Headline::new("External Agents"))
|
||||||
|
.child(
|
||||||
|
Button::new("add-agent", "Add Agent")
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.on_click(
|
||||||
|
move |_, window, cx| {
|
||||||
|
if let Some(workspace) = window.root().flatten() {
|
||||||
|
let workspace = workspace.downgrade();
|
||||||
|
window
|
||||||
|
.spawn(cx, async |cx| {
|
||||||
|
open_new_agent_servers_entry_in_settings_editor(
|
||||||
|
workspace,
|
||||||
|
cx,
|
||||||
|
).await
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
Label::new(
|
Label::new(
|
||||||
"Bring the agent of your choice to Zed via our new Agent Client Protocol",
|
"Bring the agent of your choice to Zed via our new Agent Client Protocol",
|
||||||
|
@ -1324,3 +1356,109 @@ fn show_unable_to_uninstall_extension_with_context_server(
|
||||||
|
|
||||||
workspace.toggle_status_toast(status_toast, cx);
|
workspace.toggle_status_toast(status_toast, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn open_new_agent_servers_entry_in_settings_editor(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
cx: &mut AsyncWindowContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let settings_editor = workspace
|
||||||
|
.update_in(cx, |_, window, cx| {
|
||||||
|
create_and_open_local_file(paths::settings_file(), window, cx, || {
|
||||||
|
settings::initial_user_settings_content().as_ref().into()
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.await?
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
settings_editor
|
||||||
|
.downgrade()
|
||||||
|
.update_in(cx, |item, window, cx| {
|
||||||
|
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||||
|
|
||||||
|
let settings = cx.global::<SettingsStore>();
|
||||||
|
|
||||||
|
let mut unique_server_name = None;
|
||||||
|
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
|
||||||
|
let server_name: Option<SharedString> = (0..u8::MAX)
|
||||||
|
.map(|i| {
|
||||||
|
if i == 0 {
|
||||||
|
"your_agent".into()
|
||||||
|
} else {
|
||||||
|
format!("your_agent_{}", i).into()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.find(|name| !file.custom.contains_key(name));
|
||||||
|
if let Some(server_name) = server_name {
|
||||||
|
unique_server_name = Some(server_name.clone());
|
||||||
|
file.custom.insert(
|
||||||
|
server_name,
|
||||||
|
AgentServerSettings {
|
||||||
|
command: AgentServerCommand {
|
||||||
|
path: "path_to_executable".into(),
|
||||||
|
args: vec![],
|
||||||
|
env: Some(HashMap::default()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if edits.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ranges = edits
|
||||||
|
.iter()
|
||||||
|
.map(|(range, _)| range.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
item.edit(edits, cx);
|
||||||
|
if let Some((unique_server_name, buffer)) =
|
||||||
|
unique_server_name.zip(item.buffer().read(cx).as_singleton())
|
||||||
|
{
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
if let Some(range) =
|
||||||
|
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
|
||||||
|
{
|
||||||
|
item.change_selections(
|
||||||
|
SelectionEffects::scroll(Autoscroll::newest()),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
|selections| {
|
||||||
|
selections.select_ranges(vec![range]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_text_in_buffer(
|
||||||
|
text: &str,
|
||||||
|
start: usize,
|
||||||
|
snapshot: &language::BufferSnapshot,
|
||||||
|
) -> Option<Range<usize>> {
|
||||||
|
let chars = text.chars().collect::<Vec<char>>();
|
||||||
|
|
||||||
|
let mut offset = start;
|
||||||
|
let mut char_offset = 0;
|
||||||
|
for c in snapshot.chars_at(start) {
|
||||||
|
if char_offset >= chars.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
if c == chars[char_offset] {
|
||||||
|
char_offset += 1;
|
||||||
|
} else {
|
||||||
|
char_offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if char_offset == chars.len() {
|
||||||
|
Some(offset.saturating_sub(chars.len())..offset)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -598,17 +598,6 @@ impl AgentPanel {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wait for the Gemini/Native feature flag to be available.
|
|
||||||
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
|
|
||||||
if !client.status().borrow().is_signed_out() {
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
|
|
||||||
Duration::from_secs(2),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| {
|
let panel = cx.new(|cx| {
|
||||||
Self::new(
|
Self::new(
|
||||||
|
|
|
@ -6,7 +6,8 @@ use feature_flags::ZedProFeatureFlag;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||||
|
LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
|
@ -76,6 +77,7 @@ pub struct LanguageModelPickerDelegate {
|
||||||
all_models: Arc<GroupedModels>,
|
all_models: Arc<GroupedModels>,
|
||||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
_authenticate_all_providers_task: Task<()>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +98,7 @@ impl LanguageModelPickerDelegate {
|
||||||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||||
filtered_entries: entries,
|
filtered_entries: entries,
|
||||||
get_active_model: Arc::new(get_active_model),
|
get_active_model: Arc::new(get_active_model),
|
||||||
|
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||||
_subscriptions: vec![cx.subscribe_in(
|
_subscriptions: vec![cx.subscribe_in(
|
||||||
&LanguageModelRegistry::global(cx),
|
&LanguageModelRegistry::global(cx),
|
||||||
window,
|
window,
|
||||||
|
@ -139,6 +142,56 @@ impl LanguageModelPickerDelegate {
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||||
|
///
|
||||||
|
/// We do this so that we can populate the language selector with all of the
|
||||||
|
/// models from the configured providers.
|
||||||
|
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||||
|
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.providers()
|
||||||
|
.iter()
|
||||||
|
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.spawn(async move |_cx| {
|
||||||
|
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
||||||
|
if let Err(err) = authenticate_task.await {
|
||||||
|
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
||||||
|
// Since we're authenticating these providers in the
|
||||||
|
// background for the purposes of populating the
|
||||||
|
// language selector, we don't care about providers
|
||||||
|
// where the credentials are not found.
|
||||||
|
} else {
|
||||||
|
// Some providers have noisy failure states that we
|
||||||
|
// don't want to spam the logs with every time the
|
||||||
|
// language model selector is initialized.
|
||||||
|
//
|
||||||
|
// Ideally these should have more clear failure modes
|
||||||
|
// that we know are safe to ignore here, like what we do
|
||||||
|
// with `CredentialsNotFound` above.
|
||||||
|
match provider_id.0.as_ref() {
|
||||||
|
"lmstudio" | "ollama" => {
|
||||||
|
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||||
|
//
|
||||||
|
// These fail noisily, so we don't log them.
|
||||||
|
}
|
||||||
|
"copilot_chat" => {
|
||||||
|
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to authenticate provider: {}: {err}",
|
||||||
|
provider_name.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||||
(self.get_active_model)(cx)
|
(self.get_active_model)(cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,10 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
|
||||||
// integration too, and we'd like to turn Gemini/Native on in new builds
|
// integration too, and we'd like to turn Gemini/Native on in new builds
|
||||||
// without enabling Claude Code in old builds.
|
// without enabling Claude Code in old builds.
|
||||||
const NAME: &'static str = "gemini-and-native";
|
const NAME: &'static str = "gemini-and-native";
|
||||||
|
|
||||||
|
fn enabled_for_all() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClaudeCodeFeatureFlag;
|
pub struct ClaudeCodeFeatureFlag;
|
||||||
|
@ -201,7 +205,7 @@ impl FeatureFlagAppExt for App {
|
||||||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||||
self.try_global::<FeatureFlags>()
|
self.try_global::<FeatureFlags>()
|
||||||
.map(|flags| flags.has_flag::<T>())
|
.map(|flags| flags.has_flag::<T>())
|
||||||
.unwrap_or(false)
|
.unwrap_or(T::enabled_for_all())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_staff(&self) -> bool {
|
fn is_staff(&self) -> bool {
|
||||||
|
|
|
@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
|
||||||
is_enabled
|
is_enabled
|
||||||
.then(|| {
|
.then(|| {
|
||||||
let ConfiguredModel { provider, model } =
|
let ConfiguredModel { provider, model } =
|
||||||
LanguageModelRegistry::read_global(cx).commit_message_model(cx)?;
|
LanguageModelRegistry::read_global(cx).commit_message_model()?;
|
||||||
|
|
||||||
provider.is_authenticated(cx).then(|| model)
|
provider.is_authenticated(cx).then(|| model)
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,12 +4,16 @@ use crate::{
|
||||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||||
LanguageModelRequest, LanguageModelToolChoice,
|
LanguageModelRequest, LanguageModelToolChoice,
|
||||||
};
|
};
|
||||||
|
use anyhow::anyhow;
|
||||||
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
|
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
|
||||||
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
|
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
|
||||||
use http_client::Result;
|
use http_client::Result;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::sync::Arc;
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering::SeqCst},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FakeLanguageModelProvider {
|
pub struct FakeLanguageModelProvider {
|
||||||
|
@ -106,6 +110,7 @@ pub struct FakeLanguageModel {
|
||||||
>,
|
>,
|
||||||
)>,
|
)>,
|
||||||
>,
|
>,
|
||||||
|
forbid_requests: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FakeLanguageModel {
|
impl Default for FakeLanguageModel {
|
||||||
|
@ -114,11 +119,20 @@ impl Default for FakeLanguageModel {
|
||||||
provider_id: LanguageModelProviderId::from("fake".to_string()),
|
provider_id: LanguageModelProviderId::from("fake".to_string()),
|
||||||
provider_name: LanguageModelProviderName::from("Fake".to_string()),
|
provider_name: LanguageModelProviderName::from("Fake".to_string()),
|
||||||
current_completion_txs: Mutex::new(Vec::new()),
|
current_completion_txs: Mutex::new(Vec::new()),
|
||||||
|
forbid_requests: AtomicBool::new(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeLanguageModel {
|
impl FakeLanguageModel {
|
||||||
|
pub fn allow_requests(&self) {
|
||||||
|
self.forbid_requests.store(false, SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forbid_requests(&self) {
|
||||||
|
self.forbid_requests.store(true, SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||||
self.current_completion_txs
|
self.current_completion_txs
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -251,9 +265,18 @@ impl LanguageModel for FakeLanguageModel {
|
||||||
LanguageModelCompletionError,
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
if self.forbid_requests.load(SeqCst) {
|
||||||
self.current_completion_txs.lock().push((request, tx));
|
async move {
|
||||||
async move { Ok(rx.boxed()) }.boxed()
|
Err(LanguageModelCompletionError::Other(anyhow!(
|
||||||
|
"requests are forbidden"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
let (tx, rx) = mpsc::unbounded();
|
||||||
|
self.current_completion_txs.lock().push((request, tx));
|
||||||
|
async move { Ok(rx.boxed()) }.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_fake(&self) -> &Self {
|
fn as_fake(&self) -> &Self {
|
||||||
|
|
|
@ -6,6 +6,7 @@ use collections::BTreeMap;
|
||||||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||||
use std::{str::FromStr, sync::Arc};
|
use std::{str::FromStr, sync::Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use util::maybe;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
||||||
|
@ -41,9 +42,7 @@ impl std::fmt::Debug for ConfigurationError {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct LanguageModelRegistry {
|
pub struct LanguageModelRegistry {
|
||||||
default_model: Option<ConfiguredModel>,
|
default_model: Option<ConfiguredModel>,
|
||||||
/// This model is automatically configured by a user's environment after
|
default_fast_model: Option<ConfiguredModel>,
|
||||||
/// authenticating all providers. It's only used when default_model is not available.
|
|
||||||
environment_fallback_model: Option<ConfiguredModel>,
|
|
||||||
inline_assistant_model: Option<ConfiguredModel>,
|
inline_assistant_model: Option<ConfiguredModel>,
|
||||||
commit_message_model: Option<ConfiguredModel>,
|
commit_message_model: Option<ConfiguredModel>,
|
||||||
thread_summary_model: Option<ConfiguredModel>,
|
thread_summary_model: Option<ConfiguredModel>,
|
||||||
|
@ -99,6 +98,9 @@ impl ConfiguredModel {
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
DefaultModelChanged,
|
DefaultModelChanged,
|
||||||
|
InlineAssistantModelChanged,
|
||||||
|
CommitMessageModelChanged,
|
||||||
|
ThreadSummaryModelChanged,
|
||||||
ProviderStateChanged(LanguageModelProviderId),
|
ProviderStateChanged(LanguageModelProviderId),
|
||||||
AddedProvider(LanguageModelProviderId),
|
AddedProvider(LanguageModelProviderId),
|
||||||
RemovedProvider(LanguageModelProviderId),
|
RemovedProvider(LanguageModelProviderId),
|
||||||
|
@ -224,7 +226,7 @@ impl LanguageModelRegistry {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||||
self.set_inline_assistant_model(configured_model);
|
self.set_inline_assistant_model(configured_model, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_commit_message_model(
|
pub fn select_commit_message_model(
|
||||||
|
@ -233,7 +235,7 @@ impl LanguageModelRegistry {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||||
self.set_commit_message_model(configured_model);
|
self.set_commit_message_model(configured_model, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_thread_summary_model(
|
pub fn select_thread_summary_model(
|
||||||
|
@ -242,7 +244,7 @@ impl LanguageModelRegistry {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||||
self.set_thread_summary_model(configured_model);
|
self.set_thread_summary_model(configured_model, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects and sets the inline alternatives for language models based on
|
/// Selects and sets the inline alternatives for language models based on
|
||||||
|
@ -276,60 +278,68 @@ impl LanguageModelRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
||||||
match (self.default_model(), model.as_ref()) {
|
match (self.default_model.as_ref(), model.as_ref()) {
|
||||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||||
(None, None) => {}
|
(None, None) => {}
|
||||||
_ => cx.emit(Event::DefaultModelChanged),
|
_ => cx.emit(Event::DefaultModelChanged),
|
||||||
}
|
}
|
||||||
|
self.default_fast_model = maybe!({
|
||||||
|
let provider = &model.as_ref()?.provider;
|
||||||
|
let fast_model = provider.default_fast_model(cx)?;
|
||||||
|
Some(ConfiguredModel {
|
||||||
|
provider: provider.clone(),
|
||||||
|
model: fast_model,
|
||||||
|
})
|
||||||
|
});
|
||||||
self.default_model = model;
|
self.default_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_environment_fallback_model(
|
pub fn set_inline_assistant_model(
|
||||||
&mut self,
|
&mut self,
|
||||||
model: Option<ConfiguredModel>,
|
model: Option<ConfiguredModel>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.default_model.is_none() {
|
match (self.inline_assistant_model.as_ref(), model.as_ref()) {
|
||||||
match (self.environment_fallback_model.as_ref(), model.as_ref()) {
|
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
(None, None) => {}
|
||||||
(None, None) => {}
|
_ => cx.emit(Event::InlineAssistantModelChanged),
|
||||||
_ => cx.emit(Event::DefaultModelChanged),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.environment_fallback_model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_inline_assistant_model(&mut self, model: Option<ConfiguredModel>) {
|
|
||||||
self.inline_assistant_model = model;
|
self.inline_assistant_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_commit_message_model(&mut self, model: Option<ConfiguredModel>) {
|
pub fn set_commit_message_model(
|
||||||
|
&mut self,
|
||||||
|
model: Option<ConfiguredModel>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
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;
|
self.commit_message_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_thread_summary_model(&mut self, model: Option<ConfiguredModel>) {
|
pub fn set_thread_summary_model(
|
||||||
|
&mut self,
|
||||||
|
model: Option<ConfiguredModel>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
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;
|
self.thread_summary_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
pub fn default_model(&self) -> Option<ConfiguredModel> {
|
pub fn default_model(&self) -> Option<ConfiguredModel> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.default_model
|
self.default_model.clone()
|
||||||
.clone()
|
|
||||||
.or_else(|| self.environment_fallback_model.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_fast_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
|
||||||
let provider = self.default_model()?.provider;
|
|
||||||
let fast_model = provider.default_fast_model(cx)?;
|
|
||||||
Some(ConfiguredModel {
|
|
||||||
provider,
|
|
||||||
model: fast_model,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
|
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
|
||||||
|
@ -343,7 +353,7 @@ impl LanguageModelRegistry {
|
||||||
.or_else(|| self.default_model.clone())
|
.or_else(|| self.default_model.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_message_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
pub fn commit_message_model(&self) -> Option<ConfiguredModel> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||||
return None;
|
return None;
|
||||||
|
@ -351,11 +361,11 @@ impl LanguageModelRegistry {
|
||||||
|
|
||||||
self.commit_message_model
|
self.commit_message_model
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| self.default_fast_model(cx))
|
.or_else(|| self.default_fast_model.clone())
|
||||||
.or_else(|| self.default_model.clone())
|
.or_else(|| self.default_model.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn thread_summary_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
pub fn thread_summary_model(&self) -> Option<ConfiguredModel> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||||
return None;
|
return None;
|
||||||
|
@ -363,7 +373,7 @@ impl LanguageModelRegistry {
|
||||||
|
|
||||||
self.thread_summary_model
|
self.thread_summary_model
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| self.default_fast_model(cx))
|
.or_else(|| self.default_fast_model.clone())
|
||||||
.or_else(|| self.default_model.clone())
|
.or_else(|| self.default_model.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -400,34 +410,4 @@ mod tests {
|
||||||
let providers = registry.read(cx).providers();
|
let providers = registry.read(cx).providers();
|
||||||
assert!(providers.is_empty());
|
assert!(providers.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) {
|
|
||||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
|
||||||
|
|
||||||
let provider = FakeLanguageModelProvider::default();
|
|
||||||
registry.update(cx, |registry, cx| {
|
|
||||||
registry.register_provider(provider.clone(), cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.update(|cx| provider.authenticate(cx)).await.unwrap();
|
|
||||||
|
|
||||||
registry.update(cx, |registry, cx| {
|
|
||||||
let provider = registry.provider(&provider.id()).unwrap();
|
|
||||||
|
|
||||||
registry.set_environment_fallback_model(
|
|
||||||
Some(ConfiguredModel {
|
|
||||||
provider: provider.clone(),
|
|
||||||
model: provider.default_model(cx).unwrap(),
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let default_model = registry.default_model().unwrap();
|
|
||||||
let fallback_model = registry.environment_fallback_model.clone().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(default_model.model.id(), fallback_model.model.id());
|
|
||||||
assert_eq!(default_model.provider.id(), fallback_model.provider.id());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] }
|
||||||
open_ai = { workspace = true, features = ["schemars"] }
|
open_ai = { workspace = true, features = ["schemars"] }
|
||||||
open_router = { workspace = true, features = ["schemars"] }
|
open_router = { workspace = true, features = ["schemars"] }
|
||||||
partial-json-fixer.workspace = true
|
partial-json-fixer.workspace = true
|
||||||
project.workspace = true
|
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -3,12 +3,8 @@ use std::sync::Arc;
|
||||||
use ::settings::{Settings, SettingsStore};
|
use ::settings::{Settings, SettingsStore};
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::future;
|
use gpui::{App, Context, Entity};
|
||||||
use gpui::{App, AppContext as _, Context, Entity};
|
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||||
use language_model::{
|
|
||||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
|
||||||
};
|
|
||||||
use project::DisableAiSettings;
|
|
||||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||||
|
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
@ -17,7 +13,7 @@ pub mod ui;
|
||||||
|
|
||||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||||
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
||||||
use crate::provider::cloud::{self, CloudLanguageModelProvider};
|
use crate::provider::cloud::CloudLanguageModelProvider;
|
||||||
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
||||||
use crate::provider::google::GoogleLanguageModelProvider;
|
use crate::provider::google::GoogleLanguageModelProvider;
|
||||||
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
||||||
|
@ -52,13 +48,6 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut already_authenticated = false;
|
|
||||||
if !DisableAiSettings::get_global(cx).disable_ai {
|
|
||||||
authenticate_all_providers(registry.clone(), cx);
|
|
||||||
already_authenticated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.observe_global::<SettingsStore>(move |cx| {
|
cx.observe_global::<SettingsStore>(move |cx| {
|
||||||
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
||||||
.openai_compatible
|
.openai_compatible
|
||||||
|
@ -76,12 +65,6 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
openai_compatible_providers = openai_compatible_providers_new;
|
openai_compatible_providers = openai_compatible_providers_new;
|
||||||
already_authenticated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated {
|
|
||||||
authenticate_all_providers(registry.clone(), cx);
|
|
||||||
already_authenticated = true;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -168,83 +151,3 @@ fn register_language_model_providers(
|
||||||
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
||||||
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
|
||||||
///
|
|
||||||
/// We do this so that we can populate the language selector with all of the
|
|
||||||
/// models from the configured providers.
|
|
||||||
///
|
|
||||||
/// This function won't do anything if AI is disabled.
|
|
||||||
fn authenticate_all_providers(registry: Entity<LanguageModelRegistry>, cx: &mut App) {
|
|
||||||
let providers_to_authenticate = registry
|
|
||||||
.read(cx)
|
|
||||||
.providers()
|
|
||||||
.iter()
|
|
||||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut tasks = Vec::with_capacity(providers_to_authenticate.len());
|
|
||||||
|
|
||||||
for (provider_id, provider_name, authenticate_task) in providers_to_authenticate {
|
|
||||||
tasks.push(cx.background_spawn(async move {
|
|
||||||
if let Err(err) = authenticate_task.await {
|
|
||||||
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
|
||||||
// Since we're authenticating these providers in the
|
|
||||||
// background for the purposes of populating the
|
|
||||||
// language selector, we don't care about providers
|
|
||||||
// where the credentials are not found.
|
|
||||||
} else {
|
|
||||||
// Some providers have noisy failure states that we
|
|
||||||
// don't want to spam the logs with every time the
|
|
||||||
// language model selector is initialized.
|
|
||||||
//
|
|
||||||
// Ideally these should have more clear failure modes
|
|
||||||
// that we know are safe to ignore here, like what we do
|
|
||||||
// with `CredentialsNotFound` above.
|
|
||||||
match provider_id.0.as_ref() {
|
|
||||||
"lmstudio" | "ollama" => {
|
|
||||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
|
||||||
//
|
|
||||||
// These fail noisily, so we don't log them.
|
|
||||||
}
|
|
||||||
"copilot_chat" => {
|
|
||||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to authenticate provider: {}: {err}",
|
|
||||||
provider_name.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let all_authenticated_future = future::join_all(tasks);
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
all_authenticated_future.await;
|
|
||||||
|
|
||||||
registry
|
|
||||||
.update(cx, |registry, cx| {
|
|
||||||
let cloud_provider = registry.provider(&cloud::PROVIDER_ID);
|
|
||||||
let fallback_model = cloud_provider
|
|
||||||
.iter()
|
|
||||||
.chain(registry.providers().iter())
|
|
||||||
.find(|provider| provider.is_authenticated(cx))
|
|
||||||
.and_then(|provider| {
|
|
||||||
Some(ConfiguredModel {
|
|
||||||
provider: provider.clone(),
|
|
||||||
model: provider
|
|
||||||
.default_model(cx)
|
|
||||||
.or_else(|| provider.recommended_models(cx).first().cloned())?,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
registry.set_environment_fallback_model(fallback_model, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
|
@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
|
||||||
use crate::provider::google::{GoogleEventMapper, into_google};
|
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||||
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
||||||
|
|
||||||
pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||||
pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, PartialEq)]
|
#[derive(Default, Clone, Debug, PartialEq)]
|
||||||
pub struct ZedDotDevSettings {
|
pub struct ZedDotDevSettings {
|
||||||
|
@ -146,7 +146,7 @@ impl State {
|
||||||
default_fast_model: None,
|
default_fast_model: None,
|
||||||
recommended_models: Vec::new(),
|
recommended_models: Vec::new(),
|
||||||
_fetch_models_task: cx.spawn(async move |this, cx| {
|
_fetch_models_task: cx.spawn(async move |this, cx| {
|
||||||
maybe!(async {
|
maybe!(async move {
|
||||||
let (client, llm_api_token) = this
|
let (client, llm_api_token) = this
|
||||||
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
||||||
|
|
||||||
|
|
|
@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
.tooltip(Tooltip::text("Copy Code"))
|
.tooltip(Tooltip::text("Copy"))
|
||||||
.on_click({
|
.on_click({
|
||||||
let markdown = markdown;
|
let markdown = markdown;
|
||||||
move |_event, _window, cx| {
|
move |_event, _window, cx| {
|
||||||
|
|
|
@ -4089,6 +4089,7 @@ impl ProjectPanel {
|
||||||
.when(!is_sticky, |this| {
|
.when(!is_sticky, |this| {
|
||||||
this
|
this
|
||||||
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
|
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
|
||||||
|
.when(settings.drag_and_drop, |this| this
|
||||||
.on_drag_move::<ExternalPaths>(cx.listener(
|
.on_drag_move::<ExternalPaths>(cx.listener(
|
||||||
move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
|
move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
|
||||||
let is_current_target = this.drag_target_entry.as_ref()
|
let is_current_target = this.drag_target_entry.as_ref()
|
||||||
|
@ -4222,7 +4223,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
|
this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
|
||||||
}),
|
}),
|
||||||
)
|
))
|
||||||
})
|
})
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
|
@ -4433,6 +4434,7 @@ impl ProjectPanel {
|
||||||
div()
|
div()
|
||||||
.when(!is_sticky, |div| {
|
.when(!is_sticky, |div| {
|
||||||
div
|
div
|
||||||
|
.when(settings.drag_and_drop, |div| div
|
||||||
.on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
|
.on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
|
||||||
this.hover_scroll_task.take();
|
this.hover_scroll_task.take();
|
||||||
this.drag_target_entry = None;
|
this.drag_target_entry = None;
|
||||||
|
@ -4464,7 +4466,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
))
|
)))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
Label::new(DELIMITER.clone())
|
Label::new(DELIMITER.clone())
|
||||||
|
@ -4484,6 +4486,7 @@ impl ProjectPanel {
|
||||||
.when(index != components_len - 1, |div|{
|
.when(index != components_len - 1, |div|{
|
||||||
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
|
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
|
||||||
div
|
div
|
||||||
|
.when(settings.drag_and_drop, |div| div
|
||||||
.on_drag_move(cx.listener(
|
.on_drag_move(cx.listener(
|
||||||
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
|
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
|
||||||
if event.bounds.contains(&event.event.position) {
|
if event.bounds.contains(&event.event.position) {
|
||||||
|
@ -4521,7 +4524,7 @@ impl ProjectPanel {
|
||||||
target.index == index
|
target.index == index
|
||||||
), |this| {
|
), |this| {
|
||||||
this.bg(item_colors.drag_over)
|
this.bg(item_colors.drag_over)
|
||||||
})
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(move |this, _, _, cx| {
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
|
@ -5029,7 +5032,8 @@ impl ProjectPanel {
|
||||||
|
|
||||||
sticky_parents.reverse();
|
sticky_parents.reverse();
|
||||||
|
|
||||||
let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status;
|
let panel_settings = ProjectPanelSettings::get_global(cx);
|
||||||
|
let git_status_enabled = panel_settings.git_status;
|
||||||
let root_name = OsStr::new(worktree.root_name());
|
let root_name = OsStr::new(worktree.root_name());
|
||||||
|
|
||||||
let git_summaries_by_id = if git_status_enabled {
|
let git_summaries_by_id = if git_status_enabled {
|
||||||
|
@ -5113,11 +5117,11 @@ impl Render for ProjectPanel {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let has_worktree = !self.visible_entries.is_empty();
|
let has_worktree = !self.visible_entries.is_empty();
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
|
let panel_settings = ProjectPanelSettings::get_global(cx);
|
||||||
let show_indent_guides =
|
let indent_size = panel_settings.indent_size;
|
||||||
ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
|
let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
|
||||||
let show_sticky_entries = {
|
let show_sticky_entries = {
|
||||||
if ProjectPanelSettings::get_global(cx).sticky_scroll {
|
if panel_settings.sticky_scroll {
|
||||||
let is_scrollable = self.scroll_handle.is_scrollable();
|
let is_scrollable = self.scroll_handle.is_scrollable();
|
||||||
let is_scrolled = self.scroll_handle.offset().y < px(0.);
|
let is_scrolled = self.scroll_handle.offset().y < px(0.);
|
||||||
is_scrollable && is_scrolled
|
is_scrollable && is_scrolled
|
||||||
|
@ -5205,8 +5209,10 @@ impl Render for ProjectPanel {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("project-panel")
|
.id("project-panel")
|
||||||
.group("project-panel")
|
.group("project-panel")
|
||||||
.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
|
.when(panel_settings.drag_and_drop, |this| {
|
||||||
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
|
this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
|
||||||
|
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
|
||||||
|
})
|
||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
.on_modifiers_changed(cx.listener(
|
.on_modifiers_changed(cx.listener(
|
||||||
|
@ -5544,30 +5550,32 @@ impl Render for ProjectPanel {
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when(is_local, |div| {
|
.when(is_local, |div| {
|
||||||
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
div.when(panel_settings.drag_and_drop, |div| {
|
||||||
style.bg(cx.theme().colors().drop_target_background)
|
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
||||||
|
style.bg(cx.theme().colors().drop_target_background)
|
||||||
|
})
|
||||||
|
.on_drop(cx.listener(
|
||||||
|
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||||
|
this.drag_target_entry = None;
|
||||||
|
this.hover_scroll_task.take();
|
||||||
|
if let Some(task) = this
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_workspace_for_paths(
|
||||||
|
true,
|
||||||
|
external_paths.paths().to_owned(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
task.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
cx.stop_propagation();
|
||||||
|
},
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.on_drop(cx.listener(
|
|
||||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
|
||||||
this.drag_target_entry = None;
|
|
||||||
this.hover_scroll_task.take();
|
|
||||||
if let Some(task) = this
|
|
||||||
.workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
workspace.open_workspace_for_paths(
|
|
||||||
true,
|
|
||||||
external_paths.paths().to_owned(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.log_err()
|
|
||||||
{
|
|
||||||
task.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
cx.stop_propagation();
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ pub struct ProjectPanelSettings {
|
||||||
pub scrollbar: ScrollbarSettings,
|
pub scrollbar: ScrollbarSettings,
|
||||||
pub show_diagnostics: ShowDiagnostics,
|
pub show_diagnostics: ShowDiagnostics,
|
||||||
pub hide_root: bool,
|
pub hide_root: bool,
|
||||||
|
pub drag_and_drop: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||||
|
@ -160,6 +161,10 @@ pub struct ProjectPanelSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: true
|
/// Default: true
|
||||||
pub sticky_scroll: Option<bool>,
|
pub sticky_scroll: Option<bool>,
|
||||||
|
/// Whether to enable drag-and-drop operations in the project panel.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
pub drag_and_drop: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings for ProjectPanelSettings {
|
impl Settings for ProjectPanelSettings {
|
||||||
|
|
|
@ -3243,6 +3243,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||||
"indent_size": 20,
|
"indent_size": 20,
|
||||||
"auto_reveal_entries": true,
|
"auto_reveal_entries": true,
|
||||||
"auto_fold_dirs": true,
|
"auto_fold_dirs": true,
|
||||||
|
"drag_and_drop": true,
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
"show": null
|
"show": null
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
|
||||||
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
||||||
"show_summary": true,
|
"show_summary": true,
|
||||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||||
"show_output": true,
|
"show_output": true
|
||||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||||
"tags": []
|
// "tags": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
|
@ -431,6 +431,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
|
||||||
"auto_reveal_entries": true, // Show file in panel when activating its buffer
|
"auto_reveal_entries": true, // Show file in panel when activating its buffer
|
||||||
"auto_fold_dirs": true, // Fold dirs with single subdir
|
"auto_fold_dirs": true, // Fold dirs with single subdir
|
||||||
"sticky_scroll": true, // Stick parent directories at top of the project panel.
|
"sticky_scroll": true, // Stick parent directories at top of the project panel.
|
||||||
|
"drag_and_drop": true, // Whether drag and drop is enabled
|
||||||
"scrollbar": { // Project panel scrollbar settings
|
"scrollbar": { // Project panel scrollbar settings
|
||||||
"show": null // Show/hide: (auto, system, always, never)
|
"show": null // Show/hide: (auto, system, always, never)
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue