fix(agents): tasks tab crashes when agent has autopilot run_only tasks

Autopilot `run_only` tasks have no linked issue; the server serializes
that as `issue_id: ""` (not null) via `uuidToString` on an invalid
pgtype.UUID. The agent detail Tasks tab assumed every task had a real
issue id and fed `""` into `api.getIssue(id)` → `/api/issues/` and into
`paths.issueDetail("")`, crashing the whole tab as soon as one such
task existed on the agent.

Handle the empty-issue case explicitly:

- Filter empty ids out of `issueIds` so `useQueries` doesn't fire
  `/api/issues/` for a nonexistent issue.
- Render run_only rows as non-link `<div>`s labeled "Autopilot run"
  instead of clickable issue links.

No server-side change — the `""` serialization stays as-is; callers
just need to treat it as "no issue".
This commit is contained in:
Jiang Bohan
2026-04-21 20:49:05 +08:00
parent 3fd2fb2ae3
commit 4c0259fa76

View File

@@ -31,8 +31,13 @@ export function TasksTab({ agent }: { agent: Agent }) {
// issue may or may not be in the paginated issue-list cache, so going
// through `issueDetailOptions` is the reliable lookup path (and it shares
// the same cache as the issue detail page).
//
// Autopilot `run_only` tasks have no linked issue; the server serializes
// that as issue_id = "". Filter those out before issuing detail queries
// so we don't hit `/api/issues/` with an empty id (which was crashing
// the whole tab).
const issueIds = useMemo(
() => Array.from(new Set(tasks.map((t) => t.issue_id))),
() => Array.from(new Set(tasks.map((t) => t.issue_id).filter((id) => id !== ""))),
[tasks],
);
const issueQueries = useQueries({
@@ -99,7 +104,10 @@ export function TasksTab({ agent }: { agent: Agent }) {
{sortedTasks.map((task) => {
const config = taskStatusConfig[task.status] ?? taskStatusConfig.queued!;
const Icon = config.icon;
const issue = issueMap.get(task.issue_id);
// Autopilot run_only tasks carry issue_id = "" — skip the lookup
// and render them as non-link rows labeled "Autopilot run".
const hasIssue = task.issue_id !== "";
const issue = hasIssue ? issueMap.get(task.issue_id) : undefined;
const isActive = task.status === "running" || task.status === "dispatched";
const isRunning = task.status === "running";
const rowClassName = `flex items-center gap-3 rounded-lg border px-4 py-3 transition-shadow hover:shadow-sm ${
@@ -125,7 +133,7 @@ export function TasksTab({ agent }: { agent: Agent }) {
</span>
)}
<span className={`text-sm truncate ${isActive ? "font-medium" : ""}`}>
{issue?.title ?? `Issue ${task.issue_id.slice(0, 8)}...`}
{issue?.title ?? (hasIssue ? `Issue ${task.issue_id.slice(0, 8)}...` : "Autopilot run")}
</span>
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
@@ -146,6 +154,14 @@ export function TasksTab({ agent }: { agent: Agent }) {
</>
);
if (!hasIssue) {
return (
<div key={task.id} className={rowClassName}>
{content}
</div>
);
}
return (
<AppLink
key={task.id}