Skip to content

Commit 30a8a91

Browse files
🤖 fix: restore slash commands in workspace creation mode (#1012)
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 (to escape `overflow:hidden` containers) - 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 between suggestion filtering and command execution _Generated with `mux`_
1 parent fd653d6 commit 30a8a91

File tree

5 files changed

+78
-43
lines changed

5 files changed

+78
-43
lines changed

src/browser/components/ChatInput/index.tsx

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -411,10 +411,10 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
411411

412412
// Watch input for slash commands
413413
useEffect(() => {
414-
const suggestions = getSlashCommandSuggestions(input, { providerNames });
414+
const suggestions = getSlashCommandSuggestions(input, { providerNames, variant });
415415
setCommandSuggestions(suggestions);
416416
setShowCommandSuggestions(suggestions.length > 0);
417-
}, [input, providerNames]);
417+
}, [input, providerNames, variant]);
418418

419419
// Load provider names for suggestions
420420
useEffect(() => {
@@ -1335,18 +1335,16 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
13351335
data-component="ChatInputSection"
13361336
>
13371337
<div className="mx-auto w-full max-w-4xl">
1338-
{/* Creation toast */}
1339-
{variant === "creation" && (
1340-
<ChatInputToast
1341-
toast={creationState.toast}
1342-
onDismiss={() => creationState.setToast(null)}
1343-
/>
1344-
)}
1345-
1346-
{/* Workspace toast */}
1347-
{variant === "workspace" && (
1348-
<ChatInputToast toast={toast} onDismiss={handleToastDismiss} />
1349-
)}
1338+
{/* Toast - show shared toast (slash commands) or variant-specific toast */}
1339+
<ChatInputToast
1340+
toast={toast ?? (variant === "creation" ? creationState.toast : null)}
1341+
onDismiss={() => {
1342+
handleToastDismiss();
1343+
if (variant === "creation") {
1344+
creationState.setToast(null);
1345+
}
1346+
}}
1347+
/>
13501348

13511349
{/* Attached reviews preview - show styled blocks with remove/edit buttons */}
13521350
{/* Hide during send to avoid duplicate display with the sent message */}
@@ -1369,17 +1367,17 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
13691367
</div>
13701368
)}
13711369

1372-
{/* Command suggestions - workspace only */}
1373-
{variant === "workspace" && (
1374-
<CommandSuggestions
1375-
suggestions={commandSuggestions}
1376-
onSelectSuggestion={handleCommandSelect}
1377-
onDismiss={() => setShowCommandSuggestions(false)}
1378-
isVisible={showCommandSuggestions}
1379-
ariaLabel="Slash command suggestions"
1380-
listId={commandListId}
1381-
/>
1382-
)}
1370+
{/* Command suggestions - available in both variants */}
1371+
{/* In creation mode, use portal (anchorRef) to escape overflow:hidden containers */}
1372+
<CommandSuggestions
1373+
suggestions={commandSuggestions}
1374+
onSelectSuggestion={handleCommandSelect}
1375+
onDismiss={() => setShowCommandSuggestions(false)}
1376+
isVisible={showCommandSuggestions}
1377+
ariaLabel="Slash command suggestions"
1378+
listId={commandListId}
1379+
anchorRef={variant === "creation" ? inputRef : undefined}
1380+
/>
13831381

13841382
<div className="relative flex items-end" data-component="ChatInputControls">
13851383
{/* Recording/transcribing overlay - replaces textarea when active */}

src/browser/utils/chatCommands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
1818
import type { RuntimeConfig } from "@/common/types/runtime";
1919
import { RUNTIME_MODE, SSH_RUNTIME_PREFIX } from "@/common/types/runtime";
2020
import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events";
21+
import { WORKSPACE_ONLY_COMMANDS } from "@/constants/slashCommands";
2122
import type { Toast } from "@/browser/components/ChatInputToast";
2223
import type { ParsedCommand } from "@/browser/utils/slashCommands/types";
2324
import { applyCompactionOverrides } from "@/browser/utils/messages/compactionOptions";
@@ -237,8 +238,7 @@ export async function processSlashCommand(
237238
}
238239

239240
// 2. Workspace Commands
240-
const workspaceCommands = ["clear", "truncate", "compact", "fork", "new"];
241-
const isWorkspaceCommand = workspaceCommands.includes(parsed.type);
241+
const isWorkspaceCommand = WORKSPACE_ONLY_COMMANDS.has(parsed.type);
242242

