refactor(chat-input): embed conversation context bar inside input container

Folder/branch pickers now render at the top of the input's rounded box
instead of a separate strip above it. Restyled as xs outlined buttons
matching the config selectors, always rendering the folder picker
chevron (dimmed when not editable). Removed the right-side commit/push
git action buttons and their unused imports. Scoped the bar to its
owning tab via a tabId prop so tile mode shows each tab's own folder
and disabled state independent of which tab is globally active.
This commit is contained in:
xintaofei
2026-04-21 19:26:04 +08:00
parent e567181b58
commit dc75020e1c
3 changed files with 48 additions and 192 deletions

View File

@@ -12,7 +12,6 @@ import type {
AvailableCommandInfo, AvailableCommandInfo,
} from "@/lib/types" } from "@/lib/types"
import type { QueuedMessage } from "@/hooks/use-message-queue" import type { QueuedMessage } from "@/hooks/use-message-queue"
import { ConversationContextBar } from "@/components/chat/conversation-context-bar"
import { MessageInput } from "@/components/chat/message-input" import { MessageInput } from "@/components/chat/message-input"
import { MessageQueueDisplay } from "@/components/chat/message-queue-display" import { MessageQueueDisplay } from "@/components/chat/message-queue-display"
@@ -93,7 +92,6 @@ export const ChatInput = memo(function ChatInput({
className="p-4 pt-0" className="p-4 pt-0"
onContextMenu={(event) => event.stopPropagation()} onContextMenu={(event) => event.stopPropagation()}
> >
<ConversationContextBar />
{queue && {queue &&
queue.length > 0 && queue.length > 0 &&
onQueueReorder && onQueueReorder &&

View File

@@ -9,13 +9,8 @@ import {
Folder, Folder,
FolderOpen, FolderOpen,
GitBranch, GitBranch,
GitCommit,
GitMerge,
Loader2, Loader2,
MoreHorizontal,
Upload,
Plus, Plus,
Archive,
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
import { useAppWorkspace } from "@/contexts/app-workspace-context" import { useAppWorkspace } from "@/contexts/app-workspace-context"
@@ -26,10 +21,6 @@ import {
gitCheckout, gitCheckout,
gitNewBranch, gitNewBranch,
gitDeleteBranch, gitDeleteBranch,
openCommitWindow,
openPushWindow,
openStashWindow,
openMergeWindow,
} from "@/lib/api" } from "@/lib/api"
import { isDesktop, openFileDialog } from "@/lib/platform" import { isDesktop, openFileDialog } from "@/lib/platform"
import type { GitBranchList } from "@/lib/types" import type { GitBranchList } from "@/lib/types"
@@ -49,13 +40,6 @@ import {
CommandList, CommandList,
CommandSeparator, CommandSeparator,
} from "@/components/ui/command" } from "@/components/ui/command"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -69,10 +53,15 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export const ConversationContextBar = memo(function ConversationContextBar() { interface ConversationContextBarProps {
tabId?: string | null
}
export const ConversationContextBar = memo(function ConversationContextBar({
tabId,
}: ConversationContextBarProps = {}) {
const t = useTranslations("Folder.conversationContextBar") const t = useTranslations("Folder.conversationContextBar")
const tBd = useTranslations("Folder.branchDropdown") const tBd = useTranslations("Folder.branchDropdown")
const { tabs, activeTabId, setTabFolder } = useTabContext() const { tabs, activeTabId, setTabFolder } = useTabContext()
@@ -87,32 +76,32 @@ export const ConversationContextBar = memo(function ConversationContextBar() {
} = useAppWorkspace() } = useAppWorkspace()
const { addTask, updateTask } = useTaskContext() const { addTask, updateTask } = useTaskContext()
const activeTab = useMemo( const ownTab = useMemo(() => {
() => tabs.find((x) => x.id === activeTabId) ?? null, const lookupId = tabId ?? activeTabId
[tabs, activeTabId] return tabs.find((x) => x.id === lookupId) ?? null
) }, [tabs, tabId, activeTabId])
const activeFolder = useMemo( const ownFolder = useMemo(
() => () =>
activeTab ownTab
? (allFolders.find((f) => f.id === activeTab.folderId) ?? null) ? (allFolders.find((f) => f.id === ownTab.folderId) ?? null)
: null, : null,
[activeTab, allFolders] [ownTab, allFolders]
) )
if (!activeTab || !activeFolder) return null if (!ownTab || !ownFolder) return null
const isNewConversation = activeTab.conversationId == null const isNewConversation = ownTab.conversationId == null
const currentBranch = const currentBranch =
branches.get(activeFolder.id) ?? activeFolder.git_branch ?? null branches.get(ownFolder.id) ?? ownFolder.git_branch ?? null
return ( return (
<TooltipProvider> <TooltipProvider>
<div className="flex items-center gap-1.5 h-9 px-3 border-b border-border/40 bg-muted/20 text-xs"> <div className="flex shrink-0 items-center gap-1.5 px-2 pt-2 text-xs text-muted-foreground">
<FolderPicker <FolderPicker
folders={allFolders} folders={allFolders}
currentFolderId={activeFolder.id} currentFolderId={ownFolder.id}
currentFolderName={activeFolder.name} currentFolderName={ownFolder.name}
editable={isNewConversation} editable={isNewConversation}
onSelect={async (folderId) => { onSelect={async (folderId) => {
const target = allFolders.find((f) => f.id === folderId) const target = allFolders.find((f) => f.id === folderId)
@@ -122,7 +111,7 @@ export const ConversationContextBar = memo(function ConversationContextBar() {
const detail = isOpen const detail = isOpen
? target ? target
: await addFolderToWorkspaceById(folderId) : await addFolderToWorkspaceById(folderId)
setTabFolder(activeTab.id, detail.id, detail.path) setTabFolder(ownTab.id, detail.id, detail.path)
toast.success(t("toasts.folderChanged", { name: detail.name })) toast.success(t("toasts.folderChanged", { name: detail.name }))
} catch (err) { } catch (err) {
console.error( console.error(
@@ -142,7 +131,7 @@ export const ConversationContextBar = memo(function ConversationContextBar() {
if (!result) return if (!result) return
const selected = Array.isArray(result) ? result[0] : result const selected = Array.isArray(result) ? result[0] : result
const detail = await openFolder(selected) const detail = await openFolder(selected)
setTabFolder(activeTab.id, detail.id, detail.path) setTabFolder(ownTab.id, detail.id, detail.path)
toast.success(t("toasts.folderChanged", { name: detail.name })) toast.success(t("toasts.folderChanged", { name: detail.name }))
} }
} catch (err) { } catch (err) {
@@ -155,20 +144,18 @@ export const ConversationContextBar = memo(function ConversationContextBar() {
labelSearch={t("searchFolder")} labelSearch={t("searchFolder")}
/> />
<Separator orientation="vertical" className="h-4" />
<BranchPicker <BranchPicker
folderId={activeFolder.id} folderId={ownFolder.id}
folderPath={activeFolder.path} folderPath={ownFolder.path}
currentBranch={currentBranch} currentBranch={currentBranch}
onCheckout={async (branchName) => { onCheckout={async (branchName) => {
const taskId = `checkout-${activeFolder.id}-${Date.now()}` const taskId = `checkout-${ownFolder.id}-${Date.now()}`
addTask(taskId, tBd("tasks.checkoutTo", { branchName })) addTask(taskId, tBd("tasks.checkoutTo", { branchName }))
updateTask(taskId, { status: "running" }) updateTask(taskId, { status: "running" })
try { try {
await gitCheckout(activeFolder.path, branchName) await gitCheckout(ownFolder.path, branchName)
setBranch(activeFolder.id, branchName) setBranch(ownFolder.id, branchName)
await refreshFolder(activeFolder.id) await refreshFolder(ownFolder.id)
updateTask(taskId, { status: "completed" }) updateTask(taskId, { status: "completed" })
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err) const msg = err instanceof Error ? err.message : String(err)
@@ -177,13 +164,13 @@ export const ConversationContextBar = memo(function ConversationContextBar() {
} }
}} }}
onNewBranch={async (branchName, startPoint) => { onNewBranch={async (branchName, startPoint) => {
const taskId = `new-branch-${activeFolder.id}-${Date.now()}` const taskId = `new-branch-${ownFolder.id}-${Date.now()}`
addTask(taskId, tBd("tasks.newBranch", { name: branchName })) addTask(taskId, tBd("tasks.newBranch", { name: branchName }))
updateTask(taskId, { status: "running" }) updateTask(taskId, { status: "running" })
try { try {
await gitNewBranch(activeFolder.path, branchName, startPoint) await gitNewBranch(ownFolder.path, branchName, startPoint)
setBranch(activeFolder.id, branchName) setBranch(ownFolder.id, branchName)
await refreshFolder(activeFolder.id) await refreshFolder(ownFolder.id)
updateTask(taskId, { status: "completed" }) updateTask(taskId, { status: "completed" })
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err) const msg = err instanceof Error ? err.message : String(err)
@@ -192,11 +179,11 @@ export const ConversationContextBar = memo(function ConversationContextBar() {
} }
}} }}
onDeleteBranch={async (branchName) => { onDeleteBranch={async (branchName) => {
const taskId = `delete-branch-${activeFolder.id}-${Date.now()}` const taskId = `delete-branch-${ownFolder.id}-${Date.now()}`
addTask(taskId, tBd("tasks.deleteBranch", { branchName })) addTask(taskId, tBd("tasks.deleteBranch", { branchName }))
updateTask(taskId, { status: "running" }) updateTask(taskId, { status: "running" })
try { try {
await gitDeleteBranch(activeFolder.path, branchName, false) await gitDeleteBranch(ownFolder.path, branchName, false)
updateTask(taskId, { status: "completed" }) updateTask(taskId, { status: "completed" })
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err) const msg = err instanceof Error ? err.message : String(err)
@@ -205,13 +192,6 @@ export const ConversationContextBar = memo(function ConversationContextBar() {
} }
}} }}
/> />
<div className="flex-1" />
<GitActionButtons
folderId={activeFolder.id}
currentBranch={currentBranch}
/>
</div> </div>
</TooltipProvider> </TooltipProvider>
) )
@@ -250,19 +230,16 @@ const FolderPicker = memo(function FolderPicker({
const trigger = ( const trigger = (
<Button <Button
variant="ghost" variant="outline"
size="sm" size="xs"
className={cn( className={cn(
"h-7 px-2 gap-1.5 font-normal", "min-w-0 bg-transparent",
!editable && "cursor-default hover:bg-transparent" !editable && "cursor-default opacity-60 hover:bg-transparent"
)} )}
disabled={!editable && false}
> >
<Folder className="h-3.5 w-3.5 text-muted-foreground" /> <Folder className="size-3 shrink-0 text-muted-foreground" />
<span className="max-w-[140px] truncate">{currentFolderName}</span> <span className="max-w-[140px] truncate">{currentFolderName}</span>
{editable && ( <ChevronsUpDown className="size-3 shrink-0 text-muted-foreground" />
<ChevronsUpDown className="h-3 w-3 text-muted-foreground opacity-60" />
)}
</Button> </Button>
) )
@@ -381,15 +358,15 @@ const BranchPicker = memo(function BranchPicker({
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="ghost" variant="outline"
size="sm" size="xs"
className="h-7 px-2 gap-1.5 font-normal" className="min-w-0 bg-transparent"
> >
<GitBranch className="h-3.5 w-3.5 text-muted-foreground" /> <GitBranch className="size-3 shrink-0 text-muted-foreground" />
<span className="max-w-[160px] truncate"> <span className="max-w-[160px] truncate">
{currentBranch ?? t("noBranch")} {currentBranch ?? t("noBranch")}
</span> </span>
<ChevronsUpDown className="h-3 w-3 text-muted-foreground opacity-60" /> <ChevronsUpDown className="size-3 shrink-0 text-muted-foreground" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="start" className="p-0 w-80"> <PopoverContent align="start" className="p-0 w-80">
@@ -517,124 +494,3 @@ const BranchPicker = memo(function BranchPicker({
</> </>
) )
}) })
// ============================================================================
// GitActionButtons
// ============================================================================
interface GitActionButtonsProps {
folderId: number
currentBranch: string | null
}
const GitActionButtons = memo(function GitActionButtons({
folderId,
currentBranch,
}: GitActionButtonsProps) {
const t = useTranslations("Folder.conversationContextBar")
const tBd = useTranslations("Folder.branchDropdown")
const handleCommit = useCallback(async () => {
try {
await openCommitWindow(folderId)
} catch (err) {
console.error("[GitActions] commit failed:", err)
toast.error(tBd("toasts.openCommitWindowFailed"))
}
}, [folderId, tBd])
const handlePush = useCallback(async () => {
try {
await openPushWindow(folderId)
} catch (err) {
console.error("[GitActions] push failed:", err)
toast.error(tBd("toasts.openPushWindowFailed"))
}
}, [folderId, tBd])
const handleStash = useCallback(async () => {
try {
await openStashWindow(folderId)
} catch (err) {
console.error("[GitActions] stash failed:", err)
toast.error(t("toasts.openStashFailed"))
}
}, [folderId, t])
const handleMerge = useCallback(async () => {
try {
await openMergeWindow(folderId, "merge")
} catch (err) {
console.error("[GitActions] merge failed:", err)
toast.error(t("toasts.openMergeFailed"))
}
}, [folderId, t])
return (
<div className="flex items-center gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 gap-1.5 font-normal"
onClick={handleCommit}
disabled={currentBranch == null}
>
<GitCommit className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{t("commit")}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{tBd("openCommitWindow")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 gap-1.5 font-normal"
onClick={handlePush}
disabled={currentBranch == null}
>
<Upload className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{t("push")}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{tBd("pushCode")}</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
disabled={currentBranch == null}
>
<MoreHorizontal className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={handleMerge}>
<GitMerge className="h-4 w-4" />
{t("merge")}
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleStash}>
<Archive className="h-4 w-4" />
{tBd("stashChanges")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={handleCommit}>
<GitCommit className="h-4 w-4" />
{tBd("openCommitWindow")}
</DropdownMenuItem>
<DropdownMenuItem onSelect={handlePush}>
<Upload className="h-4 w-4" />
{tBd("pushCode")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
})

View File

@@ -59,6 +59,7 @@ import {
type AttachFileToSessionDetail, type AttachFileToSessionDetail,
type AppendTextToSessionDetail, type AppendTextToSessionDetail,
} from "@/lib/session-attachment-events" } from "@/lib/session-attachment-events"
import { ConversationContextBar } from "@/components/chat/conversation-context-bar"
import { ModeSelector } from "@/components/chat/mode-selector" import { ModeSelector } from "@/components/chat/mode-selector"
import { SessionConfigSelector } from "@/components/chat/session-config-selector" import { SessionConfigSelector } from "@/components/chat/session-config-selector"
import { import {
@@ -1919,6 +1920,7 @@ export function MessageInput({
className className
)} )}
> >
<ConversationContextBar tabId={attachmentTabId} />
{(hasImageAttachments || hasResourceAttachments) && ( {(hasImageAttachments || hasResourceAttachments) && (
<div className="flex shrink-0 flex-col gap-1 px-2 pt-2"> <div className="flex shrink-0 flex-col gap-1 px-2 pt-2">
{hasImageAttachments && ( {hasImageAttachments && (