fix: hide start agent button during preparation and fix auto-start race condition on step move (#516)

This commit is contained in:
Carlos Florêncio
2026-04-01 23:00:05 +01:00
committed by GitHub
parent 348ee25e6f
commit 6fea6277a8
4 changed files with 246 additions and 0 deletions
@@ -548,6 +548,19 @@ func (s *Service) autoStartStepPrompt(
// Record a user message so the auto-start prompt is visible in chat history.
s.recordAutoStartMessage(ctx, taskID, sessionID, prompt, planMode)
// If the session is in CREATED state, the agent was never started (e.g. workspace-only
// preparation from a blocked auto-start). PromptTask will reject CREATED sessions,
// so use StartCreatedSession which properly launches the agent on the prepared workspace.
// Pass skipMessageRecord=true since recordAutoStartMessage above already recorded it.
if session.State == models.TaskSessionStateCreated {
s.logger.Info("auto-start: session is CREATED, launching agent via StartCreatedSession",
zap.String("task_id", taskID),
zap.String("session_id", sessionID),
zap.String("step_name", stepName))
_, err := s.StartCreatedSession(ctx, taskID, sessionID, session.AgentProfileID, prompt, true, planMode, nil)
return err
}
const maxRetryAttempts = 5
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
_, err := s.PromptTask(ctx, taskID, sessionID, prompt, "", planMode, nil)
@@ -7,6 +7,7 @@ import type { Message, TaskSessionState, TaskState } from "@/lib/types/http";
import type { ToolCallMetadata } from "@/components/task/chat/types";
import { launchSession } from "@/lib/services/session-launch-service";
import { buildStartCreatedRequest } from "@/lib/services/session-launch-helpers";
import { useAppStore } from "@/components/state-provider";
import { ChatMessage } from "@/components/task/chat/messages/chat-message";
import { PermissionRequestMessage } from "@/components/task/chat/messages/permission-request-message";
import { StatusMessage } from "@/components/task/chat/messages/status-message";
@@ -39,6 +40,10 @@ type AdapterContext = {
function TaskDescriptionStartButton({ taskId, sessionId }: { taskId: string; sessionId: string }) {
const [isStarting, setIsStarting] = useState(false);
const prepareStatus = useAppStore(
(state) => state.prepareProgress.bySessionId[sessionId]?.status ?? null,
);
const handleStart = useCallback(async () => {
setIsStarting(true);
try {
@@ -51,6 +56,9 @@ function TaskDescriptionStartButton({ taskId, sessionId }: { taskId: string; ses
}
}, [taskId, sessionId]);
// Hide while environment is being prepared
if (prepareStatus === "preparing") return null;
return (
<div className="flex justify-end mt-1.5">
<Button
@@ -59,6 +67,7 @@ function TaskDescriptionStartButton({ taskId, sessionId }: { taskId: string; ses
className="cursor-pointer gap-1.5"
onClick={handleStart}
disabled={isStarting}
data-testid="task-description-start-button"
>
<IconPlayerPlay className="h-3.5 w-3.5" />
{isStarting ? "Starting…" : "Start agent"}
@@ -0,0 +1,124 @@
import { test, expect } from "../../fixtures/test-base";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
test.describe("PR watcher auto-start on move", () => {
/**
* Race condition: When a task created from a PR watcher sits on a step
* WITHOUT auto_start_agent, the user opens the task (creating a CREATED
* session with workspace-only preparation), then moves the task to a step
* WITH auto_start_agent on the kanban board.
*
* BUG: The on_enter auto_start_agent action calls PromptTask which rejects
* CREATED sessions, causing the prompt to be queued instead of starting
* the agent. The queued prompt is never consumed because no agent is running.
*
* EXPECTED: The agent starts immediately on the prepared workspace.
*
* Flow:
* 1. Create workflow: Waiting (no auto_start) → Review (auto_start) → Done
* 2. Create task as PR watcher would (metadata.agent_profile_id)
* 3. Open task → session auto-prepares (CREATED, workspace-only)
* 4. Go back to kanban
* 5. Move task from Waiting to Review via API
* 6. Open the task
* 7. Assert: agent starts and completes (NOT queued)
*/
test("starts agent immediately when moving task with CREATED session to auto-start step", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
// --- Create workflow: Waiting → Review (auto_start) → Done ---
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "PR Move Workflow");
const waitingStep = await apiClient.createWorkflowStep(workflow.id, "Waiting", 0);
const reviewStep = await apiClient.createWorkflowStep(workflow.id, "Review", 1);
const doneStep = await apiClient.createWorkflowStep(workflow.id, "Done", 2);
// Configure Review step: auto_start + move to Done on turn complete
await apiClient.updateWorkflowStep(reviewStep.id, {
prompt: '/e2e:message("review complete")\n{{task_prompt}}',
events: {
on_enter: [{ type: "auto_start_agent" }],
on_turn_complete: [{ type: "move_to_step", config: { step_id: doneStep.id } }],
},
});
await apiClient.saveUserSettings({
workspace_id: seedData.workspaceId,
workflow_filter_id: workflow.id,
enable_preview_on_click: false,
});
// --- Create task as PR watcher would ---
const task = await apiClient.createTask(seedData.workspaceId, "PR #55: Refactor auth", {
description: "Review the auth refactoring changes",
workflow_id: workflow.id,
workflow_step_id: waitingStep.id,
repositories: [
{
repository_id: seedData.repositoryId,
base_branch: "main",
},
],
metadata: {
agent_profile_id: seedData.agentProfileId,
pr_number: 55,
pr_branch: "refactor/auth",
pr_repo: "testorg/testrepo",
pr_author: "contributor",
},
});
// --- Navigate to kanban and verify task in Waiting ---
const kanban = new KanbanPage(testPage);
await kanban.goto();
const card = kanban.taskCardInColumn("PR #55: Refactor auth", waitingStep.id);
await expect(card).toBeVisible({ timeout: 15_000 });
// --- Open task → creates CREATED session (workspace-only preparation) ---
await card.click();
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
const session = new SessionPage(testPage);
await session.waitForLoad();
// Wait for the session to be created (auto-prepare fires from useAutoStartSession)
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.length > 0 && sessions[0]?.state === "CREATED";
},
{ timeout: 30_000, message: "Waiting for session to be created in CREATED state" },
)
.toBe(true);
// --- Go back to kanban ---
await kanban.goto();
await expect(kanban.taskCardByTitle("PR #55: Refactor auth")).toBeVisible({ timeout: 15_000 });
// --- Move task from Waiting to Review (triggers on_enter: auto_start_agent) ---
await apiClient.moveTask(task.id, workflow.id, reviewStep.id);
// --- Task should move through Review to Done after agent completes ---
// If the bug is present, the prompt would be queued and the agent never starts,
// so the task would stay in Review indefinitely.
const cardInDone = kanban.taskCardInColumn("PR #55: Refactor auth", doneStep.id);
await expect(cardInDone).toBeVisible({ timeout: 60_000 });
// --- Navigate to session and verify agent completed (not stuck with queued prompt) ---
await cardInDone.click();
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
const sessionAfter = new SessionPage(testPage);
await sessionAfter.waitForLoad();
// The agent should have completed — idle input visible
await expect(sessionAfter.idleInput()).toBeVisible({ timeout: 30_000 });
});
});
@@ -0,0 +1,100 @@
import { test, expect } from "../../fixtures/test-base";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
test.describe("PR watcher start button timing", () => {
/**
* When a task is created from a PR watcher on a workflow step WITHOUT
* auto_start_agent, opening the task triggers environment preparation
* (workspace-only, no agent). The "Start agent" button should appear
* after preparation completes and clicking it should start the agent.
*
* Flow:
* 1. Create workflow step without auto_start_agent
* 2. Create a task as the PR watcher would (with metadata.agent_profile_id)
* 3. Navigate to task — auto-start fires, backend downgrades to prepare
* 4. Wait for CREATED session to exist (env preparing)
* 5. Reload page so SSR picks up the session and renders it
* 6. Assert: "Start agent" button becomes visible
* 7. Click the button — agent starts and completes
*/
test("start button appears after environment preparation and starts agent", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
// --- Create a workflow step WITHOUT auto_start_agent ---
const reviewStep = await apiClient.createWorkflowStep(seedData.workflowId, "Review", 0);
await apiClient.saveUserSettings({
workspace_id: seedData.workspaceId,
workflow_filter_id: seedData.workflowId,
enable_preview_on_click: false,
});
// --- Create a task as the PR watcher would ---
const task = await apiClient.createTask(seedData.workspaceId, "PR #42: Add feature", {
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: reviewStep.id,
repositories: [
{
repository_id: seedData.repositoryId,
base_branch: "main",
},
],
metadata: {
agent_profile_id: seedData.agentProfileId,
pr_number: 42,
pr_branch: "feature/add-feature",
pr_repo: "testorg/testrepo",
pr_author: "contributor",
},
});
// --- Navigate to kanban and click the task ---
const kanban = new KanbanPage(testPage);
await kanban.goto();
const card = kanban.taskCardByTitle("PR #42: Add feature");
await expect(card).toBeVisible({ timeout: 15_000 });
await card.click();
// Wait for navigation to session view
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
// --- Wait for auto-start to create a CREATED session ---
// useAutoStartSession fires with autoStart=true, backend downgrades to
// prepare (step lacks auto_start_agent). Session stays in CREATED state.
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.length > 0;
},
{ timeout: 30_000, message: "Waiting for session to be created" },
)
.toBe(true);
// --- Reload so SSR picks up the session and renders the chat ---
// After auto-start creates the CREATED session, the frontend doesn't
// automatically set it as the active session. Reloading ensures SSR
// resolves the session and the chat panel renders with the task description.
await testPage.reload();
const session = new SessionPage(testPage);
await session.waitForLoad();
// --- Assert: "Start agent" button becomes visible after prep completes ---
const startButton = session.chat.getByTestId("task-description-start-button");
await expect(startButton).toBeVisible({ timeout: 60_000 });
// --- Click the button to start the agent ---
await startButton.click();
// Agent should start and eventually complete (mock agent with simple message)
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
});
});