243243
if (isWorkspaceCommand) {
244244
if (variant !== "workspace") {

src/browser/utils/slashCommands/suggestions.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,48 @@ import type {
1313

1414
export type { SlashSuggestion } from "./types";
1515

16+
import { WORKSPACE_ONLY_COMMANDS } from "@/constants/slashCommands";
17+
1618
const COMMAND_DEFINITIONS = getSlashCommandDefinitions();
1719

1820
function filterAndMapSuggestions<T extends SuggestionDefinition>(
1921
definitions: readonly T[],
2022
partial: string,
21-
build: (definition: T) => SlashSuggestion
23+
build: (definition: T) => SlashSuggestion,
24+
filter?: (definition: T) => boolean
2225
): SlashSuggestion[] {
2326
const normalizedPartial = partial.trim().toLowerCase();
2427

2528
return definitions
26-
.filter((definition) =>
27-
normalizedPartial ? definition.key.toLowerCase().startsWith(normalizedPartial) : true
28-
)
29+
.filter((definition) => {
30+
if (filter && !filter(definition)) return false;
31+
return normalizedPartial ? definition.key.toLowerCase().startsWith(normalizedPartial) : true;
32+
})
2933
.map((definition) => build(definition));
3034
}
3135

32-
function buildTopLevelSuggestions(partial: string): SlashSuggestion[] {
33-
return filterAndMapSuggestions(COMMAND_DEFINITIONS, partial, (definition) => {
34-
const appendSpace = definition.appendSpace ?? true;
35-
const replacement = `/${definition.key}${appendSpace ? " " : ""}`;
36-
return {
37-
id: `command:${definition.key}`,
38-
display: `/${definition.key}`,
39-
description: definition.description,
40-
replacement,
41-
};
42-
});
36+
function buildTopLevelSuggestions(
37+
partial: string,
38+
context: SlashSuggestionContext
39+
): SlashSuggestion[] {
40+
const isCreation = context.variant === "creation";
41+
42+
return filterAndMapSuggestions(
43+
COMMAND_DEFINITIONS,
44+
partial,
45+
(definition) => {
46+
const appendSpace = definition.appendSpace ?? true;
47+
const replacement = `/${definition.key}${appendSpace ? " " : ""}`;
48+
return {
49+
id: `command:${definition.key}`,
50+
display: `/${definition.key}`,
51+
description: definition.description,
52+
replacement,
53+
};
54+
},
55+
// In creation mode, filter out workspace-only commands
56+
isCreation ? (definition) => !WORKSPACE_ONLY_COMMANDS.has(definition.key) : undefined
57+
);
4358
}
4459

4560
function buildSubcommandSuggestions(
@@ -83,7 +98,7 @@ export function getSlashCommandSuggestions(
8398
const stage = completedTokens.length;
8499

85100
if (stage === 0) {
86-
return buildTopLevelSuggestions(partialToken);
101+
return buildTopLevelSuggestions(partialToken, context);
87102
}
88103

89104
const rootKey = completedTokens[0] ?? tokens[0];
@@ -96,6 +111,11 @@ export function getSlashCommandSuggestions(
96111
return [];
97112
}
98113

114+
// In creation mode, don't show subcommand suggestions for workspace-only commands
115+
if (context.variant === "creation" && WORKSPACE_ONLY_COMMANDS.has(rootKey)) {
116+
return [];
117+
}
118+
99119
const definitionPath: SlashCommandDefinition[] = [rootDefinition];
100120
let lastDefinition = rootDefinition;
101121

src/browser/utils/slashCommands/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export interface SlashSuggestion {
7474

7575
export interface SlashSuggestionContext {
7676
providerNames?: string[];
77+
/** Variant determines which commands are available */
78+
variant?: "workspace" | "creation";
7779
}
7880

7981
export interface SuggestionDefinition {

src/constants/slashCommands.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Slash command constants shared between suggestion filtering and command execution.
3+
*/
4+
5+
/**
6+
* Commands that only work in workspace context (not during creation).
7+
* These commands require an existing workspace with conversation history.
8+
*/
9+
export const WORKSPACE_ONLY_COMMANDS: ReadonlySet<string> = new Set([
10+
"clear",
11+
"truncate",
12+
"compact",
13+
"fork",
14+
"new",
15+
]);

0 commit comments

Comments
 (0)