From dfc89cbbaa2648acea9b82ae9e183e94cb1c0479 Mon Sep 17 00:00:00 2001
From: ZhenYi <434836402@qq.com>
Date: Sun, 17 May 2026 16:36:56 +0800
Subject: [PATCH] feat(chat): add slash command menu and context
---
src/app/chat/ChatConversationList.tsx | 17 +-
src/app/chat/ChatHeader.tsx | 15 +-
src/app/chat/ChatMessageBubble.tsx | 53 +++-
src/app/chat/ChatMessageInput.tsx | 337 +++++++++++++++++++++++++-
src/app/chat/ChatMessageList.tsx | 39 +--
src/app/chat/ChatModelSelector.tsx | 9 +-
src/app/chat/ChatSlashCommandMenu.tsx | 150 ++++++++++++
src/app/chat/chatSlashContext.ts | 136 +++++++++++
8 files changed, 702 insertions(+), 54 deletions(-)
create mode 100644 src/app/chat/ChatSlashCommandMenu.tsx
create mode 100644 src/app/chat/chatSlashContext.ts
diff --git a/src/app/chat/ChatConversationList.tsx b/src/app/chat/ChatConversationList.tsx
index e452267..5da0d7c 100644
--- a/src/app/chat/ChatConversationList.tsx
+++ b/src/app/chat/ChatConversationList.tsx
@@ -4,6 +4,7 @@ import { Plus, Trash2, MessageSquare, Loader2, Search, Edit2, X } from "lucide-r
import { useConversationsQuery, useCreateConversationMutation, useDeleteConversationMutation } from "@/hooks/useAiChatQuery";
import { useChatPage } from "./ChatPageContext";
import type { ConversationResponse } from "@/client/model";
+import { t } from "@/i18n/T";
interface ChatConversationListProps {
selectedId: string | null;
@@ -139,14 +140,14 @@ export function ChatConversationList({ selectedId, onSelect, onNew }: ChatConver
style={{ borderBottom: "1px solid var(--border-subtle)" }}
>
- Chat History
+ {t("chat.conversations.chat_history")}
setIsSearchOpen(!isSearchOpen)}
className="flex items-center justify-center w-7 h-7 rounded-lg transition-colors hover:bg-[var(--hover-bg)]"
style={{ color: "var(--text-muted)" }}
- title="Search (Ctrl+K)"
+ title={t("navigation.search_shortcut")}
>
@@ -155,7 +156,7 @@ export function ChatConversationList({ selectedId, onSelect, onNew }: ChatConver
disabled={createMutation.isPending}
className="flex items-center justify-center w-7 h-7 rounded-lg transition-colors hover:bg-[var(--hover-bg)]"
style={{ color: "var(--text-secondary)" }}
- title="New chat"
+ title={t("chat.header.new_chat")}
>
{createMutation.isPending ? (
@@ -180,7 +181,7 @@ export function ChatConversationList({ selectedId, onSelect, onNew }: ChatConver
setSearchQuery(e.target.value)}
className="flex-1 text-sm bg-transparent outline-none min-w-0"
@@ -220,7 +221,7 @@ export function ChatConversationList({ selectedId, onSelect, onNew }: ChatConver
}}
>
- New Chat
+ {t("chat.conversations.new_chat")}
@@ -248,13 +249,13 @@ export function ChatConversationList({ selectedId, onSelect, onNew }: ChatConver
className="text-[13px] font-medium text-center"
style={{ color: "var(--text-primary)" }}
>
- {searchQuery ? "No matching conversations" : "No conversations yet"}
+ {searchQuery ? t("chat.conversations.no_matching") : t("chat.conversations.no_conversations")}
- {searchQuery ? "Try a different search term." : "Start a new chat to begin exploring with AI."}
+ {searchQuery ? t("chat.conversations.try_different_search") : t("chat.conversations.start_new_chat")}
) : (
@@ -337,7 +338,7 @@ function ConversationItem({
/>
diff --git a/src/app/chat/ChatHeader.tsx b/src/app/chat/ChatHeader.tsx
index c395d1a..3f97a10 100644
--- a/src/app/chat/ChatHeader.tsx
+++ b/src/app/chat/ChatHeader.tsx
@@ -1,5 +1,6 @@
import {Loader2, MoreHorizontal, Pencil, PanelLeftOpen, PanelLeftClose, Share2} from "lucide-react";
import {useConversationQuery} from "@/hooks/useAiChatQuery";
+import {t} from "@/i18n/T";
interface ChatHeaderProps {
conversationId: string | null;
@@ -11,7 +12,7 @@ interface ChatHeaderProps {
export function ChatHeader({conversationId, isStreaming, isSidebarCollapsed, onToggleSidebar}: ChatHeaderProps) {
const {data: conversation} = useConversationQuery(conversationId || "");
- const title = conversation?.title || "New Chat";
+ const title = conversation?.title || t("chat.header.new_chat");
return (
{isSidebarCollapsed ?
:
}
@@ -34,7 +35,7 @@ export function ChatHeader({conversationId, isStreaming, isSidebarCollapsed, onT
className="text-sm font-medium truncate"
style={{color: "var(--text-primary)"}}
>
- {conversationId ? title : "Chat"}
+ {conversationId ? title : t("search.chat")}
{isStreaming && (
- Streaming...
+ {t("chat.header.streaming")}
)}
@@ -53,21 +54,21 @@ export function ChatHeader({conversationId, isStreaming, isSidebarCollapsed, onT
diff --git a/src/app/chat/ChatMessageBubble.tsx b/src/app/chat/ChatMessageBubble.tsx
index 64ef391..8a40048 100644
--- a/src/app/chat/ChatMessageBubble.tsx
+++ b/src/app/chat/ChatMessageBubble.tsx
@@ -1,4 +1,13 @@
-import { Copy, Check, Sparkles, ClipboardList, Pencil, RefreshCw, GitFork } from "lucide-react";
+import {
+ Copy,
+ Check,
+ Sparkles,
+ ClipboardList,
+ Pencil,
+ RefreshCw,
+ GitFork,
+ FolderGit2,
+} from "lucide-react";
import { memo, useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useCurrentUserQuery } from "@/hooks/useAuth";
@@ -18,7 +27,9 @@ import { Reasoning, ReasoningTrigger, ReasoningContent } from "@/components/ai-e
import type { MessageResponse } from "@/hooks/useAiChatQuery";
import { getModelIcon } from "@/lib/icons/modelIcons";
import { ToolCallBlock } from "@/components/chat/ToolCallBlock";
+import { Badge } from "@/components/ui/badge";
import { useChatPage } from "./ChatPageContext";
+import { parseSlashContextMetadata } from "./chatSlashContext";
interface ChatMessageBubbleProps {
message: MessageResponse;
@@ -60,9 +71,7 @@ export const ChatMessageBubble = memo(function ChatMessageBubble({ message, conv
// Parse content into IrContentBlock[] (handles both old and future formats)
const blocks: IrContentBlock[] = useMemo(() =>
- isUser
- ? [{ role: "user", nodes: [] }]
- : parseContentBlocks(message.content),
+ isUser ? [] : parseContentBlocks(message.content),
[isUser, message.content]
);
@@ -74,6 +83,10 @@ export const ChatMessageBubble = memo(function ChatMessageBubble({ message, conv
: "";
const plainText = isUser ? userText : extractAnswerText(blocks);
const hasThinking = blocks.some((b) => b.role === "thinking");
+ const selectedContexts = useMemo(
+ () => parseSlashContextMetadata(message.metadata),
+ [message.metadata]
+ );
// Fetch versions when showing version switcher
const versionsQuery = useMessageVersionsQuery(conversationId, message.id);
@@ -194,6 +207,28 @@ export const ChatMessageBubble = memo(function ChatMessageBubble({ message, conv
{/* Interleaved blocks — thinking (collapsible) + answer (IrRenderer) */}
+ {isUser && selectedContexts.length > 0 && (
+
+ {selectedContexts.map((context) => (
+
+ {context.kind === "repo" ? (
+
+ ) : (
+
+ )}
+ {context.label}
+
+ {context.kind}
+
+
+ ))}
+
+ )}
+
{isUser && isEditing ? (