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 173 additions and 113 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;
@@ -53,9 +60,7 @@ async fn check_for_docker() -> Result<(), DevContainerError> {
}
}
async fn ensure_devcontainer_cli(
node_runtime: &NodeRuntime,
) -> Result<(PathBuf, bool), DevContainerError> {
async fn ensure_devcontainer_cli(node_runtime: NodeRuntime) -> Result<PathBuf, DevContainerError> {
let mut command = util::command::new_smol_command(&dev_container_cli());
command.arg("--version");
@@ -65,42 +70,23 @@ async fn ensure_devcontainer_cli(
e
);
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let datadir_cli_path = paths::devcontainer_dir()
.join("node_modules")
.join("@devcontainers")
.join("cli")
.join(format!("{}.js", &dev_container_cli()));
log::debug!(
"devcontainer not found in path, using local location: ${}",
datadir_cli_path.display()
);
.join(".bin")
.join(&dev_container_cli());
let mut command =
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
command.arg(datadir_cli_path.display().to_string());
util::command::new_smol_command(&datadir_cli_path.as_os_str().display().to_string());
command.arg("--version");
match command.output().await {
Err(e) => log::error!(
if let Err(e) = command.output().await {
log::error!(
"Unable to find devcontainer CLI in Data dir. Will try to install. Error: {:?}",
e
),
Ok(output) => {
if output.status.success() {
log::info!("Found devcontainer CLI in Data dir");
return Ok((datadir_cli_path.clone(), false));
} else {
log::error!(
"Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}",
output
);
}
}
);
} else {
log::info!("Found devcontainer CLI in Data dir");
return Ok(datadir_cli_path.clone());
}
if let Err(e) = fs::create_dir_all(paths::devcontainer_dir()).await {
@@ -122,9 +108,7 @@ async fn ensure_devcontainer_cli(
return Err(DevContainerError::DevContainerCliNotAvailable);
};
let mut command =
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
command.arg(datadir_cli_path.display().to_string());
let mut command = util::command::new_smol_command(&datadir_cli_path.display().to_string());
command.arg("--version");
if let Err(e) = command.output().await {
log::error!(
@@ -133,42 +117,22 @@ async fn ensure_devcontainer_cli(
);
Err(DevContainerError::DevContainerCliNotAvailable)
} else {
Ok((datadir_cli_path, false))
Ok(datadir_cli_path)
}
} else {
log::info!("Found devcontainer cli on $PATH, using it");
Ok((PathBuf::from(&dev_container_cli()), true))
Ok(PathBuf::from(&dev_container_cli()))
}
}
async fn devcontainer_up(
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime: &NodeRuntime,
path: Arc<Path>,
) -> Result<DevContainerUp, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
log::error!("Unable to find node runtime path");
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let mut command = if found_in_path {
let mut command = util::command::new_smol_command(path_to_cli.display().to_string());
command.arg("up");
command.arg("--workspace-folder");
command.arg(path.display().to_string());
command
} else {
let mut command =
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
command.arg(path_to_cli.display().to_string());
command.arg("up");
command.arg("--workspace-folder");
command.arg(path.display().to_string());
command
};
log::debug!("Running full devcontainer up command: {:?}", command);
let mut command = util::command::new_smol_command(path_to_cli.display().to_string());
command.arg("up");
command.arg("--workspace-folder");
command.arg(path.display().to_string());
match command.output().await {
Ok(output) => {
@@ -278,7 +242,7 @@ pub(crate) async fn start_dev_container(
) -> Result<(Connection, String), DevContainerError> {
check_for_docker().await?;
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
let path_to_devcontainer_cli = ensure_devcontainer_cli(node_runtime).await?;
let Some(directory) = project_directory(cx) else {
return Err(DevContainerError::DevContainerNotFound);
@@ -288,13 +252,7 @@ pub(crate) async fn start_dev_container(
container_id,
remote_workspace_folder,
..
}) = devcontainer_up(
&path_to_devcontainer_cli,
found_in_path,
&node_runtime,
directory.clone(),
)
.await
}) = devcontainer_up(&path_to_devcontainer_cli, directory.clone()).await
{
let project_name = get_project_name(
&path_to_devcontainer_cli,
@@ -322,7 +280,122 @@ pub(crate) enum DevContainerError {
DevContainerUpFailed,
DevContainerNotFound,
DevContainerParseFailed,
NodeRuntimeNotAvailable,
}
#[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)]

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);