mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
Adds a PUT /dashboard/api/plan/{thread_id} endpoint and PlanReview UI edit
mode so the thread owner can refine the published plan markdown by hand. The
edited markdown is re-published as "ready" (preserving reviewer comments),
mirrored into the sandbox plan.md, and handed to the agent as the source of
truth on approve.
Approve now reads the published plan content strictly (raise_on_error=True)
so a transient store failure aborts instead of silently dropping an edited
plan, matching the comment-read contract.
The banner-overlap fix (collapsed git-panel clearing the "Review plan" link)
was already ported in #128; this picks up the remaining edit-mode pieces.
Refs #80
136 lines
3.2 KiB
TypeScript
136 lines
3.2 KiB
TypeScript
/**
|
|
* Client for the plan-review API — plain HTTP, no realtime transport.
|
|
*
|
|
* The agent publishes the plan markdown; reviewers read it and leave
|
|
* whole-document comments. On approve/reject the server reads those comments and
|
|
* hands them to the agent as the instruction for the follow-up run.
|
|
*/
|
|
|
|
const API_BASE = (import.meta.env.VITE_DASHBOARD_API_BASE_URL ?? "").replace(
|
|
/\/$/,
|
|
""
|
|
)
|
|
|
|
function apiBase(): string {
|
|
if (API_BASE) return API_BASE
|
|
return typeof window !== "undefined" ? window.location.origin : ""
|
|
}
|
|
|
|
export interface PlanUser {
|
|
id: string
|
|
login: string
|
|
email: string | null
|
|
name: string
|
|
}
|
|
|
|
export type PlanStatus =
|
|
| "planning"
|
|
| "ready"
|
|
| "revising"
|
|
| "approved"
|
|
| "cancelled"
|
|
|
|
export interface PlanData {
|
|
threadId: string
|
|
status: PlanStatus
|
|
markdown: string
|
|
isOwner: boolean
|
|
user: PlanUser
|
|
}
|
|
|
|
export interface PlanComment {
|
|
id: string
|
|
author: string
|
|
author_login: string
|
|
body: string
|
|
created_at: string
|
|
}
|
|
|
|
export class PlanApiError extends Error {
|
|
constructor(
|
|
public readonly status: number,
|
|
message: string
|
|
) {
|
|
super(message)
|
|
this.name = "PlanApiError"
|
|
}
|
|
}
|
|
|
|
async function req<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
const res = await fetch(`${apiBase()}/dashboard/api${path}`, {
|
|
...init,
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
|
|
})
|
|
if (!res.ok) {
|
|
let message = res.statusText
|
|
try {
|
|
const body = await res.json()
|
|
if (body?.detail)
|
|
message =
|
|
typeof body.detail === "string"
|
|
? body.detail
|
|
: JSON.stringify(body.detail)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new PlanApiError(res.status, message)
|
|
}
|
|
if (res.status === 204) return undefined as T
|
|
return (await res.json()) as T
|
|
}
|
|
|
|
export function getPlan(threadId: string): Promise<PlanData> {
|
|
return req<PlanData>(`/plan/${encodeURIComponent(threadId)}`)
|
|
}
|
|
|
|
export async function getPlanComments(
|
|
threadId: string
|
|
): Promise<Array<PlanComment>> {
|
|
const { comments } = await req<{ comments: Array<PlanComment> }>(
|
|
`/plan/${encodeURIComponent(threadId)}/comments`
|
|
)
|
|
return comments
|
|
}
|
|
|
|
export function addPlanComment(
|
|
threadId: string,
|
|
body: string
|
|
): Promise<PlanComment> {
|
|
return req(`/plan/${encodeURIComponent(threadId)}/comments`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ body }),
|
|
})
|
|
}
|
|
|
|
export function deletePlanComment(
|
|
threadId: string,
|
|
commentId: string
|
|
): Promise<{ ok: boolean }> {
|
|
return req(
|
|
`/plan/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`,
|
|
{ method: "DELETE" }
|
|
)
|
|
}
|
|
|
|
export function updatePlan(
|
|
threadId: string,
|
|
markdown: string
|
|
): Promise<{ status: PlanStatus; markdown: string }> {
|
|
return req(`/plan/${encodeURIComponent(threadId)}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ markdown }),
|
|
})
|
|
}
|
|
|
|
export function approvePlan(threadId: string): Promise<{ status: string }> {
|
|
return req(`/plan/${encodeURIComponent(threadId)}/approve`, {
|
|
method: "POST",
|
|
})
|
|
}
|
|
|
|
export function rejectPlan(threadId: string): Promise<{ status: string }> {
|
|
return req(`/plan/${encodeURIComponent(threadId)}/reject`, {
|
|
method: "POST",
|
|
})
|
|
}
|