mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
fix(agent-manager): color PR badge by state, show checks as icon
Badge color now reflects only the PR lifecycle state (open/draft/merged/closed), so a failing check is no longer mistaken for a closed PR. CI and review results are shown as a separate icon (failing checks, changes requested, approved), and an open PR whose checks are still running keeps its pulsing amber badge.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Color Agent Manager PR badges by pull request state (open, draft, merged, closed) and show CI and review status as a separate icon, so a failing check is no longer mistaken for a closed PR.
|
||||
@@ -2,7 +2,7 @@
|
||||
* Sidebar worktree item with inline delete confirmation, HoverCard, rename, and stats.
|
||||
* Extracted from AgentManagerApp for reuse and visual-regression testing via Storybook.
|
||||
*/
|
||||
import { Component, For, Show, createSignal } from "solid-js"
|
||||
import { Component, For, Match, Show, Switch, createSignal } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Spinner } from "@kilocode/kilo-ui/spinner"
|
||||
@@ -84,17 +84,43 @@ const MAX_SHORTCUT = 9
|
||||
const hasStats = (s: WorktreeGitStats | undefined): s is WorktreeGitStats =>
|
||||
!!s && (s.files > 0 || s.additions > 0 || s.deletions > 0 || s.ahead > 0 || s.behind > 0)
|
||||
|
||||
/** Returns the accent color for a PR badge based on state priority. */
|
||||
/**
|
||||
* Accent color for a PR badge, derived from the PR's lifecycle state
|
||||
* (open/draft/merged/closed). The one exception is an open PR with checks still
|
||||
* running, which uses amber and pulses its background (see prChecksRunning) — a
|
||||
* transient, unambiguous signal since no lifecycle state uses amber. Terminal CI
|
||||
* and review results are conveyed by a separate status icon (see prBadgeIndicator)
|
||||
* so a failing check is not mistaken for a closed PR.
|
||||
*/
|
||||
export function prAccentColor(pr: PRStatus): string {
|
||||
if (pr.state === "draft") return "var(--text-weaker)"
|
||||
if (pr.state === "merged") return "#a78bfa"
|
||||
if (pr.state === "closed") return "#f87171"
|
||||
if (pr.checks.status === "failure") return "#ef4444"
|
||||
if (pr.review === "changes_requested") return "#fbbf24"
|
||||
if (pr.checks.status === "pending") return "#fbbf24"
|
||||
return "#34d399"
|
||||
}
|
||||
|
||||
/** True while an open PR's checks are still running — drives the pulsing amber badge. */
|
||||
export function prChecksRunning(pr: PRStatus): boolean {
|
||||
return pr.state === "open" && pr.checks.status === "pending"
|
||||
}
|
||||
|
||||
export type PRBadgeIndicator = "failure" | "changes" | "approved" | "none"
|
||||
|
||||
/**
|
||||
* Terminal CI/review status shown as an icon overlaid on the PR badge, independent
|
||||
* of the badge's state-based accent color. Running checks are not represented here —
|
||||
* they are shown by the pulsing amber background instead. Terminal PRs (merged/closed)
|
||||
* show no indicator since their checks are no longer actionable.
|
||||
*/
|
||||
export function prBadgeIndicator(pr: PRStatus): PRBadgeIndicator {
|
||||
if (pr.state === "merged" || pr.state === "closed") return "none"
|
||||
if (pr.checks.status === "failure") return "failure"
|
||||
if (pr.review === "changes_requested") return "changes"
|
||||
if (pr.review === "approved") return "approved"
|
||||
return "none"
|
||||
}
|
||||
|
||||
function prStateLabel(state: PRStatus["state"]): string {
|
||||
if (state === "draft") return "Draft"
|
||||
if (state === "merged") return "Merged"
|
||||
@@ -315,14 +341,40 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
|
||||
>
|
||||
{(pr) => {
|
||||
const accent = () => prAccentColor(pr())
|
||||
const indicator = () => prBadgeIndicator(pr())
|
||||
return (
|
||||
<span
|
||||
class="am-pr-badge"
|
||||
style={{ "--pr-accent": accent() }}
|
||||
data-pending={pr().state === "open" && pr().checks.status === "pending" ? "" : undefined}
|
||||
data-pending={prChecksRunning(pr()) ? "" : undefined}
|
||||
onClick={handleOpenPR}
|
||||
>
|
||||
<Icon name={pr().review === "approved" ? "check-small" : "branch"} size="small" />
|
||||
<Switch fallback={<Icon name="branch" size="small" />}>
|
||||
<Match when={indicator() === "failure"}>
|
||||
<Icon
|
||||
name="circle-x"
|
||||
size="small"
|
||||
class="am-pr-badge-status"
|
||||
style={{ color: "#ef4444" }}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={indicator() === "changes"}>
|
||||
<Icon
|
||||
name="warning"
|
||||
size="small"
|
||||
class="am-pr-badge-status"
|
||||
style={{ color: "#fbbf24" }}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={indicator() === "approved"}>
|
||||
<Icon
|
||||
name="circle-check"
|
||||
size="small"
|
||||
class="am-pr-badge-status"
|
||||
style={{ color: "#34d399" }}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<span class="am-pr-badge-number">#{pr().number}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -710,6 +710,7 @@ button.am-section-toggle:hover .am-section-label {
|
||||
.am-pr-badge:hover .am-pr-badge-number {
|
||||
color: var(--pr-accent);
|
||||
}
|
||||
/* Checks still running — pulse the whole amber badge. */
|
||||
.am-pr-badge[data-pending] {
|
||||
animation: am-pr-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -573,10 +573,11 @@ export const WithPRBadges: Story = {
|
||||
name: "Section — worktrees with PR badges",
|
||||
render: () => (
|
||||
<StoryProviders noPadding>
|
||||
<div style={{ "max-height": "400px", overflow: "auto" }}>
|
||||
<div style={{ "max-height": "560px", overflow: "auto" }}>
|
||||
<DndWrap>
|
||||
<SectionHeader section={sec("s1", 0, { name: "In Review", color: "Blue" })} count={3} {...sectionProps}>
|
||||
<SectionHeader section={sec("s1", 0, { name: "In Review", color: "Blue" })} count={8} {...sectionProps}>
|
||||
<div class="am-section-group-body">
|
||||
{/* Open + passing + approved → green badge, green check */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-1", "feat/api-v2")}
|
||||
@@ -595,6 +596,7 @@ export const WithPRBadges: Story = {
|
||||
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Open + failing checks → green badge, red ✗ (no longer confusable with closed) */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-2", "fix/race-cond")}
|
||||
@@ -605,28 +607,101 @@ export const WithPRBadges: Story = {
|
||||
title: "fix: race condition",
|
||||
url: "#",
|
||||
state: "open",
|
||||
review: "changes_requested",
|
||||
review: null,
|
||||
additions: 15,
|
||||
deletions: 8,
|
||||
files: 3,
|
||||
checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Open + changes requested → green badge, amber warning */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-3", "feat/cache")}
|
||||
worktree={wt("wt-3", "feat/search")}
|
||||
label="feat/search"
|
||||
subtitle="feat/search"
|
||||
pr={{
|
||||
number: 91,
|
||||
title: "feat: search",
|
||||
url: "#",
|
||||
state: "open",
|
||||
review: "changes_requested",
|
||||
additions: 60,
|
||||
deletions: 12,
|
||||
files: 4,
|
||||
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Open + checks running → pulsing amber badge (animation disabled in snapshots) */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-4", "feat/cache")}
|
||||
label="feat/cache"
|
||||
subtitle="feat/cache"
|
||||
pr={{
|
||||
number: 103,
|
||||
title: "feat: cache layer",
|
||||
url: "#",
|
||||
state: "draft",
|
||||
state: "open",
|
||||
review: null,
|
||||
additions: 200,
|
||||
deletions: 0,
|
||||
files: 8,
|
||||
checks: { status: "pending", total: 5, passed: 0, failed: 0, pending: 5, items: [] },
|
||||
checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Draft → gray badge */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-5", "wip/refactor")}
|
||||
label="wip/refactor"
|
||||
subtitle="wip/refactor"
|
||||
pr={{
|
||||
number: 110,
|
||||
title: "wip: refactor",
|
||||
url: "#",
|
||||
state: "draft",
|
||||
review: null,
|
||||
additions: 30,
|
||||
deletions: 5,
|
||||
files: 2,
|
||||
checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Merged → purple badge, no status icon */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-6", "feat/done")}
|
||||
label="feat/done"
|
||||
subtitle="feat/done"
|
||||
pr={{
|
||||
number: 70,
|
||||
title: "feat: done",
|
||||
url: "#",
|
||||
state: "merged",
|
||||
review: "approved",
|
||||
additions: 90,
|
||||
deletions: 20,
|
||||
files: 6,
|
||||
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
{/* Closed → red badge, no status icon (distinct from a failing open PR) */}
|
||||
<WorktreeItem
|
||||
{...wtProps}
|
||||
worktree={wt("wt-7", "spike/idea")}
|
||||
label="spike/idea"
|
||||
subtitle="spike/idea"
|
||||
pr={{
|
||||
number: 65,
|
||||
title: "spike: idea",
|
||||
url: "#",
|
||||
state: "closed",
|
||||
review: null,
|
||||
additions: 10,
|
||||
deletions: 4,
|
||||
files: 1,
|
||||
checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, items: [] },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user