fix(dashboard): preserve literal types when adding draft to planning sessions

Inline object passed to setPlanningSessions widened type/status to string,
breaking AiSessionSummary's literal union. Hoist the draft into a typed
variable so the literals narrow correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-02 15:55:01 -07:00
parent 99f5ca3fb3
commit 9fb1950e99

View File

@@ -4,6 +4,7 @@ import type { Task, PlanningQuestion, PlanningSummary } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import {
startPlanningStreaming,
createPlanningDraft,
respondToPlanning,
retryPlanningSession,
createTaskFromPlanning,
@@ -116,6 +117,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
const modalRef = useRef<HTMLDivElement>(null);
const streamConnectionRef = useRef<{ close: () => void; isConnected: () => boolean } | null>(null);
const currentSessionIdRef = useRef<string | null>(null);
const draftSessionIdRef = useRef<string | null>(null);
const draftDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Tracks resumeSessionId values the user has explicitly dismissed (via "New
// Session"). Without this, the resume effect re-fires on every callback
// identity change (e.g. typing into the textarea recreates loadSession) and
@@ -477,6 +480,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
? Number.parseInt(customQuestionCount, 10)
: undefined;
const draftSessionId = draftSessionIdRef.current;
const { sessionId } = await startPlanningStreaming(
plan.trim(),
projectId,
@@ -487,7 +491,9 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
? parsedCustomQuestionCount
: undefined,
},
draftSessionId ?? undefined,
);
draftSessionIdRef.current = null;
currentSessionIdRef.current = sessionId;
setLockSessionId(sessionId);
setSelectedSessionId(sessionId);
@@ -738,6 +744,11 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
const handleNewSession = useCallback(() => {
streamConnectionRef.current?.close();
streamConnectionRef.current = null;
draftSessionIdRef.current = null;
if (draftDebounceRef.current) {
clearTimeout(draftDebounceRef.current);
draftDebounceRef.current = null;
}
if (resumeSessionId) {
dismissedResumeRef.current = resumeSessionId;
}
@@ -890,6 +901,10 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
// Cleanup stream connection on unmount
useEffect(() => {
return () => {
if (draftDebounceRef.current) {
clearTimeout(draftDebounceRef.current);
draftDebounceRef.current = null;
}
streamConnectionRef.current?.close();
streamConnectionRef.current = null;
@@ -923,6 +938,11 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
savePlanningDescription(initialPlan, projectId);
}
draftSessionIdRef.current = null;
if (draftDebounceRef.current) {
clearTimeout(draftDebounceRef.current);
draftDebounceRef.current = null;
}
streamConnectionRef.current?.close();
streamConnectionRef.current = null;
setIsReconnecting(false);
@@ -1300,7 +1320,55 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
className="planning-textarea"
placeholder="e.g., Build a user authentication system with login, signup, and password reset..."
value={initialPlan}
onChange={(e) => setInitialPlan(e.target.value)}
onChange={(e) => {
const nextValue = e.target.value;
setInitialPlan(nextValue);
if (draftSessionIdRef.current || nextValue.trim().length === 0) {
return;
}
if (draftDebounceRef.current) {
clearTimeout(draftDebounceRef.current);
}
draftDebounceRef.current = setTimeout(() => {
if (draftSessionIdRef.current) {
return;
}
const content = nextValue.trim();
if (!content) {
return;
}
const modelOverride =
planningModelProvider && planningModelId
? { planningModelProvider, planningModelId }
: undefined;
void createPlanningDraft(content, projectId, modelOverride)
.then((response) => {
draftSessionIdRef.current = response.sessionId;
setPlanningSessions((prev) => {
if (prev.some((s) => s.id === response.sessionId)) {
return prev;
}
const draft: AiSessionSummary = {
id: response.sessionId,
type: "planning",
status: "draft",
title: response.title,
projectId: projectId ?? null,
lockedByTab: null,
updatedAt: new Date().toISOString(),
archived: false,
};
return [draft, ...prev].sort(
(a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt),
);
});
setSelectedSessionId(response.sessionId);
})
.catch(() => {
// best-effort
});
}, 300);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && initialPlan.trim()) {
e.preventDefault();