fix(app): reuse draft command cache across tabs

This commit is contained in:
Mohamed Boudra
2026-06-11 13:17:29 +07:00
parent 08ebe5e7e1
commit a70b5c1e1d
5 changed files with 186 additions and 30 deletions
@@ -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";