Add manual refresh button to git diff controls (#1216)
* feat(git): add manual refresh button to diff controls Adds a checkout_refresh RPC that forces a hard re-read of the git and GitHub snapshot plus the diff, bypassing polling. The diff controls now show a refresh button (feature-gated) that triggers it and surfaces errors via a toast — an escape hatch when the polled state goes stale. * refactor(git): address review — dotted RPC name + withUnistyles - Rename checkout_refresh RPC to the dotted convention (checkout.refresh.request/response) per docs/rpc-namespacing.md. - Replace the banned useUnistyles() call in DiffRefreshButton with withUnistyles wrappers for the icon/spinner color, per docs/unistyles.md. * refactor(git): move refresh into the checkout actions store Per the feature audit: the manual refresh was the one checkout action whose UI logic lived inline in the component instead of alongside its siblings (commit/pull/push) in useCheckoutGitActionsStore. Move it into the store so there's one home for "UI triggers a checkout RPC" — it now reuses runCheckoutAction's pending/success status and query invalidation. The component just reads status and shows an error toast. * fix(git): size refresh loader to the icon to stop layout shift ActivityIndicator size="small" renders ~20px regardless of platform, wider than the 14px refresh icon, so swapping to it grew the button and shifted the controls row. Use SyncedLoader at the same iconSize — it renders into a size×size box, so both states share one footprint. * fix(git): match file-explorer refresh control (RotateCw + LoadingSpinner) Mirror the file-explorer pane's refresh button exactly: RotateCw icon, LoadingSpinner while refreshing, both centered in a fixed icon-sized box so the spinner can't grow the control or shift the row. Replaces the ad-hoc RefreshCw + SyncedLoader pairing.
This commit is contained in:
@@ -170,6 +170,46 @@ describe("checkout-git-actions-store", () => {
|
||||
).toBe("idle");
|
||||
});
|
||||
|
||||
it("refreshes git and GitHub state and reports success", async () => {
|
||||
const client = {
|
||||
checkoutRefresh: vi.fn(async () => ({ success: true, error: null })),
|
||||
};
|
||||
useSessionStore.setState((state) => ({
|
||||
...state,
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
||||
},
|
||||
}));
|
||||
|
||||
await useCheckoutGitActionsStore.getState().refresh({ serverId, cwd });
|
||||
|
||||
expect(client.checkoutRefresh).toHaveBeenCalledWith(cwd);
|
||||
expect(
|
||||
useCheckoutGitActionsStore.getState().getStatus({ serverId, cwd, actionId: "refresh" }),
|
||||
).toBe("success");
|
||||
});
|
||||
|
||||
it("surfaces a refresh error and returns to idle", async () => {
|
||||
const client = {
|
||||
checkoutRefresh: vi.fn(async () => ({ error: { message: "not a git repository" } })),
|
||||
};
|
||||
useSessionStore.setState((state) => ({
|
||||
...state,
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[serverId]: { client } as unknown as (typeof state.sessions)[string],
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(useCheckoutGitActionsStore.getState().refresh({ serverId, cwd })).rejects.toThrow(
|
||||
"not a git repository",
|
||||
);
|
||||
expect(
|
||||
useCheckoutGitActionsStore.getState().getStatus({ serverId, cwd, actionId: "refresh" }),
|
||||
).toBe("idle");
|
||||
});
|
||||
|
||||
it("enables PR auto-merge when the daemon advertises auto-merge actions", async () => {
|
||||
const client = {
|
||||
checkoutGithubSetAutoMerge: vi.fn(async () => ({
|
||||
|
||||
Reference in New Issue
Block a user