mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
Merge branch 'main' into fix-worktree-drag-reorder
This commit is contained in:
commit
d23a2ccf81
13 changed files with 370 additions and 61 deletions
|
|
@ -1264,6 +1264,7 @@
|
|||
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
|
||||
"cmd-c": "terminal::Copy",
|
||||
"cmd-v": "terminal::Paste",
|
||||
"ctrl-cmd-v": "terminal::PasteText",
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-k": "terminal::Clear",
|
||||
|
|
|
|||
|
|
@ -1221,6 +1221,7 @@
|
|||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-v": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-alt-v": "terminal::PasteText",
|
||||
"ctrl-i": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
|
|
|
|||
|
|
@ -30,6 +30,33 @@ mod web_search_tool;
|
|||
|
||||
use crate::AgentTool;
|
||||
use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
de::{DeserializeOwned, Error as _},
|
||||
};
|
||||
|
||||
/// Deserialize a value that may have been provided as a JSON-encoded string
|
||||
/// instead of the structured value. Some models occasionally stringify nested
|
||||
/// arguments, so we accept either form.
|
||||
pub(crate) fn deserialize_maybe_stringified<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ValueOrJsonString<T> {
|
||||
Value(T),
|
||||
String(String),
|
||||
}
|
||||
|
||||
match ValueOrJsonString::<T>::deserialize(deserializer)? {
|
||||
ValueOrJsonString::Value(value) => Ok(value),
|
||||
ValueOrJsonString::String(string) => serde_json::from_str::<T>(&string).map_err(|error| {
|
||||
D::Error::custom(format!("failed to parse stringified value: {error}"))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub use apply_code_action_tool::*;
|
||||
pub use context_server_registry::*;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ mod reindent;
|
|||
mod streaming_fuzzy_matcher;
|
||||
mod streaming_parser;
|
||||
|
||||
use super::deserialize_maybe_stringified;
|
||||
use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;
|
||||
use super::save_file_tool::SaveFileTool;
|
||||
use crate::ToolInputPayload;
|
||||
|
|
@ -24,10 +25,7 @@ use language_model::LanguageModelToolResultContent;
|
|||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use project::{AgentLocation, Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{DeserializeOwned, Error as _},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -134,26 +132,6 @@ pub struct PartialEdit {
|
|||
pub new_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ValueOrJsonString<T> {
|
||||
Value(T),
|
||||
String(String),
|
||||
}
|
||||
|
||||
fn deserialize_maybe_stringified<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match ValueOrJsonString::<T>::deserialize(deserializer)? {
|
||||
ValueOrJsonString::Value(value) => Ok(value),
|
||||
ValueOrJsonString::String(string) => serde_json::from_str::<T>(&string).map_err(|error| {
|
||||
D::Error::custom(format!("failed to parse stringified value: {error}"))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum EditFileToolOutput {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use gpui::{App, SharedString, Task};
|
|||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::deserialize_maybe_stringified;
|
||||
use crate::{AgentTool, ToolCallEventStream, ToolInput};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
|
|
@ -23,6 +24,7 @@ pub enum Timezone {
|
|||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NowToolInput {
|
||||
/// The timezone to use for the datetime. Use `utc` for UTC, or `local` for the system's local time.
|
||||
#[serde(deserialize_with = "deserialize_maybe_stringified")]
|
||||
timezone: Timezone,
|
||||
}
|
||||
|
||||
|
|
@ -62,3 +64,28 @@ impl AgentTool for NowTool {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use serde_json::json;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stringified_timezone_input_succeeds(cx: &mut TestAppContext) {
|
||||
let tool = Arc::new(NowTool);
|
||||
let (mut sender, input) = ToolInput::<NowToolInput>::test();
|
||||
let (event_stream, _receiver) = ToolCallEventStream::test();
|
||||
let task = cx.update(|cx| tool.clone().run(input, event_stream, cx));
|
||||
|
||||
sender.send_full(json!({
|
||||
"timezone": "\"utc\""
|
||||
}));
|
||||
|
||||
let result = task.await.unwrap();
|
||||
assert!(
|
||||
result.starts_with("The current datetime is "),
|
||||
"unexpected output: {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ use git::{
|
|||
};
|
||||
use git_ui::{commit_tooltip::CommitAvatar, commit_view::CommitView, git_status_icon};
|
||||
use gpui::{
|
||||
Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength, DragMoveEvent,
|
||||
ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, PathBuilder, Pixels,
|
||||
Point, ScrollStrategy, ScrollWheelEvent, SharedString, Subscription, Task, TextStyleRefinement,
|
||||
Action, Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength,
|
||||
DismissEvent, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Hsla, MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollStrategy,
|
||||
ScrollWheelEvent, SharedString, Subscription, Task, TextStyleRefinement,
|
||||
UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, point, prelude::*,
|
||||
px, uniform_list,
|
||||
};
|
||||
|
|
@ -278,6 +279,8 @@ impl SplitState {
|
|||
actions!(
|
||||
git_graph,
|
||||
[
|
||||
/// Copies the SHA of the selected commit to the clipboard.
|
||||
CopyCommitSha,
|
||||
/// Opens the commit view for the selected commit.
|
||||
OpenCommitView,
|
||||
/// Focuses the search field.
|
||||
|
|
@ -981,13 +984,20 @@ fn compute_diff_stats(diff: &CommitDiff) -> (usize, usize) {
|
|||
})
|
||||
}
|
||||
|
||||
struct GitGraphContextMenu {
|
||||
menu: Entity<ContextMenu>,
|
||||
position: Point<Pixels>,
|
||||
entry_idx: usize,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
pub struct GitGraph {
|
||||
focus_handle: FocusHandle,
|
||||
search_state: SearchState,
|
||||
graph_data: GraphData,
|
||||
git_store: Entity<GitStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
context_menu: Option<GitGraphContextMenu>,
|
||||
table_interaction_state: Entity<TableInteractionState>,
|
||||
column_widths: Entity<RedistributableColumnsState>,
|
||||
selected_entry_idx: Option<usize>,
|
||||
|
|
@ -1010,6 +1020,7 @@ impl GitGraph {
|
|||
self.search_state.matches.clear();
|
||||
self.search_state.selected_index = None;
|
||||
self.search_state.state.next_state();
|
||||
self.context_menu = None;
|
||||
cx.emit(ItemEvent::Edit);
|
||||
cx.notify();
|
||||
}
|
||||
|
|
@ -1328,6 +1339,10 @@ impl GitGraph {
|
|||
git_store.repositories().get(&self.repo_id).cloned()
|
||||
}
|
||||
|
||||
fn has_context_menu(&self) -> bool {
|
||||
self.context_menu.is_some()
|
||||
}
|
||||
|
||||
/// Checks whether a ref name from git's `%D` decoration
|
||||
/// format refers to the currently checked-out branch.
|
||||
fn is_head_ref(ref_name: &str, head_branch_name: &Option<SharedString>) -> bool {
|
||||
|
|
@ -1374,6 +1389,7 @@ impl GitGraph {
|
|||
});
|
||||
|
||||
let row_height = Self::row_height(window, cx);
|
||||
let has_context_menu = self.has_context_menu();
|
||||
|
||||
// We fetch data outside the visible viewport to avoid loading entries when
|
||||
// users scroll through the git graph
|
||||
|
|
@ -1481,7 +1497,9 @@ impl GitGraph {
|
|||
div()
|
||||
.id(ElementId::NamedInteger("commit-subject".into(), idx as u64))
|
||||
.overflow_hidden()
|
||||
.tooltip(Tooltip::text(subject))
|
||||
.when(!has_context_menu, |this| {
|
||||
this.tooltip(Tooltip::text(subject))
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
|
@ -1851,6 +1869,96 @@ impl GitGraph {
|
|||
);
|
||||
}
|
||||
|
||||
fn copy_commit_sha(&mut self, entry_index: usize, cx: &mut Context<Self>) {
|
||||
let Some(commit) = self.graph_data.commits.get(entry_index) else {
|
||||
return;
|
||||
};
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(commit.data.sha.to_string()));
|
||||
}
|
||||
|
||||
fn copy_selected_commit_sha(
|
||||
&mut self,
|
||||
_: &CopyCommitSha,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(selected_entry_index) = self.selected_entry_idx else {
|
||||
return;
|
||||
};
|
||||
self.copy_commit_sha(selected_entry_index, cx);
|
||||
}
|
||||
|
||||
fn deploy_entry_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
index: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(commit) = self.graph_data.commits.get(index) else {
|
||||
return;
|
||||
};
|
||||
let short_sha = commit.data.sha.display_short();
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let git_graph = cx.entity();
|
||||
let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| {
|
||||
context_menu
|
||||
.context(focus_handle)
|
||||
.header(format!("Commit {short_sha}"))
|
||||
.entry(
|
||||
"View Commit",
|
||||
Some(OpenCommitView.boxed_clone()),
|
||||
window.handler_for(&git_graph, move |this, window, cx| {
|
||||
this.open_commit_view(index, window, cx);
|
||||
}),
|
||||
)
|
||||
.entry(
|
||||
"Copy SHA",
|
||||
Some(CopyCommitSha.boxed_clone()),
|
||||
window.handler_for(&git_graph, move |this, _window, cx| {
|
||||
this.copy_commit_sha(index, cx);
|
||||
}),
|
||||
)
|
||||
});
|
||||
self.set_context_menu(context_menu, position, index, window, cx);
|
||||
}
|
||||
|
||||
fn set_context_menu(
|
||||
&mut self,
|
||||
context_menu: Entity<ContextMenu>,
|
||||
position: Point<Pixels>,
|
||||
entry_idx: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
if this.context_menu.as_ref().is_some_and(|context_menu| {
|
||||
context_menu
|
||||
.menu
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
}) {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
self.context_menu = Some(GitGraphContextMenu {
|
||||
menu: context_menu,
|
||||
position,
|
||||
entry_idx,
|
||||
_subscription: subscription,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn get_remote(
|
||||
&self,
|
||||
repository: &Repository,
|
||||
|
|
@ -2434,6 +2542,7 @@ impl GitGraph {
|
|||
|
||||
let hovered_entry_idx = self.hovered_entry_idx;
|
||||
let selected_entry_idx = self.selected_entry_idx;
|
||||
let context_menu_entry_idx = self.context_menu.as_ref().map(|menu| menu.entry_idx);
|
||||
let is_focused = self.focus_handle.is_focused(window);
|
||||
let graph_canvas_bounds = self.graph_canvas_bounds.clone();
|
||||
|
||||
|
|
@ -2456,8 +2565,10 @@ impl GitGraph {
|
|||
let absolute_row_idx = first_visible_row + visible_row_idx;
|
||||
let is_hovered = hovered_entry_idx == Some(absolute_row_idx);
|
||||
let is_selected = selected_entry_idx == Some(absolute_row_idx);
|
||||
let is_context_menu_target =
|
||||
context_menu_entry_idx == Some(absolute_row_idx);
|
||||
|
||||
if is_hovered || is_selected {
|
||||
if is_hovered || is_selected || is_context_menu_target {
|
||||
let row_y = bounds.origin.y + visible_row_idx as f32 * row_height
|
||||
- vertical_scroll_offset;
|
||||
|
||||
|
|
@ -2469,7 +2580,11 @@ impl GitGraph {
|
|||
},
|
||||
);
|
||||
|
||||
let bg_color = if is_selected { selected_bg } else { hover_bg };
|
||||
let bg_color = if is_selected || is_context_menu_target {
|
||||
selected_bg
|
||||
} else {
|
||||
hover_bg
|
||||
};
|
||||
window.paint_quad(gpui::fill(row_bounds, bg_color));
|
||||
}
|
||||
}
|
||||
|
|
@ -2697,6 +2812,31 @@ impl GitGraph {
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_entry_click(
|
||||
&mut self,
|
||||
entry_idx: usize,
|
||||
event: &ClickEvent,
|
||||
scroll_strategy: ScrollStrategy,
|
||||
focus_handle: Option<&FocusHandle>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Right-clicks open the context menu, not the details panel.
|
||||
if event.is_right_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(focus_handle) = focus_handle {
|
||||
focus_handle.focus(window, cx);
|
||||
}
|
||||
|
||||
self.select_entry(entry_idx, scroll_strategy, cx);
|
||||
|
||||
if event.click_count() >= 2 {
|
||||
self.open_commit_view(entry_idx, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_graph_click(
|
||||
&mut self,
|
||||
event: &ClickEvent,
|
||||
|
|
@ -2704,13 +2844,34 @@ impl GitGraph {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(row) = self.row_at_position(event.position().y, window, cx) {
|
||||
self.select_entry(row, ScrollStrategy::Nearest, cx);
|
||||
if event.click_count() >= 2 {
|
||||
self.open_commit_view(row, window, cx);
|
||||
}
|
||||
self.handle_entry_click(row, event, ScrollStrategy::Nearest, None, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_entry_secondary_mouse_down(
|
||||
&mut self,
|
||||
entry_idx: usize,
|
||||
event: &MouseDownEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.deploy_entry_context_menu(event.position, entry_idx, window, cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn handle_graph_secondary_mouse_down(
|
||||
&mut self,
|
||||
event: &MouseDownEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(row) = self.row_at_position(event.position.y, window, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.handle_entry_secondary_mouse_down(row, event, window, cx);
|
||||
}
|
||||
|
||||
fn handle_graph_scroll(
|
||||
&mut self,
|
||||
event: &ScrollWheelEvent,
|
||||
|
|
@ -2905,6 +3066,8 @@ impl Render for GitGraph {
|
|||
let row_height = Self::row_height(window, cx);
|
||||
let selected_entry_idx = self.selected_entry_idx;
|
||||
let hovered_entry_idx = self.hovered_entry_idx;
|
||||
let context_menu_entry_idx =
|
||||
self.context_menu.as_ref().map(|menu| menu.entry_idx);
|
||||
let weak_self = cx.weak_entity();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let table_focus_handle =
|
||||
|
|
@ -2923,6 +3086,10 @@ impl Render for GitGraph {
|
|||
.on_scroll_wheel(cx.listener(Self::handle_graph_scroll))
|
||||
.on_mouse_move(cx.listener(Self::handle_graph_mouse_move))
|
||||
.on_click(cx.listener(Self::handle_graph_click))
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
cx.listener(Self::handle_graph_secondary_mouse_down),
|
||||
)
|
||||
.on_hover(cx.listener(|this, &is_hovered: &bool, _, cx| {
|
||||
if !is_hovered && this.hovered_entry_idx.is_some() {
|
||||
this.hovered_entry_idx = None;
|
||||
|
|
@ -2938,11 +3105,14 @@ impl Render for GitGraph {
|
|||
.map_row(move |(index, row), window, cx| {
|
||||
let is_selected = selected_entry_idx == Some(index);
|
||||
let is_hovered = hovered_entry_idx == Some(index);
|
||||
let is_context_menu_target =
|
||||
context_menu_entry_idx == Some(index);
|
||||
let table_focus_handle = table_focus_handle.clone();
|
||||
let is_focused = focus_handle.is_focused(window)
|
||||
|| table_focus_handle.is_focused(window);
|
||||
let weak = weak_self.clone();
|
||||
let weak_for_hover = weak.clone();
|
||||
let weak_for_context_menu = weak.clone();
|
||||
|
||||
let hover_bg = cx.theme().colors().element_hover.opacity(0.6);
|
||||
let selected_bg = if is_focused {
|
||||
|
|
@ -2953,8 +3123,13 @@ impl Render for GitGraph {
|
|||
|
||||
row.h(row_height)
|
||||
.cursor_pointer()
|
||||
.when(is_selected, |row| row.bg(selected_bg))
|
||||
.when(is_hovered && !is_selected, |row| row.bg(hover_bg))
|
||||
.when(is_selected || is_context_menu_target, |row| {
|
||||
row.bg(selected_bg)
|
||||
})
|
||||
.when(
|
||||
is_hovered && !is_selected && !is_context_menu_target,
|
||||
|row| row.bg(hover_bg),
|
||||
)
|
||||
.on_hover(move |&is_hovered, _, cx| {
|
||||
weak_for_hover
|
||||
.update(cx, |this, cx| {
|
||||
|
|
@ -2972,20 +3147,30 @@ impl Render for GitGraph {
|
|||
.ok();
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
let click_count = event.click_count();
|
||||
table_focus_handle.focus(window, cx);
|
||||
weak.update(cx, |this, cx| {
|
||||
this.select_entry(
|
||||
this.handle_entry_click(
|
||||
index,
|
||||
event,
|
||||
ScrollStrategy::Center,
|
||||
Some(&table_focus_handle),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if click_count >= 2 {
|
||||
this.open_commit_view(index, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
weak_for_context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.handle_entry_secondary_mouse_down(
|
||||
index, event, window, cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.uniform_list(
|
||||
|
|
@ -3057,6 +3242,7 @@ impl Render for GitGraph {
|
|||
.on_action(cx.listener(|this, _: &OpenCommitView, window, cx| {
|
||||
this.open_selected_commit_view(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(Self::copy_selected_commit_sha))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|this, _: &FocusSearch, window, cx| {
|
||||
this.search_state
|
||||
|
|
@ -3091,12 +3277,12 @@ impl Render for GitGraph {
|
|||
.child(self.render_search_bar(cx))
|
||||
.child(div().flex_1().child(content)),
|
||||
)
|
||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||
.children(self.context_menu.as_ref().map(|context_menu| {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.position(context_menu.position)
|
||||
.anchor(Anchor::TopLeft)
|
||||
.child(menu.clone()),
|
||||
.child(context_menu.menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ impl LanguageModelProvider for OpenCodeLanguageModelProvider {
|
|||
protocol,
|
||||
reasoning_effort_levels: model.reasoning_effort_levels.clone(),
|
||||
custom_model_api_url: model.custom_model_api_url.clone(),
|
||||
interleaved_reasoning: model.interleaved_reasoning,
|
||||
};
|
||||
let key = format!("{}/{}", subscription.id_prefix(), model.name);
|
||||
models.insert(key, (custom_model, subscription));
|
||||
|
|
@ -664,7 +665,7 @@ impl LanguageModel for OpenCodeLanguageModel {
|
|||
false,
|
||||
self.model.max_output_tokens(),
|
||||
reasoning_effort,
|
||||
false,
|
||||
self.model.interleaved_reasoning(),
|
||||
);
|
||||
let stream = self.stream_openai_chat(openai_request, http_client, cx);
|
||||
async move {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ pub enum Model {
|
|||
protocol: ApiProtocol,
|
||||
reasoning_effort_levels: Option<Vec<ReasoningEffort>>,
|
||||
custom_model_api_url: Option<String>,
|
||||
interleaved_reasoning: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -385,8 +386,6 @@ impl Model {
|
|||
|
||||
Self::Gemini3_1Pro | Self::Gemini3Flash => ApiProtocol::Google,
|
||||
|
||||
Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => ApiProtocol::Anthropic,
|
||||
|
||||
Self::MiniMaxM2_5Free
|
||||
| Self::Glm5
|
||||
| Self::Glm5_1
|
||||
|
|
@ -398,6 +397,8 @@ impl Model {
|
|||
| Self::MimoV2_5
|
||||
| Self::Qwen3_5Plus
|
||||
| Self::Qwen3_6Plus
|
||||
| Self::DeepSeekV4Pro
|
||||
| Self::DeepSeekV4Flash
|
||||
| Self::BigPickle
|
||||
| Self::Nemotron3SuperFree
|
||||
| Self::Ling2_6FlashFree
|
||||
|
|
@ -407,6 +408,27 @@ impl Model {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn interleaved_reasoning(&self) -> bool {
|
||||
match self {
|
||||
Self::DeepSeekV4Pro
|
||||
| Self::DeepSeekV4Flash
|
||||
| Self::KimiK2_5
|
||||
| Self::KimiK2_6
|
||||
| Self::MimoV2Omni
|
||||
| Self::MimoV2_5
|
||||
| Self::MimoV2_5Pro
|
||||
| Self::Glm5
|
||||
| Self::Glm5_1 => true,
|
||||
|
||||
Self::Custom {
|
||||
interleaved_reasoning,
|
||||
..
|
||||
} => *interleaved_reasoning,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
// Anthropic models
|
||||
|
|
@ -487,9 +509,6 @@ impl Model {
|
|||
// Google models
|
||||
Self::Gemini3_1Pro | Self::Gemini3Flash => Some(65_536),
|
||||
|
||||
// Anthropic-compatible models
|
||||
Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(384_000),
|
||||
|
||||
// OpenAI-compatible models
|
||||
Self::MiniMaxM2_7 => Some(131_072),
|
||||
Self::MiniMaxM2_5 | Self::MiniMaxM2_5Free => Some(131_072),
|
||||
|
|
@ -497,6 +516,7 @@ impl Model {
|
|||
Self::BigPickle => Some(128_000),
|
||||
Self::KimiK2_6 | Self::KimiK2_5 => Some(65_536),
|
||||
Self::Qwen3_5Plus | Self::Qwen3_6Plus => Some(65_536),
|
||||
Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(384_000),
|
||||
Self::Nemotron3SuperFree => Some(128_000),
|
||||
Self::MimoV2_5Pro | Self::MimoV2_5 | Self::MimoV2Pro | Self::MimoV2Omni => {
|
||||
Some(128_000)
|
||||
|
|
@ -565,14 +585,13 @@ impl Model {
|
|||
| Self::MiniMaxM2_7
|
||||
| Self::MimoV2Pro
|
||||
| Self::MimoV2_5Pro
|
||||
| Self::DeepSeekV4Pro
|
||||
| Self::DeepSeekV4Flash
|
||||
| Self::BigPickle
|
||||
| Self::Nemotron3SuperFree
|
||||
| Self::Ling2_6FlashFree
|
||||
| Self::Hy3PreviewFree => false,
|
||||
|
||||
// DeepSeek models (Anthropic protocol) don't support images
|
||||
Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => false,
|
||||
|
||||
Self::Custom { protocol, .. } => matches!(
|
||||
protocol,
|
||||
ApiProtocol::Anthropic
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use rpc::{
|
|||
proto::{self, ExternalExtensionAgent},
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{RegisterSetting, SettingsStore};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
|
@ -1535,7 +1536,7 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
|
|||
let node_runtime = self.node_runtime.clone();
|
||||
let project_environment = self.project_environment.downgrade();
|
||||
let registry_id = self.registry_id.clone();
|
||||
let package = self.package.clone();
|
||||
let package = bounded_npm_package_spec(&self.package);
|
||||
let args = self.args.clone();
|
||||
let distribution_env = self.distribution_env.clone();
|
||||
let settings_env = self.settings_env.clone();
|
||||
|
|
@ -1554,7 +1555,7 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
|
|||
.join(sanitize_path_component(®istry_id));
|
||||
fs.create_dir(&prefix_dir).await?;
|
||||
|
||||
let mut exec_args = vec!["--yes".to_string(), "--".to_string(), package.to_string()];
|
||||
let mut exec_args = vec!["--yes".to_string(), "--".to_string(), package];
|
||||
exec_args.extend(args);
|
||||
|
||||
let npm_command = node_runtime
|
||||
|
|
@ -1592,6 +1593,30 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
|
|||
}
|
||||
}
|
||||
|
||||
/// People are using min-release-age more frequently. Which means a fresh registry will likely have
|
||||
/// new package versions than the user can install.
|
||||
/// We set the version to now be a ceiling and not an exact pin instead. This allows npm to resolve
|
||||
/// the latest version it can find that satisfies the constraint. npm seems to check regularly enough
|
||||
/// that new versions are available. This does have a few downsides:
|
||||
/// - The user might have an older cached version of the package that satisfies the constraint, until
|
||||
/// npm checks for updates again.
|
||||
/// - The registry args/env may not be valid for the resolved version.
|
||||
///
|
||||
/// This is a best-effort attempt to install a version that works without overriding the user's
|
||||
/// security settings, as the args don't change often. The registry will need to support this better
|
||||
/// at some point, but until then, this is a best-effort workaround that hopefully solves the issue
|
||||
/// for most users.
|
||||
fn bounded_npm_package_spec(package_spec: &str) -> String {
|
||||
let Some((package_name, version)) = package_spec.rsplit_once('@') else {
|
||||
return package_spec.to_string();
|
||||
};
|
||||
if package_name.is_empty() || Version::parse(version).is_err() {
|
||||
return package_spec.to_string();
|
||||
}
|
||||
|
||||
format!("{package_name}@<={version}")
|
||||
}
|
||||
|
||||
struct LocalCustomAgent {
|
||||
project_environment: Entity<ProjectEnvironment>,
|
||||
command: AgentServerCommand,
|
||||
|
|
@ -1996,6 +2021,26 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_bounded_npm_package_specs() {
|
||||
assert_eq!(
|
||||
bounded_npm_package_spec("agent-package@1.2.3"),
|
||||
"agent-package@<=1.2.3"
|
||||
);
|
||||
assert_eq!(
|
||||
bounded_npm_package_spec("@scope/agent-package@1.2.3-beta.1"),
|
||||
"@scope/agent-package@<=1.2.3-beta.1"
|
||||
);
|
||||
assert_eq!(
|
||||
bounded_npm_package_spec("@scope/agent-package"),
|
||||
"@scope/agent-package"
|
||||
);
|
||||
assert_eq!(
|
||||
bounded_npm_package_spec("agent-package@latest"),
|
||||
"agent-package@latest"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_supported_archive_suffixes() {
|
||||
assert!(matches!(
|
||||
|
|
|
|||
|
|
@ -181,6 +181,9 @@ pub struct OpenCodeAvailableModel {
|
|||
pub custom_model_api_url: Option<String>,
|
||||
/// Supported reasoning effort levels, for example `["low", "medium", "high"].
|
||||
pub reasoning_effort_levels: Option<Vec<ReasoningEffort>>,
|
||||
/// When using OpenAiChat protocol, whether thinking tokens are sent as a dedicated `reasoning_content` field or inline in message text.
|
||||
#[serde(default)]
|
||||
pub interleaved_reasoning: bool,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ actions!(
|
|||
Copy,
|
||||
/// Pastes from the clipboard.
|
||||
Paste,
|
||||
/// Pastes the text from the clipboard.
|
||||
PasteText,
|
||||
/// Shows the character palette for special characters.
|
||||
ShowCharacterPalette,
|
||||
/// Searches for text in the terminal.
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ use std::{
|
|||
};
|
||||
use task::TaskId;
|
||||
use terminal::{
|
||||
Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp,
|
||||
ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskState,
|
||||
TaskStatus, Terminal, TerminalBounds, ToggleViMode,
|
||||
Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, PasteText, ScrollLineDown,
|
||||
ScrollLineUp, ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette,
|
||||
TaskState, TaskStatus, Terminal, TerminalBounds, ToggleViMode,
|
||||
alacritty_terminal::{
|
||||
index::Point as AlacPoint,
|
||||
term::{TermMode, point_to_viewport, search::RegexSearch},
|
||||
|
|
@ -508,6 +508,7 @@ impl TerminalView {
|
|||
.separator()
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.action("Paste Text", Box::new(PasteText))
|
||||
.action("Select All", Box::new(SelectAll))
|
||||
.action("Clear", Box::new(Clear))
|
||||
.when(assistant_enabled, |menu| {
|
||||
|
|
@ -811,7 +812,7 @@ impl TerminalView {
|
|||
}
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -820,6 +821,9 @@ impl TerminalView {
|
|||
Some(ClipboardEntry::Image(image)) if !image.bytes.is_empty() => {
|
||||
self.forward_ctrl_v(cx);
|
||||
}
|
||||
Some(ClipboardEntry::ExternalPaths(paths)) => {
|
||||
self.add_paths_to_terminal(paths.paths(), window, cx);
|
||||
}
|
||||
_ => {
|
||||
if let Some(text) = clipboard.text() {
|
||||
self.terminal
|
||||
|
|
@ -829,6 +833,18 @@ impl TerminalView {
|
|||
}
|
||||
}
|
||||
|
||||
///Attempt to paste the clipboard text into the terminal
|
||||
fn paste_text(&mut self, _: &PasteText, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(text) = clipboard.text() {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _cx| terminal.paste(&text));
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a raw Ctrl+V so TUI agents can read the OS clipboard directly
|
||||
/// and attach images using their native workflows.
|
||||
fn forward_ctrl_v(&self, cx: &mut Context<Self>) {
|
||||
|
|
@ -1226,6 +1242,7 @@ impl Render for TerminalView {
|
|||
.on_action(cx.listener(TerminalView::send_keystroke))
|
||||
.on_action(cx.listener(TerminalView::copy))
|
||||
.on_action(cx.listener(TerminalView::paste))
|
||||
.on_action(cx.listener(TerminalView::paste_text))
|
||||
.on_action(cx.listener(TerminalView::clear))
|
||||
.on_action(cx.listener(TerminalView::scroll_line_up))
|
||||
.on_action(cx.listener(TerminalView::scroll_line_down))
|
||||
|
|
|
|||
|
|
@ -663,6 +663,7 @@ The Zed agent comes pre-configured with OpenCode models. If you wish to use newe
|
|||
"max_output_tokens": 98765,
|
||||
"protocol": "openai_chat",
|
||||
"reasoning_effort_levels": ["low", "medium", "high"],
|
||||
"interleaved_reasoning": false,
|
||||
"subscription": "go",
|
||||
"custom_model_api_url": "https://example.com/zen"
|
||||
}
|
||||
|
|
@ -680,6 +681,7 @@ The available configuration options for custom models are:
|
|||
- `max_output_tokens` (optional): maximum tokens the model can generate, for example `64000`
|
||||
- `protocol` (required): model API protocol, one of `"anthropic"`, `"openai_responses"`, `"openai_chat"`, or `"google"`
|
||||
- `reasoning_effort_levels` (optional): list of supported reasoning effort levels, for example `["low", "medium", "high"]`. The latest value in the list is used as the default
|
||||
- `interleaved_reasoning` (optional, default `false`): if thinking tokens are sent as a dedicated `reasoning_content` field (`true`) or inline in message text (`false`). Applies only when using the `openai_chat` protocol
|
||||
- `subscription` (optional): `"zen"`, `"go"`, or `"free"` (defaults to `"zen"`)
|
||||
- `custom_model_api_url` (optional): custom API base URL to use instead of the default OpenCode API
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue