feat(dashboard): add draft planning sessions with debounced auto-creation
Introduce a draft lifecycle for planning sessions: typing into the PlanningModeModal textarea now creates a server-side draft after a 300ms debounce, persisted with status='draft' so the user's in-flight plan survives modal close/reopen and shows up immediately in the session list. - planning.ts: new createDraftSession path; persistSession status union widened to include 'draft'; Session gains an explicit title field so subsequent updates don't clobber it. - register-planning-subtask-routes.ts: wires the createPlanningDraft POST endpoint that the modal calls on debounce. - ai-session-store.ts: tracks the draft status across queries so the session list and locks behave the same as any active session. - legacy.ts: client wrapper for createPlanningDraft. - PlanningModeModal styling, tests, and ModalReentry coverage updated for the new flow. - docs/architecture.md notes the expanded ai_sessions.status lifecycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -443,6 +443,58 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/planning/create-draft", async (req, res) => {
|
||||
try {
|
||||
const { initialPlan, planningModelProvider, planningModelId } = req.body;
|
||||
|
||||
if (!initialPlan || typeof initialPlan !== "string" || initialPlan.trim().length === 0) {
|
||||
throw badRequest("initialPlan is required and must be a string");
|
||||
}
|
||||
|
||||
if (planningModelProvider !== undefined && typeof planningModelProvider !== "string") {
|
||||
throw badRequest("planningModelProvider must be a string when provided");
|
||||
}
|
||||
|
||||
if (planningModelId !== undefined && typeof planningModelId !== "string") {
|
||||
throw badRequest("planningModelId must be a string when provided");
|
||||
}
|
||||
|
||||
const { store: scopedStore, projectId } = await getProjectContext(req);
|
||||
const settings = await scopedStore.getSettings();
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
const rootDir = scopedStore.getRootDir();
|
||||
|
||||
const resolvedPlanningSettings = resolvePlanningSettingsModel(settings);
|
||||
const resolvedPlanningProvider =
|
||||
(planningModelProvider && planningModelId ? planningModelProvider : undefined) ||
|
||||
resolvedPlanningSettings.provider;
|
||||
|
||||
const resolvedPlanningModelId =
|
||||
(planningModelProvider && planningModelId ? planningModelId : undefined) ||
|
||||
resolvedPlanningSettings.modelId;
|
||||
|
||||
const { createDraftSession } = await import("../planning.js");
|
||||
const draft = await createDraftSession(
|
||||
ip,
|
||||
initialPlan,
|
||||
rootDir,
|
||||
resolvedPlanningProvider,
|
||||
resolvedPlanningModelId,
|
||||
settings.promptOverrides,
|
||||
{ projectId },
|
||||
);
|
||||
res.status(201).json(draft);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
if (err instanceof Error && err.name === "RateLimitError") {
|
||||
throw rateLimited(err.message);
|
||||
}
|
||||
rethrowAsApiError(err, "Failed to create planning draft");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/planning/start-streaming
|
||||
* Start a new planning session with AI agent streaming.
|
||||
@@ -462,6 +514,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
planningModelId,
|
||||
planningDepth,
|
||||
customQuestionCount,
|
||||
existingSessionId,
|
||||
} = req.body;
|
||||
|
||||
if (!initialPlan || typeof initialPlan !== "string") {
|
||||
@@ -492,6 +545,10 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
throw badRequest("customQuestionCount must be an integer between 1 and 20 when provided");
|
||||
}
|
||||
|
||||
if (existingSessionId !== undefined && typeof existingSessionId !== "string") {
|
||||
throw badRequest("existingSessionId must be a string when provided");
|
||||
}
|
||||
|
||||
const { store: scopedStore, projectId } = await getProjectContext(req);
|
||||
const settings = await scopedStore.getSettings();
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
@@ -511,6 +568,19 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
(planningModelProvider && planningModelId ? planningModelId : undefined) ||
|
||||
resolvedPlanningSettings.modelId;
|
||||
|
||||
if (existingSessionId) {
|
||||
const { startExistingSession } = await import("../planning.js");
|
||||
await startExistingSession(
|
||||
existingSessionId,
|
||||
rootDir,
|
||||
resolvedPlanningProvider,
|
||||
resolvedPlanningModelId,
|
||||
settings.promptOverrides,
|
||||
);
|
||||
res.status(201).json({ sessionId: existingSessionId });
|
||||
return;
|
||||
}
|
||||
|
||||
const { createSessionWithAgent, RateLimitError: _RateLimitError2 } = await import("../planning.js");
|
||||
const sessionId = await createSessionWithAgent(
|
||||
ip,
|
||||
|
||||
Reference in New Issue
Block a user