fix(dashboard): plumb planning draft text + model selection end-to-end

The planning modal's draft sync only updated SQLite, never the in-memory
session, so Start Planning ran the agent against whatever fragment was
typed before the first 500ms debounce and silently dropped everything
after. Drafts that survived a backend restart couldn't be started at
all (lazy rebuild was missing for the start path), reopened drafts left
the textarea empty, and the model override the user picked at create
time was forgotten on reopen — Start Planning would silently use the
project default instead.

This change rehydrates initialPlan from SQLite in startExistingSession,
flushes the latest text from the request body before reading it back,
lazy-rebuilds the in-memory session from the persisted row when needed,
and round-trips the model override through inputPayload so reopen
restores it into modal state. Sidebar drafts now render a derived
preview (from inputPayload, never persisted as title) so multiple
drafts are distinguishable while editing, and summarizeDraftTitle fires
on textarea blur and modal close — gated on status rather than title
content so blur-then-edit still refreshes rather than locking to the
first snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-03 00:05:28 -07:00
parent d73070c135
commit 2affc143dc
10 changed files with 785 additions and 60 deletions

View File

@@ -7161,6 +7161,8 @@ export interface AiSessionSummary {
type: "planning" | "subtask" | "mission_interview" | "milestone_interview" | "slice_interview";
status: "draft" | "generating" | "awaiting_input" | "complete" | "error";
title: string;
/** Server-derived preview of the in-progress initialPlan; only set for draft planning sessions. */
preview?: string;
projectId: string | null;
lockedByTab: string | null;
updatedAt: string;
@@ -7279,7 +7281,7 @@ export function pingSession(sessionId: string, projectId?: string): Promise<{ ok
export function updatePlanningSessionDraft(
sessionId: string,
draft: { title: string; initialPlan: string },
draft: { initialPlan: string; modelProvider?: string; modelId?: string },
projectId?: string,
): Promise<{ ok: boolean }> {
return api<{ ok: boolean }>(withProjectId(`/ai-sessions/${encodeURIComponent(sessionId)}/draft`, projectId), {
@@ -7288,6 +7290,22 @@ export function updatePlanningSessionDraft(
});
}
/**
* Ask the server to (re)generate the sidebar title for a draft planning
* session from its persisted initialPlan. Server-side is idempotent and
* a no-op once the session has been started, so callers can fire-and-
* forget on textarea blur and modal close.
*/
export function summarizePlanningDraftTitle(
sessionId: string,
projectId?: string,
): Promise<{ title: string | null }> {
return api<{ title: string | null }>(
withProjectId(`/planning/${encodeURIComponent(sessionId)}/summarize-draft-title`, projectId),
{ method: "POST" },
);
}
// ── Messages API ──────────────────────────────────────────────────────────
/** Response shape for GET /messages/inbox */