mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
feat(tray): rebuild the tray menu around apps and their modes
The native menu now lists Claude Code, Claude Desktop, Codex, Gemini CLI and Grok Build (hidden apps are skipped; additive apps stay out). Each app row reads "name · mode · provider · quota" and opens a submenu that states the mode and only offers providers that switching can actually use: providers that need routing open the app page with the same needs-routing dialog, official subscriptions are disabled under routing, and the menu is read-only while failover is on instead of silently turning failover off. Projects move into the app submenus. Quota text matches the cards (remaining, "running low" under 10%, no emoji). A problems section appears only when something needs action: the routing service is down while apps use it, an app fell back to direct at startup, or a tray switch failed. On Windows a left click opens the window and a right click shows the menu; macOS and Linux keep the menu on left click.
This commit is contained in:
1 parent
b999b07bec
commit
4f8d1424a6
10 files changed
+3182
-994
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
parseTrayNavigation,
|
||||
useTrayNavigation,
|
||||
} from "@/hooks/useTrayNavigation";
|
||||
import { server } from "../msw/server";
|
||||
import { emitTauriEvent } from "../msw/tauriMocks";
|
||||
|
||||
const TAURI_ENDPOINT = "http://tauri.local";
|
||||
|
||||
/** 后端留下的导航:取一次就清空(同 `tray::take_tray_navigation`) */
|
||||
function pendingNavigation(initial: unknown) {
|
||||
let pending: unknown = initial;
|
||||
server.use(
|
||||
http.post(`${TAURI_ENDPOINT}/take_tray_navigation`, () => {
|
||||
const value = pending;
|
||||
pending = null;
|
||||
return HttpResponse.json(value as any);
|
||||
}),
|
||||
);
|
||||
return (next: unknown) => {
|
||||
pending = next;
|
||||
};
|
||||
}
|
||||
|
||||
describe("parseTrayNavigation", () => {
|
||||
it("keeps known apps, sections and intents", () => {
|
||||
expect(
|
||||
parseTrayNavigation({
|
||||
app: "claude",
|
||||
intent: "needsRoute",
|
||||
providerId: "copilot",
|
||||
}),
|
||||
).toEqual({ app: "claude", intent: "needsRoute", providerId: "copilot" });
|
||||
expect(parseTrayNavigation({ app: "grokbuild", intent: "add" })).toEqual({
|
||||
app: "grokbuild",
|
||||
intent: "add",
|
||||
providerId: undefined,
|
||||
});
|
||||
expect(parseTrayNavigation({ section: "routing" })).toEqual({
|
||||
section: "routing",
|
||||
});
|
||||
});
|
||||
|
||||
it("drops unknown targets and incomplete needs-route requests", () => {
|
||||
expect(parseTrayNavigation(null)).toBeNull();
|
||||
expect(parseTrayNavigation({ app: "nope" })).toBeNull();
|
||||
expect(parseTrayNavigation({ section: "nope" })).toBeNull();
|
||||
expect(parseTrayNavigation({ app: "codex", intent: "needsRoute" })).toEqual(
|
||||
{ app: "codex" },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTrayNavigation", () => {
|
||||
it("takes a navigation left before the window existed", async () => {
|
||||
pendingNavigation({ app: "codex" });
|
||||
const onNavigate = vi.fn();
|
||||
renderHook(() => useTrayNavigation(onNavigate));
|
||||
await waitFor(() =>
|
||||
expect(onNavigate).toHaveBeenCalledWith({
|
||||
app: "codex",
|
||||
intent: undefined,
|
||||
providerId: undefined,
|
||||
}),
|
||||
);
|
||||
expect(onNavigate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("takes the navigation again on every tray-navigate event", async () => {
|
||||
const leave = pendingNavigation(null);
|
||||
const onNavigate = vi.fn();
|
||||
renderHook(() => useTrayNavigation(onNavigate));
|
||||
// 挂载时取过一次(空的),监听已经挂上
|
||||
await waitFor(() => expect(onNavigate).not.toHaveBeenCalled());
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
leave({ app: "claude", intent: "needsRoute", providerId: "copilot" });
|
||||
emitTauriEvent("tray-navigate", null);
|
||||
await waitFor(() =>
|
||||
expect(onNavigate).toHaveBeenCalledWith({
|
||||
app: "claude",
|
||||
intent: "needsRoute",
|
||||
providerId: "copilot",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user