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': '斜杠命令',