fix(agent): context-aware task input, auto-select active session

- Running/pending: input locked with "agent is working" message
- Done: shows "+ Follow up" and "New task" buttons instead of open input
- No session: normal new-task input (unchanged UX)
- On mount: auto-selects the most recent running/pending session,
  falls back to latest session — so navigating away and back doesn't
  lose context and doesn't require manual re-selection

Made-with: Cursor
This commit is contained in:
2026-03-07 13:01:16 -08:00
parent 7f61295637
commit 7b228ebad2

View File

@@ -285,15 +285,26 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
if (el) el.scrollTop = el.scrollHeight; if (el) el.scrollTop = el.scrollHeight;
}, []); }, []);
// Load session list // Load session list — auto-select the most recent active or last session
useEffect(() => { useEffect(() => {
if (!appName) return; if (!appName) return;
setLoadingSessions(true); setLoadingSessions(true);
fetch(`/api/projects/${projectId}/agent/sessions`) fetch(`/api/projects/${projectId}/agent/sessions`)
.then(r => r.json()) .then(r => r.json())
.then(d => { setSessions(d.sessions ?? []); setLoadingSessions(false); }) .then(d => {
const list: AgentSession[] = d.sessions ?? [];
setSessions(list);
setLoadingSessions(false);
// Auto-select: prefer running/pending, then the most recent
if (list.length > 0 && !activeSessionId) {
const live = list.find(s => s.status === "running" || s.status === "pending");
const pick = live ?? list[0];
setActiveSessionId(pick.id);
setActiveSession(pick);
}
})
.catch(() => setLoadingSessions(false)); .catch(() => setLoadingSessions(false));
}, [projectId, appName]); }, [projectId, appName]); // eslint-disable-line react-hooks/exhaustive-deps
// Adaptive polling — 500ms while running, 5s when idle // Adaptive polling — 500ms while running, 5s when idle
useEffect(() => { useEffect(() => {
@@ -619,14 +630,94 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
</div> </div>
)} )}
{/* Task input */} {/* Task input — context-aware */}
{(() => {
const st = activeSession?.status;
// Agent is actively working — lock input
if (st === "running" || st === "pending") {
return (
<div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#3d5afe", flexShrink: 0, display: "inline-block" }} />
<span style={{ fontSize: "0.78rem", color: "#6b6560", fontFamily: "Outfit, sans-serif", flex: 1 }}>
Agent is working wait for it to finish, or stop it above.
</span>
</div>
</div>
);
}
// Session completed successfully — offer follow-up or new task
if (st === "done") {
return (
<div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}>
{showFollowUp ? (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ fontSize: "0.72rem", color: "#6b6560", fontFamily: "Outfit, sans-serif" }}>
Describe the next thing to build (continues from this session's context):
</div>
<div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
<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="e.g. Now add email notifications for the same feature…"
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",
}}
/>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<button onClick={() => { handleRun(); setShowFollowUp(false); }} disabled={submitting || !task.trim()}
style={{ padding: "8px 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", whiteSpace: "nowrap" }}>
{submitting ? "Starting…" : "Run"}
</button>
<button onClick={() => { setShowFollowUp(false); setTask(""); }}
style={{ padding: "6px 14px", background: "transparent", color: "#a09a90", border: "1px solid #e8e4dc", borderRadius: 7, fontSize: "0.73rem", cursor: "pointer", fontFamily: "Outfit, sans-serif" }}>
Cancel
</button>
</div>
</div>
<div style={{ fontSize: "0.63rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>
to run · Esc to cancel · Starts a new session for this task
</div>
</div>
) : (
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<span style={{ fontSize: "0.75rem", color: "#2e7d32", fontFamily: "Outfit, sans-serif", flex: 1 }}>
Done approve the changes above, or continue building.
</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>
)}
</div>
);
}
// No active session (or failed/stopped handled above) — new task input
if (!activeSession) {
return (
<div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}> <div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}>
<div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}> <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
<textarea <textarea
value={task} value={task}
onChange={e => setTask(e.target.value)} onChange={e => setTask(e.target.value)}
onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleRun(); }} onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleRun(); }}
placeholder={`Tell the agent what to build in ${appName || "this app"}`} placeholder={`Describe what to build in ${appName || "this app"}`}
rows={2} rows={2}
style={{ style={{
flex: 1, resize: "none", border: "1px solid #e8e4dc", borderRadius: 8, flex: 1, resize: "none", border: "1px solid #e8e4dc", borderRadius: 8,
@@ -648,9 +739,14 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
</button> </button>
</div> </div>
<div style={{ fontSize: "0.65rem", color: "#b5b0a6", marginTop: 5, fontFamily: "Outfit, sans-serif" }}> <div style={{ fontSize: "0.65rem", color: "#b5b0a6", marginTop: 5, fontFamily: "Outfit, sans-serif" }}>
to start · Each task is a new session · Use "Follow up" on a failed session to continue it to start · The agent works autonomously until done · You approve before anything is committed
</div> </div>
</div> </div>
);
}
return null;
})()}
</div> </div>
</div> </div>
); );