import { BUILTIN_SLASH_COMMANDS } from "../../packages/coding-agent/src/core/slash-commands.ts"; export type BrowserSlashCommandSurface = | "settings" | "model" | "thinking" | "git" | "resume" | "name" | "fork" | "compact" | "login" | "logout" | "session" | "export" // SF command surfaces | "sf-status" | "sf-visualize" | "sf-forensics" | "sf-doctor" | "sf-skill-health" | "sf-knowledge" | "sf-capture" | "sf-triage" | "sf-quick" | "sf-history" | "sf-undo" | "sf-inspect" | "sf-prefs" | "sf-config" | "sf-hooks" | "sf-mode" | "sf-steer" | "sf-export" | "sf-cleanup" | "sf-queue"; export type BrowserSlashCommandLocalAction = | "clear_terminal" | "refresh_workspace" | "sf_help"; export type BrowserSlashPromptCommandType = "prompt" | "follow_up"; export interface BrowserSlashCommandDispatchOptions { isStreaming?: boolean; } export type BrowserSlashCommandDispatchResult = | { kind: "prompt"; input: string; slashCommandName: string | null; command: { type: BrowserSlashPromptCommandType; message: string; }; } | { kind: "rpc"; input: string; commandName: string; command: { type: "get_state" } | { type: "new_session" }; } | { kind: "surface"; input: string; commandName: string; surface: BrowserSlashCommandSurface; args: string; } | { kind: "local"; input: string; commandName: string; action: BrowserSlashCommandLocalAction; } | { kind: "reject"; input: string; commandName: string; reason: string; guidance: string; } | { kind: "view-navigate"; input: string; commandName: string; view: string; }; export interface BrowserSlashCommandTerminalNotice { type: "system" | "error"; message: string; } const BUILTIN_COMMAND_DESCRIPTIONS = new Map( BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command.description]), ); const BUILTIN_COMMAND_NAMES = new Set(BUILTIN_COMMAND_DESCRIPTIONS.keys()); const SURFACE_COMMANDS = new Map([ ["settings", "settings"], ["model", "model"], ["thinking", "thinking"], ["git", "git"], ["resume", "resume"], ["name", "name"], ["fork", "fork"], ["compact", "compact"], ["login", "login"], ["logout", "logout"], ["session", "session"], ["export", "export"], ]); // --- SF command dispatch --- const SF_SURFACE_COMMANDS = new Map([ ["status", "sf-status"], ["visualize", "sf-visualize"], ["forensics", "sf-forensics"], ["doctor", "sf-doctor"], ["skill-health", "sf-skill-health"], ["knowledge", "sf-knowledge"], ["capture", "sf-capture"], ["triage", "sf-triage"], ["quick", "sf-quick"], ["history", "sf-history"], ["undo", "sf-undo"], ["inspect", "sf-inspect"], ["prefs", "sf-prefs"], ["config", "sf-config"], ["hooks", "sf-hooks"], ["mode", "sf-mode"], ["steer", "sf-steer"], ["cleanup", "sf-cleanup"], ["queue", "sf-queue"], ]); const SF_PASSTHROUGH_COMMANDS = new Set([ "autonomous", "next", "pause", "skip", "discuss", "run-hook", "migrate", "remote", "new-milestone", "init", ]); export const SF_HELP_TEXT = `Available SF commands: Workflow: next · autonomous · autonomous stop · pause · skip · queue · quick · capture · triage Diagnostics: status · visualize · forensics · doctor · skill-health · inspect Context: knowledge · history · undo · discuss Settings: model · prefs · config · hooks · mode · steer Advanced: export · cleanup · run-hook · migrate · remote Type / to run. Use /help for this message.`; function dispatchSFCommand( input: string, commandName: string, args: string, options: BrowserSlashCommandDispatchOptions, ): BrowserSlashCommandDispatchResult { // `/help` — render inline help locally if (commandName === "help") { return { kind: "local", input, commandName, action: "sf_help", }; } // `/visualize` — navigate to the visualizer view directly if (commandName === "visualize") { return { kind: "view-navigate", input, commandName, view: "visualize", }; } // Surface-routed commands — open browser-native UI const surface = SF_SURFACE_COMMANDS.get(commandName); if (surface) { return { kind: "surface", input, commandName, surface, args, }; } // Bridge-passthrough commands — let the extension handle them if (SF_PASSTHROUGH_COMMANDS.has(commandName)) { return { kind: "prompt", input, slashCommandName: commandName, command: { type: getPromptCommandType(options), message: input, }, }; } return buildDeferredBuiltinReject(input, commandName); } function parseSlashCommand( input: string, ): { name: string; args: string } | null { if (!input.startsWith("/")) return null; const body = input.slice(1).trim(); if (!body) return null; const firstSpaceIndex = body.search(/\s/); if (firstSpaceIndex === -1) { return { name: body, args: "" }; } return { name: body.slice(0, firstSpaceIndex), args: body.slice(firstSpaceIndex + 1).trim(), }; } function getPromptCommandType( options: BrowserSlashCommandDispatchOptions, ): BrowserSlashPromptCommandType { return options.isStreaming ? "follow_up" : "prompt"; } function formatBuiltinDescription(commandName: string): string { return ( BUILTIN_COMMAND_DESCRIPTIONS.get(commandName) ?? "Browser handling is reserved for this built-in command." ); } function buildDeferredBuiltinReject( input: string, commandName: string, ): BrowserSlashCommandDispatchResult { const description = formatBuiltinDescription(commandName); return { kind: "reject", input, commandName, reason: `/${commandName} is a built-in pi command (${description}) that is not available in the browser yet.`, guidance: "It was blocked instead of falling through to the model.", }; } export function isAuthoritativeBuiltinSlashCommand( commandName: string, ): boolean { return BUILTIN_COMMAND_NAMES.has(commandName); } export function dispatchBrowserSlashCommand( input: string, options: BrowserSlashCommandDispatchOptions = {}, ): BrowserSlashCommandDispatchResult { const trimmed = input.trim(); const parsed = parseSlashCommand(trimmed); if (trimmed === "/clear") { return { kind: "local", input: trimmed, commandName: "clear", action: "clear_terminal", }; } if (trimmed === "/refresh") { return { kind: "local", input: trimmed, commandName: "refresh", action: "refresh_workspace", }; } if (trimmed === "/state") { return { kind: "rpc", input: trimmed, commandName: "state", command: { type: "get_state" }, }; } if (trimmed === "/new-session") { return { kind: "rpc", input: trimmed, commandName: "new", command: { type: "new_session" }, }; } if (!parsed) { return { kind: "prompt", input: trimmed, slashCommandName: null, command: { type: getPromptCommandType(options), message: trimmed, }, }; } if (parsed.name === "new") { return { kind: "rpc", input: trimmed, commandName: "new", command: { type: "new_session" }, }; } if ( parsed.name === "help" || parsed.name === "visualize" || SF_SURFACE_COMMANDS.has(parsed.name) || SF_PASSTHROUGH_COMMANDS.has(parsed.name) ) { return dispatchSFCommand(trimmed, parsed.name, parsed.args, options); } const browserSurface = SURFACE_COMMANDS.get(parsed.name); if (browserSurface) { return { kind: "surface", input: trimmed, commandName: parsed.name, surface: browserSurface, args: parsed.args, }; } if (BUILTIN_COMMAND_NAMES.has(parsed.name)) { return buildDeferredBuiltinReject(trimmed, parsed.name); } return { kind: "prompt", input: trimmed, slashCommandName: parsed.name, command: { type: getPromptCommandType(options), message: trimmed, }, }; } export function getBrowserSlashCommandTerminalNotice( outcome: BrowserSlashCommandDispatchResult, ): BrowserSlashCommandTerminalNotice | null { switch (outcome.kind) { case "surface": return { type: "system", message: `/${outcome.commandName} is reserved for browser-native handling and was not sent to the model.`, }; case "reject": return { type: "error", message: `${outcome.reason} ${outcome.guidance}`.trim(), }; default: return null; } }