fix(ui): route answer worktrees from source session
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user