fix(ui): route answer worktrees from source session

This commit is contained in:
Bohdan Triapitsyn
2026-09-04 19:46:21 +03:00
parent f160f3aac4
commit b4a38061bc
7 changed files with 234 additions and 61 deletions
@@ -56,6 +56,7 @@ import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedC
import { useProviderLogo } from '@/hooks/useProviderLogo';
import { getAgentColor } from '@/lib/agentColors';
import { isCapacitorMobileApp } from '@/apps/mobileNativeChrome';
import { WorktreeRequiresGitRepositoryError } from '@/lib/worktrees/worktreeCreate';
const CONTAIN_LAYOUT_STYLE = { contain: 'layout' as const, transform: 'translateZ(0)' };
@@ -1331,17 +1332,14 @@ const AssistantMessageBody = React.memo(({
const effectiveStreamPhase: StreamPhase = hasStopFinish ? 'completed' : streamPhase;
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
const currentProjectRef = React.useMemo(() => {
if (!canUseProjectPlanActions) {
return null;
}
const sessionProjectRef = React.useMemo(() => {
const directory = effectiveDirectory
?? (currentSessionId ? getDirectoryForSession(currentSessionId) : null)
?? '';
const resolved = resolveProjectForSessionDirectory(projects, availableWorktreesByProject, directory);
return resolved ? { id: resolved.id, path: resolved.path } : null;
}, [availableWorktreesByProject, canUseProjectPlanActions, currentSessionId, effectiveDirectory, getDirectoryForSession, projects]);
}, [availableWorktreesByProject, currentSessionId, effectiveDirectory, getDirectoryForSession, projects]);
const currentProjectRef = canUseProjectPlanActions ? sessionProjectRef : null;
const isActiveTool = React.useCallback((toolPart: ToolPartType): boolean => {
const state = (toolPart as Record<string, unknown>).state as Record<string, unknown> | undefined ?? {};
@@ -1377,28 +1375,48 @@ const AssistantMessageBody = React.memo(({
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.preventDefault();
if (!createSessionFromAssistantMessage || !assistantPlanText.trim()) {
if (!assistantPlanText.trim()) {
return;
}
setIsForkDialogOpen(true);
},
[createSessionFromAssistantMessage, assistantPlanText]
[assistantPlanText]
);
const handleConfirmFork = React.useCallback(
async (execution: ForkSessionExecution) => {
if (!createSessionFromAssistantMessage) {
return;
}
setIsForkSubmitting(true);
try {
await createSessionFromAssistantMessage(messageId, execution);
if (!sessionId) {
throw new Error('Source session is unavailable');
}
const sourceDirectory = effectiveDirectory ?? getDirectoryForSession(sessionId);
if (!sourceDirectory) {
throw new Error('Source session directory is unavailable');
}
await createSessionFromAssistantMessage({
sessionId,
directory: sourceDirectory,
text: assistantPlanText,
}, execution);
setIsForkDialogOpen(false);
} catch (error) {
console.error('Failed to start a session from an assistant message:', error);
if (error instanceof WorktreeRequiresGitRepositoryError) {
toast.error(t('rightSidebar.contextNotesTodo.toast.worktreeRequiresGitRepo'));
return;
}
const description = error instanceof Error ? error.message : undefined;
toast.error(
t('rightSidebar.contextNotesTodo.toast.createSessionFailed'),
description ? { description } : undefined
);
} finally {
setIsForkSubmitting(false);
}
},
[createSessionFromAssistantMessage, messageId]
[assistantPlanText, createSessionFromAssistantMessage, effectiveDirectory, getDirectoryForSession, sessionId, t]
);
const handleForkMultiRunClick = React.useCallback(
@@ -2136,6 +2154,8 @@ const AssistantMessageBody = React.memo(({
open={isForkDialogOpen}
onOpenChange={setIsForkDialogOpen}
projectDirectory={effectiveDirectory ?? null}
sourceSessionId={sessionId ?? null}
worktreeProjectDirectory={sessionProjectRef?.path ?? null}
submitting={isForkSubmitting}
onConfirm={handleConfirmFork}
/>
@@ -17,6 +17,9 @@ import { isPrimaryMode } from '@/components/chat/mobileControlsUtils';
import { EXECUTION_FORK_DEFAULT_INSTRUCTIONS, EXECUTION_FORK_GOAL_INSTRUCTIONS } from '@/lib/messages/executionMeta';
import { useI18n } from '@/lib/i18n';
import { isVSCodeRuntime } from '@/lib/desktop';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useGitStore, useIsGitRepo } from '@/stores/useGitStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
export type ForkSessionExecution = {
providerID: string;
@@ -32,13 +35,22 @@ type ForkSessionDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
projectDirectory: string | null;
sourceSessionId: string | null;
worktreeProjectDirectory: string | null;
submitting?: boolean;
onConfirm: (execution: ForkSessionExecution) => Promise<void> | void;
};
export function ForkSessionDialog(props: ForkSessionDialogProps) {
const { t } = useI18n();
const { open, onOpenChange, projectDirectory, submitting = false, onConfirm } = props;
const { open, onOpenChange, projectDirectory, sourceSessionId, worktreeProjectDirectory, submitting = false, onConfirm } = props;
const metadataProjectDirectory = useSessionUIStore((state) => (
sourceSessionId ? state.worktreeMetadata.get(sourceSessionId)?.projectDirectory ?? null : null
));
const resolvedWorktreeProjectDirectory = metadataProjectDirectory ?? worktreeProjectDirectory;
const git = useRuntimeAPIs().git;
const isGitRepository = useIsGitRepo(resolvedWorktreeProjectDirectory);
const ensureGitStatus = useGitStore((state) => state.ensureStatus);
const loadProviders = useConfigStore((state) => state.loadProviders);
const loadConfigAgents = useConfigStore((state) => state.loadAgents);
@@ -56,7 +68,7 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
const [instructions, setInstructions] = React.useState(EXECUTION_FORK_DEFAULT_INSTRUCTIONS);
const [createWorktree, setCreateWorktree] = React.useState(false);
const [runAsGoal, setRunAsGoal] = React.useState(false);
const showCreateWorktree = React.useMemo(() => !isVSCodeRuntime(), []);
const showCreateWorktree = !isVSCodeRuntime() && isGitRepository === true;
// The goal loop lives in the web server; VS Code only renders goal state.
const showRunAsGoal = React.useMemo(() => !isVSCodeRuntime(), []);
@@ -79,6 +91,11 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
void loadAgentsStoreAgents();
}, [open, loadProviders, loadConfigAgents, loadAgentsStoreAgents, projectDirectory]);
React.useEffect(() => {
if (!open || !resolvedWorktreeProjectDirectory || !git) return;
void ensureGitStatus(resolvedWorktreeProjectDirectory, git);
}, [ensureGitStatus, git, open, resolvedWorktreeProjectDirectory]);
// Reset only when the dialog transitions to open. Reading the store snapshot
// here (instead of subscribing) avoids clobbering in-progress user edits when
// the config store refreshes in the background while the dialog is open.