diff --git a/desktop/src/components/chat/AssistantMessage.tsx b/desktop/src/components/chat/AssistantMessage.tsx index 32647c45..06c2ac86 100644 --- a/desktop/src/components/chat/AssistantMessage.tsx +++ b/desktop/src/components/chat/AssistantMessage.tsx @@ -1,13 +1,14 @@ import { MarkdownRenderer } from '../markdown/MarkdownRenderer' -import { MessageActionBar } from './MessageActionBar' +import { MessageActionBar, type MessageBranchAction } from './MessageActionBar' import { InlineImageGallery } from './InlineImageGallery' type Props = { content: string isStreaming?: boolean + branchAction?: MessageBranchAction } -export function AssistantMessage({ content, isStreaming }: Props) { +export function AssistantMessage({ content, isStreaming, branchAction }: Props) { if (!content.trim()) return null const documentLayout = shouldUseDocumentLayout(content) @@ -36,6 +37,7 @@ export function AssistantMessage({ content, isStreaming }: Props) { diff --git a/desktop/src/components/chat/MessageActionBar.tsx b/desktop/src/components/chat/MessageActionBar.tsx index 3861bd40..993e0243 100644 --- a/desktop/src/components/chat/MessageActionBar.tsx +++ b/desktop/src/components/chat/MessageActionBar.tsx @@ -1,19 +1,28 @@ +import { GitFork } from 'lucide-react' import { CopyButton } from '../shared/CopyButton' +export type MessageBranchAction = { + label: string + loading?: boolean + onBranch: () => void +} + type Props = { copyText?: string copyLabel: string + branchAction?: MessageBranchAction align?: 'start' | 'end' } export function MessageActionBar({ copyText, copyLabel, + branchAction, align = 'start', }: Props) { const hasCopy = Boolean(copyText?.trim()) - if (!hasCopy) return null + if (!hasCopy && !branchAction) return null return (
- + {hasCopy ? ( + + ) : null} + {branchAction ? ( + + ) : null}
) diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index da2c1577..449d7629 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -2339,8 +2339,15 @@ describe('MessageList nested tool calls', () => { render() - const branchButtons = screen.getAllByRole('button', { name: 'Branch from here' }) + const branchButtons = screen.getAllByRole('button', { name: 'Fork a new conversation' }) expect(branchButtons).toHaveLength(2) + expect(branchButtons[0]!.closest('[data-message-actions]')).toBe( + screen.getByRole('button', { name: 'Copy prompt' }).closest('[data-message-actions]') + ) + expect(branchButtons[1]!.closest('[data-message-actions]')).toBe( + screen.getByRole('button', { name: 'Copy reply' }).closest('[data-message-actions]') + ) + expect(branchButtons[1]?.getAttribute('title')).toBe('Fork a new conversation') fireEvent.click(branchButtons[1]!) @@ -2358,7 +2365,7 @@ describe('MessageList nested tool calls', () => { const toasts = useUIStore.getState().toasts expect(toasts[toasts.length - 1]).toMatchObject({ type: 'success', - message: 'Created branched session "Branched session".', + message: 'Created forked conversation "Branched session".', }) }) @@ -2390,7 +2397,7 @@ describe('MessageList nested tool calls', () => { render() - expect(screen.queryByRole('button', { name: 'Branch from here' })).toBeNull() + expect(screen.queryByRole('button', { name: 'Fork a new conversation' })).toBeNull() }) it('keeps historical sessions readable when turn checkpoint payloads are missing', async () => { diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 35fda7ed..adb380f8 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1,5 +1,5 @@ import { useRef, useEffect, useMemo, memo, useState, useCallback, useLayoutEffect, type ReactNode } from 'react' -import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, CircleStop, GitBranch, LoaderCircle, MessageCircle, Settings, Target, XCircle } from 'lucide-react' +import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, CircleStop, LoaderCircle, MessageCircle, Settings, Target, XCircle } from 'lucide-react' import { ApiError } from '../../api/client' import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions' import { useChatStore } from '../../stores/chatStore' @@ -359,37 +359,6 @@ function SelectableChatMessage({ ) } -function MessageBranchAction({ - align, - label, - loading, - onClick, -}: { - align: ChatMessageRole - label: string - loading?: boolean - onClick: () => void -}) { - return ( -
- -
- ) -} - function appendChildToolCall( childToolCallsByParent: Map, parentToolUseId: string, @@ -1263,20 +1232,11 @@ export const MessageBlock = memo(function MessageBlock({ role="user" content={message.content} > -
- - {branchAction ? ( - - ) : null} -
+ ) case 'assistant_text': @@ -1287,17 +1247,7 @@ export const MessageBlock = memo(function MessageBlock({ role="assistant" content={message.content} > -
- - {branchAction ? ( - - ) : null} -
+ ) case 'thinking': diff --git a/desktop/src/components/chat/UserMessage.tsx b/desktop/src/components/chat/UserMessage.tsx index d66dadea..7773e560 100644 --- a/desktop/src/components/chat/UserMessage.tsx +++ b/desktop/src/components/chat/UserMessage.tsx @@ -1,13 +1,14 @@ import type { UIAttachment } from '../../types/chat' import { AttachmentGallery } from './AttachmentGallery' -import { MessageActionBar } from './MessageActionBar' +import { MessageActionBar, type MessageBranchAction } from './MessageActionBar' type Props = { content: string attachments?: UIAttachment[] + branchAction?: MessageBranchAction } -export function UserMessage({ content, attachments }: Props) { +export function UserMessage({ content, attachments, branchAction }: Props) { const hasText = content.trim().length > 0 return ( @@ -37,6 +38,7 @@ export function UserMessage({ content, attachments }: Props) { )} diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 43466957..eb60e1fd 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -952,8 +952,8 @@ export const en = { 'chat.workspaceReferencesOnly': 'Added {count} workspace references', 'chat.contextReferencesOnly': 'Added {count} references', 'chat.addSelectionToChat': 'Add to chat', - 'chat.branchFromHere': 'Branch from here', - 'chat.branchSuccess': 'Created branched session "{title}".', + 'chat.branchFromHere': 'Fork a new conversation', + 'chat.branchSuccess': 'Created forked conversation "{title}".', 'chat.branchError': 'Failed to branch from this message. Detail: {detail}', 'chat.userMessageReference': 'User message', 'chat.assistantMessageReference': 'Assistant message', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index db472bb9..61cab2ad 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -954,9 +954,9 @@ export const zh: Record = { 'chat.workspaceReferencesOnly': '已添加 {count} 个工作区引用', 'chat.contextReferencesOnly': '已添加 {count} 个引用', 'chat.addSelectionToChat': '添加到对话', - 'chat.branchFromHere': '从这里派生', - 'chat.branchSuccess': '已创建派生会话“{title}”。', - 'chat.branchError': '从该消息派生失败。详情:{detail}', + 'chat.branchFromHere': 'Fork 一个新对话', + 'chat.branchSuccess': '已 Fork 新对话“{title}”。', + 'chat.branchError': '从该消息 Fork 新对话失败。详情:{detail}', 'chat.userMessageReference': '用户消息', 'chat.assistantMessageReference': 'AI 回复', 'chat.slashCommands': '斜杠命令',