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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user