Compare commits

...

4 Commits

Author SHA1 Message Date
KyleBarton
c0ebb401c4 First pass at the modal for setting up devcontainer from scratch. 2025-12-23 21:46:33 -08:00
Teoh Han Hui
ca47822667 Associate devcontainer.json with JSONC language (#45593)
Release Notes:

- N/A
2025-12-23 21:23:28 +00:00
Danilo Leal
a34fe06bb1 agent_ui: Allow "token reached" callout to be dismissed (#45595)
It was previously impossible to dismiss the "token usage
reaching/reached the limit" callout.

<img width="500" height="392" alt="Screenshot 2025-12-23 at 5  49@2x"
src="https://github.com/user-attachments/assets/7fd8b126-dd3f-430b-9fea-ca05c73e5643"
/>

Release Notes:

- N/A
2025-12-23 21:14:58 +00:00
Kirill Bulatov
0ce484e66c Do not trust Docker hosts by default (#45587)
It's still possible to leak secrets by spawning odd MCP/LSP servers from
`.zed/settings.json`

Release Notes:

- N/A
2025-12-23 19:27:09 +00:00
6 changed files with 154 additions and 44 deletions

View File

@@ -278,6 +278,7 @@ pub struct AcpThreadView {
thread_retry_status: Option<RetryStatus>,
thread_error: Option<ThreadError>,
thread_error_markdown: Option<Entity<Markdown>>,
token_limit_callout_dismissed: bool,
thread_feedback: ThreadFeedbackState,
list_state: ListState,
auth_task: Option<Task<()>>,
@@ -430,13 +431,13 @@ impl AcpThreadView {
message_editor,
model_selector: None,
profile_selector: None,
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
list_state: list_state,
thread_retry_status: None,
thread_error: None,
thread_error_markdown: None,
token_limit_callout_dismissed: false,
thread_feedback: Default::default(),
auth_task: None,
expanded_tool_calls: HashSet::default(),
@@ -1394,6 +1395,7 @@ impl AcpThreadView {
fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
self.thread_error = None;
self.thread_error_markdown = None;
self.token_limit_callout_dismissed = true;
cx.notify();
}
@@ -5391,22 +5393,26 @@ impl AcpThreadView {
cx.notify();
}
fn render_token_limit_callout(
&self,
line_height: Pixels,
cx: &mut Context<Self>,
) -> Option<Callout> {
fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
if self.token_limit_callout_dismissed {
return None;
}
let token_usage = self.thread()?.read(cx).token_usage()?;
let ratio = token_usage.ratio();
let (severity, title) = match ratio {
let (severity, icon, title) = match ratio {
acp_thread::TokenUsageRatio::Normal => return None,
acp_thread::TokenUsageRatio::Warning => {
(Severity::Warning, "Thread reaching the token limit soon")
}
acp_thread::TokenUsageRatio::Exceeded => {
(Severity::Error, "Thread reached the token limit")
}
acp_thread::TokenUsageRatio::Warning => (
Severity::Warning,
IconName::Warning,
"Thread reaching the token limit soon",
),
acp_thread::TokenUsageRatio::Exceeded => (
Severity::Error,
IconName::XCircle,
"Thread reached the token limit",
),
};
let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| {
@@ -5426,7 +5432,7 @@ impl AcpThreadView {
Some(
Callout::new()
.severity(severity)
.line_height(line_height)
.icon(icon)
.title(title)
.description(description)
.actions_slot(
@@ -5458,7 +5464,8 @@ impl AcpThreadView {
})),
)
}),
),
)
.dismiss_action(self.dismiss_error_button(cx)),
)
}
@@ -5892,7 +5899,7 @@ impl AcpThreadView {
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
IconButton::new("dismiss", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss Error"))
.tooltip(Tooltip::text("Dismiss"))
.on_click(cx.listener({
move |this, _, _, cx| {
this.clear_thread_error(cx);
@@ -6152,7 +6159,7 @@ impl Render for AcpThreadView {
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
Some(usage_callout.into_any_element())
} else {
self.render_token_limit_callout(line_height, cx)
self.render_token_limit_callout(cx)
.map(|token_limit_callout| token_limit_callout.into_any_element())
},
)

View File

@@ -1,6 +1,6 @@
name = "JSONC"
grammar = "jsonc"
path_suffixes = ["jsonc", "bun.lock", "tsconfig.json", "pyrightconfig.json"]
path_suffixes = ["jsonc", "bun.lock", "devcontainer.json", "pyrightconfig.json", "tsconfig.json"]
line_comments = ["// "]
autoclose_before = ",]}"
brackets = [

View File

@@ -1293,34 +1293,13 @@ impl Project {
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
if init_worktree_trust {
let trust_remote_project = match &connection_options {
RemoteConnectionOptions::Ssh(..) | RemoteConnectionOptions::Wsl(..) => false,
RemoteConnectionOptions::Docker(..) => true,
};
let remote_host = RemoteHostLocation::from(connection_options);
trusted_worktrees::track_worktree_trust(
worktree_store.clone(),
Some(remote_host.clone()),
Some(RemoteHostLocation::from(connection_options)),
None,
Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
cx,
);
if trust_remote_project {
if let Some(trusted_worktres) = TrustedWorktrees::try_get_global(cx) {
trusted_worktres.update(cx, |trusted_worktres, cx| {
trusted_worktres.trust(
worktree_store
.read(cx)
.worktrees()
.map(|worktree| worktree.read(cx).id())
.map(PathTrust::Worktree)
.collect(),
Some(remote_host),
cx,
);
})
}
}
}
let weak_self = cx.weak_entity();

View File

@@ -1,12 +1,19 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use gpui::AsyncWindowContext;
use gpui::{
Action, AsyncWindowContext, DismissEvent, EventEmitter, FocusHandle, Focusable, RenderOnce,
};
use node_runtime::NodeRuntime;
use serde::Deserialize;
use settings::DevContainerConnection;
use smol::fs;
use workspace::Workspace;
use ui::{
App, Color, Context, Headline, HeadlineSize, Icon, IconName, InteractiveElement, IntoElement,
Label, ListItem, ListSeparator, ModalHeader, Navigable, NavigableEntry, ParentElement, Render,
Styled, StyledExt, Toggleable, Window, div, rems,
};
use workspace::{ModalView, Workspace, with_active_or_new_workspace};
use crate::remote_connections::Connection;
@@ -275,6 +282,122 @@ pub(crate) enum DevContainerError {
DevContainerParseFailed,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[action(namespace = containers)]
#[serde(deny_unknown_fields)]
pub struct InitDevContainer;
pub fn init(cx: &mut App) {
cx.on_action(|_: &InitDevContainer, cx| {
with_active_or_new_workspace(cx, move |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| DevContainerModal::new(window, cx));
});
});
}
struct DevContainerModal {
focus_handle: FocusHandle,
search_navigable_entry: NavigableEntry,
other_navigable_entry: NavigableEntry,
}
impl DevContainerModal {
fn new(window: &mut Window, cx: &mut App) -> Self {
let search_navigable_entry = NavigableEntry::focusable(cx);
let other_navigable_entry = NavigableEntry::focusable(cx);
let focus_handle = cx.focus_handle();
DevContainerModal {
focus_handle,
search_navigable_entry,
other_navigable_entry,
}
}
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl ModalView for DevContainerModal {}
impl EventEmitter<DismissEvent> for DevContainerModal {}
impl Focusable for DevContainerModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for DevContainerModal {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let mut view =
Navigable::new(
div()
.child(div().track_focus(&self.focus_handle).child(
ModalHeader::new().child(
Headline::new("Create Dev Container").size(HeadlineSize::XSmall),
),
))
.child(ListSeparator)
.child(
div()
.track_focus(&self.search_navigable_entry.focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
println!("action on search containers");
}))
.child(
ListItem::new("li-search-containers")
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
.toggle_state(
self.search_navigable_entry
.focus_handle
.contains_focused(window, cx),
)
.child(Label::new("Search for dev containers in registry")),
),
)
.child(
div()
.track_focus(&self.other_navigable_entry.focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
println!("action on other containers");
}))
.child(
ListItem::new("li-search-containers")
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
.toggle_state(
self.other_navigable_entry
.focus_handle
.contains_focused(window, cx),
)
.child(Label::new("Do another thing")),
),
)
.into_any_element(),
);
view = view.entry(self.search_navigable_entry.clone());
view = view.entry(self.other_navigable_entry.clone());
// // This is an interesting edge. Can't focus in render, or you'll just override whatever was focused before.
// // self.search_navigable_entry.focus_handle.focus(window, cx);
// view.render(window, cx).into_any_element()
div()
.elevation_3(cx)
.w(rems(34.))
// WHY IS THIS NEEDED FOR ACTION DISPATCH OMG
.key_context("ContainerModal")
.on_action(cx.listener(Self::dismiss))
.child(view.render(window, cx).into_any_element())
}
}
#[cfg(test)]
mod test {

View File

@@ -1,4 +1,4 @@
mod dev_container;
pub mod dev_container;
mod dev_container_suggest;
pub mod disconnected_overlay;
mod remote_connections;

View File

@@ -33,7 +33,7 @@ use assets::Assets;
use node_runtime::{NodeBinaryOptions, NodeRuntime};
use parking_lot::Mutex;
use project::{project_settings::ProjectSettings, trusted_worktrees};
use recent_projects::{SshSettings, open_remote_project};
use recent_projects::{SshSettings, dev_container, open_remote_project};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use session::{AppSession, Session};
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
@@ -616,6 +616,7 @@ fn main() {
agent_ui_v2::agents_panel::init(cx);
repl::init(app_state.fs.clone(), cx);
recent_projects::init(cx);
dev_container::init(cx);
load_embedded_fonts(cx);