33 useRef ,
44 useEffect ,
55 useCallback ,
6+ useMemo ,
67 memo ,
78 type KeyboardEvent ,
89} from "react" ;
@@ -535,6 +536,7 @@ const FOLDER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24
535536
536537interface InputBarProps {
537538 onSend : ( text : string , images ?: ImageAttachment [ ] , displayText ?: string ) => void ;
539+ onClear ?: ( ) => void | Promise < void > ;
538540 onStop : ( ) => void ;
539541 isProcessing : boolean ;
540542 model : string ;
@@ -580,6 +582,39 @@ interface InputBarProps {
580582 isIslandLayout ?: boolean ;
581583}
582584
585+ export const LOCAL_CLEAR_COMMAND : SlashCommand = {
586+ name : "clear" ,
587+ description : "Open a new chat without sending anything to the agent" ,
588+ argumentHint : "" ,
589+ source : "local" ,
590+ } ;
591+
592+ export function getAvailableSlashCommands ( slashCommands ?: SlashCommand [ ] ) : SlashCommand [ ] {
593+ const commands = slashCommands ?. filter ( ( cmd ) => cmd . name !== LOCAL_CLEAR_COMMAND . name ) ?? [ ] ;
594+ return [ LOCAL_CLEAR_COMMAND , ...commands ] ;
595+ }
596+
597+ export function isClearCommandText ( text : string ) : boolean {
598+ return text . trim ( ) === `/${ LOCAL_CLEAR_COMMAND . name } ` ;
599+ }
600+
601+ export function getSlashCommandReplacement ( cmd : SlashCommand ) : string {
602+ switch ( cmd . source ) {
603+ case "claude" :
604+ case "acp" :
605+ return `/${ cmd . name } ` ;
606+ case "codex-skill" :
607+ return cmd . defaultPrompt
608+ ? `$${ cmd . name } ${ cmd . defaultPrompt } `
609+ : `$${ cmd . name } ` ;
610+ case "codex-app" :
611+ return `$${ cmd . appSlug ?? cmd . name } ` ;
612+ case "local" :
613+ // Local commands execute directly, so keep the exact command text with no trailing space.
614+ return `/${ cmd . name } ` ;
615+ }
616+ }
617+
583618// Simple fuzzy match: all query chars must appear in order
584619function fuzzyMatch ( query : string , target : string ) : { match : boolean ; score : number } {
585620 const q = query . toLowerCase ( ) ;
@@ -688,6 +723,7 @@ function extractEditableContent(el: HTMLElement): { text: string; mentionPaths:
688723
689724export const InputBar = memo ( function InputBar ( {
690725 onSend,
726+ onClear,
691727 onStop,
692728 isProcessing,
693729 model,
@@ -758,6 +794,10 @@ export const InputBar = memo(function InputBar({
758794 const isACPAgent = selectedAgent != null && selectedAgent . engine === "acp" ;
759795 const isCodexAgent = selectedAgent != null && selectedAgent . engine === "codex" ;
760796 const showACPConfigOptions = isACPAgent && ( acpConfigOptions ?. length ?? 0 ) > 0 ;
797+ const availableSlashCommands = useMemo (
798+ ( ) => getAvailableSlashCommands ( slashCommands ) ,
799+ [ slashCommands ] ,
800+ ) ;
761801 const isAwaitingAcpOptions = isACPAgent && ! ! acpConfigOptionsLoading ;
762802 const modelsLoading = modelList . length === 0 ;
763803 const modelsLoadingText = isCodexAgent
@@ -885,10 +925,10 @@ export const InputBar = memo(function InputBar({
885925
886926 // Slash command filtered results
887927 const cmdResults = ( ( ) => {
888- if ( ! showCommands || ! slashCommands ? .length ) return [ ] ;
928+ if ( ! showCommands || availableSlashCommands . length === 0 ) return [ ] ;
889929 const q = commandQuery . toLowerCase ( ) ;
890- if ( ! q ) return slashCommands . slice ( 0 , 15 ) ;
891- return slashCommands
930+ if ( ! q ) return availableSlashCommands . slice ( 0 , 15 ) ;
931+ return availableSlashCommands
892932 . filter ( cmd => cmd . name . toLowerCase ( ) . includes ( q ) || cmd . description . toLowerCase ( ) . includes ( q ) )
893933 . slice ( 0 , 15 ) ;
894934 } ) ( ) ;
@@ -915,6 +955,15 @@ export const InputBar = memo(function InputBar({
915955 mentionStartOffset . current = 0 ;
916956 } , [ ] ) ;
917957
958+ const clearComposer = useCallback ( ( el : HTMLDivElement ) => {
959+ el . innerHTML = "" ;
960+ hasContentRef . current = false ;
961+ setHasContent ( false ) ;
962+ setAttachments ( [ ] ) ;
963+ closeMentions ( ) ;
964+ setShowCommands ( false ) ;
965+ } , [ closeMentions ] ) ;
966+
918967 const addImageFiles = useCallback ( async ( files : FileList | globalThis . File [ ] ) => {
919968 const validFiles = Array . from ( files ) . filter ( isAcceptedImage ) ;
920969 if ( validFiles . length === 0 ) return ;
@@ -941,24 +990,7 @@ export const InputBar = memo(function InputBar({
941990 const el = editableRef . current ;
942991 if ( ! el ) return ;
943992
944- // Build the replacement text based on source engine
945- let replacement : string ;
946- switch ( cmd . source ) {
947- case "claude" :
948- case "acp" :
949- replacement = `/${ cmd . name } ` ;
950- break ;
951- case "codex-skill" :
952- replacement = cmd . defaultPrompt
953- ? `$${ cmd . name } ${ cmd . defaultPrompt } `
954- : `$${ cmd . name } ` ;
955- break ;
956- case "codex-app" :
957- replacement = `$${ cmd . appSlug ?? cmd . name } ` ;
958- break ;
959- }
960-
961- el . textContent = replacement ;
993+ el . textContent = getSlashCommandReplacement ( cmd ) ;
962994
963995 // Move cursor to end
964996 const range = document . createRange ( ) ;
@@ -1035,6 +1067,15 @@ export const InputBar = memo(function InputBar({
10351067 const grabbedElementDisplayTokens : string [ ] = [ ] ;
10361068 let hasContext = false ;
10371069
1070+ if ( isClearCommandText ( trimmed ) ) {
1071+ try {
1072+ await onClear ?.( ) ;
1073+ } finally {
1074+ clearComposer ( el ) ;
1075+ }
1076+ return ;
1077+ }
1078+
10381079 // File mentions → <file>/<folder> context blocks
10391080 if ( mentionPaths . length > 0 && projectPath ) {
10401081 setIsSending ( true ) ;
@@ -1104,13 +1145,8 @@ export const InputBar = memo(function InputBar({
11041145 onSend ( trimmed , currentImages ) ;
11051146 }
11061147
1107- // Clear input
1108- el . innerHTML = "" ;
1109- hasContentRef . current = false ;
1110- setHasContent ( false ) ;
1111- setAttachments ( [ ] ) ;
1112- closeMentions ( ) ;
1113- } , [ attachments , isAwaitingAcpOptions , isSending , projectPath , onSend , closeMentions , grabbedElements ] ) ;
1148+ clearComposer ( el ) ;
1149+ } , [ attachments , isAwaitingAcpOptions , isSending , projectPath , onSend , onClear , clearComposer , grabbedElements ] ) ;
11141150
11151151 const handleKeyDown = ( e : KeyboardEvent < HTMLDivElement > ) => {
11161152 // Slash command picker keyboard navigation
@@ -1245,14 +1281,14 @@ export const InputBar = memo(function InputBar({
12451281 // Slash command detection — "/" at position 0 with no spaces (still typing the command name)
12461282 const fullText = ( el . textContent ?? "" ) . trimStart ( ) ;
12471283 const slashMatch = fullText . match ( / ^ \/ ( \S * ) $ / ) ;
1248- if ( slashMatch && slashCommands ? .length ) {
1284+ if ( slashMatch && availableSlashCommands . length > 0 ) {
12491285 setShowCommands ( true ) ;
12501286 setCommandQuery ( slashMatch [ 1 ] ) ;
12511287 setCommandIndex ( 0 ) ;
12521288 } else if ( showCommands ) {
12531289 setShowCommands ( false ) ;
12541290 }
1255- } , [ showMentions , showCommands , closeMentions , projectPath , slashCommands ] ) ;
1291+ } , [ showMentions , showCommands , closeMentions , projectPath , availableSlashCommands ] ) ;
12561292
12571293 const handlePaste = useCallback (
12581294 ( e : React . ClipboardEvent < HTMLDivElement > ) => {
@@ -1433,7 +1469,7 @@ export const InputBar = memo(function InputBar({
14331469 ? "Loading agent options..."
14341470 : isProcessing
14351471 ? `${ selectedAgent ?. name ?? "Claude" } is responding... (messages will be queued)`
1436- : slashCommands ? .length
1472+ : availableSlashCommands . length > 0
14371473 ? "Ask anything, @ to tag files, / for commands"
14381474 : "Ask anything, @ to tag files" }
14391475 </ div >
0 commit comments