feat(tray): color the mode name in the panel with its theme color

The app rows in the tray panel now show Direct / Route / Aggregation in
the same mode colors as the main window (direct blue, route teal,
aggregation purple, with the dimmed dark-theme variants), so the modes
are easy to tell apart at a glance. The panel model carries a
machine-readable modeKind next to the localized mode name.
This commit is contained in:
Jason committed 2026-10-19 18:01:47 +08:00
1 parent e003829be8
commit e2af66ca61
4 files changed
+48 -4

No files matched your search

+7
View File
@@ -2056,6 +2056,8 @@ pub struct PanelApp {
pub current: Option<String>,
/// 直连 / 路由 / 聚合。
pub mode: &'static str,
/// 同一模式的机器可读值(`direct` / `route` / `stack`),面板按它给模式名上主题色。
pub mode_kind: &'static str,
/// 子面板标题下的说明(「路由 · 故障转移开启中」等)。
pub header: Option<String>,
pub attention: Option<&'static str>,
@@ -2280,6 +2282,11 @@ pub(crate) fn collect_panel_model(app_state: &AppState) -> Result<PanelModel, Ap
TrayMode::Route | TrayMode::Failover => texts.mode_route,
TrayMode::Stack => texts.mode_stack,
},
mode_kind: match snapshot.mode {
TrayMode::Direct => "direct",
TrayMode::Route | TrayMode::Failover => "route",
TrayMode::Stack => "stack",
},
header,
attention: snapshot.needs_attention.then_some(texts.needs_attention),
quota,
+18 -4
View File
@@ -25,6 +25,7 @@ import {
import { AppGlyph } from "@/components/shell/AppGlyph";
import { ProviderIcon } from "@/components/ProviderIcon";
import { cn } from "@/lib/utils";
import type { AppMode } from "@/types/proxy";
import {
CHANGED_EVENT,
SHOW_EVENT,
@@ -167,6 +168,13 @@ function QuotaBlock({
// ─── 主面板的应用行 ───────────────────────────────────────────────────────────
// 模式名用主界面的模式色(与 ModeTabs 的 MODE_TONE.text 同一组 token),一眼分出直连 / 路由 / 聚合。
const MODE_TEXT: Record<AppMode, string> = {
direct: "text-direct-text",
route: "text-route-text",
stack: "text-stack-text",
};
function AppRow({
app,
texts,
@@ -185,9 +193,6 @@ function AppRow({
onOpen: () => void;
}) {
const Chevron = side === "left" ? ChevronLeft : ChevronRight;
const subtitle = app.addId
? app.addText
: [app.current, app.mode].filter(Boolean).join(" · ");
return (
<button
type="button"
@@ -222,7 +227,16 @@ function AppRow({
)}
</span>
<span className="block truncate text-xs text-muted-foreground">
{subtitle}
{app.addId ? (
app.addText
) : (
<>
{app.current && `${app.current} · `}
<span className={cn("font-medium", MODE_TEXT[app.modeKind])}>
{app.mode}
</span>
</>
)}
</span>
{app.quota && <QuotaBlock quota={app.quota} texts={texts} />}
</span>
+2
View File
@@ -1,5 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import type { AppId } from "@/lib/api/types";
import type { AppMode } from "@/types/proxy";
/** 托盘面板的数据(后端 `tray_panel::TrayPanelState`)。 */
export interface PanelTexts {
@@ -63,6 +64,7 @@ export interface PanelApp {
name: string;
current: string | null;
mode: string;
modeKind: AppMode;
header: string | null;
attention: string | null;
quota: PanelQuota | null;
+21
View File
@@ -54,6 +54,7 @@ const state: TrayPanelState = {
name: "Claude Code",
current: "default",
mode: "Direct",
modeKind: "direct",
header: null,
attention: null,
quota: null,
@@ -242,6 +243,26 @@ describe("TrayPanel window layout", () => {
);
});
it.each([
["direct", "Direct", "text-direct-text"],
["route", "Route", "text-route-text"],
["stack", "Aggregation", "text-stack-text"],
] as const)(
"colors the %s mode name with its theme color",
async (modeKind, mode, className) => {
vi.mocked(trayPanelApi.state).mockResolvedValue({
...state,
apps: [{ ...state.apps[0], mode, modeKind }],
});
vi.mocked(trayPanelApi.placement).mockResolvedValue(windows);
renderPanel();
await screen.findByRole("button", {
name: `Claude Code default · ${mode}`,
});
expect(screen.getByText(mode)).toHaveClass(className);
},
);
it("opens Windows top/left taskbars toward the opposite edges", async () => {
const { container } = await openPanel(windows);
await act(async () => {