fix(app): reuse draft command cache across tabs
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
agentCommandsQueryKey,
|
||||
agentCommandsQueryRoot,
|
||||
draftAgentCommandsQueryKey,
|
||||
normalizeAgentCommandsCwd,
|
||||
sessionAgentCommandsQueryKey,
|
||||
} from "./agent-commands-query";
|
||||
|
||||
describe("agent command query keys", () => {
|
||||
it("shares draft commands across draft tabs with the same provider setup", () => {
|
||||
const draftConfig = {
|
||||
provider: "codex" as const,
|
||||
cwd: "/repo",
|
||||
modeId: "auto",
|
||||
model: "gpt-5.5",
|
||||
thinkingOptionId: "medium",
|
||||
};
|
||||
|
||||
expect(
|
||||
agentCommandsQueryKey({ serverId: "server-1", agentId: "draft-a", draftConfig }),
|
||||
).toEqual(agentCommandsQueryKey({ serverId: "server-1", agentId: "draft-b", draftConfig }));
|
||||
});
|
||||
|
||||
it("keeps draft commands separate from running agent session commands", () => {
|
||||
expect(sessionAgentCommandsQueryKey({ serverId: "server-1", agentId: "agent-1" })).toEqual([
|
||||
"agentCommands",
|
||||
"server-1",
|
||||
"session",
|
||||
"agent-1",
|
||||
]);
|
||||
expect(
|
||||
draftAgentCommandsQueryKey({
|
||||
serverId: "server-1",
|
||||
draftConfig: { provider: "codex", cwd: "/repo" },
|
||||
}),
|
||||
).toEqual([
|
||||
"agentCommands",
|
||||
"server-1",
|
||||
"draft",
|
||||
"codex",
|
||||
"cwd",
|
||||
"/repo",
|
||||
"mode",
|
||||
null,
|
||||
"model",
|
||||
null,
|
||||
"thinking",
|
||||
null,
|
||||
"features",
|
||||
null,
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes cwd values so equivalent workspace paths share one draft scope", () => {
|
||||
expect(normalizeAgentCommandsCwd("C:\\Users\\Ezekiel Bulver\\project")).toBe(
|
||||
"C:/Users/Ezekiel Bulver/project",
|
||||
);
|
||||
expect(
|
||||
draftAgentCommandsQueryKey({
|
||||
serverId: "server-1",
|
||||
draftConfig: { provider: "codex", cwd: "C:\\Users\\Ezekiel Bulver\\project" },
|
||||
}),
|
||||
).toEqual(
|
||||
draftAgentCommandsQueryKey({
|
||||
serverId: "server-1",
|
||||
draftConfig: { provider: "codex", cwd: "C:/Users/Ezekiel Bulver/project" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes a server-level root for refresh invalidation", () => {
|
||||
expect(agentCommandsQueryRoot("server-1")).toEqual(["agentCommands", "server-1"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||
|
||||
export const AGENT_COMMANDS_QUERY_ROOT = "agentCommands";
|
||||
|
||||
export interface AgentCommandsDraftConfig {
|
||||
provider: AgentProvider;
|
||||
cwd: string;
|
||||
modeId?: string;
|
||||
model?: string;
|
||||
thinkingOptionId?: string;
|
||||
featureValues?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function normalizeAgentCommandsCwd(cwd: string): string {
|
||||
return normalizeWorkspacePath(cwd) ?? "";
|
||||
}
|
||||
|
||||
export function agentCommandsQueryRoot(serverId: string) {
|
||||
return [AGENT_COMMANDS_QUERY_ROOT, serverId] as const;
|
||||
}
|
||||
|
||||
export function sessionAgentCommandsQueryKey(input: { serverId: string; agentId: string }) {
|
||||
return [...agentCommandsQueryRoot(input.serverId), "session", input.agentId] as const;
|
||||
}
|
||||
|
||||
export function draftAgentCommandsQueryKey(input: {
|
||||
serverId: string;
|
||||
draftConfig: AgentCommandsDraftConfig;
|
||||
}) {
|
||||
const { draftConfig } = input;
|
||||
return [
|
||||
...agentCommandsQueryRoot(input.serverId),
|
||||
"draft",
|
||||
draftConfig.provider,
|
||||
"cwd",
|
||||
normalizeAgentCommandsCwd(draftConfig.cwd),
|
||||
"mode",
|
||||
draftConfig.modeId ?? null,
|
||||
"model",
|
||||
draftConfig.model ?? null,
|
||||
"thinking",
|
||||
draftConfig.thinkingOptionId ?? null,
|
||||
"features",
|
||||
draftConfig.featureValues ?? null,
|
||||
] as const;
|
||||
}
|
||||
|
||||
export function agentCommandsQueryKey(input: {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
draftConfig?: AgentCommandsDraftConfig;
|
||||
}) {
|
||||
if (input.draftConfig) {
|
||||
return draftAgentCommandsQueryKey({
|
||||
serverId: input.serverId,
|
||||
draftConfig: input.draftConfig,
|
||||
});
|
||||
}
|
||||
return sessionAgentCommandsQueryKey({ serverId: input.serverId, agentId: input.agentId });
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||
import { agentCommandsQueryKey, type AgentCommandsDraftConfig } from "@/hooks/agent-commands-query";
|
||||
|
||||
const COMMANDS_STALE_TIME = 60_000; // Commands rarely change, cache for 1 minute
|
||||
const DRAFT_COMMANDS_STALE_TIME = Number.POSITIVE_INFINITY;
|
||||
const SESSION_COMMANDS_STALE_TIME = 60_000;
|
||||
|
||||
export interface AgentSlashCommand {
|
||||
name: string;
|
||||
@@ -13,14 +14,7 @@ export interface AgentSlashCommand {
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
export interface DraftCommandConfig {
|
||||
provider: AgentProvider;
|
||||
cwd: string;
|
||||
modeId?: string;
|
||||
model?: string;
|
||||
thinkingOptionId?: string;
|
||||
featureValues?: Record<string, unknown>;
|
||||
}
|
||||
export type DraftCommandConfig = AgentCommandsDraftConfig;
|
||||
|
||||
export type AgentCommandsClient = Pick<DaemonClient, "listCommands">;
|
||||
|
||||
@@ -35,24 +29,6 @@ export async function fetchAgentCommands(input: {
|
||||
return response.commands as AgentSlashCommand[];
|
||||
}
|
||||
|
||||
function agentCommandsQueryKey(
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
draftConfig?: DraftCommandConfig,
|
||||
) {
|
||||
return [
|
||||
"agentCommands",
|
||||
serverId,
|
||||
agentId,
|
||||
draftConfig?.provider ?? null,
|
||||
draftConfig?.cwd ?? null,
|
||||
draftConfig?.modeId ?? null,
|
||||
draftConfig?.model ?? null,
|
||||
draftConfig?.thinkingOptionId ?? null,
|
||||
draftConfig?.featureValues ?? null,
|
||||
] as const;
|
||||
}
|
||||
|
||||
interface UseAgentCommandsQueryOptions {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
@@ -71,7 +47,7 @@ export function useAgentCommandsQuery({
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: agentCommandsQueryKey(serverId, agentId, draftConfig),
|
||||
queryKey: agentCommandsQueryKey({ serverId, agentId, draftConfig }),
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error(t("common.errors.daemonClientUnavailable"));
|
||||
@@ -79,7 +55,7 @@ export function useAgentCommandsQuery({
|
||||
return fetchAgentCommands({ client, agentId, draftConfig });
|
||||
},
|
||||
enabled: enabled && !!client && isConnected && (!!agentId || !!draftConfig),
|
||||
staleTime: COMMANDS_STALE_TIME,
|
||||
staleTime: draftConfig ? DRAFT_COMMANDS_STALE_TIME : SESSION_COMMANDS_STALE_TIME,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 5000),
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
||||
import { draftAgentCommandsQueryKey } from "@/hooks/agent-commands-query";
|
||||
import {
|
||||
applyProvidersSnapshotUpdate,
|
||||
fetchProvidersSnapshot,
|
||||
@@ -195,6 +196,24 @@ describe("refreshAndApplyProvidersSnapshot", () => {
|
||||
queryClient.getQueryState(providersSnapshotQueryKey(serverId, "/repo-b"))?.isInvalidated,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("invalidates cached agent commands when refreshing providers", async () => {
|
||||
const client = createClient({ snapshots: [providersSnapshot([])] });
|
||||
const commandsKey = draftAgentCommandsQueryKey({
|
||||
serverId,
|
||||
draftConfig: { provider: "codex", cwd: "/repo-a" },
|
||||
});
|
||||
queryClient.setQueryData(commandsKey, [{ name: "compact", description: "", argumentHint: "" }]);
|
||||
|
||||
await refreshAndApplyProvidersSnapshot({
|
||||
client,
|
||||
queryClient,
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryState(commandsKey)?.isInvalidated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyProvidersSnapshotUpdate", () => {
|
||||
@@ -271,6 +290,22 @@ describe("applyProvidersSnapshotUpdate", () => {
|
||||
requestId: "providers_snapshot_update",
|
||||
});
|
||||
});
|
||||
|
||||
it("invalidates cached agent commands when a provider snapshot update arrives", () => {
|
||||
const commandsKey = draftAgentCommandsQueryKey({
|
||||
serverId,
|
||||
draftConfig: { provider: "codex", cwd: "/repo-a" },
|
||||
});
|
||||
queryClient.setQueryData(commandsKey, [{ name: "compact", description: "", argumentHint: "" }]);
|
||||
|
||||
applyProvidersSnapshotUpdate({
|
||||
serverId,
|
||||
queryClient,
|
||||
message: updateMessage([codexEntry("ready", [readyCodexModel])], "/repo-a"),
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryState(commandsKey)?.isInvalidated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectorOpenRefetchDecision", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { queryClient as singletonQueryClient } from "@/query/query-client";
|
||||
import { agentCommandsQueryRoot } from "@/hooks/agent-commands-query";
|
||||
import {
|
||||
isProvidersSnapshotHomeScope,
|
||||
normalizeProvidersSnapshotCwd,
|
||||
@@ -52,6 +53,10 @@ export async function refreshAndApplyProvidersSnapshot(input: {
|
||||
);
|
||||
const snapshot = await fetchProvidersSnapshot({ client: input.client, cwd: input.cwd });
|
||||
input.queryClient.setQueryData(providersSnapshotQueryKey(input.serverId, input.cwd), snapshot);
|
||||
void input.queryClient.invalidateQueries({
|
||||
queryKey: agentCommandsQueryRoot(input.serverId),
|
||||
exact: false,
|
||||
});
|
||||
if (isProvidersSnapshotHomeScope(input.cwd)) {
|
||||
void input.queryClient.invalidateQueries({
|
||||
queryKey: providersSnapshotQueryRoot(input.serverId),
|
||||
@@ -75,6 +80,10 @@ export function applyProvidersSnapshotUpdate(input: {
|
||||
generatedAt: input.message.payload.generatedAt,
|
||||
requestId: "providers_snapshot_update",
|
||||
});
|
||||
void input.queryClient.invalidateQueries({
|
||||
queryKey: agentCommandsQueryRoot(input.serverId),
|
||||
exact: false,
|
||||
});
|
||||
}
|
||||
|
||||
export type SelectorOpenRefetchDecision = "refetch-stale" | "refetch-always";
|
||||
|
||||
Reference in New Issue
Block a user