From 3b916b006b5d435946c52c9e9a3da5845ab1f4d6 Mon Sep 17 00:00:00 2001 From: ethan Date: Tue, 9 Dec 2025 18:15:26 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20restore=20slash=20command?= =?UTF-8?q?s=20in=20workspace=20creation=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a regression introduced in #784 where slash commands were accidentally removed from creation mode during the ORPC migration. Supporting global commands like /providers in creation mode was originally introduced in #704. This PR restores that functionality with an improvement: workspace-only commands (clear, truncate, compact, fork, new) are now filtered from suggestions in creation mode rather than just showing an error toast when executed. Changes: - Restore CommandSuggestions in creation mode with portal rendering - Filter workspace-only commands from suggestions based on variant - Unify toast handling to show slash command feedback in both modes - Extract WORKSPACE_ONLY_COMMANDS to shared constant to avoid duplication --- src/browser/components/ChatInput/index.tsx | 48 ++++++++--------- src/browser/utils/chatCommands.ts | 4 +- .../utils/slashCommands/suggestions.ts | 52 +++++++++++++------ src/browser/utils/slashCommands/types.ts | 2 + src/constants/slashCommands.ts | 15 ++++++ 5 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 src/constants/slashCommands.ts diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx index 97aba4f191..88a04edd53 100644 --- a/src/browser/components/ChatInput/index.tsx +++ b/src/browser/components/ChatInput/index.tsx @@ -411,10 +411,10 @@ export const ChatInput: React.FC = (props) => { // Watch input for slash commands useEffect(() => { - const suggestions = getSlashCommandSuggestions(input, { providerNames }); + const suggestions = getSlashCommandSuggestions(input, { providerNames, variant }); setCommandSuggestions(suggestions); setShowCommandSuggestions(suggestions.length > 0); - }, [input, providerNames]); + }, [input, providerNames, variant]); // Load provider names for suggestions useEffect(() => { @@ -1335,18 +1335,16 @@ export const ChatInput: React.FC = (props) => { data-component="ChatInputSection" >
- {/* Creation toast */} - {variant === "creation" && ( - creationState.setToast(null)} - /> - )} - - {/* Workspace toast */} - {variant === "workspace" && ( - - )} + {/* Toast - show shared toast (slash commands) or variant-specific toast */} + { + handleToastDismiss(); + if (variant === "creation") { + creationState.setToast(null); + } + }} + /> {/* Attached reviews preview - show styled blocks with remove/edit buttons */} {/* Hide during send to avoid duplicate display with the sent message */} @@ -1369,17 +1367,17 @@ export const ChatInput: React.FC = (props) => {
)} - {/* Command suggestions - workspace only */} - {variant === "workspace" && ( - setShowCommandSuggestions(false)} - isVisible={showCommandSuggestions} - ariaLabel="Slash command suggestions" - listId={commandListId} - /> - )} + {/* Command suggestions - available in both variants */} + {/* In creation mode, use portal (anchorRef) to escape overflow:hidden containers */} + setShowCommandSuggestions(false)} + isVisible={showCommandSuggestions} + ariaLabel="Slash command suggestions" + listId={commandListId} + anchorRef={variant === "creation" ? inputRef : undefined} + />
{/* Recording/transcribing overlay - replaces textarea when active */} diff --git a/src/browser/utils/chatCommands.ts b/src/browser/utils/chatCommands.ts index 809b1be044..089c3bb6df 100644 --- a/src/browser/utils/chatCommands.ts +++ b/src/browser/utils/chatCommands.ts @@ -18,6 +18,7 @@ import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import type { RuntimeConfig } from "@/common/types/runtime"; import { RUNTIME_MODE, SSH_RUNTIME_PREFIX } from "@/common/types/runtime"; import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events"; +import { WORKSPACE_ONLY_COMMANDS } from "@/constants/slashCommands"; import type { Toast } from "@/browser/components/ChatInputToast"; import type { ParsedCommand } from "@/browser/utils/slashCommands/types"; import { applyCompactionOverrides } from "@/browser/utils/messages/compactionOptions"; @@ -237,8 +238,7 @@ export async function processSlashCommand( } // 2. Workspace Commands - const workspaceCommands = ["clear", "truncate", "compact", "fork", "new"]; - const isWorkspaceCommand = workspaceCommands.includes(parsed.type); + const isWorkspaceCommand = WORKSPACE_ONLY_COMMANDS.has(parsed.type); if (isWorkspaceCommand) { if (variant !== "workspace") { diff --git a/src/browser/utils/slashCommands/suggestions.ts b/src/browser/utils/slashCommands/suggestions.ts index 84b56f1965..e6a23f01ac 100644 --- a/src/browser/utils/slashCommands/suggestions.ts +++ b/src/browser/utils/slashCommands/suggestions.ts @@ -13,33 +13,48 @@ import type { export type { SlashSuggestion } from "./types"; +import { WORKSPACE_ONLY_COMMANDS } from "@/constants/slashCommands"; + const COMMAND_DEFINITIONS = getSlashCommandDefinitions(); function filterAndMapSuggestions( definitions: readonly T[], partial: string, - build: (definition: T) => SlashSuggestion + build: (definition: T) => SlashSuggestion, + filter?: (definition: T) => boolean ): SlashSuggestion[] { const normalizedPartial = partial.trim().toLowerCase(); return definitions - .filter((definition) => - normalizedPartial ? definition.key.toLowerCase().startsWith(normalizedPartial) : true - ) + .filter((definition) => { + if (filter && !filter(definition)) return false; + return normalizedPartial ? definition.key.toLowerCase().startsWith(normalizedPartial) : true; + }) .map((definition) => build(definition)); } -function buildTopLevelSuggestions(partial: string): SlashSuggestion[] { - return filterAndMapSuggestions(COMMAND_DEFINITIONS, partial, (definition) => { - const appendSpace = definition.appendSpace ?? true; - const replacement = `/${definition.key}${appendSpace ? " " : ""}`; - return { - id: `command:${definition.key}`, - display: `/${definition.key}`, - description: definition.description, - replacement, - }; - }); +function buildTopLevelSuggestions( + partial: string, + context: SlashSuggestionContext +): SlashSuggestion[] { + const isCreation = context.variant === "creation"; + + return filterAndMapSuggestions( + COMMAND_DEFINITIONS, + partial, + (definition) => { + const appendSpace = definition.appendSpace ?? true; + const replacement = `/${definition.key}${appendSpace ? " " : ""}`; + return { + id: `command:${definition.key}`, + display: `/${definition.key}`, + description: definition.description, + replacement, + }; + }, + // In creation mode, filter out workspace-only commands + isCreation ? (definition) => !WORKSPACE_ONLY_COMMANDS.has(definition.key) : undefined + ); } function buildSubcommandSuggestions( @@ -83,7 +98,7 @@ export function getSlashCommandSuggestions( const stage = completedTokens.length; if (stage === 0) { - return buildTopLevelSuggestions(partialToken); + return buildTopLevelSuggestions(partialToken, context); } const rootKey = completedTokens[0] ?? tokens[0]; @@ -96,6 +111,11 @@ export function getSlashCommandSuggestions( return []; } + // In creation mode, don't show subcommand suggestions for workspace-only commands + if (context.variant === "creation" && WORKSPACE_ONLY_COMMANDS.has(rootKey)) { + return []; + } + const definitionPath: SlashCommandDefinition[] = [rootDefinition]; let lastDefinition = rootDefinition; diff --git a/src/browser/utils/slashCommands/types.ts b/src/browser/utils/slashCommands/types.ts index d42c5462c6..bbf3a17be8 100644 --- a/src/browser/utils/slashCommands/types.ts +++ b/src/browser/utils/slashCommands/types.ts @@ -74,6 +74,8 @@ export interface SlashSuggestion { export interface SlashSuggestionContext { providerNames?: string[]; + /** Variant determines which commands are available */ + variant?: "workspace" | "creation"; } export interface SuggestionDefinition { diff --git a/src/constants/slashCommands.ts b/src/constants/slashCommands.ts new file mode 100644 index 0000000000..5e6a294315 --- /dev/null +++ b/src/constants/slashCommands.ts @@ -0,0 +1,15 @@ +/** + * Slash command constants shared between suggestion filtering and command execution. + */ + +/** + * Commands that only work in workspace context (not during creation). + * These commands require an existing workspace with conversation history. + */ +export const WORKSPACE_ONLY_COMMANDS: ReadonlySet = new Set([ + "clear", + "truncate", + "compact", + "fork", + "new", +]);