refactor(webui): read slash commands through the chat metadata store (#124931)

This commit is contained in:
Peter Steinberger
2026-08-16 17:20:59 -07:00
committed by GitHub
parent 562ac194e0
commit e95300fa3e
2 changed files with 92 additions and 17 deletions
+81 -1
View File
@@ -2,13 +2,21 @@
import { describe, expect, it, vi } from "vitest";
import type { GatewayBrowserClient } from "../../api/gateway.ts";
import type { ApplicationGatewaySnapshot } from "../../app/gateway.ts";
import {
invalidateChatMetadataStore,
rememberChatMetadata,
} from "../../lib/chat/chat-metadata-store.ts";
import {
SLASH_COMMANDS,
getSlashCommandCategoryLabel,
getSlashCommandDescription,
type SlashCommandDef,
} from "../../lib/chat/commands.ts";
import { dispatchChatSlashCommand, refreshSlashCommands } from "./chat-commands.ts";
import {
applyRemoteSlashCommandsResult,
dispatchChatSlashCommand,
refreshSlashCommands,
} from "./chat-commands.ts";
function requireCommandByName(name: string): Record<string, unknown> {
const command = SLASH_COMMANDS.find((entry) => entry.name === name);
@@ -36,6 +44,17 @@ function legacyConnectedSessionAccess() {
};
}
function remoteCommand(name: string, description: string) {
return {
name,
textAliases: [`/${name}`],
description,
source: "plugin" as const,
scope: "text" as const,
acceptsArgs: false,
};
}
describe("refreshSlashCommands", () => {
it("resolves localized UI command metadata", () => {
const clear = SLASH_COMMANDS.find((entry) => entry.name === "clear");
@@ -252,6 +271,67 @@ describe("refreshSlashCommands", () => {
description: "Generate setup codes.",
});
});
it("reads commands from the chat metadata store without requesting commands.list", async () => {
const request = vi.fn();
const client = { request } as never;
rememberChatMetadata(client, "main", {
commands: [remoteCommand("metadata-command", "Loaded from chat metadata.")],
});
await refreshSlashCommands({ client, agentId: "main" });
expect(request).not.toHaveBeenCalled();
expectRecordFields(requireCommandByName("metadata-command"), "metadata command", {
description: "Loaded from chat metadata.",
executeLocal: false,
});
});
it("prefers stored metadata after the commands.list cache expires", async () => {
vi.useFakeTimers();
try {
const request = vi.fn().mockResolvedValue({
commands: [remoteCommand("cached-command", "Loaded from commands.list.")],
});
const client = { request } as never;
await refreshSlashCommands({ client, agentId: "main" });
vi.advanceTimersByTime(60_001);
rememberChatMetadata(client, "main", {
commands: [remoteCommand("metadata-command", "Loaded from chat metadata.")],
});
await refreshSlashCommands({ client, agentId: "main" });
expect(request).toHaveBeenCalledOnce();
expectRecordFields(requireCommandByName("metadata-command"), "metadata command", {
description: "Loaded from chat metadata.",
});
} finally {
vi.useRealTimers();
}
});
it("does not retain applied metadata commands in the commands.list cache", async () => {
const request = vi.fn().mockResolvedValue({
commands: [remoteCommand("requested-command", "Loaded after metadata invalidation.")],
});
const client = { request } as never;
const metadata = {
commands: [remoteCommand("metadata-command", "Loaded from chat metadata.")],
};
rememberChatMetadata(client, "main", metadata);
applyRemoteSlashCommandsResult({ client, agentId: "main", result: metadata });
invalidateChatMetadataStore(client);
await refreshSlashCommands({ client, agentId: "main" });
expect(request).toHaveBeenCalledOnce();
expectRecordFields(requireCommandByName("requested-command"), "requested command", {
description: "Loaded after metadata invalidation.",
});
});
});
describe("conversation reset confirmation", () => {
+11 -16
View File
@@ -3,6 +3,7 @@ import type { CommandsListResult } from "../../../../packages/gateway-protocol/s
import type { GatewayBrowserClient } from "../../api/gateway.ts";
import type { ModelCatalogEntry, SessionsListResult } from "../../api/types.ts";
import type { ApplicationGatewaySnapshot } from "../../app/gateway.ts";
import { peekChatMetadata } from "../../lib/chat/chat-metadata-store.ts";
import type { ChatQueueItem } from "../../lib/chat/chat-types.ts";
import {
buildFallbackSlashCommands,
@@ -200,17 +201,6 @@ function getRemoteSlashCommandCache(
return cache;
}
function storeRemoteSlashCommands(
client: GatewayBrowserClient,
agentId: string | undefined,
commands: SlashCommandDef[],
) {
getRemoteSlashCommandCache(client).set(remoteSlashCommandCacheKey(agentId), {
commands,
expiresAt: Date.now() + REMOTE_SLASH_COMMAND_CACHE_TTL_MS,
});
}
async function requestRemoteSlashCommands(
client: GatewayBrowserClient,
agentId: string | undefined,
@@ -226,7 +216,10 @@ async function requestRemoteSlashCommands(
return buildFallbackSlashCommands();
}
const commands = buildSlashCommandsFromEntries(getRemoteCommandEntries(result));
storeRemoteSlashCommands(client, agentId, commands);
getRemoteSlashCommandCache(client).set(remoteSlashCommandCacheKey(agentId), {
commands,
expiresAt: Date.now() + REMOTE_SLASH_COMMAND_CACHE_TTL_MS,
});
return commands;
} catch {
return fallback ?? buildFallbackSlashCommands();
@@ -237,6 +230,12 @@ function loadRemoteSlashCommands(
client: GatewayBrowserClient,
agentId: string | undefined,
): Promise<SlashCommandDef[]> {
const metadata = peekChatMetadata(client, agentId);
// Store-held metadata carries app-level invalidation on config changes and logical reconnects,
// so no TTL applies here. The cache below owns only commands.list-derived entries.
if (Array.isArray(metadata?.commands)) {
return Promise.resolve(buildSlashCommandsFromEntries(getRemoteCommandEntries(metadata)));
}
const cache = getRemoteSlashCommandCache(client);
const key = remoteSlashCommandCacheKey(agentId);
const cached = cache.get(key);
@@ -269,11 +268,7 @@ export function applyRemoteSlashCommandsResult(params: {
if (!Array.isArray(params.result?.commands)) {
return false;
}
const agentId = params.agentId?.trim();
const commands = buildSlashCommandsFromEntries(getRemoteCommandEntries(params.result));
if (params.client) {
storeRemoteSlashCommands(params.client, agentId, commands);
}
refreshSeq += 1;
replaceSlashCommands(commands);
return true;