Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ mod smoke;
use std::{borrow::Cow, time::Duration};

use gpui::{
App, AppContext as _, Entity, KeyBinding, ParentElement as _, TitlebarOptions, WindowBounds,
WindowOptions, point, px, size,
App, AppContext as _, Entity, KeyBinding, ParentElement as _, Styled as _, TitlebarOptions,
WindowBackgroundAppearance, WindowBounds, WindowOptions, point, px, size,
};
use tcode_runtime::app::AppState;
use tcode_services::{shell_env, store::SessionStore};
Expand All @@ -22,6 +22,23 @@ use gpui_component::{
};

const TCODE_THEME: &str = include_str!("../../../themes/tcode.json");

/// On platforms with an opaque window the translucent canvas colors would
/// composite against black; flatten them to their solid RGB. Keep the literals
/// in sync with themes/tcode.json (checked by `smoke` builds via debug_assert).
/// Vibrancy is macOS-only and can be disabled with `TCODE_NO_VIBRANCY=1`
/// (diagnostic escape hatch: opaque window + flattened palette).
fn vibrancy_enabled() -> bool {
cfg!(target_os = "macos") && !std::env::var("TCODE_NO_VIBRANCY").is_ok_and(|v| v == "1")
}

fn flatten_canvas_for_opaque_window(theme_json: &str) -> String {
let flattened = theme_json
.replace("#F2F4F7C7", "#F2F4F7")
.replace("#15171CC7", "#15171C");
debug_assert_ne!(flattened, theme_json, "canvas colors moved; update flatten");
flattened
}
const QUIT_PROMPT_TIMEOUT: Duration = Duration::from_secs(15);

