Compare commits

...

2 Commits

Author SHA1 Message Date
Jiang Bohan
dd05b3ce4b fix(execution-log): reset retry button state on rerun success
The previous handler only reset `retrying` on error, but the past row
stays mounted (its `task.id` is unchanged) after a successful rerun, so
the Retry button hovered into a permanent spinner. Move the reset into
a finally block so both paths clear the loading state.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 14:21:21 +08:00
Jiang Bohan
be21aafb05 feat(execution-log): add one-click retry for failed/cancelled tasks (MUL-1922)
Adds a Retry icon button to past-run rows in the issue execution log so
users can re-enqueue failed or cancelled tasks without leaving the page.
The button calls POST /api/issues/{id}/rerun (already exposed by the CLI
issue rerun command) which cancels any prior task on the assignee and
spawns a fresh task with a new agent session.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 14:15:55 +08:00
4 changed files with 59 additions and 3 deletions

View File

@@ -803,6 +803,12 @@ export class ApiClient {
});
}
async rerunIssue(issueId: string): Promise<AgentTask> {
return this.fetch(`/api/issues/${issueId}/rerun`, {
method: "POST",
});
}
// Inbox
async listInbox(): Promise<InboxItem[]> {
return this.fetch("/api/inbox");

View File

@@ -2,7 +2,7 @@
import { useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight, Loader2, Square } from "lucide-react";
import { ChevronRight, Loader2, RotateCcw, Square } from "lucide-react";
import { toast } from "sonner";
import { api } from "@multica/core/api";
import { issueKeys } from "@multica/core/issues/queries";
@@ -165,7 +165,7 @@ export function ExecutionLogSection({ issueId }: ExecutionLogSectionProps) {
{showPast && (
<div className="mt-0.5 space-y-0.5">
{pastTasks.map((task) => (
<PastRow key={task.id} task={task} />
<PastRow key={task.id} task={task} issueId={issueId} />
))}
</div>
)}
@@ -320,8 +320,9 @@ function ActiveRow({ task, issueId }: { task: AgentTask; issueId: string }) {
// ─── Past row ──────────────────────────────────────────────────────────────
function PastRow({ task }: { task: AgentTask }) {
function PastRow({ task, issueId }: { task: AgentTask; issueId: string }) {
const { t } = useT("issues");
const [retrying, setRetrying] = useState(false);
const tone = STATUS_TONE[task.status];
const label = useStatusLabel(task.status);
const trigger = useTriggerText(task);
@@ -331,6 +332,27 @@ function PastRow({ task }: { task: AgentTask }) {
? failureReasonLabel[task.failure_reason as TaskFailureReason]
: null;
// Retry only makes sense for terminal-but-not-success rows. The rerun
// endpoint creates a fresh task on the issue's current agent assignee
// (not necessarily this row's agent) — clicking retry on a row whose
// agent has since been reassigned will rerun under the new assignee.
const canRetry = task.status === "failed" || task.status === "cancelled";
const handleRetry = async () => {
if (retrying) return;
setRetrying(true);
try {
await api.rerunIssue(issueId);
} catch (e) {
toast.error(e instanceof Error ? e.message : t(($) => $.execution_log.retry_failed));
} finally {
// Reset on both success and failure: the past row stays mounted
// (its task.id is unchanged), so leaving `retrying` true on success
// would pin the button as a permanent spinner.
setRetrying(false);
}
};
return (
<RowShell task={task}>
<TriggerText text={trigger} />
@@ -340,6 +362,28 @@ function PastRow({ task }: { task: AgentTask }) {
</span>
<RowActions>
<TranscriptButton task={task} agentName="" title={t(($) => $.execution_log.transcript_tooltip)} />
{canRetry && (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
onClick={handleRetry}
disabled={retrying}
aria-label={t(($) => $.execution_log.retry_task_aria)}
/>
}
className="flex items-center justify-center rounded p-1 text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
>
{retrying ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RotateCcw className="h-3.5 w-3.5" />
)}
</TooltipTrigger>
<TooltipContent>{t(($) => $.execution_log.retry_task_tooltip)}</TooltipContent>
</Tooltip>
)}
</RowActions>
</RowShell>
);

View File

@@ -214,6 +214,9 @@
"hide_past": "Hide past runs ({{count}})",
"cancel_task_tooltip": "Cancel task",
"cancel_task_aria": "Cancel task",
"retry_task_tooltip": "Retry task",
"retry_task_aria": "Retry task",
"retry_failed": "Failed to retry task",
"transcript_tooltip": "View transcript",
"cancel_failed": "Failed to cancel task",
"trigger_retry": "Retry",

View File

@@ -209,6 +209,9 @@
"hide_past": "隐藏历史运行({{count}}",
"cancel_task_tooltip": "取消 task",
"cancel_task_aria": "取消 task",
"retry_task_tooltip": "重试 task",
"retry_task_aria": "重试 task",
"retry_failed": "重试 task 失败",
"transcript_tooltip": "查看记录",
"cancel_failed": "取消 task 失败",
"trigger_retry": "重试",