feat: auto-approve UI + session status approved

- sessions POST: look up coolifyServiceUuid, pass autoApprove:true to runner
- sessions PATCH: approved added to terminal statuses (sets completed_at)
- build/page.tsx: approved status, STATUS_COLORS/LABELS for "Shipped",
  auto-committed UI in changed files panel, bottom bar for approved state
- Architecture doc: fully updated with current state

Made-with: Cursor
This commit is contained in:
2026-03-07 13:17:33 -08:00
parent 7b228ebad2
commit fc59333383
3 changed files with 69 additions and 13 deletions

View File

@@ -242,7 +242,7 @@ interface AgentSession {
id: string;
app_name: string;
task: string;
status: "pending" | "running" | "done" | "failed" | "stopped";
status: "pending" | "running" | "done" | "approved" | "failed" | "stopped";
output: Array<{ ts: string; type: string; text: string }>;
changed_files: Array<{ path: string; status: string }>;
error: string | null;
@@ -252,10 +252,12 @@ interface AgentSession {
}
const STATUS_COLORS: Record<string, string> = {
running: "#3d5afe", done: "#2e7d32", failed: "#c62828", stopped: "#a09a90", pending: "#d4a04a",
running: "#3d5afe", done: "#2e7d32", approved: "#1b5e20",
failed: "#c62828", stopped: "#a09a90", pending: "#d4a04a",
};
const STATUS_LABELS: Record<string, string> = {
running: "Running", done: "Done", failed: "Failed", stopped: "Stopped", pending: "Starting…",
running: "Running", done: "Done", approved: "Shipped",
failed: "Failed", stopped: "Stopped", pending: "Starting…",
};
const FILE_STATUS_COLORS: Record<string, string> = { added: "#2e7d32", modified: "#d4a04a", deleted: "#c62828" };
@@ -295,9 +297,9 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
const list: AgentSession[] = d.sessions ?? [];
setSessions(list);
setLoadingSessions(false);
// Auto-select: prefer running/pending, then the most recent
// Auto-select: prefer live session, then the most recent
if (list.length > 0 && !activeSessionId) {
const live = list.find(s => s.status === "running" || s.status === "pending");
const live = list.find(s => ["running", "pending"].includes(s.status));
const pick = live ?? list[0];
setActiveSessionId(pick.id);
setActiveSession(pick);
@@ -324,13 +326,13 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
.catch(() => {});
}
}
} catch { /* network hiccup — ignore */ }
};
poll();
const interval = setInterval(() => {
poll();
}, ["running", "pending"].includes(activeSession?.status ?? "") ? 500 : 5000);
const isLive = ["running", "pending"].includes(activeSession?.status ?? "");
const interval = setInterval(poll, isLive ? 500 : 5000);
return () => { cancelled = true; clearInterval(interval); };
}, [activeSessionId, projectId, activeSession?.status]);
@@ -554,6 +556,11 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
</div>
))}
</div>
{activeSession.status === "approved" && (
<div style={{ marginTop: 8, fontSize: "0.73rem", color: "#1b5e20", fontFamily: "Outfit, sans-serif" }}>
Auto-committed these changes are live.
</div>
)}
{activeSession.status === "done" && (
<div style={{ marginTop: 12 }}>
{approveResult && (
@@ -648,6 +655,47 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
);
}
// Session auto-committed — show confirmation, offer next task
if (st === "approved") {
return (
<div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<span style={{ fontSize: "0.75rem", color: "#1b5e20", fontFamily: "Outfit, sans-serif", flex: 1 }}>
Shipped changes committed and deployment triggered automatically.
</span>
<button onClick={() => setShowFollowUp(true)}
style={{ padding: "7px 14px", background: "#f0ece4", color: "#1a1a1a", border: "1px solid #e8e4dc", borderRadius: 7, fontSize: "0.75rem", fontWeight: 600, cursor: "pointer", fontFamily: "Outfit, sans-serif", whiteSpace: "nowrap" }}>
+ Follow up
</button>
<button onClick={() => { setActiveSession(null); setActiveSessionId(null); setTask(""); }}
style={{ padding: "7px 14px", background: "transparent", color: "#a09a90", border: "1px solid #e8e4dc", borderRadius: 7, fontSize: "0.75rem", cursor: "pointer", fontFamily: "Outfit, sans-serif", whiteSpace: "nowrap" }}>
New task
</button>
</div>
{showFollowUp && (
<div style={{ display: "flex", gap: 8, alignItems: "flex-end", marginTop: 10 }}>
<textarea
autoFocus
value={task}
onChange={e => setTask(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { handleRun(); setShowFollowUp(false); }
if (e.key === "Escape") { setShowFollowUp(false); setTask(""); }
}}
placeholder="What should the agent build next?"
rows={2}
style={{ flex: 1, resize: "none", border: "1px solid #e8e4dc", borderRadius: 7, padding: "8px 11px", fontSize: "0.78rem", fontFamily: "Outfit, sans-serif", outline: "none", background: "#faf8f5" }}
/>
<button onClick={() => { handleRun(); setShowFollowUp(false); }} disabled={submitting || !task.trim()}
style={{ padding: "9px 14px", background: task.trim() ? "#1a1a1a" : "#e8e4dc", color: task.trim() ? "#fff" : "#a09a90", border: "none", borderRadius: 7, fontSize: "0.75rem", fontWeight: 600, cursor: task.trim() ? "pointer" : "default", fontFamily: "Outfit, sans-serif" }}>
{submitting ? "Starting…" : "Run"}
</button>
</div>
)}
</div>
);
}
// Session completed successfully — offer follow-up or new task
if (st === "done") {
return (