diff --git a/docs/pi-main-project-zh.md b/docs/pi-main-project-zh.md
index 25d27e050..378e4cac0 100644
--- a/docs/pi-main-project-zh.md
+++ b/docs/pi-main-project-zh.md
@@ -122,7 +122,7 @@
## Scope audit(主工程预审检查点)
以主工程基线 `26a95aeb059235612c665d6b6bcd92fde9c3572e` 计,终态候选为
-140 个文件、约 +15.8k 净行,触发历史 90 文件/12k 净行审计线。审计结论为
+141 个文件、约 +16.9k 净行,触发历史 90 文件/12k 净行审计线。审计结论为
**范围大但未越界**:
- Rust 70 文件:Pi catalog/typed endpoints、共享文件与 ownership、gateway/runtime/
@@ -132,7 +132,7 @@
- 前端与构建配置 55 文件:3 个 Pi 专用 panel/form、typed IPC/query、既有导航/
设置/代理/使用统计接线、四语 i18n 与 Windows 进程树能力;没有复制第二套 app
shell。
-- 前端测试 12 文件:专用表单/prompt/usage 测试及既有跨 app matrix 增量。
+- 前端测试 13 文件:专用表单/prompt/usage 测试及既有跨 app matrix 增量。
- 证据与文档 3 文件:transport capture、本文账本、extensions 设计附录。
- `schema.rs`、`migration.rs`、`backup.rs`、三份认证套件及七个 pinned fixtures
均零改动;restore、themes 与 extensions 实现零文件。
diff --git a/src/components/proxy/ProxyToggle.tsx b/src/components/proxy/ProxyToggle.tsx
index 905b06561..2b246bb8d 100644
--- a/src/components/proxy/ProxyToggle.tsx
+++ b/src/components/proxy/ProxyToggle.tsx
@@ -45,21 +45,21 @@ export function ProxyToggle({ className, activeApp }: ProxyToggleProps) {
defaultValue: `${appLabel} routing is desired but currently degraded; CC Switch will retry it`,
})
: takeoverEnabled
- ? isRunning
- ? t("proxy.takeover.tooltip.active", {
+ ? isRunning
+ ? t("proxy.takeover.tooltip.active", {
+ appLabel,
+ address: status?.address,
+ port: status?.port,
+ defaultValue: `${appLabel} 已接管 - ${status?.address}:${status?.port}\n切换该应用供应商为热切换`,
+ })
+ : t("proxy.takeover.tooltip.broken", {
+ appLabel,
+ defaultValue: `${appLabel} 已接管,但代理服务未运行`,
+ })
+ : t("proxy.takeover.tooltip.inactive", {
appLabel,
- address: status?.address,
- port: status?.port,
- defaultValue: `${appLabel} 已接管 - ${status?.address}:${status?.port}\n切换该应用供应商为热切换`,
- })
- : t("proxy.takeover.tooltip.broken", {
- appLabel,
- defaultValue: `${appLabel} 已接管,但代理服务未运行`,
- })
- : t("proxy.takeover.tooltip.inactive", {
- appLabel,
- defaultValue: `接管 ${appLabel} 的 Live 配置,让该应用请求走本地代理`,
- });
+ defaultValue: `接管 ${appLabel} 的 Live 配置,让该应用请求走本地代理`,
+ });
return (
{
const detail =
@@ -144,6 +141,20 @@ export function useProxyStatus() {
}),
);
},
+ // Pi persists desired takeover before attempting listener/runtime work.
+ // A rejected IPC can therefore still have changed authoritative state.
+ onSettled: async () => {
+ await Promise.all([
+ queryClient.refetchQueries({
+ queryKey: proxyKeys.status,
+ type: "active",
+ }),
+ queryClient.refetchQueries({
+ queryKey: proxyKeys.takeoverStatus,
+ type: "active",
+ }),
+ ]);
+ },
});
return {
diff --git a/tests/hooks/useProxyStatus.test.tsx b/tests/hooks/useProxyStatus.test.tsx
index 675f5fb46..18192f098 100644
--- a/tests/hooks/useProxyStatus.test.tsx
+++ b/tests/hooks/useProxyStatus.test.tsx
@@ -131,4 +131,74 @@ describe("useProxyStatus", () => {
expect(invokeMock).toHaveBeenCalledWith("start_proxy_server");
expect(invokeMock).toHaveBeenCalledWith("stop_proxy_server");
});
+
+ it("refreshes desired takeover state even when activation rejects", async () => {
+ let desiredPi = false;
+ invokeMock.mockImplementation((command: string) => {
+ if (command === "get_proxy_status") {
+ return Promise.resolve({
+ running: false,
+ address: "127.0.0.1",
+ port: 15721,
+ active_connections: 0,
+ total_requests: 0,
+ success_requests: 0,
+ failed_requests: 0,
+ success_rate: 0,
+ uptime_seconds: 0,
+ current_provider: null,
+ current_provider_id: null,
+ last_request_at: null,
+ last_error: null,
+ failover_count: 0,
+ });
+ }
+ if (command === "get_proxy_takeover_status") {
+ return Promise.resolve({
+ claude: false,
+ codex: false,
+ gemini: false,
+ grokbuild: false,
+ opencode: false,
+ openclaw: false,
+ pi: desiredPi,
+ piOperationalState: desiredPi ? "degraded" : "disabled",
+ });
+ }
+ if (command === "set_proxy_takeover_for_app") {
+ desiredPi = true;
+ return Promise.reject(new Error("listener unavailable"));
+ }
+ return Promise.resolve(null);
+ });
+
+ const { wrapper, queryClient } = createWrapper();
+ const { result, rerender } = renderHook(() => useProxyStatus(), {
+ wrapper,
+ });
+ await waitFor(() => expect(result.current.takeoverStatus).toBeDefined());
+
+ await expect(
+ act(async () => {
+ await result.current.setTakeoverForApp({
+ appType: "pi",
+ enabled: true,
+ });
+ }),
+ ).rejects.toThrow("listener unavailable");
+
+ await waitFor(() =>
+ expect(
+ invokeMock.mock.calls.filter(
+ ([command]) => command === "get_proxy_takeover_status",
+ ).length,
+ ).toBeGreaterThan(1),
+ );
+ expect(queryClient.getQueryData(proxyKeys.takeoverStatus)).toMatchObject({
+ pi: true,
+ piOperationalState: "degraded",
+ });
+ rerender();
+ await waitFor(() => expect(result.current.takeoverStatus?.pi).toBe(true));
+ });
});