Follow up after stop (vibe-kanban 92c931e6) (#224)
I want the user to be able to continue a task after he pressed the stop attempt button via the follow-up functionality
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT \n t.id AS \"id!: Uuid\",\n t.project_id AS \"project_id!: Uuid\",\n t.title,\n t.description,\n t.status AS \"status!: TaskStatus\",\n t.parent_task_attempt AS \"parent_task_attempt: Uuid\", \n t.created_at AS \"created_at!: DateTime<Utc>\",\n t.updated_at AS \"updated_at!: DateTime<Utc>\",\n CASE \n WHEN ip.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_in_progress_attempt!: i64\",\n CASE \n WHEN ma.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_merged_attempt!: i64\",\n CASE \n WHEN fa.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_failed_attempt!: i64\",\n latest_executor_attempts.executor AS \"latest_attempt_executor\"\n FROM tasks t\n\n -- in-progress if any running setupscript/codingagent\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.status = 'running'\n AND ep.process_type IN ('setupscript','codingagent')\n ) ip \n ON t.id = ip.task_id\n\n -- merged if merge_commit not null\n LEFT JOIN (\n SELECT DISTINCT task_id\n FROM task_attempts\n WHERE merge_commit IS NOT NULL\n ) ma \n ON t.id = ma.task_id\n\n -- failed if latest attempt has a failed setupscript/codingagent\n LEFT JOIN (\n SELECT sub.task_id\n FROM (\n SELECT\n ta.task_id,\n ep.status,\n ep.process_type,\n ROW_NUMBER() OVER (\n PARTITION BY ta.task_id \n ORDER BY ta.created_at DESC\n ) AS rn\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.process_type IN ('setupscript','codingagent')\n ) sub\n WHERE sub.rn = 1\n AND sub.status IN ('failed','killed')\n ) fa\n ON t.id = fa.task_id\n\n -- get the executor of the latest attempt\n LEFT JOIN (\n SELECT task_id, executor\n FROM (\n SELECT task_id, executor, created_at,\n ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn\n FROM task_attempts\n ) latest_attempts\n WHERE rn = 1\n ) latest_executor_attempts \n ON t.id = latest_executor_attempts.task_id\n\n WHERE t.project_id = $1\n ORDER BY t.created_at DESC;\n ",
|
||||
"query": "SELECT \n t.id AS \"id!: Uuid\",\n t.project_id AS \"project_id!: Uuid\",\n t.title,\n t.description,\n t.status AS \"status!: TaskStatus\",\n t.parent_task_attempt AS \"parent_task_attempt: Uuid\", \n t.created_at AS \"created_at!: DateTime<Utc>\",\n t.updated_at AS \"updated_at!: DateTime<Utc>\",\n CASE \n WHEN ip.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_in_progress_attempt!: i64\",\n CASE \n WHEN ma.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_merged_attempt!: i64\",\n CASE \n WHEN fa.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"last_attempt_failed!: i64\",\n latest_executor_attempts.executor AS \"latest_attempt_executor\"\n FROM tasks t\n\n -- in-progress if any running setupscript/codingagent\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.status = 'running'\n AND ep.process_type IN ('setupscript','codingagent')\n ) ip \n ON t.id = ip.task_id\n\n -- merged if merge_commit not null\n LEFT JOIN (\n SELECT DISTINCT task_id\n FROM task_attempts\n WHERE merge_commit IS NOT NULL\n ) ma \n ON t.id = ma.task_id\n\n -- failed if latest execution process has a failed setupscript/codingagent\n LEFT JOIN (\n SELECT sub.task_id\n FROM (\n SELECT\n ta.task_id,\n ep.status,\n ep.process_type,\n ROW_NUMBER() OVER (\n PARTITION BY ta.task_id \n ORDER BY ep.created_at DESC\n ) AS rn\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.process_type IN ('setupscript','codingagent')\n ) sub\n WHERE sub.rn = 1\n AND sub.status IN ('failed','killed')\n ) fa\n ON t.id = fa.task_id\n\n -- get the executor of the latest attempt\n LEFT JOIN (\n SELECT task_id, executor\n FROM (\n SELECT task_id, executor, created_at,\n ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn\n FROM task_attempts\n ) latest_attempts\n WHERE rn = 1\n ) latest_executor_attempts \n ON t.id = latest_executor_attempts.task_id\n\n WHERE t.project_id = $1\n ORDER BY t.created_at DESC;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -54,7 +54,7 @@
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "has_failed_attempt!: i64",
|
||||
"name": "last_attempt_failed!: i64",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
@@ -82,5 +82,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a"
|
||||
"hash": "93a279fc7df10c38bdc1b0a3d7b48006147092bfb459e12424a55d42e6390d6a"
|
||||
}
|
||||
@@ -91,8 +91,8 @@ pub struct TaskSummary {
|
||||
pub has_in_progress_attempt: Option<bool>,
|
||||
#[schemars(description = "Whether the task has a merged execution attempt")]
|
||||
pub has_merged_attempt: Option<bool>,
|
||||
#[schemars(description = "Whether the task has a failed execution attempt")]
|
||||
pub has_failed_attempt: Option<bool>,
|
||||
#[schemars(description = "Whether the last execution attempt failed")]
|
||||
pub last_attempt_failed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, schemars::JsonSchema)]
|
||||
@@ -449,7 +449,7 @@ impl TaskServer {
|
||||
updated_at: task.updated_at.to_rfc3339(),
|
||||
has_in_progress_attempt: Some(task.has_in_progress_attempt),
|
||||
has_merged_attempt: Some(task.has_merged_attempt),
|
||||
has_failed_attempt: Some(task.has_failed_attempt),
|
||||
last_attempt_failed: Some(task.last_attempt_failed),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -597,7 +597,7 @@ impl TaskServer {
|
||||
updated_at: updated_task.updated_at.to_rfc3339(),
|
||||
has_in_progress_attempt: None,
|
||||
has_merged_attempt: None,
|
||||
has_failed_attempt: None,
|
||||
last_attempt_failed: None,
|
||||
};
|
||||
|
||||
let response = UpdateTaskResponse {
|
||||
@@ -768,7 +768,7 @@ impl TaskServer {
|
||||
updated_at: task.updated_at.to_rfc3339(),
|
||||
has_in_progress_attempt: None,
|
||||
has_merged_attempt: None,
|
||||
has_failed_attempt: None,
|
||||
last_attempt_failed: None,
|
||||
};
|
||||
|
||||
let response = GetTaskResponse {
|
||||
|
||||
@@ -42,7 +42,7 @@ pub struct TaskWithAttemptStatus {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub has_in_progress_attempt: bool,
|
||||
pub has_merged_attempt: bool,
|
||||
pub has_failed_attempt: bool,
|
||||
pub last_attempt_failed: bool,
|
||||
pub latest_attempt_executor: Option<String>,
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ impl Task {
|
||||
CASE
|
||||
WHEN fa.task_id IS NOT NULL THEN true
|
||||
ELSE false
|
||||
END AS "has_failed_attempt!: i64",
|
||||
END AS "last_attempt_failed!: i64",
|
||||
latest_executor_attempts.executor AS "latest_attempt_executor"
|
||||
FROM tasks t
|
||||
|
||||
@@ -123,7 +123,7 @@ impl Task {
|
||||
) ma
|
||||
ON t.id = ma.task_id
|
||||
|
||||
-- failed if latest attempt has a failed setupscript/codingagent
|
||||
-- failed if latest execution process has a failed setupscript/codingagent
|
||||
LEFT JOIN (
|
||||
SELECT sub.task_id
|
||||
FROM (
|
||||
@@ -133,7 +133,7 @@ impl Task {
|
||||
ep.process_type,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY ta.task_id
|
||||
ORDER BY ta.created_at DESC
|
||||
ORDER BY ep.created_at DESC
|
||||
) AS rn
|
||||
FROM task_attempts ta
|
||||
JOIN execution_processes ep
|
||||
@@ -178,7 +178,7 @@ impl Task {
|
||||
updated_at: rec.updated_at,
|
||||
has_in_progress_attempt: rec.has_in_progress_attempt != 0,
|
||||
has_merged_attempt: rec.has_merged_attempt != 0,
|
||||
has_failed_attempt: rec.has_failed_attempt != 0,
|
||||
last_attempt_failed: rec.last_attempt_failed != 0,
|
||||
latest_attempt_executor: rec.latest_attempt_executor,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -102,7 +102,7 @@ export function TaskCard({
|
||||
<CheckCircle className="h-3 w-3 text-green-500" />
|
||||
)}
|
||||
{/* Failed Indicator */}
|
||||
{task.has_failed_attempt && !task.has_merged_attempt && (
|
||||
{task.last_attempt_failed && !task.has_merged_attempt && (
|
||||
<XCircle className="h-3 w-3 text-red-500" />
|
||||
)}
|
||||
{/* Actions Menu */}
|
||||
|
||||
@@ -78,12 +78,33 @@ function LogsTab() {
|
||||
|
||||
// When setup failed or was stopped
|
||||
if (isSetupFailed || isSetupStopped) {
|
||||
const setupProcess = executionState.setup_process_id
|
||||
let setupProcess = executionState.setup_process_id
|
||||
? attemptData.runningProcessDetails[executionState.setup_process_id]
|
||||
: Object.values(attemptData.runningProcessDetails).find(
|
||||
(process) => process.process_type === 'setupscript'
|
||||
);
|
||||
|
||||
// If not found in runningProcessDetails, try to find in processes array
|
||||
if (!setupProcess) {
|
||||
const setupSummary = attemptData.processes.find(
|
||||
(process) => process.process_type === 'setupscript'
|
||||
);
|
||||
|
||||
if (setupSummary) {
|
||||
setupProcess = Object.values(attemptData.runningProcessDetails).find(
|
||||
(process) => process.id === setupSummary.id
|
||||
);
|
||||
|
||||
if (!setupProcess) {
|
||||
setupProcess = {
|
||||
...setupSummary,
|
||||
stdout: null,
|
||||
stderr: null,
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="mb-4">
|
||||
@@ -106,38 +127,15 @@ function LogsTab() {
|
||||
);
|
||||
}
|
||||
|
||||
// When coding agent failed or was stopped
|
||||
if (isCodingAgentFailed || isCodingAgentStopped) {
|
||||
const codingAgentProcess = executionState.coding_agent_process_id
|
||||
? attemptData.runningProcessDetails[
|
||||
executionState.coding_agent_process_id
|
||||
]
|
||||
: Object.values(attemptData.runningProcessDetails).find(
|
||||
(process) => process.process_type === 'codingagent'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="mb-4">
|
||||
<p
|
||||
className={`text-lg font-semibold mb-2 ${isCodingAgentFailed ? 'text-destructive' : ''}`}
|
||||
>
|
||||
{isCodingAgentFailed
|
||||
? 'Coding Agent Failed'
|
||||
: 'Coding Agent Stopped'}
|
||||
</p>
|
||||
{isCodingAgentFailed && (
|
||||
<p className="text-muted-foreground mb-4">
|
||||
The coding agent encountered an error. Error details below:
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{codingAgentProcess && (
|
||||
<NormalizedConversationViewer executionProcess={codingAgentProcess} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
// When coding agent is in any state (running, complete, failed, stopped)
|
||||
if (
|
||||
isCodingAgentRunning ||
|
||||
isCodingAgentComplete ||
|
||||
isCodingAgentFailed ||
|
||||
isCodingAgentStopped ||
|
||||
hasChanges
|
||||
) {
|
||||
return <Conversation />;
|
||||
}
|
||||
|
||||
// When setup is complete but coding agent hasn't started, show waiting state
|
||||
|
||||
@@ -171,6 +171,21 @@ function Conversation() {
|
||||
allEntries,
|
||||
]);
|
||||
|
||||
// Check if we should show the status banner - only if the most recent process failed/stopped
|
||||
const getMostRecentProcess = () => {
|
||||
if (followUpLogs.length > 0) {
|
||||
// Sort by creation time or use last in array as most recent
|
||||
return followUpLogs[followUpLogs.length - 1];
|
||||
}
|
||||
return mainCodingAgentLog;
|
||||
};
|
||||
|
||||
const mostRecentProcess = getMostRecentProcess();
|
||||
const showStatusBanner =
|
||||
mostRecentProcess &&
|
||||
(mostRecentProcess.status === 'failed' ||
|
||||
mostRecentProcess.status === 'killed');
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
@@ -209,6 +224,28 @@ function Conversation() {
|
||||
className="py-8"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Status banner for failed/stopped states - shown at bottom */}
|
||||
{showStatusBanner && mostRecentProcess && (
|
||||
<div className="mt-4 p-4 rounded-lg border">
|
||||
<p
|
||||
className={`text-lg font-semibold mb-2 ${
|
||||
mostRecentProcess.status === 'failed'
|
||||
? 'text-destructive'
|
||||
: 'text-orange-600'
|
||||
}`}
|
||||
>
|
||||
{mostRecentProcess.status === 'failed'
|
||||
? 'Coding Agent Failed'
|
||||
: 'Coding Agent Stopped'}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{mostRecentProcess.status === 'failed'
|
||||
? 'The coding agent encountered an error.'
|
||||
: 'The coding agent was stopped.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,12 +32,13 @@ export function TaskFollowUpSection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const completedCodingAgentProcesses = attemptData.processes.filter(
|
||||
const completedOrKilledCodingAgentProcesses = attemptData.processes.filter(
|
||||
(process) =>
|
||||
process.process_type === 'codingagent' && process.status === 'completed'
|
||||
process.process_type === 'codingagent' &&
|
||||
(process.status === 'completed' || process.status === 'killed')
|
||||
);
|
||||
|
||||
return completedCodingAgentProcesses.length > 0;
|
||||
return completedOrKilledCodingAgentProcesses.length > 0;
|
||||
}, [
|
||||
selectedAttempt,
|
||||
attemptData.processes,
|
||||
@@ -81,7 +82,7 @@ export function TaskFollowUpSection() {
|
||||
)}
|
||||
<div className="flex gap-2 items-start">
|
||||
<FileSearchTextarea
|
||||
placeholder="Ask a follow-up question... Type @ to search files."
|
||||
placeholder="Continue working on this task... Type @ to search files."
|
||||
value={followUpMessage}
|
||||
onChange={(value) => {
|
||||
setFollowUpMessage(value);
|
||||
|
||||
@@ -50,7 +50,7 @@ export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelle
|
||||
|
||||
export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, parent_task_attempt: string | null, created_at: string, updated_at: string, };
|
||||
|
||||
export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, parent_task_attempt: string | null, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, has_failed_attempt: boolean, latest_attempt_executor: string | null, };
|
||||
export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, parent_task_attempt: string | null, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, last_attempt_failed: boolean, latest_attempt_executor: string | null, };
|
||||
|
||||
export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, parent_task_attempt: string | null, };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user