Classify slash commands by surface

This commit is contained in:
murashit 2026-06-01 21:01:42 +09:00
parent ebb16fad30
commit d8937f3191
5 changed files with 131 additions and 19 deletions

View file

@ -1,42 +1,115 @@
export type SlashCommandArgsKind = "none" | "optionalThread" | "requiredThread" | "optionalMessage" | "threadAndMessage" | "showOrSet";
export type SlashCommandSurface = "panelAction" | "threadSetting" | "diagnostic" | "composition";
export const SLASH_COMMAND_SURFACE_LABELS: Record<SlashCommandSurface, string> = {
panelAction: "Panel actions",
threadSetting: "Thread settings",
diagnostic: "Diagnostics",
composition: "Composition",
};
export const SLASH_COMMANDS = [
{
command: "/clear",
usage: "/clear",
argsKind: "none",
surface: "panelAction",
detail: "Clear the current panel and start a fresh Codex thread.",
},
{ command: "/resume", usage: "/resume [thread]", argsKind: "optionalThread", detail: "Resume a recent Codex thread." },
{
command: "/resume",
usage: "/resume [thread]",
argsKind: "optionalThread",
surface: "panelAction",
detail: "Resume a recent Codex thread.",
},
{
command: "/refer",
usage: "/refer <thread> <message>",
argsKind: "threadAndMessage",
surface: "composition",
detail: "Send a message with recent turns from another non-archived thread.",
},
{ command: "/fork", usage: "/fork", argsKind: "none", detail: "Fork the active Codex thread." },
{ command: "/rollback", usage: "/rollback", argsKind: "none", detail: "Roll back the latest turn and restore its prompt." },
{ command: "/compact", usage: "/compact", argsKind: "none", detail: "Compact the current thread context." },
{ command: "/archive", usage: "/archive <thread>", argsKind: "requiredThread", detail: "Archive the selected Codex thread." },
{ command: "/fork", usage: "/fork", argsKind: "none", surface: "panelAction", detail: "Fork the active Codex thread." },
{
command: "/rollback",
usage: "/rollback",
argsKind: "none",
surface: "panelAction",
detail: "Roll back the latest turn and restore its prompt.",
},
{ command: "/compact", usage: "/compact", argsKind: "none", surface: "panelAction", detail: "Compact the current thread context." },
{
command: "/archive",
usage: "/archive <thread>",
argsKind: "requiredThread",
surface: "panelAction",
detail: "Archive the selected Codex thread.",
},
{
command: "/auto-review",
usage: "/auto-review",
argsKind: "none",
surface: "threadSetting",
detail: "Toggle approval auto-review.",
},
{ command: "/fast", usage: "/fast", argsKind: "none", detail: "Toggle fast service tier for subsequent turns." },
{ command: "/plan", usage: "/plan [message]", argsKind: "optionalMessage", detail: "Toggle Plan mode, optionally with a message." },
{ command: "/status", usage: "/status", argsKind: "none", detail: "Show current thread, context, and usage limits." },
{ command: "/doctor", usage: "/doctor", argsKind: "none", detail: "Show Codex CLI and Codex App Server diagnostics." },
{ command: "/mcp", usage: "/mcp", argsKind: "none", detail: "Show MCP servers reported by Codex App Server." },
{ command: "/model", usage: "/model [model|default]", argsKind: "showOrSet", detail: "Show or set the model for subsequent turns." },
{
command: "/fast",
usage: "/fast",
argsKind: "none",
surface: "threadSetting",
detail: "Toggle fast service tier for subsequent turns.",
},
{
command: "/plan",
usage: "/plan [message]",
argsKind: "optionalMessage",
surface: "threadSetting",
detail: "Toggle Plan mode, optionally with a message.",
},
{
command: "/status",
usage: "/status",
argsKind: "none",
surface: "diagnostic",
detail: "Show current thread, context, and usage limits.",
},
{
command: "/doctor",
usage: "/doctor",
argsKind: "none",
surface: "diagnostic",
detail: "Show Codex CLI and Codex App Server diagnostics.",
},
{
command: "/mcp",
usage: "/mcp",
argsKind: "none",
surface: "diagnostic",
detail: "Show MCP servers reported by Codex App Server.",
},
{
command: "/model",
usage: "/model [model|default]",
argsKind: "showOrSet",
surface: "threadSetting",
detail: "Show or set the model for subsequent turns.",
},
{
command: "/effort",
usage: "/effort [effort|default]",
argsKind: "showOrSet",
surface: "threadSetting",
detail: "Show or set reasoning effort for subsequent turns.",
},
{ command: "/help", usage: "/help", argsKind: "none", detail: "Show available Codex slash commands." },
{
command: "/help",
usage: "/help",
argsKind: "none",
surface: "diagnostic",
detail: "Show available Codex slash commands.",
},
] as const;
type SlashCommand = (typeof SLASH_COMMANDS)[number]["command"];
@ -58,3 +131,12 @@ export function slashCommandHelpLines(): string[] {
export function slashCommandHelpRows(): { key: string; value: string }[] {
return SLASH_COMMANDS.map((item) => ({ key: item.usage, value: item.detail }));
}
export function slashCommandHelpSections(): { title: string; rows: { key: string; value: string }[] }[] {
return (Object.keys(SLASH_COMMAND_SURFACE_LABELS) as SlashCommandSurface[])
.map((surface) => ({
title: SLASH_COMMAND_SURFACE_LABELS[surface],
rows: SLASH_COMMANDS.filter((item) => item.surface === surface).map((item) => ({ key: item.usage, value: item.detail })),
}))
.filter((section) => section.rows.length > 0);
}

View file

@ -9,7 +9,7 @@ import {
sortedAvailableModels,
supportedEffortsForModel,
} from "../../../runtime/model";
import { SLASH_COMMANDS, type SlashCommandName } from "./slash-commands";
import { SLASH_COMMANDS, SLASH_COMMAND_SURFACE_LABELS, type SlashCommandName } from "./slash-commands";
import { getThreadTitle } from "../../../domain/threads/model";
import { shortThreadId } from "../../../utils";
@ -170,7 +170,7 @@ export function activeSlashCommandSuggestions(beforeCursor: string): ComposerSug
.slice(0, 8)
.map((item) => ({
display: item.command,
detail: `${item.usage} - ${item.detail}`,
detail: `${SLASH_COMMAND_SURFACE_LABELS[item.surface]}: ${item.usage} - ${item.detail}`,
replacement: item.command,
start,
appendSpaceOnInsert: true,

View file

@ -3,7 +3,7 @@ import type { Thread } from "../../generated/app-server/v2/Thread";
import type { UserInput } from "../../generated/app-server/v2/UserInput";
import { getThreadTitle } from "../../domain/threads/model";
import type { ReferencedThreadDisplay } from "../../domain/threads/reference";
import { slashCommandDefinition, slashCommandHelpRows, type SlashCommandName } from "./composer/slash-commands";
import { slashCommandDefinition, slashCommandHelpSections, type SlashCommandName } from "./composer/slash-commands";
import type { DisplayDetailSection, DisplayDetailMetaRow } from "./display/types";
import {
modelOverrideMessage,
@ -206,7 +206,7 @@ export async function executeSlashCommand(
return;
}
context.addStructuredSystemMessage("Available slash commands", [{ rows: slashCommandHelpRows() }]);
context.addStructuredSystemMessage("Available slash commands", slashCommandHelpSections());
}
function validateSlashCommandArguments(command: SlashCommandName, args: string): string | null {

View file

@ -293,7 +293,7 @@ describe("composer suggestions", () => {
const wikilink = expectPresent(activeComposerSuggestions("[[bet", notes, [])[0]);
expect(slash).toMatchObject({
detail: "/status - Show current thread, context, and usage limits.",
detail: "Diagnostics: /status - Show current thread, context, and usage limits.",
replacement: "/status",
appendSpaceOnInsert: true,
});

View file

@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { slashCommandHelpLines, slashCommandHelpRows } from "../../../../src/features/chat/composer/slash-commands";
import { slashCommandHelpLines, slashCommandHelpSections } from "../../../../src/features/chat/composer/slash-commands";
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
import { executeSlashCommand, type SlashCommandExecutionContext } from "../../../../src/features/chat/slash-commands";
@ -303,7 +303,37 @@ describe("slash commands", () => {
await executeSlashCommand("help", "", ctx);
expect(ctx.addSystemMessage).not.toHaveBeenCalled();
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Available slash commands", [{ rows: slashCommandHelpRows() }]);
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Available slash commands", slashCommandHelpSections());
});
it("groups slash command help by command surface", () => {
expect(slashCommandHelpSections()).toEqual([
{
title: "Panel actions",
rows: expect.arrayContaining([
{ key: "/clear", value: "Clear the current panel and start a fresh Codex thread." },
{ key: "/archive <thread>", value: "Archive the selected Codex thread." },
]),
},
{
title: "Thread settings",
rows: expect.arrayContaining([
{ key: "/plan [message]", value: "Toggle Plan mode, optionally with a message." },
{ key: "/model [model|default]", value: "Show or set the model for subsequent turns." },
]),
},
{
title: "Diagnostics",
rows: expect.arrayContaining([
{ key: "/status", value: "Show current thread, context, and usage limits." },
{ key: "/help", value: "Show available Codex slash commands." },
]),
},
{
title: "Composition",
rows: [{ key: "/refer <thread> <message>", value: "Send a message with recent turns from another non-archived thread." }],
},
]);
});
it("rejects /help arguments", async () => {