feat(shell-core): TopBar empty-agent chip — 'Agents 与 MCP' affordance

Default Document has no connected agent, so for_document now sets
agent_count = 0 and the chip switches to the empty-state look:
LayoutGrid icon + 'Agents 与 MCP' label (TS topbar.agentsAndMcp /
en topbar.agentsAndMcp). Active state (agent_count >= 1) keeps the
Sparkles + green dot + 'N agent' look.

Chip width is now driven by RenderBackend::measure_text so the
border ring tracks the actual rendered string instead of a
per-char estimate.
This commit is contained in:
Kayshen-X 2026-05-10 19:11:06 +08:00
parent c6020e3aeb
commit e40c3a6e1c

View file

@ -32,6 +32,13 @@ pub struct TopBar {
pub file_name: String,
pub agent_count: u32,
pub theme: Theme,
/// Localised "Agents 与 MCP" label — rendered when
/// `agent_count == 0` (empty state).
pub label_agents_and_mcp: String,
/// Localised "agent" / "agents" label — appended to the
/// count when `agent_count > 0`. Falls back to "agent"
/// when the locale doesn't ship the key.
pub label_agent_singular: String,
}
impl TopBar {
@ -41,6 +48,8 @@ impl TopBar {
file_name: file_name.into(),
agent_count: 1,
theme: Theme::dark(),
label_agents_and_mcp: "Agents & MCP".to_string(),
label_agent_singular: "agent".to_string(),
}
}
@ -50,13 +59,17 @@ impl TopBar {
/// Document-aware constructor — reads the active theme + the
/// localised "untitled" label so the bar flips with theme +
/// locale toggles.
/// locale toggles. Default `agent_count = 0` matches the TS
/// app's empty state ("Agents 与 MCP" affordance instead of
/// the green-dot "1 agent" chip).
pub fn for_document(doc: &Document) -> Self {
Self {
id: WidgetId::new(5000),
file_name: doc.t("common.untitled").to_string(),
agent_count: 1,
agent_count: 0,
theme: doc.theme(),
label_agents_and_mcp: doc.t("topbar.agentsAndMcp").to_string(),
label_agent_singular: doc.t("topbar.agentSingular").to_string(),
}
}
@ -178,45 +191,55 @@ impl Widget for TopBar {
// i18n globe.
paint_icon_button(cx, &self.theme, rx, center_y, Icon::Globe);
// Agent indicator chip (sparkles + green dot + "N agent").
// Agent chip — two states:
// - empty (agent_count == 0): LayoutGrid icon + "Agents
// 与 MCP" label. Affordance for "set up agents/MCP".
// - active (≥ 1): Sparkles + green dot + "N agent" text.
// Anchored just left of the globe icon button (+small gap).
let chip_text = format!("{} agent", self.agent_count);
let chip_w =
8.0 + ICON_SIZE + 6.0 + 8.0 + 6.0 + chip_text.chars().count() as f32 * 7.0 + 8.0;
let (chip_text, leading_icon, show_dot) = if self.agent_count == 0 {
(self.label_agents_and_mcp.clone(), Icon::LayoutGrid, false)
} else {
(
format!("{} {}", self.agent_count, self.label_agent_singular),
Icon::Sparkles,
true,
)
};
let dot_w = if show_dot { 8.0 + 6.0 } else { 0.0 };
let text_w = cx.backend.measure_text(&chip_text, 12.0);
let chip_w = 8.0 + ICON_SIZE + 6.0 + dot_w + text_w + 12.0;
let chip_rect = Rect {
origin: Point2D::new(rx - chip_w - 6.0, center_y - 13.0),
size: Point2D::new(chip_w, 26.0),
};
cx.backend
.stroke_round_rect(chip_rect, 13.0, self.theme.border, 1.0);
// Sparkles icon at the chip's left.
draw_icon(
cx.backend,
Icon::Sparkles,
leading_icon,
Point2D::new(chip_rect.origin.x + 8.0, chip_rect.origin.y + 4.0),
ICON_SIZE,
self.theme.foreground,
1.4,
);
// Green dot.
let dot_color = Color {
r: 0.0,
g: 0.85,
b: 0.4,
a: 1.0,
};
cx.backend.fill_round_rect(
Rect {
origin: Point2D::new(
chip_rect.origin.x + 8.0 + ICON_SIZE + 6.0,
chip_rect.origin.y + chip_rect.size.y / 2.0 - 4.0,
),
size: Point2D::new(8.0, 8.0),
},
4.0,
dot_color,
);
// "N agent" text.
let mut text_x = chip_rect.origin.x + 8.0 + ICON_SIZE + 6.0;
if show_dot {
let dot_color = Color {
r: 0.0,
g: 0.85,
b: 0.4,
a: 1.0,
};
cx.backend.fill_round_rect(
Rect {
origin: Point2D::new(text_x, chip_rect.origin.y + chip_rect.size.y / 2.0 - 4.0),
size: Point2D::new(8.0, 8.0),
},
4.0,
dot_color,
);
text_x += 8.0 + 6.0;
}
let chip_label = TextLayout::single_run(
&chip_text,
"system-ui",
@ -224,13 +247,8 @@ impl Widget for TopBar {
to_jian_color(self.theme.foreground),
Point2D::new(0.0, 0.0),
);
cx.backend.draw_text(
&chip_label,
Point2D::new(
chip_rect.origin.x + 8.0 + ICON_SIZE + 6.0 + 14.0,
chip_rect.origin.y + 18.0,
),
);
cx.backend
.draw_text(&chip_label, Point2D::new(text_x, chip_rect.origin.y + 18.0));
}
fn access_node(&self) -> accesskit::Node {