Add slash commands for ending and restarting agents (#1034)

* Add slash commands for ending agents

* Stabilize slash command e2e submit

* Unslop client slash command draft setup

* Fix slash commands while agent is running

* Fix slash command submit race

* Reshape client slash command execution

* Fix slash command tab cleanup
This commit is contained in:
Mohamed Boudra
2026-05-16 17:03:59 +08:00
committed by GitHub
parent 667f441cc0
commit 4b02daed8a
19 changed files with 934 additions and 53 deletions
@@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import {
CLIENT_SLASH_COMMANDS,
buildDraftAgentSetup,
resolveClientSlashCommand,
} from "@/client-slash-commands";
import type { Agent } from "@/stores/session-store";
function createAgent(overrides: Partial<Agent> = {}): Agent {
const now = new Date("2026-05-15T00:00:00.000Z");
return {
serverId: "server-1",
id: "agent-1",
provider: "codex",
status: "idle",
createdAt: now,
updatedAt: now,
lastUserMessageAt: now,
lastActivityAt: now,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: true,
},
currentModeId: "mode-current",
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: {
provider: "codex",
sessionId: "session-1",
model: "runtime-model",
modeId: "runtime-mode",
thinkingOptionId: "runtime-thinking",
},
title: "Agent",
cwd: "/repo",
model: "agent-model",
thinkingOptionId: "think-hard",
features: [
{ type: "toggle", id: "web-search", label: "Web search", value: true },
{
type: "select",
id: "effort",
label: "Effort",
value: "high",
options: [{ id: "high", label: "High" }],
},
],
parentAgentId: null,
labels: {},
...overrides,
};
}
describe("resolveClientSlashCommand", () => {
it("declares the exact client commands that execute immediately", () => {
expect(CLIENT_SLASH_COMMANDS.map((command) => [command.name, command.execution])).toEqual([
["quit", "immediate"],
["exit", "immediate"],
["q", "immediate"],
["clear", "immediate"],
["new", "immediate"],
]);
});
it("resolves exact client commands after trimming", () => {
expect(resolveClientSlashCommand({ text: " /quit ", hasAttachments: false })).toMatchObject({
kind: "archive-agent",
execution: "immediate",
});
expect(resolveClientSlashCommand({ text: "/exit", hasAttachments: false })?.kind).toBe(
"archive-agent",
);
expect(resolveClientSlashCommand({ text: "/q", hasAttachments: false })?.kind).toBe(
"archive-agent",
);
expect(resolveClientSlashCommand({ text: "/clear", hasAttachments: false })?.kind).toBe(
"replace-agent-with-draft",
);
expect(resolveClientSlashCommand({ text: "/new", hasAttachments: false })?.kind).toBe(
"replace-agent-with-draft",
);
});
it("leaves provider commands, arguments, ordinary messages, and attachment submits alone", () => {
expect(resolveClientSlashCommand({ text: "/clear now", hasAttachments: false })).toBeNull();
expect(resolveClientSlashCommand({ text: "/quit now", hasAttachments: false })).toBeNull();
expect(
resolveClientSlashCommand({ text: "/provider-command", hasAttachments: false }),
).toBeNull();
expect(resolveClientSlashCommand({ text: "hello /quit", hasAttachments: false })).toBeNull();
expect(resolveClientSlashCommand({ text: "/quit", hasAttachments: true })).toBeNull();
});
});
describe("buildDraftAgentSetup", () => {
it("builds draft setup from the active agent snapshot", () => {
expect(buildDraftAgentSetup(createAgent())).toEqual({
provider: "codex",
cwd: "/repo",
modeId: "mode-current",
model: "agent-model",
thinkingOptionId: "think-hard",
featureValues: {
"web-search": true,
effort: "high",
},
});
});
it("falls back to runtime model setup when top-level fields are absent", () => {
expect(
buildDraftAgentSetup(
createAgent({
currentModeId: null,
model: null,
thinkingOptionId: null,
}),
),
).toMatchObject({
modeId: "runtime-mode",
model: "runtime-model",
thinkingOptionId: "runtime-thinking",
});
});
});