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:
@@ -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 (
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function PATCH(
|
||||
if (body.status) {
|
||||
updates.push(`status = $${idx++}`);
|
||||
values.push(body.status);
|
||||
if (body.status === "done" || body.status === "failed" || body.status === "stopped") {
|
||||
if (["done", "approved", "failed", "stopped"].includes(body.status)) {
|
||||
updates.push(`completed_at = now()`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,17 +64,23 @@ export async function POST(
|
||||
|
||||
const giteaRepo = owns[0].data?.giteaRepo as string | undefined;
|
||||
|
||||
// Find the Coolify UUID for this specific app so the runner can trigger a deploy
|
||||
interface AppEntry { name: string; coolifyServiceUuid?: string | null; }
|
||||
const apps = (owns[0].data?.apps ?? []) as AppEntry[];
|
||||
const matchedApp = apps.find(a => a.name === appName);
|
||||
const coolifyAppUuid = matchedApp?.coolifyServiceUuid ?? undefined;
|
||||
|
||||
// Create the session row
|
||||
const rows = await query<{ id: string }>(
|
||||
`INSERT INTO agent_sessions (project_id, app_name, app_path, task, status, started_at)
|
||||
VALUES ($1::uuid, $2, $3, $4, 'running', now())
|
||||
VALUES ($1::text, $2, $3, $4, 'running', now())
|
||||
RETURNING id`,
|
||||
[projectId, appName, appPath, task.trim()]
|
||||
);
|
||||
const sessionId = rows[0].id;
|
||||
|
||||
// Fire-and-forget: call agent-runner to start the execution loop
|
||||
// The agent runner is responsible for updating the session row as it works.
|
||||
// Fire-and-forget: call agent-runner to start the execution loop.
|
||||
// autoApprove: true — agent commits + deploys automatically on completion.
|
||||
fetch(`${AGENT_RUNNER_URL}/agent/execute`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -83,8 +89,10 @@ export async function POST(
|
||||
projectId,
|
||||
appName,
|
||||
appPath,
|
||||
giteaRepo, // e.g. "mark/sportsy" — agent runner uses this to clone/update the repo
|
||||
giteaRepo,
|
||||
task: task.trim(),
|
||||
autoApprove: true,
|
||||
coolifyAppUuid,
|
||||
}),
|
||||
}).catch(err => {
|
||||
// Agent runner may not be wired yet — log but don't fail
|
||||
|
||||
Reference in New Issue
Block a user