feat(chat): add slash command dropdown button in message input toolbar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-04 13:24:26 +08:00
parent 576944ac1f
commit 6359651247
12 changed files with 85 additions and 17 deletions

View File

@@ -20,6 +20,7 @@ import {
ListPlus,
Plus,
Send,
Command,
Square,
X,
} from "lucide-react"
@@ -54,6 +55,7 @@ import { ModeSelector } from "@/components/chat/mode-selector"
import { SessionConfigSelector } from "@/components/chat/session-config-selector"
import { SlashCommandMenu } from "@/components/chat/slash-command-menu"
import { FileMentionMenu } from "@/components/chat/file-mention-menu"
import { DropdownRadioItemContent } from "@/components/chat/dropdown-radio-item-content"
import { useFileTree } from "@/hooks/use-file-tree"
import { joinFsPath } from "@/lib/path-utils"
import {
@@ -314,6 +316,7 @@ export function MessageInput({
const textareaRef = useRef<HTMLTextAreaElement>(null)
const lastDomDropAtRef = useRef(0)
const composingRef = useRef(false)
const cursorPosRef = useRef<number | null>(null)
const textRef = useRef(text)
const disabledRef = useRef(disabled)
const isPromptingRef = useRef(isPrompting)
@@ -778,6 +781,24 @@ export function MessageInput({
setSlashMenuOpen(false)
}, [])
const handleSlashPopoverSelect = useCallback((cmd: AvailableCommandInfo) => {
const pos = cursorPosRef.current ?? textRef.current.length
const before = textRef.current.slice(0, pos)
const after = textRef.current.slice(pos)
const needsSpace = pos > 0 && !/\s$/.test(before)
const insertion = `${needsSpace ? " " : ""}/${cmd.name} `
const newText = before + insertion + after
setText(newText)
requestAnimationFrame(() => {
const ta = textareaRef.current
if (ta) {
ta.focus()
const newPos = pos + insertion.length
ta.setSelectionRange(newPos, newPos)
}
})
}, [])
const atTriggerPosRef = useRef(atTriggerPos)
useEffect(() => {
atTriggerPosRef.current = atTriggerPos
@@ -1476,26 +1497,60 @@ export function MessageInput({
autoFocus={autoFocus}
/>
<div className="@container flex shrink-0 items-end justify-between gap-2 px-2 pb-2">
<div className="flex min-w-0 items-end gap-1">
<div className="flex min-w-0 items-end gap-2">
<Button
onClick={handlePickFiles}
disabled={disabled}
variant="ghost"
variant="outline"
size="icon"
className="h-6 w-6 shrink-0"
className="h-6 w-6 shrink-0 bg-transparent"
title={t("attachFiles")}
>
<Plus className="size-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
disabled={disabled || slashCommands.length === 0}
variant="outline"
size="icon"
className="h-6 w-6 shrink-0 bg-transparent"
onPointerDown={() => {
cursorPosRef.current =
textareaRef.current?.selectionStart ?? null
}}
title={t("slashCommands")}
>
<Command className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="min-w-72"
>
{slashCommands.map((cmd) => (
<DropdownMenuItem
key={cmd.name}
onClick={() => handleSlashPopoverSelect(cmd)}
>
<DropdownRadioItemContent
label={`/${cmd.name}`}
description={cmd.description}
/>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* 宽屏内联显示,窄屏(<300px通过"更多"气泡显示 */}
<div className="hidden @[300px]:contents">{selectorItems}</div>
{hasAnySelector && (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
variant="outline"
size="icon"
className="h-6 w-6 shrink-0 @[300px]:hidden"
className="h-6 w-6 shrink-0 bg-transparent @[300px]:hidden"
>
<Ellipsis className="size-4" />
</Button>

View File

@@ -37,9 +37,12 @@ export function SessionConfigSelector({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
variant="outline"
size="xs"
className={cn("gap-1 min-w-0", isActive && "text-primary")}
className={cn(
"gap-1 min-w-0 bg-transparent",
isActive && "text-primary"
)}
title={option.description ?? option.name}
>
<span className="truncate">{label}</span>

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "جارٍ تحميل الوضع...",
"cancel": "إلغاء",
"send": "إرسال",
"forkAndSend": "تفريع وإرسال"
"forkAndSend": "تفريع وإرسال",
"slashCommands": "أوامر الشرطة المائلة"
},
"messageQueue": {
"addToQueue": "إضافة للقائمة",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "Modus wird geladen...",
"cancel": "Abbrechen",
"send": "Senden",
"forkAndSend": "Fork & Senden"
"forkAndSend": "Fork & Senden",
"slashCommands": "Slash-Befehle"
},
"messageQueue": {
"addToQueue": "Zur Warteschlange",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "Loading mode...",
"cancel": "Cancel",
"send": "Send",
"forkAndSend": "Fork & Send"
"forkAndSend": "Fork & Send",
"slashCommands": "Slash commands"
},
"messageQueue": {
"addToQueue": "Queue message",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "Cargando modo...",
"cancel": "Cancelar",
"send": "Enviar",
"forkAndSend": "Fork y Enviar"
"forkAndSend": "Fork y Enviar",
"slashCommands": "Comandos de barra"
},
"messageQueue": {
"addToQueue": "Agregar a la cola",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "Chargement du mode...",
"cancel": "Annuler",
"send": "Envoyer",
"forkAndSend": "Fork & Envoyer"
"forkAndSend": "Fork & Envoyer",
"slashCommands": "Commandes slash"
},
"messageQueue": {
"addToQueue": "Mettre en file",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "モードを読み込み中...",
"cancel": "キャンセル",
"send": "送信",
"forkAndSend": "フォークして送信"
"forkAndSend": "フォークして送信",
"slashCommands": "スラッシュコマンド"
},
"messageQueue": {
"addToQueue": "キューに追加",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "모드 불러오는 중...",
"cancel": "취소",
"send": "보내기",
"forkAndSend": "포크 & 전송"
"forkAndSend": "포크 & 전송",
"slashCommands": "슬래시 명령"
},
"messageQueue": {
"addToQueue": "대기열에 추가",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "Carregando modo...",
"cancel": "Cancelar",
"send": "Enviar",
"forkAndSend": "Fork & Enviar"
"forkAndSend": "Fork & Enviar",
"slashCommands": "Comandos de barra"
},
"messageQueue": {
"addToQueue": "Adicionar à fila",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "正在加载模式...",
"cancel": "取消",
"send": "发送",
"forkAndSend": "分叉发送"
"forkAndSend": "分叉发送",
"slashCommands": "斜杠命令"
},
"messageQueue": {
"addToQueue": "加入队列",

View File

@@ -1421,7 +1421,8 @@
"loadingMode": "正在載入模式...",
"cancel": "取消",
"send": "傳送",
"forkAndSend": "分叉發送"
"forkAndSend": "分叉發送",
"slashCommands": "斜線命令"
},
"messageQueue": {
"addToQueue": "加入佇列",