From 5ffbcd1450d30a036125bb6febcad6bc938fb3cb Mon Sep 17 00:00:00 2001 From: chr1syy Date: Tue, 7 Apr 2026 09:14:46 +0200 Subject: [PATCH 1/5] MAESTRO: feat: add fork conversation from any point in AI message history Add "Fork conversation from here" action to AI response and user messages in the chat view. Clicking the GitFork icon creates a new session with conversation history truncated at the selected message, formats context, and auto-spawns the agent with the forked context. Follows the existing "Send Context to Agent" pattern from useMergeTransferHandlers.ts. Creates a new session via createMergedSession() with source session config (custom model, SSH, env vars) carried over. Files changed: - New: src/renderer/hooks/agent/useForkConversation.ts (hook) - Modified: TerminalOutput.tsx (fork button on ai/user messages) - Modified: MainPanel types/content/component (prop threading) - Modified: useMainPanelProps.ts (deps wiring) - Modified: App.tsx (hook instantiation and dep passing) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/renderer/App.tsx | 10 + .../components/MainPanel/MainPanel.tsx | 1 + .../components/MainPanel/MainPanelContent.tsx | 3 + src/renderer/components/MainPanel/types.ts | 1 + src/renderer/components/TerminalOutput.tsx | 21 +- src/renderer/hooks/agent/index.ts | 3 + .../hooks/agent/useForkConversation.ts | 235 ++++++++++++++++++ src/renderer/hooks/props/useMainPanelProps.ts | 3 + 8 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 src/renderer/hooks/agent/useForkConversation.ts diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index e790aa80f..eb3f5d2a4 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -57,6 +57,7 @@ import { useAgentExecution, useAgentCapabilities, useMergeTransferHandlers, + useForkConversation, useSummarizeAndContinue, // Git useFileTreeManagement, @@ -1082,6 +1083,14 @@ function MaestroConsoleInner() { setActiveSessionId, }); + // Fork conversation hook - creates a new session from a point in conversation history + const handleForkConversation = useForkConversation( + sessions, + setSessions, + activeSessionId, + setActiveSessionId + ); + // Summarize & Continue hook for context compaction (non-blocking, per-tab) const { summarizeState, @@ -2277,6 +2286,7 @@ function MaestroConsoleInner() { handleMainPanelInputBlur, handleOpenPromptComposer, handleReplayMessage, + handleForkConversation, handleMainPanelFileClick, handleNavigateBack: handleFileTabNavigateBack, handleNavigateForward: handleFileTabNavigateForward, diff --git a/src/renderer/components/MainPanel/MainPanel.tsx b/src/renderer/components/MainPanel/MainPanel.tsx index 2142db5d9..a9eddfd7c 100644 --- a/src/renderer/components/MainPanel/MainPanel.tsx +++ b/src/renderer/components/MainPanel/MainPanel.tsx @@ -670,6 +670,7 @@ export const MainPanel = React.memo( onInputBlur={props.onInputBlur} onOpenPromptComposer={props.onOpenPromptComposer} onReplayMessage={props.onReplayMessage} + onForkConversation={props.onForkConversation} fileTree={props.fileTree} onFileClick={props.onFileClick} refreshFileTree={props.refreshFileTree} diff --git a/src/renderer/components/MainPanel/MainPanelContent.tsx b/src/renderer/components/MainPanel/MainPanelContent.tsx index 806d1dcca..50358d14d 100644 --- a/src/renderer/components/MainPanel/MainPanelContent.tsx +++ b/src/renderer/components/MainPanel/MainPanelContent.tsx @@ -173,6 +173,7 @@ export interface MainPanelContentProps { onInputBlur?: () => void; onOpenPromptComposer?: () => void; onReplayMessage?: (text: string, images?: string[]) => void; + onForkConversation?: (logIndex: number) => void; fileTree?: FileNode[]; onFileClick?: (relativePath: string, options?: { openInNewTab?: boolean }) => void; refreshFileTree?: ( @@ -327,6 +328,7 @@ export const MainPanelContent = React.memo(function MainPanelContent(props: Main onInputBlur, onOpenPromptComposer, onReplayMessage, + onForkConversation, fileTree, onFileClick, refreshFileTree, @@ -547,6 +549,7 @@ export const MainPanelContent = React.memo(function MainPanelContent(props: Main markdownEditMode={chatRawTextMode} setMarkdownEditMode={useSettingsStore.getState().setChatRawTextMode} onReplayMessage={onReplayMessage} + onForkConversation={onForkConversation} fileTree={fileTree} cwd={ activeSession.cwd?.startsWith(activeSession.fullPath) diff --git a/src/renderer/components/MainPanel/types.ts b/src/renderer/components/MainPanel/types.ts index 9511476dd..0f7536ce0 100644 --- a/src/renderer/components/MainPanel/types.ts +++ b/src/renderer/components/MainPanel/types.ts @@ -196,6 +196,7 @@ export interface MainPanelProps { onOpenPromptComposer?: () => void; // Replay a user message (AI mode) onReplayMessage?: (text: string, images?: string[]) => void; + onForkConversation?: (logIndex: number) => void; // File tree for linking file references in AI responses fileTree?: import('../../types/fileTree').FileNode[]; // Callback when a file link is clicked in AI response diff --git a/src/renderer/components/TerminalOutput.tsx b/src/renderer/components/TerminalOutput.tsx index 74c511c97..163bbe6a8 100644 --- a/src/renderer/components/TerminalOutput.tsx +++ b/src/renderer/components/TerminalOutput.tsx @@ -13,6 +13,7 @@ import { Save, Share2, Hammer, + GitFork, } from 'lucide-react'; import type { Session, Theme, LogEntry, FocusArea, AgentError } from '../types'; import type { FileNode } from '../types/fileTree'; @@ -176,6 +177,8 @@ interface LogItemProps { // Publish to GitHub Gist (AI mode only, non-user messages, requires gh CLI) ghCliAvailable?: boolean; onPublishGist?: (text: string) => void; + // Fork conversation from this message (AI mode only, user and ai source messages) + onForkConversation?: (index: number) => void; // Message alignment userMessageAlignment: 'left' | 'right'; } @@ -218,6 +221,7 @@ const LogItemComponent = memo( onSaveToFile, ghCliAvailable, onPublishGist, + onForkConversation, userMessageAlignment, }: LogItemProps) => { // Ref for the log item container - used for scroll-into-view on expand @@ -913,6 +917,17 @@ const LogItemComponent = memo( )} + {/* Fork conversation from this message - AI and user messages only */} + {(log.source === 'ai' || log.source === 'user') && isAIMode && onForkConversation && ( + + )} {/* Publish to GitHub Gist - only for AI responses when gh CLI available */} {log.source !== 'user' && isAIMode && ghCliAvailable && onPublishGist && (