open-swe/ui/src/components/agents/AgentRunCard.tsx
Johannes du Plessis dee78e7e84
fix: make repository optional when starting a dashboard run (#1397)
* fix: make repository optional when starting a dashboard run

The agent infers and clones the target repo from the task itself, so a
default repo is never actually required to run — but the dashboard 400'd
("no default repository configured") when a user had none set.

Treat repo as optional: _resolve_repo_config returns {} instead of
raising, repo metadata/config are only written when a repo is present,
and the "missing repository metadata" gate on follow-up messages is
dropped. UI hides the repo chip when absent.

* feat: add repo picker to the run prompt bar

Adds an optional, searchable repository selector next to the model picker
on the Agents home prompt bar (Cursor-style). It pre-fills the user's saved
default repo and can be cleared to "No repository" for a repo-less run.

Because the picker now resolves the default on the client, the create
endpoint honors the request value verbatim: _resolve_repo_config just parses
what's sent ({} when empty) instead of falling back to the saved default,
so an explicit "No repository" is respected.

* refactor: match Cursor layout for repo placement

Move the repo selector out of the prompt-box footer to a pill row above
the input (folder + caret, dropdown opens downward); the model picker
stays inside the box. In the thread view, show the thread title and repo
in a header at the top of the chat, with the follow-up input pinned to
the bottom as before.
2026-06-03 14:24:08 -07:00

105 lines
3.6 KiB
TypeScript

import { Link } from "@tanstack/react-router";
import {
ChatCircleIcon,
CheckCircleIcon,
GitBranchIcon,
GitPullRequestIcon,
} from "@phosphor-icons/react";
import { IoLogoGithub, IoLogoSlack } from "react-icons/io5";
import { SiLinear } from "react-icons/si";
import type { ComponentType, SVGProps } from "react";
import type { AgentSource, AgentThread } from "@/lib/agents/types";
import { formatRelativeTime } from "@/lib/agents/api";
import { cn } from "@/lib/utils";
type SourceIcon = ComponentType<SVGProps<SVGSVGElement>>;
const SOURCE_META: Record<AgentSource, { icon: SourceIcon; label: string }> = {
dashboard: { icon: ChatCircleIcon, label: "Dashboard" },
github: { icon: IoLogoGithub, label: "GitHub" },
slack: { icon: IoLogoSlack, label: "Slack" },
linear: { icon: SiLinear, label: "Linear" },
};
interface AgentRunCardProps {
thread: AgentThread;
}
export function AgentRunCard({ thread }: AgentRunCardProps) {
const stats = thread.diffStats;
const hasPr = Boolean(thread.pr);
const source = thread.source ? SOURCE_META[thread.source] : null;
const SourceIcon = source?.icon;
return (
<Link
to="/agents/$threadId"
params={{ threadId: thread.id }}
className="group flex items-center gap-4 rounded-xl border border-[var(--ui-border)] bg-[var(--ui-surface)] px-4 py-3 transition-colors hover:border-[var(--ui-accent)]/30 hover:bg-[var(--ui-panel)]"
>
<div className="flex size-[72px] shrink-0 flex-col items-center justify-center rounded-lg border border-[var(--ui-border)] bg-[var(--ui-panel-2)] text-center">
{stats ? (
<>
<div className="text-[11px] font-medium text-[var(--ui-text-muted)]">
{stats.files} {stats.files === 1 ? "file" : "files"}
</div>
<div className="mt-0.5 flex items-center gap-1.5 text-xs font-medium">
<span className="text-[var(--ui-success)]">+{stats.additions}</span>
<span className="text-[var(--ui-danger)]">-{stats.deletions}</span>
</div>
</>
) : (
<GitBranchIcon className="size-5 text-[var(--ui-text-dim)]" />
)}
<div className="mt-1.5 flex items-center gap-1 text-[10px] text-[var(--ui-text-dim)]">
{hasPr ? (
<>
<GitPullRequestIcon className="size-3" />
Draft
</>
) : (
<>
<GitBranchIcon className="size-3" />
Branch
</>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-[var(--ui-text)]">{thread.title}</div>
<div className="mt-1 flex items-center gap-2 text-xs text-[var(--ui-text-dim)]">
{source && SourceIcon && (
<>
<span className="flex items-center gap-1" title={source.label}>
<SourceIcon className="size-3.5" aria-label={source.label} />
{source.label}
</span>
<span>·</span>
</>
)}
<span>{thread.model}</span>
{thread.repo && (
<>
<span>·</span>
<span>{thread.repo}</span>
</>
)}
<span>·</span>
<span>{formatRelativeTime(thread.updatedAt)}</span>
</div>
</div>
<CheckCircleIcon
className={cn(
"size-5 shrink-0",
thread.status === "finished"
? "text-[var(--ui-text-dim)] opacity-100"
: "opacity-0",
)}
weight="regular"
/>
</Link>
);
}