diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 3d9b7a51..0b91c32a 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -15,6 +15,7 @@ import { useInlineCommentDraftStore, type InlineCommentDraft } from '@/stores/us import { useSnippetsStore } from '@/stores/useSnippetsStore'; import { appendInlineComments } from '@/lib/messages/inlineComments'; import { renderMagicPrompt } from '@/lib/magicPrompts'; +import { startReviewFlow } from '@/lib/reviewFlow'; import { AttachedFilesList, AttachedVSCodeFileChips, ActiveEditorFileSuggestion } from './FileAttachment'; import ToolOutputDialog from './message/ToolOutputDialog'; import type { ToolPopupContent } from './message/types'; @@ -1106,10 +1107,11 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const names = new Set([ 'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'summary', 'workspace-review', 'plan-feature', 'catch-up', 'debug', 'weigh', 'explore', ]); + if (!isMobile && !isVSCodeRuntime()) names.add('handoff-review'); for (const command of availableCommands) names.add(command.name.toLowerCase()); for (const skill of availableSkills) names.add(skill.name.toLowerCase()); return names; - }, [availableCommands, availableSkills]); + }, [availableCommands, availableSkills, isMobile]); // /command and /skill spans (primary color). Only tokens that match a known // command/skill name are highlighted — partial/unknown tokens stay plain. @@ -1937,6 +1939,27 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } return; } + else if (commandName === 'handoff-review' && currentSessionId && !isMobile && !isVSCodeRuntime()) { + try { + const directory = useSessionUIStore.getState().getDirectoryForSession(currentSessionId) || currentDirectory || ''; + if (!directory) { + throw new Error('Session directory is unavailable'); + } + await startReviewFlow({ + originalSessionID: currentSessionId, + directory, + providerID: providerIdToSend, + modelID: modelIdToSend, + agent: agentNameToSend, + variant: variantToSend, + agentMentionName, + }); + scrollToBottom?.(); + } catch (error) { + console.error('[review-flow] failed to start review flow', error); + } + return; + } else if (commandName === 'plan-feature' && (currentSessionId || newSessionDraftOpen)) { try { await sessionActions.waitForConnectionOrThrow(); diff --git a/packages/ui/src/components/chat/CommandAutocomplete.tsx b/packages/ui/src/components/chat/CommandAutocomplete.tsx index eeed9c21..7985563b 100644 --- a/packages/ui/src/components/chat/CommandAutocomplete.tsx +++ b/packages/ui/src/components/chat/CommandAutocomplete.tsx @@ -7,6 +7,8 @@ import { useSkillsStore } from '@/stores/useSkillsStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; +import { useUIStore } from '@/stores/useUIStore'; +import { isVSCodeRuntime } from '@/lib/desktop'; type CommandSource = 'openchamber' | 'opencode' | 'skill'; @@ -65,6 +67,8 @@ export const CommandAutocomplete = React.forwardRef Boolean(state.newSessionDraft?.open)); const canStartSessionCommand = hasSession || hasNewSessionDraft; + const isMobile = useUIStore((state) => state.isMobile); + const canUseReviewHandoffFlow = hasSession && !isMobile && !isVSCodeRuntime(); const [commands, setCommands] = React.useState([]); const [loading, setLoading] = React.useState(false); @@ -152,6 +156,10 @@ export const CommandAutocomplete = React.forwardRef { setSelectedIndex(0); diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index 79ae9f6c..1eeb8659 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -48,6 +48,12 @@ import { useI18n } from '@/lib/i18n'; import { extractLoopbackUrls } from '@/lib/url'; import { useDeviceInfo } from '@/lib/device'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; +import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; +import { + getReviewTransferDirection, + sendImplementationResponseToReviewer, + sendReviewFeedbackToOriginal, +} from '@/lib/reviewFlow'; const CONTAIN_LAYOUT_STYLE = { contain: 'layout' as const, transform: 'translateZ(0)' }; @@ -641,6 +647,11 @@ interface AssistantMessageActionButtonsProps { hasCopyableText: boolean; isTouchContext: boolean; onCopyMessage?: () => void | boolean | Promise; + reviewTransferAction?: { + ariaLabel: string; + tooltip: string; + onClick: () => void | Promise; + }; onShareImage: (sourceElement?: HTMLElement | null) => Promise; ttsText: string; } @@ -649,6 +660,7 @@ const AssistantMessageActionButtons = React.memo(({ hasCopyableText, isTouchContext, onCopyMessage, + reviewTransferAction, onShareImage, ttsText, }: AssistantMessageActionButtonsProps) => { @@ -660,6 +672,7 @@ const AssistantMessageActionButtons = React.memo(({ const [copyHintVisible, setCopyHintVisible] = React.useState(false); const [isMessageCopied, setIsMessageCopied] = React.useState(false); const [isSharing, setIsSharing] = React.useState(false); + const [isTransferringReview, setIsTransferringReview] = React.useState(false); const copyHintTimeoutRef = React.useRef(null); const copiedResetTimeoutRef = React.useRef(null); const canCopyMessage = Boolean(onCopyMessage); @@ -758,6 +771,21 @@ const AssistantMessageActionButtons = React.memo(({ [hasCopyableText, isSharing, onShareImage] ); + const handleReviewTransferClick = React.useCallback( + async (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + if (!reviewTransferAction || isTransferringReview || !hasCopyableText) return; + setIsTransferringReview(true); + try { + await reviewTransferAction.onClick(); + } finally { + setIsTransferringReview(false); + } + }, + [hasCopyableText, isTransferringReview, reviewTransferAction] + ); + const readAloudTooltip = React.useMemo(() => { if (isTTSPlaying) { return t('chat.messageBody.tts.stopSpeaking'); @@ -831,6 +859,34 @@ const AssistantMessageActionButtons = React.memo(({ {t('chat.messageBody.actions.copyAnswer')} )} + {reviewTransferAction && chatSurfaceMode !== 'mini-chat' ? ( + + + + + {reviewTransferAction.tooltip} + + ) : null} {chatSurfaceMode !== 'mini-chat' ?