fn finish_quit_prompt(app_state: &Entity<AppState>, epoch: u64, cx: &mut App) -> bool {
Expand Down Expand Up @@ -69,7 +86,8 @@ fn handle_quit(_: &Quit, app_state: &Entity<AppState>, cx: &mut App) {
let cancel_state = prompt_state.clone();
let enter_state = prompt_state.clone();
let escape_state = prompt_state.clone();
window.open_alert_dialog(cx, move |alert, _, _| {
window.open_alert_dialog(cx, move |alert, _, cx| {
let alert = alert.bg(cx.theme().popover);
let quit_state = quit_state.clone();
let cancel_state = cancel_state.clone();
let enter_state = enter_state.clone();
Expand Down Expand Up @@ -279,8 +297,17 @@ fn main() {
cx.text_system()
.add_fonts(vec![Cow::Borrowed(assets::DM_SANS)])
.expect("failed to register bundled DM Sans font");
// The theme's canvas color is translucent so the macOS vibrancy
// material shows through (docs/visual-redesign.md). Elsewhere the
// window is opaque and that alpha would composite against black,
// so flatten the canvas onto each mode's solid base first.
let theme_json: Cow<'_, str> = if vibrancy_enabled() {
Cow::Borrowed(TCODE_THEME)
} else {
Cow::Owned(flatten_canvas_for_opaque_window(TCODE_THEME))
};
ThemeRegistry::global_mut(cx)
.load_themes_from_str(TCODE_THEME)
.load_themes_from_str(&theme_json)
.expect("embedded themes/tcode.json must be valid");
let light = ThemeRegistry::global(cx).themes()["tcode Light"].clone();
let dark = ThemeRegistry::global(cx).themes()["tcode Dark"].clone();
Expand Down Expand Up @@ -396,6 +423,13 @@ fn main() {
appears_transparent: cfg!(target_os = "macos"),
traffic_light_position: Some(point(px(12.), px(18.))),
}),
// macOS vibrancy: blur whatever is behind the window; theme
// background colors carry alpha so the material shows through.
window_background: if vibrancy_enabled() {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Opaque
},
..Default::default()
};

Expand Down
97 changes: 48 additions & 49 deletions crates/ui/src/acp_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use gpui::{
AnyElement, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Subscription, Window,
div, prelude::FluentBuilder as _, px, rgb,
div, prelude::FluentBuilder as _, px,
};
use gpui_component::{
ActiveTheme as _, Icon, IconName, Sizable as _, StyledExt as _, WindowExt as _,
Expand All @@ -19,6 +19,8 @@ use tcode_core::acp::InstalledAcpAgent;
use tcode_runtime::app::AppState;
use tcode_runtime::ui_facade::AcpMarketplaceItem;

use crate::material;

/// One installed ACP agent, rendered with the same anatomy as a native provider card.
pub struct AcpAgentCard {
app_state: Entity<AppState>,
Expand Down Expand Up @@ -67,7 +69,7 @@ impl AcpAgentCard {
let dot_color = if agent.enabled {
cx.theme().success
} else {
rgb(0xf59e0b).into()
cx.theme().warning
};
let glyph = div()
.relative()
Expand All @@ -93,15 +95,15 @@ impl AcpAgentCard {
.items_center()
.child(
div()
.text_size(px(14.))
.text_size(px(15.))
.font_semibold()
.child(agent.name.clone()),
)
.when(!agent.version.is_empty(), |row| {
row.child(
div()
.font_family("monospace")
.text_size(px(12.))
.text_size(px(13.))
.text_color(muted)
.child(format!("v{}", agent.version.trim_start_matches('v'))),
)
Expand All @@ -117,7 +119,7 @@ impl AcpAgentCard {
.child(
v_flex().flex_1().min_w_0().gap_0p5().child(title).child(
div()
.text_size(px(12.))
.text_size(px(13.))
.text_color(muted)
.child(launch_summary(agent)),
),
Expand Down Expand Up @@ -150,14 +152,12 @@ impl AcpAgentCard {
.into_any_element()
}

fn field_block(&self, label: String, control: AnyElement, cx: &Context<Self>) -> AnyElement {
fn field_block(&self, label: String, control: AnyElement, _cx: &Context<Self>) -> AnyElement {
v_flex()
.w_full()
.px_4()
.py_3()
.gap_1p5()
.border_t_1()
.border_color(cx.theme().border)
.child(div().text_size(px(13.)).font_medium().child(label))
.child(control)
.into_any_element()
Expand Down Expand Up @@ -214,25 +214,18 @@ impl AcpAgentCard {
),
)
.child(
h_flex()
.w_full()
.justify_end()
.px_4()
.py_3()
.border_t_1()
.border_color(cx.theme().border)
.child(
Button::new(gpui::SharedString::from(format!("acp-remove-{remove_id}")))
.outline()
.danger()
.small()
.label(tcode_i18n::tr!("providers.acp.remove").into_owned())
.on_click(cx.listener(move |this, _, _, cx| {
let id = remove_id.clone();
this.app_state
.update(cx, |state, cx| state.remove_acp_agent(&id, cx));
})),
),
h_flex().w_full().justify_end().px_4().py_3().child(
Button::new(gpui::SharedString::from(format!("acp-remove-{remove_id}")))
.outline()
.danger()
.small()
.label(tcode_i18n::tr!("providers.acp.remove").into_owned())
.on_click(cx.listener(move |this, _, _, cx| {
let id = remove_id.clone();
this.app_state
.update(cx, |state, cx| state.remove_acp_agent(&id, cx));
})),
),
)
.into_any_element()
}
Expand All @@ -246,10 +239,15 @@ impl Render for AcpAgentCard {
.settings
.acp_agent(&self.agent_id)
.cloned();
v_flex().w_full().when_some(agent, |card, agent| {
card.child(self.render_header(&agent, cx))
.when(self.expanded, |card| card.child(self.render_details(cx)))
})
v_flex()
.w_full()
.rounded(material::radius_card())
.bg(cx.theme().secondary)
.overflow_hidden()
.when_some(agent, |card, agent| {
card.child(self.render_header(&agent, cx))
.when(self.expanded, |card| card.child(self.render_details(cx)))
})
}
}

Expand Down Expand Up @@ -302,10 +300,12 @@ impl AcpPanel {
fn render_market_row(&self, agent: &AcpMarketplaceItem, cx: &mut Context<Self>) -> AnyElement {
let id = agent.id.clone();
h_flex()
.id(gpui::SharedString::from(format!("acp-market-row-{id}")))
.w_full()
.p_3()
.gap_3()
.items_start()
.hover(|row| row.bg(cx.theme().list_hover))
.child(
Icon::empty()
.path("icons/box.svg")
Expand All @@ -322,36 +322,40 @@ impl AcpPanel {
.items_center()
.child(
div()
.text_size(px(14.))
.text_size(px(15.))
.font_medium()
.child(agent.name.clone()),
)
.when(!agent.version.is_empty(), |row| {
row.child(
div()
.font_family("monospace")
.text_size(px(12.))
.text_size(px(13.))
.text_color(cx.theme().muted_foreground)
.child(format!("v{}", agent.version)),
)
}),
)
.child(
div()
.text_size(px(12.))
.text_size(px(13.))
.text_color(cx.theme().muted_foreground)
.child(agent.description.clone()),
),
)
.child(if agent.installed {
div()
.text_size(px(12.))
.text_color(cx.theme().muted_foreground)
.rounded_full()
.bg(cx.theme().success.opacity(0.12))
.text_size(px(13.))
.text_color(cx.theme().success_foreground)
.child(tcode_i18n::tr!("providers.acp.installed").into_owned())
.into_any_element()
} else if !agent.supported {
div()
.text_size(px(12.))
.rounded_full()
.bg(cx.theme().muted)
.text_size(px(13.))
.text_color(cx.theme().muted_foreground)
.child(tcode_i18n::tr!("providers.acp.unsupported").into_owned())
.into_any_element()
Expand Down Expand Up @@ -390,33 +394,31 @@ impl AcpPanel {
let error = state.acp_registry_error.clone();
let loading = state.acp_registry_loading;
let empty = market.is_empty();
let border = cx.theme().border;
let mut rows = v_flex().w_full();
if let Some(error) = error.filter(|_| empty) {
rows = rows.child(
div()
.flex_none()
.p_3()
.text_size(px(12.))
.text_color(cx.theme().danger)
.text_size(px(13.))
.text_color(cx.theme().danger_foreground)
.child(error),
);
} else if empty && loading {
rows = rows.child(
div()
.flex_none()
.p_3()
.text_size(px(12.))
.text_size(px(13.))
.text_color(cx.theme().muted_foreground)
.child(tcode_i18n::tr!("providers.acp.loading").into_owned()),
);
}
for (index, agent) in market.iter().enumerate() {
for agent in &market {
rows = rows.child(
v_flex()
.w_full()
.flex_none()
.when(index > 0, |row| row.border_t_1().border_color(border))
.child(self.render_market_row(agent, cx)),
);
}
Expand All @@ -429,9 +431,8 @@ impl AcpPanel {
.w_full()
.h(px(360.))
.overflow_y_scrollbar()
.rounded(cx.theme().radius)
.border_1()
.border_color(border)
.rounded(material::radius_card())
.bg(cx.theme().muted)
.child(div().size_full().child(rows)),
)
.child(
Expand All @@ -441,8 +442,6 @@ impl AcpPanel {
.pt_3()
.gap_2()
.items_center()
.border_t_1()
.border_color(border)
.cursor_pointer()
.child(Icon::new(IconName::Plus).text_color(cx.theme().muted_foreground))
.child(
Expand Down Expand Up @@ -487,7 +486,7 @@ impl AcpPanel {
)
.child(
div()
.text_size(px(12.))
.text_size(px(13.))
.text_color(cx.theme().muted_foreground)
.child(tcode_i18n::tr!("providers.acp.custom_help").into_owned()),
)
Expand Down
Loading