Reduce redundant runtime updates
This commit is contained in:
@@ -49,7 +49,10 @@ vi.mock("@server/client/daemon-client", () => ({}));
|
|||||||
function createTestClient() {
|
function createTestClient() {
|
||||||
return {
|
return {
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
subscribeConnectionStatus: vi.fn(() => vi.fn()),
|
subscribeConnectionStatus: vi.fn((listener: (state: { status: "connected" }) => void) => {
|
||||||
|
listener({ status: "connected" });
|
||||||
|
return vi.fn();
|
||||||
|
}),
|
||||||
sendHeartbeat: vi.fn<(payload: HeartbeatPayload) => void>(),
|
sendHeartbeat: vi.fn<(payload: HeartbeatPayload) => void>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -68,13 +71,15 @@ function heartbeatTimeMs(client: ReturnType<typeof createTestClient>): number {
|
|||||||
|
|
||||||
async function renderActivityHook({
|
async function renderActivityHook({
|
||||||
client = createTestClient(),
|
client = createTestClient(),
|
||||||
|
focusedAgentId = "agent-1",
|
||||||
}: {
|
}: {
|
||||||
client?: ReturnType<typeof createTestClient>;
|
client?: ReturnType<typeof createTestClient>;
|
||||||
|
focusedAgentId?: string | null;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
function Probe() {
|
function Probe({ focusedAgentId }: { focusedAgentId: string | null }) {
|
||||||
useClientActivity({
|
useClientActivity({
|
||||||
client: client as unknown as Parameters<typeof useClientActivity>[0]["client"],
|
client: client as unknown as Parameters<typeof useClientActivity>[0]["client"],
|
||||||
focusedAgentId: "agent-1",
|
focusedAgentId,
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -84,10 +89,16 @@ async function renderActivityHook({
|
|||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.render(<Probe />);
|
root.render(<Probe focusedAgentId={focusedAgentId} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { client, root };
|
const rerender = async (nextFocusedAgentId: string | null) => {
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<Probe focusedAgentId={nextFocusedAgentId} />);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { client, rerender, root };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function advance(ms: number) {
|
async function advance(ms: number) {
|
||||||
@@ -138,6 +149,24 @@ describe("useClientActivity", () => {
|
|||||||
expect(getDesktopSystemIdleTimeMs).not.toHaveBeenCalled();
|
expect(getDesktopSystemIdleTimeMs).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sends one immediate heartbeat when the focused agent changes", async () => {
|
||||||
|
const rendered = await renderActivityHook();
|
||||||
|
root = rendered.root;
|
||||||
|
|
||||||
|
expect(rendered.client.sendHeartbeat).toHaveBeenCalledTimes(1);
|
||||||
|
rendered.client.sendHeartbeat.mockClear();
|
||||||
|
|
||||||
|
vi.setSystemTime(new Date("2026-04-19T10:00:05.000Z"));
|
||||||
|
await rendered.rerender("agent-2");
|
||||||
|
|
||||||
|
expect(rendered.client.subscribeConnectionStatus).toHaveBeenCalledTimes(1);
|
||||||
|
expect(rendered.client.sendHeartbeat).toHaveBeenCalledTimes(1);
|
||||||
|
expect(latestHeartbeat(rendered.client)).toMatchObject({
|
||||||
|
focusedAgentId: "agent-2",
|
||||||
|
lastActivityAt: "2026-04-19T10:00:05.000Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("drives lastActivityAt forward from Electron idle polling", async () => {
|
it("drives lastActivityAt forward from Electron idle polling", async () => {
|
||||||
platformState.isElectron = true;
|
platformState.isElectron = true;
|
||||||
getDesktopSystemIdleTimeMs.mockResolvedValue(0);
|
getDesktopSystemIdleTimeMs.mockResolvedValue(0);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AppState } from "react-native";
|
|||||||
import type { DaemonClient } from "@server/client/daemon-client";
|
import type { DaemonClient } from "@server/client/daemon-client";
|
||||||
import { getIsElectron, isWeb, isNative } from "@/constants/platform";
|
import { getIsElectron, isWeb, isNative } from "@/constants/platform";
|
||||||
import { getDesktopSystemIdleTimeMs } from "@/desktop/electron/idle";
|
import { getDesktopSystemIdleTimeMs } from "@/desktop/electron/idle";
|
||||||
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
|
|
||||||
const HEARTBEAT_INTERVAL_MS = 15_000;
|
const HEARTBEAT_INTERVAL_MS = 15_000;
|
||||||
const ACTIVITY_HEARTBEAT_THROTTLE_MS = 5_000;
|
const ACTIVITY_HEARTBEAT_THROTTLE_MS = 5_000;
|
||||||
@@ -41,7 +42,7 @@ export function useClientActivity({
|
|||||||
lastActivityAtRef.current = new Date();
|
lastActivityAtRef.current = new Date();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sendHeartbeat = useCallback(() => {
|
const sendHeartbeat = useStableEvent(() => {
|
||||||
if (!client.isConnected) return;
|
if (!client.isConnected) return;
|
||||||
client.sendHeartbeat({
|
client.sendHeartbeat({
|
||||||
deviceType,
|
deviceType,
|
||||||
@@ -50,7 +51,7 @@ export function useClientActivity({
|
|||||||
appVisible: appVisibleRef.current,
|
appVisible: appVisibleRef.current,
|
||||||
appVisibilityChangedAt: appVisibilityChangedAtRef.current.toISOString(),
|
appVisibilityChangedAt: appVisibilityChangedAtRef.current.toISOString(),
|
||||||
});
|
});
|
||||||
}, [client, deviceType, focusedAgentId]);
|
});
|
||||||
|
|
||||||
const setAppVisible = useCallback(
|
const setAppVisible = useCallback(
|
||||||
(nextVisible: boolean) => {
|
(nextVisible: boolean) => {
|
||||||
@@ -191,10 +192,6 @@ export function useClientActivity({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (client.isConnected) {
|
|
||||||
startHeartbeat();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const ELIGIBLE_WORKTREE_SNAPSHOT: WorkspaceGitRuntimeSnapshot = {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,6 @@ function createWorkspaceGitSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,6 @@ function createWorkspaceGitSnapshot(
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
...overrides?.github,
|
...overrides?.github,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5837,7 +5837,6 @@ export class Session {
|
|||||||
featuresEnabled: snapshot.github.featuresEnabled,
|
featuresEnabled: snapshot.github.featuresEnabled,
|
||||||
pullRequest: snapshot.github.pullRequest,
|
pullRequest: snapshot.github.pullRequest,
|
||||||
error: snapshot.github.error,
|
error: snapshot.github.error,
|
||||||
refreshedAt: snapshot.github.refreshedAt,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ function createWorkspaceRuntimeSnapshot(
|
|||||||
featuresEnabled: true,
|
featuresEnabled: true,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: "2026-04-12T00:00:00.000Z",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -477,7 +476,6 @@ describe("workspace git watch targets", () => {
|
|||||||
headRefName: "workspace-git-service",
|
headRefName: "workspace-git-service",
|
||||||
isMerged: true,
|
isMerged: true,
|
||||||
},
|
},
|
||||||
refreshedAt: "2026-04-12T00:05:00.000Z",
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ function createNoopWorkspaceGitService() {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
@@ -159,7 +158,6 @@ function createNoopWorkspaceGitService() {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
resolveRepoRemoteUrl: async () => null,
|
resolveRepoRemoteUrl: async () => null,
|
||||||
@@ -210,7 +208,6 @@ function createWorkspaceRuntimeSnapshot(
|
|||||||
isMerged: false,
|
isMerged: false,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: "2026-04-12T00:00:00.000Z",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2592,7 +2589,6 @@ describe("workspace aggregation", () => {
|
|||||||
headRefName: "runtime-branch",
|
headRefName: "runtime-branch",
|
||||||
isMerged: false,
|
isMerged: false,
|
||||||
},
|
},
|
||||||
refreshedAt: "2026-04-12T00:05:00.000Z",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const workspaceGitService = createNoopWorkspaceGitService();
|
const workspaceGitService = createNoopWorkspaceGitService();
|
||||||
@@ -2674,7 +2670,6 @@ describe("workspace aggregation", () => {
|
|||||||
isMerged: false,
|
isMerged: false,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: "2026-04-12T00:05:00.000Z",
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -2696,7 +2691,6 @@ describe("workspace aggregation", () => {
|
|||||||
headRefName: "feature/runtime-payloads",
|
headRefName: "feature/runtime-payloads",
|
||||||
isMerged: true,
|
isMerged: true,
|
||||||
},
|
},
|
||||||
refreshedAt: "2026-04-12T00:10:00.000Z",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const workspaceGitService = createNoopWorkspaceGitService();
|
const workspaceGitService = createNoopWorkspaceGitService();
|
||||||
@@ -2770,7 +2764,6 @@ describe("workspace aggregation", () => {
|
|||||||
title: "Updated runtime payloads",
|
title: "Updated runtime payloads",
|
||||||
isMerged: true,
|
isMerged: true,
|
||||||
}),
|
}),
|
||||||
refreshedAt: "2026-04-12T00:10:00.000Z",
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ function createFallbackWorkspaceGitService(): WorkspaceGitServiceImpl {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
@@ -114,7 +113,6 @@ function createFallbackWorkspaceGitService(): WorkspaceGitServiceImpl {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
refresh: async () => {},
|
refresh: async () => {},
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ function createSnapshot(
|
|||||||
isMerged: false,
|
isMerged: false,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: "2026-04-12T00:00:00.000Z",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -412,9 +411,6 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
|||||||
currentBranch: "feature",
|
currentBranch: "feature",
|
||||||
diffStat: { additions: 4, deletions: 2 },
|
diffStat: { additions: 4, deletions: 2 },
|
||||||
},
|
},
|
||||||
github: {
|
|
||||||
refreshedAt: "2026-04-12T00:00:16.000Z",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
await expect(Promise.all([first, second])).resolves.toEqual([freshSnapshot, freshSnapshot]);
|
await expect(Promise.all([first, second])).resolves.toEqual([freshSnapshot, freshSnapshot]);
|
||||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ function createSnapshot(
|
|||||||
isMerged: false,
|
isMerged: false,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: "2026-04-12T00:00:00.000Z",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -267,7 +266,6 @@ describe("WorkspaceGitServiceImpl", () => {
|
|||||||
headRefName: "workspace-git-service",
|
headRefName: "workspace-git-service",
|
||||||
isMerged: false,
|
isMerged: false,
|
||||||
},
|
},
|
||||||
refreshedAt: "2026-04-12T02:03:04.000Z",
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -276,6 +274,26 @@ describe("WorkspaceGitServiceImpl", () => {
|
|||||||
service.dispose();
|
service.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("non-forced GitHub refresh does not emit when pull request state is unchanged", async () => {
|
||||||
|
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
|
||||||
|
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
|
||||||
|
const service = createService({
|
||||||
|
getPullRequestStatus,
|
||||||
|
now: () => new Date(nowMs),
|
||||||
|
});
|
||||||
|
const listener = vi.fn();
|
||||||
|
const subscription = await service.subscribe({ cwd: "/tmp/repo" }, listener);
|
||||||
|
|
||||||
|
nowMs += 3_000;
|
||||||
|
await service.refresh("/tmp/repo");
|
||||||
|
|
||||||
|
expect(getPullRequestStatus).toHaveBeenCalledTimes(2);
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
subscription.unsubscribe();
|
||||||
|
service.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test("cold getSnapshot calls share one workspace target setup and cache the snapshot", async () => {
|
test("cold getSnapshot calls share one workspace target setup and cache the snapshot", async () => {
|
||||||
const checkoutStatusDeferred = createDeferred<CheckoutStatusGit>();
|
const checkoutStatusDeferred = createDeferred<CheckoutStatusGit>();
|
||||||
const getCheckoutStatus = vi.fn(async () => checkoutStatusDeferred.promise);
|
const getCheckoutStatus = vi.fn(async () => checkoutStatusDeferred.promise);
|
||||||
@@ -361,13 +379,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
|||||||
|
|
||||||
nowMs += 3_000;
|
nowMs += 3_000;
|
||||||
await service.refresh("/tmp/repo");
|
await service.refresh("/tmp/repo");
|
||||||
await expect(service.getSnapshot("/tmp/repo/.")).resolves.toEqual(
|
await expect(service.getSnapshot("/tmp/repo/.")).resolves.toEqual(createSnapshot("/tmp/repo"));
|
||||||
createSnapshot("/tmp/repo", {
|
|
||||||
github: {
|
|
||||||
refreshedAt: "2026-04-12T00:00:03.000Z",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(2);
|
expect(getPullRequestStatus).toHaveBeenCalledTimes(2);
|
||||||
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(1);
|
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(1);
|
||||||
@@ -460,7 +472,6 @@ describe("WorkspaceGitServiceImpl", () => {
|
|||||||
headRefName: "feature",
|
headRefName: "feature",
|
||||||
isMerged: true,
|
isMerged: true,
|
||||||
},
|
},
|
||||||
refreshedAt: "2026-04-12T00:05:00.000Z",
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -534,7 +545,6 @@ describe("WorkspaceGitServiceImpl", () => {
|
|||||||
github: {
|
github: {
|
||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export type WorkspaceGitRuntimeSnapshot = {
|
|||||||
reviewDecision?: "approved" | "changes_requested" | "pending" | null;
|
reviewDecision?: "approved" | "changes_requested" | "pending" | null;
|
||||||
} | null;
|
} | null;
|
||||||
error: { message: string } | null;
|
error: { message: string } | null;
|
||||||
refreshedAt: string | null;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1499,7 +1498,6 @@ async function loadGitHubSnapshot(options: {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1510,7 +1508,6 @@ async function loadGitHubSnapshot(options: {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1523,7 +1520,6 @@ async function loadGitHubSnapshot(options: {
|
|||||||
featuresEnabled: true,
|
featuresEnabled: true,
|
||||||
pullRequest: result.status,
|
pullRequest: result.status,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: options.now.toISOString(),
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
@@ -1532,7 +1528,6 @@ async function loadGitHubSnapshot(options: {
|
|||||||
error: {
|
error: {
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : String(error),
|
||||||
},
|
},
|
||||||
refreshedAt: options.now.toISOString(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1607,7 +1602,6 @@ function buildNotGitSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ function createNoopWorkspaceGitService(): WorkspaceGitService {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
@@ -57,7 +56,6 @@ function createNoopWorkspaceGitService(): WorkspaceGitService {
|
|||||||
featuresEnabled: false,
|
featuresEnabled: false,
|
||||||
pullRequest: null,
|
pullRequest: null,
|
||||||
error: null,
|
error: null,
|
||||||
refreshedAt: null,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
resolveRepoRemoteUrl: async () => null,
|
resolveRepoRemoteUrl: async () => null,
|
||||||
|
|||||||
Reference in New Issue
Block a user