refine polish: before/after diff view + accept/cancel/restore
Refine no longer clobbers the mission description on click. Flow:
1. Click Refine → server generates the rewrite, returns
{ original, refined } WITHOUT persisting
2. RefineDiffModal shows a side-by-side pane (raw before,
Markdown-rendered after)
3. User picks:
- Accept → PATCH /api/missions/{id}/description commits refined
- Cancel → discards the proposal, description unchanged
- Restore original → forces a write of `original` (undo path
for accidentally-accepted refines, since Accept+Cancel is
still a two-step confirmation)
Backend:
- mission_refiner::refine returns a RefineResult { original, refined }
struct instead of persisting + returning the text
- routes::missions::refine now returns { original, refined }
- routes::missions::set_description added on PATCH
/api/missions/{id}/description (draft-only)
Frontend:
- lib/api/missions — refineMission return type is now RefineResult;
added setMissionDescription
- MissionCanvas — RefineDiffModal + DiffPane subcomponents;
accept / cancel / restore handlers wired to state
Closes task #20.
This commit is contained in:
@@ -221,15 +221,22 @@ pub async fn trigger_security_scan(
|
||||
Ok(Json(SecurityScanResponse { findings, tasks }))
|
||||
}
|
||||
|
||||
/// POST /api/missions/{id}/refine — rewrite the description into a
|
||||
/// coherent, sectioned Markdown brief ready for downstream agent
|
||||
/// ingestion. Draft-only.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RefineResponse {
|
||||
pub original: String,
|
||||
pub refined: String,
|
||||
}
|
||||
|
||||
/// POST /api/missions/{id}/refine — generate a coherent, sectioned
|
||||
/// Markdown rewrite of the current description WITHOUT persisting.
|
||||
/// Frontend renders a before/after diff; user hits Accept (PATCH
|
||||
/// /description) or Cancel. Draft-only.
|
||||
pub async fn refine(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<Mission>, ApiError> {
|
||||
crate::mission_refiner::refine(&state.pool, user.workspace_id, id)
|
||||
) -> Result<Json<RefineResponse>, ApiError> {
|
||||
let result = crate::mission_refiner::refine(&state.pool, user.workspace_id, id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("mission {id}: refine failed: {e}");
|
||||
@@ -241,6 +248,39 @@ pub async fn refine(
|
||||
ApiError::Internal
|
||||
}
|
||||
})?;
|
||||
Ok(Json(RefineResponse {
|
||||
original: result.original,
|
||||
refined: result.refined,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetDescriptionRequest {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// PATCH /api/missions/{id}/description — commit a new description.
|
||||
/// Draft-only. Used by the Refine Accept flow (and any future
|
||||
/// direct-edit surface).
|
||||
pub async fn set_description(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<SetDescriptionRequest>,
|
||||
) -> Result<Json<Mission>, ApiError> {
|
||||
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
if mission.status != "draft" {
|
||||
return Err(ApiError::BadRequest);
|
||||
}
|
||||
cm_db::repo::missions::set_description(
|
||||
&state.pool,
|
||||
id,
|
||||
user.workspace_id.as_uuid(),
|
||||
&body.description,
|
||||
)
|
||||
.await?;
|
||||
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
Reference in New Issue
Block a user