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;
|
id: string;
|
||||||
app_name: string;
|
app_name: string;
|
||||||
task: string;
|
task: string;
|
||||||
status: "pending" | "running" | "done" | "failed" | "stopped";
|
status: "pending" | "running" | "done" | "approved" | "failed" | "stopped";
|
||||||
output: Array<{ ts: string; type: string; text: string }>;
|
output: Array<{ ts: string; type: string; text: string }>;
|
||||||
changed_files: Array<{ path: string; status: string }>;
|
changed_files: Array<{ path: string; status: string }>;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@@ -252,10 +252,12 @@ interface AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
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> = {
|
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" };
|
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 ?? [];
|
const list: AgentSession[] = d.sessions ?? [];
|
||||||
setSessions(list);
|
setSessions(list);
|
||||||
setLoadingSessions(false);
|
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) {
|
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];
|
const pick = live ?? list[0];
|
||||||
setActiveSessionId(pick.id);
|
setActiveSessionId(pick.id);
|
||||||
setActiveSession(pick);
|
setActiveSession(pick);
|
||||||
@@ -324,13 +326,13 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch { /* network hiccup — ignore */ }
|
} catch { /* network hiccup — ignore */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
poll();
|
poll();
|
||||||
const interval = setInterval(() => {
|
const isLive = ["running", "pending"].includes(activeSession?.status ?? "");
|
||||||
poll();
|
const interval = setInterval(poll, isLive ? 500 : 5000);
|
||||||
}, ["running", "pending"].includes(activeSession?.status ?? "") ? 500 : 5000);
|
|
||||||
return () => { cancelled = true; clearInterval(interval); };
|
return () => { cancelled = true; clearInterval(interval); };
|
||||||
}, [activeSessionId, projectId, activeSession?.status]);
|
}, [activeSessionId, projectId, activeSession?.status]);
|
||||||
|
|
||||||
@@ -554,6 +556,11 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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" && (
|
{activeSession.status === "done" && (
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
{approveResult && (
|
{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
|
// Session completed successfully — offer follow-up or new task
|
||||||
if (st === "done") {
|
if (st === "done") {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export async function PATCH(
|
|||||||
if (body.status) {
|
if (body.status) {
|
||||||
updates.push(`status = $${idx++}`);
|
updates.push(`status = $${idx++}`);
|
||||||
values.push(body.status);
|
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()`);
|
updates.push(`completed_at = now()`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,17 +64,23 @@ export async function POST(
|
|||||||
|
|
||||||
const giteaRepo = owns[0].data?.giteaRepo as string | undefined;
|
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
|
// Create the session row
|
||||||
const rows = await query<{ id: string }>(
|
const rows = await query<{ id: string }>(
|
||||||
`INSERT INTO agent_sessions (project_id, app_name, app_path, task, status, started_at)
|
`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`,
|
RETURNING id`,
|
||||||
[projectId, appName, appPath, task.trim()]
|
[projectId, appName, appPath, task.trim()]
|
||||||
);
|
);
|
||||||
const sessionId = rows[0].id;
|
const sessionId = rows[0].id;
|
||||||
|
|
||||||
// Fire-and-forget: call agent-runner to start the execution loop
|
// Fire-and-forget: call agent-runner to start the execution loop.
|
||||||
// The agent runner is responsible for updating the session row as it works.
|
// autoApprove: true — agent commits + deploys automatically on completion.
|
||||||
fetch(`${AGENT_RUNNER_URL}/agent/execute`, {
|
fetch(`${AGENT_RUNNER_URL}/agent/execute`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -83,8 +89,10 @@ export async function POST(
|
|||||||
projectId,
|
projectId,
|
||||||
appName,
|
appName,
|
||||||
appPath,
|
appPath,
|
||||||
giteaRepo, // e.g. "mark/sportsy" — agent runner uses this to clone/update the repo
|
giteaRepo,
|
||||||
task: task.trim(),
|
task: task.trim(),
|
||||||
|
autoApprove: true,
|
||||||
|
coolifyAppUuid,
|
||||||
}),
|
}),
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
// Agent runner may not be wired yet — log but don't fail
|
// Agent runner may not be wired yet — log but don't fail
|
||||||
|
|||||||
Reference in New Issue
Block a user