feat(FN-1700): merge fusion/fn-1700

This commit is contained in:
gsxdsm
2026-04-13 16:20:04 -07:00
parent 794d541b0b
commit 07cc2019fe
5 changed files with 320 additions and 1 deletions

View File

@@ -11,6 +11,8 @@ import {
applySliceInterview,
skipMilestoneInterview,
skipSliceInterview,
fetchAiSession,
parseConversationHistory,
type TargetInterviewSummary,
} from "../api";
import {
@@ -37,6 +39,8 @@ interface MilestoneSliceInterviewModalProps {
targetTitle: string;
missionContext?: string;
projectId?: string;
/** Resume a session from background (fetches session and restores state) */
resumeSessionId?: string;
}
interface QuestionResponse {
@@ -66,6 +70,7 @@ export function MilestoneSliceInterviewModal({
targetTitle,
missionContext,
projectId,
resumeSessionId,
}: MilestoneSliceInterviewModalProps) {
const [view, setView] = useState<ViewState>({ type: "initial" });
const [error, setError] = useState<string | null>(null);
@@ -239,6 +244,68 @@ export function MilestoneSliceInterviewModal({
}
}, [isOpen, view.type]);
// Reconnect to a persisted session when resumeSessionId is provided
useEffect(() => {
if (!isOpen || !resumeSessionId || view.type !== "initial") return;
let cancelled = false;
fetchAiSession(resumeSessionId).then((session) => {
if (cancelled || !session) return;
const parsedHistory = parseConversationHistory(session.conversationHistory);
setConversationHistory(parsedHistory);
setLockSessionId(session.id);
setResponseHistory(
parsedHistory
.map((entry) => entry.response)
.filter((response): response is QuestionResponse =>
Boolean(response && typeof response === "object" && !Array.isArray(response)),
),
);
if (session.status === "awaiting_input" && session.currentQuestion) {
try {
const question = JSON.parse(session.currentQuestion) as PlanningQuestion;
currentSessionIdRef.current = session.id;
setView({ type: "question", sessionId: session.id, question });
} catch {
setError("Failed to restore session question.");
}
} else if (session.status === "complete" && session.result) {
try {
const summary = JSON.parse(session.result) as TargetInterviewSummary;
currentSessionIdRef.current = session.id;
setEditedSummary(summary);
setView({ type: "summary", sessionId: session.id, summary });
} catch {
setError("Failed to restore session result.");
}
} else if (session.status === "generating") {
currentSessionIdRef.current = session.id;
if (session.thinkingOutput) {
setStreamingOutput(session.thinkingOutput);
}
setView({ type: "loading" });
connectToInterviewStream(session.id);
} else if (session.status === "error") {
currentSessionIdRef.current = session.id;
setError(null);
setView({
type: "error",
sessionId: session.id,
errorMessage: session.error ?? "The session encountered an error.",
});
}
}).catch(() => {
if (!cancelled) setError("Failed to resume session.");
});
return () => {
cancelled = true;
};
}, [connectToInterviewStream, isOpen, resumeSessionId, view.type]);
// Cleanup on close
useEffect(() => {
if (!isOpen) {