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

@@ -569,6 +569,28 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
resolvedPlanningSettings.modelId;
if (existingSessionId) {
// Defeat the start-before-debounced-sync race: the textarea contents
// submitted with this request are authoritative — write them through
// to the draft row before startExistingSession reads back from SQLite.
// Otherwise a Start Planning click within the 500 ms debounce window
// would launch the session against stale text.
if (aiSessionStore) {
try {
aiSessionStore.updateDraft(existingSessionId, {
initialPlan,
// Persist the explicit body override (if both fields set) so a
// later summarizeDraftTitle picks the same model the user just
// chose; pass undefined to clear any half-set state otherwise.
modelProvider: planningModelProvider && planningModelId ? planningModelProvider : undefined,
modelId: planningModelProvider && planningModelId ? planningModelId : undefined,
});
} catch (error) {
planningLogger.warn(
"Failed to flush draft initialPlan before start",
{ sessionId: existingSessionId, error: String(error) },
);
}
}
const { startExistingSession } = await import("../planning.js");
await startExistingSession(
existingSessionId,
@@ -614,6 +636,43 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
}
});
/**
* POST /api/planning/:sessionId/summarize-draft-title
* Generate (or regenerate) the sidebar title for a draft session from its
* latest persisted initialPlan. Fired by the modal on textarea blur and on
* close so that drafts the user walks away from end up with a real title
* instead of "New planning session". Idempotent server-side: only acts on
* draft rows still holding the placeholder title.
*
* UTILITY PATH: This route is independent of task-lane saturation.
*/
router.post("/planning/:sessionId/summarize-draft-title", async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId) {
throw badRequest("sessionId is required");
}
const { store: scopedStore } = await getProjectContext(req);
const settings = await scopedStore.getSettings();
const rootDir = scopedStore.getRootDir();
const resolvedPlanningSettings = resolvePlanningSettingsModel(settings);
const { summarizeDraftTitle } = await import("../planning.js");
const title = await summarizeDraftTitle(
sessionId,
rootDir,
resolvedPlanningSettings.provider,
resolvedPlanningSettings.modelId,
);
res.json({ title });
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err, "Failed to summarize draft title");
}
});
/**
* POST /api/planning/respond
* Submit a response to the current planning question.