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:
@@ -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 &&
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user