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,38 +630,123 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
</div> </div>
)} )}
{/* Task input */} {/* Task input — context-aware */}
<div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}> {(() => {
<div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}> const st = activeSession?.status;
<textarea
value={task} // Agent is actively working — lock input
onChange={e => setTask(e.target.value)} if (st === "running" || st === "pending") {
onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleRun(); }} return (
placeholder={`Tell the agent what to build in ${appName || "this app"}`} <div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}>
rows={2} <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
style={{ <span style={{ width: 7, height: 7, borderRadius: "50%", background: "#3d5afe", flexShrink: 0, display: "inline-block" }} />
flex: 1, resize: "none", border: "1px solid #e8e4dc", borderRadius: 8, <span style={{ fontSize: "0.78rem", color: "#6b6560", fontFamily: "Outfit, sans-serif", flex: 1 }}>
padding: "9px 12px", fontSize: "0.8rem", fontFamily: "Outfit, sans-serif", Agent is working wait for it to finish, or stop it above.
color: "#1a1a1a", outline: "none", background: "#faf8f5", </span>
}} </div>
/> </div>
<button );
onClick={handleRun} }
disabled={submitting || !task.trim()}
style={{ // Session completed successfully — offer follow-up or new task
padding: "10px 18px", background: task.trim() ? "#1a1a1a" : "#e8e4dc", if (st === "done") {
color: task.trim() ? "#fff" : "#a09a90", border: "none", borderRadius: 8, return (
fontSize: "0.78rem", fontWeight: 600, cursor: task.trim() ? "pointer" : "default", <div style={{ borderTop: "1px solid #e8e4dc", background: "#fff", padding: "12px 16px", flexShrink: 0 }}>
fontFamily: "Outfit, sans-serif", whiteSpace: "nowrap", {showFollowUp ? (
}} <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
> <div style={{ fontSize: "0.72rem", color: "#6b6560", fontFamily: "Outfit, sans-serif" }}>
{submitting ? "Starting…" : "Run"} Describe the next thing to build (continues from this session's context):
</button> </div>
</div> <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
<div style={{ fontSize: "0.65rem", color: "#b5b0a6", marginTop: 5, fontFamily: "Outfit, sans-serif" }}> <textarea
to start · Each task is a new session · Use "Follow up" on a failed session to continue it autoFocus
</div> value={task}
</div> 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={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
<textarea
value={task}
onChange={e => setTask(e.target.value)}
onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleRun(); }}
placeholder={`Describe what to build in ${appName || "this app"}`}
rows={2}
style={{
flex: 1, resize: "none", border: "1px solid #e8e4dc", borderRadius: 8,
padding: "9px 12px", fontSize: "0.8rem", fontFamily: "Outfit, sans-serif",
color: "#1a1a1a", outline: "none", background: "#faf8f5",
}}
/>
<button
onClick={handleRun}
disabled={submitting || !task.trim()}
style={{
padding: "10px 18px", background: task.trim() ? "#1a1a1a" : "#e8e4dc",
color: task.trim() ? "#fff" : "#a09a90", border: "none", borderRadius: 8,
fontSize: "0.78rem", fontWeight: 600, cursor: task.trim() ? "pointer" : "default",
fontFamily: "Outfit, sans-serif", whiteSpace: "nowrap",
}}
>
{submitting ? "Starting…" : "Run"}
</button>
</div>
<div style={{ fontSize: "0.65rem", color: "#b5b0a6", marginTop: 5, fontFamily: "Outfit, sans-serif" }}>
to start · The agent works autonomously until done · You approve before anything is committed
</div>
</div>
);
}
return null;
})()}
</div> </div>
</div> </div>
); );