From e1685ad41a0e9f581377a223a96ac1a7538cdae1 Mon Sep 17 00:00:00 2001 From: shanyuhai123 <864299347@qq.com> Date: Tue, 27 Oct 2026 14:15:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(buddy):=20=E4=B8=BA=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E8=A1=A5=E5=85=85=E5=88=86=E6=94=AF=E8=83=BD?= =?UTF-8?q?=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/conversation-canvas.e2e.mjs | 2 + apps/buddy/src/i18n/locales/en-US/chat.ts | 12 +- apps/buddy/src/i18n/locales/zh-CN/chat.ts | 12 +- .../model/canvas/conversationCanvasLayout.ts | 5 +- .../__tests__/chatMessageActions.spec.ts | 117 ++++++++++++++++++ .../__tests__/chatMessageBranches.spec.ts | 4 +- .../model/transcript/chatMessageActions.ts | 44 +++++++ .../canvas/ConversationMessageNode.vue | 38 ++++-- .../canvas/DesktopConversationCanvas.vue | 5 +- .../__tests__/ConversationMessageNode.spec.ts | 5 +- .../transcript/BuddyChatActionToolbar.vue | 36 +++++- .../widgets/transcript/BuddyChatAgentTurn.vue | 7 ++ .../transcript/BuddyChatMessageList.vue | 24 +++- .../transcript/BuddyChatMessageRow.vue | 18 ++- .../transcript/BuddyChatTurnResult.vue | 7 +- .../transcript/DesktopChatTranscript.vue | 10 ++ .../workspace/DesktopChatWorkspace.vue | 75 +++++++++-- .../workspace/DesktopFollowupContext.vue | 38 ++++++ .../shared/ui/icon/assets/message-branch.svg | 6 + apps/buddy/src/shared/ui/icon/desktopIcons.ts | 2 + 20 files changed, 421 insertions(+), 46 deletions(-) create mode 100644 apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageActions.spec.ts create mode 100644 apps/buddy/src/modules/tasks/widgets/workspace/DesktopFollowupContext.vue create mode 100644 apps/buddy/src/shared/ui/icon/assets/message-branch.svg diff --git a/.playwright/scripts/__tests__/conversation-canvas.e2e.mjs b/.playwright/scripts/__tests__/conversation-canvas.e2e.mjs index 469a6c043..6a6ec5636 100644 --- a/.playwright/scripts/__tests__/conversation-canvas.e2e.mjs +++ b/.playwright/scripts/__tests__/conversation-canvas.e2e.mjs @@ -4,6 +4,7 @@ import { expect, test } from '../fixtures/electron.mjs' async function prepare(buddy, label) { const instance = await buddy.createInstance(label) const desktop = await instance.launch() + await expect.poll(async () => (await desktop.page.evaluate(() => window.lexoraDesktop.localChat.runtime.getStatus())).status).toBe('ready') await desktop.app.evaluate(({ BrowserWindow, ipcMain }, databasePath) => { BrowserWindow.getAllWindows()[0].setSize(2000, 1100) const { DatabaseSync } = process.getBuiltinModule('node:sqlite') @@ -72,6 +73,7 @@ test('canvas actions, zoom and node lifetimes stay scoped across split panes and await first.getByTestId('conversation-canvas-toggle').click() await expect(first.locator('.conversation-node')).toHaveCount(1) await first.getByTestId('pane-layout-menu').click() + await page.locator('.n-dropdown-menu:visible').getByText('分屏', { exact: true }).hover() await page.locator('.n-dropdown-menu:visible').getByText('向右分屏', { exact: true }).click() await task(page, 'b').click() const second = pane(page, 'Canvas B') diff --git a/apps/buddy/src/i18n/locales/en-US/chat.ts b/apps/buddy/src/i18n/locales/en-US/chat.ts index 3a64e0c2e..27b9f84a9 100644 --- a/apps/buddy/src/i18n/locales/en-US/chat.ts +++ b/apps/buddy/src/i18n/locales/en-US/chat.ts @@ -63,7 +63,6 @@ export default { 'desktop.canvas.failed': 'Failed', 'desktop.canvas.cancelled': 'Stopped', 'desktop.canvas.completed': 'Completed', - 'desktop.canvas.followup': 'Follow up on this answer', 'desktop.canvas.retry': 'Regenerate answer', 'desktop.canvas.writeInComposer': 'Continue in the composer', 'desktop.canvas.generating': 'Generating an answer…', @@ -77,8 +76,6 @@ export default { 'desktop.canvas.resetZoom': 'Reset zoom to 100%', 'desktop.canvas.loading': 'Loading canvas…', 'desktop.canvas.reload': 'Reload', - 'desktop.canvas.composerTarget': 'Following up on this answer', - 'desktop.canvas.cancelFollowup': 'Back to current branch', 'activity.status.completed': 'Completed', 'activity.status.failed': 'Failed', 'activity.status.running': 'Running', @@ -318,6 +315,15 @@ export default { 'desktop.chat.runtimeRestart': 'Restart', 'desktop.chat.runtimeStarting': 'Starting local runtime', 'desktop.chat.regenerate': 'Regenerate', + 'desktop.chat.followup': 'Branch from here', + 'desktop.chat.followupTarget': 'Branch from this answer', + 'desktop.chat.followupHint': 'Use the conversation up to this answer only. Sending creates a new branch and keeps the original conversation.', + 'desktop.chat.followupSource': 'Selected answer', + 'desktop.chat.locateFollowup': 'View origin', + 'desktop.chat.cancelFollowup': 'Cancel branch', + 'desktop.chat.cancelFollowupHint': 'Return to the current branch and keep this follow-up draft.', + 'desktop.chat.branchPosition': 'Branch {index} / {count}', + 'desktop.chat.branchNavigationHint': 'Switch the conversation after this point. Other branches are kept.', 'desktop.chat.nextBranch': 'Next branch', 'desktop.chat.previousBranch': 'Previous branch', 'desktop.chat.editingHistoryMessage': 'Editing a historical message', diff --git a/apps/buddy/src/i18n/locales/zh-CN/chat.ts b/apps/buddy/src/i18n/locales/zh-CN/chat.ts index ed5c334d2..09d86135a 100644 --- a/apps/buddy/src/i18n/locales/zh-CN/chat.ts +++ b/apps/buddy/src/i18n/locales/zh-CN/chat.ts @@ -61,7 +61,6 @@ export default { 'desktop.canvas.failed': '失败', 'desktop.canvas.cancelled': '已停止', 'desktop.canvas.completed': '已完成', - 'desktop.canvas.followup': '从这条回答追问', 'desktop.canvas.retry': '重新生成回答', 'desktop.canvas.writeInComposer': '在输入框继续提问', 'desktop.canvas.generating': '正在生成回答…', @@ -75,8 +74,6 @@ export default { 'desktop.canvas.resetZoom': '恢复 100% 缩放', 'desktop.canvas.loading': '正在读取画布…', 'desktop.canvas.reload': '重新加载', - 'desktop.canvas.composerTarget': '正在这条回答后追问', - 'desktop.canvas.cancelFollowup': '返回当前分支', 'activity.status.completed': '完成', 'activity.status.failed': '失败', 'activity.status.running': '进行中', @@ -316,6 +313,15 @@ export default { 'desktop.chat.runtimeRestart': '重新启动', 'desktop.chat.runtimeStarting': '正在启动本地运行时', 'desktop.chat.regenerate': '重新生成', + 'desktop.chat.followup': '从这里分支', + 'desktop.chat.followupTarget': '从这条回答创建分支', + 'desktop.chat.followupHint': '只沿用到这条回答为止的对话。发送后创建新分支,原对话保留。', + 'desktop.chat.followupSource': '所选回答', + 'desktop.chat.locateFollowup': '查看起点', + 'desktop.chat.cancelFollowup': '取消分支', + 'desktop.chat.cancelFollowupHint': '返回当前分支,保留这份追问草稿。', + 'desktop.chat.branchPosition': '分支 {index} / {count}', + 'desktop.chat.branchNavigationHint': '切换此处之后的对话,其他分支仍会保留。', 'desktop.chat.nextBranch': '下一条分支', 'desktop.chat.previousBranch': '上一条分支', 'desktop.chat.editingHistoryMessage': '正在编辑历史消息', diff --git a/apps/buddy/src/modules/tasks/model/canvas/conversationCanvasLayout.ts b/apps/buddy/src/modules/tasks/model/canvas/conversationCanvasLayout.ts index 619376301..708f499e6 100644 --- a/apps/buddy/src/modules/tasks/model/canvas/conversationCanvasLayout.ts +++ b/apps/buddy/src/modules/tasks/model/canvas/conversationCanvasLayout.ts @@ -1,7 +1,10 @@ import type { LocalConversationTreeNode } from '@buddy-shared/conversation/conversationTree' export type ConversationCanvasDirection = 'horizontal' | 'vertical' -export type ConversationCanvasNode = Omit & { kind: LocalConversationTreeNode['kind'] | 'draft' } +export type ConversationCanvasNode = Omit & { + kind: LocalConversationTreeNode['kind'] | 'draft' + canFollowup?: boolean +} export interface ConversationNodePosition { x: number, y: number, width: number, height: number } export function layoutConversationCanvas(nodes: readonly ConversationCanvasNode[], direction: ConversationCanvasDirection): ReadonlyMap { diff --git a/apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageActions.spec.ts b/apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageActions.spec.ts new file mode 100644 index 000000000..875607645 --- /dev/null +++ b/apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageActions.spec.ts @@ -0,0 +1,117 @@ +import type { ChatTranscriptMessageRow, ChatTranscriptTurnResultRow } from '../chatTranscriptTypes' +import { describe, expect, it } from 'vitest' +import { getChatMessageFollowupSourceId, projectChatAgentTurnActions, projectChatFollowupMessageIds, projectChatMessageActions } from '../chatMessageActions' + +function reply(id: string, runId: string, patch: Partial = {}): ChatTranscriptMessageRow { + return { + key: id, + kind: 'message', + message: { + id, + runId, + role: 'assistant', + branchId: 'branch', + conversationId: 'conversation', + attachments: [], + content: { text: id }, + createdAt: '2026-09-08T00:00:00.000Z', + }, + turnOutputs: null, + ...patch, + } +} + +function question(id: string): ChatTranscriptMessageRow { + const row = reply(id, 'unused') + return { ...row, message: { ...row.message, role: 'user', runId: null } } +} + +function result(runId: string, finalMessageId: string): ChatTranscriptTurnResultRow { + return { + key: `result-${runId}`, + kind: 'turn-result', + turn: { + branchId: 'branch', + completedAt: '2026-09-08T00:00:01.000Z', + finalMessageId, + nodes: [], + processMessageIds: [], + progress: null, + reasoningLevel: null, + runId, + startedAt: '2026-09-08T00:00:00.000Z', + status: 'completed', + triggeringMessageId: 'question', + usage: null, + }, + } +} + +describe('chat turn branch origins', () => { + it('offers history branching only after another public message, not for the current turn or trailing result metadata', () => { + const outcome = result('root', 'final') + const source = reply('final', 'root', { turnResult: outcome.turn }) + const completed = new Set(['root']) + expect([...projectChatFollowupMessageIds([source], completed)]).toEqual([]) + expect([...projectChatFollowupMessageIds([reply('final', 'root'), outcome], completed)]).toEqual([]) + expect([...projectChatFollowupMessageIds([source, question('next')], completed)]).toEqual(['final']) + expect(projectChatAgentTurnActions(outcome.turn, false, true).showFollowup).toBe(false) + expect(projectChatAgentTurnActions(outcome.turn, false, true, true).showFollowup).toBe(true) + }) + + it('keeps branching at the root result even when the toolbar is presented on a partner reply', () => { + const firstPartner = reply('first-partner-reply', 'first-partner') + const finalPartner = reply('last-partner-reply', 'last-partner', { turnResult: result('root', 'root-final').turn }) + const completed = new Set(['root', 'first-partner', 'last-partner']) + expect([...projectChatFollowupMessageIds([firstPartner, finalPartner], completed)]).toEqual([]) + expect([...projectChatFollowupMessageIds([firstPartner, finalPartner, question('next')], completed)]).toEqual(['root-final']) + expect(getChatMessageFollowupSourceId(firstPartner)).toBeNull() + expect(getChatMessageFollowupSourceId(finalPartner)).toBe('root-final') + expect(projectChatMessageActions(firstPartner.message, false, false, true)).toMatchObject({ + showCopy: true, + showTime: true, + showRegenerate: false, + showFollowup: false, + }) + expect(projectChatMessageActions(finalPartner.message, false, true, true)).toMatchObject({ + showRegenerate: true, + showFollowup: true, + }) + }) + + it('keeps independent root turns separate while ignoring completed partner replies', () => { + const rows = [ + reply('root-final', 'root', { turnResult: result('root', 'root-final').turn }), + reply('partner-final', 'partner'), + question('second-question'), + reply('second-final', 'second', { turnResult: result('second', 'second-final').turn }), + question('next'), + ] + expect([...projectChatFollowupMessageIds(rows, new Set(['root', 'second', 'partner']))]).toEqual(['root-final', 'second-final']) + }) + + it('preserves a historical completed result without a visible body, using its own final message as the source', () => { + const outcome = result('root', 'empty-root-final') + const partner = reply('partner-final', 'partner') + expect([...projectChatFollowupMessageIds([partner, outcome], new Set(['root', 'partner']))]).toEqual([]) + expect([...projectChatFollowupMessageIds([partner, outcome, question('next')], new Set(['root', 'partner']))]).toEqual(['empty-root-final']) + }) + + it.each([ + { streaming: true as const }, + { isIntermediate: true as const }, + { showActions: false as const }, + ])('does not branch from an unfinished or hidden result: %j', (patch) => { + expect([...projectChatFollowupMessageIds([ + reply('last', 'root', { turnResult: result('root', 'last').turn, ...patch }), + question('next'), + ], new Set(['root']))]).toEqual([]) + }) + + it('keeps turn controls disabled during mutation and excludes messages without an execution', () => { + const message = reply('final', 'root').message + expect(projectChatMessageActions(message, true, true, true)).toMatchObject({ disabled: true, showFollowup: true }) + expect(projectChatMessageActions({ ...message, runId: null }, false, true, true).showFollowup).toBe(false) + expect(projectChatMessageActions({ ...message, role: 'user' }, false, true, true).showFollowup).toBe(false) + }) +}) diff --git a/apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageBranches.spec.ts b/apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageBranches.spec.ts index 2bfb9fed1..9716fd456 100644 --- a/apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageBranches.spec.ts +++ b/apps/buddy/src/modules/tasks/model/transcript/__tests__/chatMessageBranches.spec.ts @@ -134,8 +134,8 @@ describe('projectChatMessageBranchNavigators', () => { nextBranchId: 'branch-retry', previousBranchId: null, }) - expect(projectChatAgentTurnActions(turn, false, true)).toEqual({ disabled: false, showCopy: status !== 'completed', showEdit: false, showRegenerate: true, showTime: true }) - expect(projectChatAgentTurnActions(turn, false, false)).toEqual({ disabled: false, showCopy: false, showEdit: false, showRegenerate: false, showTime: false }) + expect(projectChatAgentTurnActions(turn, false, true, true)).toEqual({ disabled: false, showCopy: status !== 'completed', showEdit: false, showFollowup: status === 'completed', showRegenerate: true, showTime: true }) + expect(projectChatAgentTurnActions(turn, false, false)).toEqual({ disabled: false, showCopy: false, showEdit: false, showFollowup: false, showRegenerate: false, showTime: false }) }) it('attaches edited user-input siblings to the edited user message', () => { diff --git a/apps/buddy/src/modules/tasks/model/transcript/chatMessageActions.ts b/apps/buddy/src/modules/tasks/model/transcript/chatMessageActions.ts index c55875d21..e787b2161 100644 --- a/apps/buddy/src/modules/tasks/model/transcript/chatMessageActions.ts +++ b/apps/buddy/src/modules/tasks/model/transcript/chatMessageActions.ts @@ -1,25 +1,67 @@ import type { LocalMessage } from '@buddy-shared/conversation/conversationApi' import type { ChatAgentTurn } from './chatAgentTurn' +import type { ChatTranscriptMessageRow, ChatTranscriptRow } from './chatTranscriptTypes' export interface ChatMessageActions { disabled: boolean showCopy: boolean showEdit: boolean + showFollowup: boolean showRegenerate: boolean showTime: boolean } +export function getChatMessageFollowupSourceId(row: Pick & { ownsResultActions?: boolean }): string | null { + return row.turnResult?.finalMessageId ?? (row.ownsResultActions ? row.message.id : null) +} + +export function projectChatFollowupMessageIds( + rows: readonly ChatTranscriptRow[], + completedRunIds: ReadonlySet, +): ReadonlySet { + const finalMessages = new Map() + let lastMessageIndex = -1 + for (const [index, row] of rows.entries()) { + if (row.kind === 'message' && (row.message.role === 'user' || row.message.role === 'assistant')) { + lastMessageIndex = index + const messageId = getChatMessageFollowupSourceId(row) + const runId = row.turnResult?.runId ?? row.message.runId + if (row.message.role === 'assistant' && messageId && runId) { + finalMessages.set(runId, { + messageId, + index, + available: !row.streaming && !row.isIntermediate && row.showActions !== false, + }) + } + } + else if ((row.kind === 'turn-result' || (row.kind === 'agent-turn' && row.ownsResultActions)) && row.turn.finalMessageId) { + finalMessages.set(row.turn.runId, { + messageId: row.turn.finalMessageId, + index, + available: row.turn.status === 'completed', + }) + } + } + return new Set([...finalMessages].flatMap(([runId, source]) => + completedRunIds.has(runId) && source.available && source.index < lastMessageIndex + ? [source.messageId] + : [], + )) +} + export function projectChatMessageActions( message: LocalMessage, disabled: boolean, ownsResultActions: boolean, + canFollowup = false, ): ChatMessageActions { const lifecycleMessage = message.role === 'user' || message.role === 'assistant' return { disabled, showCopy: lifecycleMessage, showEdit: message.role === 'user', + showFollowup: ownsResultActions && canFollowup && message.role === 'assistant' && message.runId !== null, showRegenerate: ownsResultActions && message.role === 'assistant' && message.runId !== null, showTime: lifecycleMessage, } @@ -29,6 +71,7 @@ export function projectChatAgentTurnActions( turn: ChatAgentTurn, disabled: boolean, ownsResultActions: boolean, + canFollowup = false, ): ChatMessageActions { const showActions = ownsResultActions && (turn.status === 'completed' || turn.status === 'failed' || turn.status === 'cancelled') @@ -36,6 +79,7 @@ export function projectChatAgentTurnActions( disabled, showCopy: showActions && turn.status !== 'completed', showEdit: false, + showFollowup: canFollowup && showActions && turn.status === 'completed' && turn.finalMessageId !== null, showRegenerate: showActions, showTime: showActions, } diff --git a/apps/buddy/src/modules/tasks/widgets/canvas/ConversationMessageNode.vue b/apps/buddy/src/modules/tasks/widgets/canvas/ConversationMessageNode.vue index b3a4e8873..4060cde69 100644 --- a/apps/buddy/src/modules/tasks/widgets/canvas/ConversationMessageNode.vue +++ b/apps/buddy/src/modules/tasks/widgets/canvas/ConversationMessageNode.vue @@ -1,8 +1,9 @@ diff --git a/apps/buddy/src/modules/tasks/widgets/transcript/DesktopChatTranscript.vue b/apps/buddy/src/modules/tasks/widgets/transcript/DesktopChatTranscript.vue index 950412a14..4ef44f368 100644 --- a/apps/buddy/src/modules/tasks/widgets/transcript/DesktopChatTranscript.vue +++ b/apps/buddy/src/modules/tasks/widgets/transcript/DesktopChatTranscript.vue @@ -12,6 +12,7 @@ import type { ChatMessageScrollMetrics, } from '@/modules/tasks/widgets/transcript/chatMessageViewport' import { computed, useTemplateRef } from 'vue' +import { projectChatFollowupMessageIds } from '@/modules/tasks/model/transcript/chatMessageActions' import { createChatMessageBranchNavigatorProjector } from '@/modules/tasks/model/transcript/chatMessageBranches' import { createChatTranscriptDisplayRowProjector } from '@/modules/tasks/model/transcript/chatMessageTime' import { createChatRunTranscriptProjector } from '@/modules/tasks/model/transcript/chatRunTranscriptProjector' @@ -28,6 +29,7 @@ const props = defineProps<{ changeSets: ReadonlyArray conversationId: string editingMessageId: string | null + followupMessageId?: string | null hasOlderMessages: boolean isLoadingOlderMessages: boolean isRestoringPosition?: boolean @@ -50,6 +52,7 @@ const emit = defineEmits<{ readerLayoutIntent: [] readerScrollIntent: [] regenerateAssistant: [sourceRunId: string] + followupAssistant: [assistantMessageId: string, branchId: string] returnToLatest: [] selectOutlineMessage: [messageId: string] scroll: [metrics: ChatMessageScrollMetrics, options?: { userInitiated?: boolean }] @@ -77,6 +80,10 @@ const transcriptProjection = computed(() => transcriptProjector.project({ timelineItems: props.timelineItems, })) const displayRows = computed(() => displayRowProjector.project(transcriptProjection.value)) +const followupMessageIds = computed(() => projectChatFollowupMessageIds( + transcriptProjection.value.rows, + new Set(props.runs.filter(run => run.status === 'completed').map(run => run.id)), +)) const branchNavigators = computed(() => branchNavigatorProjector.project( transcriptProjection.value, props.branches, @@ -144,6 +151,8 @@ defineExpose({ :conversation-id="conversationId" :display-rows="displayRows" :editing-message-id="editingMessageId" + :followup-message-id="followupMessageId" + :followup-message-ids="followupMessageIds" :has-older-messages="hasOlderMessages" :is-loading-older-messages="isLoadingOlderMessages" :is-restoring-position="isRestoringPosition" @@ -161,6 +170,7 @@ defineExpose({ @prepare-outline="prepareOutline" @reader-layout-intent="emit('readerLayoutIntent')" @regenerate-assistant="emit('regenerateAssistant', $event)" + @followup-assistant="(messageId, branchId) => emit('followupAssistant', messageId, branchId)" @reader-scroll-intent="emit('readerScrollIntent')" @return-to-latest="emit('returnToLatest')" @select-outline-message="emit('selectOutlineMessage', $event)" diff --git a/apps/buddy/src/modules/tasks/widgets/workspace/DesktopChatWorkspace.vue b/apps/buddy/src/modules/tasks/widgets/workspace/DesktopChatWorkspace.vue index 5bb6854ea..16f23160c 100644 --- a/apps/buddy/src/modules/tasks/widgets/workspace/DesktopChatWorkspace.vue +++ b/apps/buddy/src/modules/tasks/widgets/workspace/DesktopChatWorkspace.vue @@ -1,13 +1,16 @@ + + diff --git a/apps/buddy/src/shared/ui/icon/assets/message-branch.svg b/apps/buddy/src/shared/ui/icon/assets/message-branch.svg new file mode 100644 index 000000000..2e2c5d9fd --- /dev/null +++ b/apps/buddy/src/shared/ui/icon/assets/message-branch.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/buddy/src/shared/ui/icon/desktopIcons.ts b/apps/buddy/src/shared/ui/icon/desktopIcons.ts index ec3d60528..9643e86f3 100644 --- a/apps/buddy/src/shared/ui/icon/desktopIcons.ts +++ b/apps/buddy/src/shared/ui/icon/desktopIcons.ts @@ -7,6 +7,7 @@ import editFilledUrl from './assets/edit-filled.svg' import editUrl from './assets/edit.svg' import messageBranchNextUrl from './assets/message-branch-next.svg' import messageBranchPreviousUrl from './assets/message-branch-previous.svg' +import messageBranchUrl from './assets/message-branch.svg' import messageCopiedUrl from './assets/message-copied.svg' import messageCopyUrl from './assets/message-copy.svg' import messageQueueUrl from './assets/message-queue.svg' @@ -36,6 +37,7 @@ export const DESKTOP_ICON_URLS = { delete: deleteUrl, edit: editUrl, editFilled: editFilledUrl, + messageBranch: messageBranchUrl, messageBranchNext: messageBranchNextUrl, messageBranchPrevious: messageBranchPreviousUrl, messageCopied: messageCopiedUrl,