Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 23 additions & 25 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,10 @@ export const ChatInput: React.FC<ChatInputProps> = (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(() => {
Expand Down Expand Up @@ -1335,18 +1335,16 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
data-component="ChatInputSection"
>
<div className="mx-auto w-full max-w-4xl">
{/* Creation toast */}
{variant === "creation" && (
<ChatInputToast
toast={creationState.toast}
onDismiss={() => creationState.setToast(null)}
/>
)}

{/* Workspace toast */}
{variant === "workspace" && (
<ChatInputToast toast={toast} onDismiss={handleToastDismiss} />
)}
{/* Toast - show shared toast (slash commands) or variant-specific toast */}
<ChatInputToast
toast={toast ?? (variant === "creation" ? creationState.toast : null)}
onDismiss={() => {
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 */}
Expand All @@ -1369,17 +1367,17 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
</div>
)}

{/* Command suggestions - workspace only */}
{variant === "workspace" && (
<CommandSuggestions
suggestions={commandSuggestions}
onSelectSuggestion={handleCommandSelect}
onDismiss={() => 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 */}
<CommandSuggestions
suggestions={commandSuggestions}
onSelectSuggestion={handleCommandSelect}
onDismiss={() => setShowCommandSuggestions(false)}
isVisible={showCommandSuggestions}
ariaLabel="Slash command suggestions"
listId={commandListId}
anchorRef={variant === "creation" ? inputRef : undefined}
/>

<div className="relative flex items-end" data-component="ChatInputControls">
{/* Recording/transcribing overlay - replaces textarea when active */}
Expand Down
4 changes: 2 additions & 2 deletions src/browser/utils/chatCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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") {
Expand Down
52 changes: 36 additions & 16 deletions src/browser/utils/slashCommands/suggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,48 @@ import type {

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

import { WORKSPACE_ONLY_COMMANDS } from "@/constants/slashCommands";

const COMMAND_DEFINITIONS = getSlashCommandDefinitions();

function filterAndMapSuggestions<T extends SuggestionDefinition>(
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(
Expand Down Expand Up @@ -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];
Expand All @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions src/browser/utils/slashCommands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export interface SlashSuggestion {

export interface SlashSuggestionContext {
providerNames?: string[];
/** Variant determines which commands are available */
variant?: "workspace" | "creation";
}

export interface SuggestionDefinition {
Expand Down
15 changes: 15 additions & 0 deletions src/constants/slashCommands.ts
Original file line number Diff line number Diff line change
@@ -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<string> = new Set([
"clear",
"truncate",
"compact",
"fork",
"new",
]);