diff --git a/frontend/src/app/git-projects/issues/page.tsx b/frontend/src/app/git-projects/issues/page.tsx
index ff32ecf..bcb8adc 100644
--- a/frontend/src/app/git-projects/issues/page.tsx
+++ b/frontend/src/app/git-projects/issues/page.tsx
@@ -2,12 +2,22 @@
export const dynamic = "force-dynamic";
-import { useState, useEffect, useMemo } from "react";
+import { type ReactNode, useState, useEffect, useMemo } from "react";
import { useSearchParams } from "next/navigation";
-import { AlertCircle } from "lucide-react";
+import {
+ AlertCircle,
+ CheckCircle2,
+ CircleDot,
+ GitBranch,
+ Loader2,
+ Plus,
+ RefreshCw,
+ Timer,
+} from "lucide-react";
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
import { ApiError } from "@/api/mutator";
import {
type getMyMembershipApiV1OrganizationsMeMemberGetResponse,
@@ -56,6 +66,74 @@ const isRecentlyClosedIssue = (
return closedAt.getTime() >= cutoffMs;
};
+type StatTone = "blue" | "green" | "amber" | "rose" | "violet";
+
+const statToneClasses: Record<
+ StatTone,
+ { card: string; icon: string; glow: string }
+> = {
+ blue: {
+ card: "border-[color:rgba(96,165,250,0.34)] bg-[linear-gradient(145deg,rgba(96,165,250,0.16),var(--surface)_46%)]",
+ icon: "border-[color:rgba(96,165,250,0.3)] bg-[color:rgba(96,165,250,0.14)] text-[color:var(--accent-strong)]",
+ glow: "bg-[color:rgba(96,165,250,0.32)]",
+ },
+ green: {
+ card: "border-[color:rgba(52,211,153,0.34)] bg-[linear-gradient(145deg,rgba(52,211,153,0.15),var(--surface)_46%)]",
+ icon: "border-[color:rgba(52,211,153,0.3)] bg-[color:rgba(52,211,153,0.14)] text-[color:var(--success)]",
+ glow: "bg-[color:rgba(52,211,153,0.28)]",
+ },
+ amber: {
+ card: "border-[color:rgba(251,191,36,0.36)] bg-[linear-gradient(145deg,rgba(251,191,36,0.14),var(--surface)_46%)]",
+ icon: "border-[color:rgba(251,191,36,0.32)] bg-[color:rgba(251,191,36,0.13)] text-[color:var(--warning)]",
+ glow: "bg-[color:rgba(251,191,36,0.28)]",
+ },
+ rose: {
+ card: "border-[color:rgba(248,113,113,0.34)] bg-[linear-gradient(145deg,rgba(248,113,113,0.14),var(--surface)_46%)]",
+ icon: "border-[color:rgba(248,113,113,0.3)] bg-[color:rgba(248,113,113,0.13)] text-[color:var(--danger)]",
+ glow: "bg-[color:rgba(248,113,113,0.26)]",
+ },
+ violet: {
+ card: "border-[color:rgba(168,85,247,0.32)] bg-[linear-gradient(145deg,rgba(168,85,247,0.14),var(--surface)_46%)]",
+ icon: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.13)] text-[color:rgb(196,181,253)]",
+ glow: "bg-[color:rgba(168,85,247,0.25)]",
+ },
+};
+
+function IssueStatCard({
+ icon,
+ label,
+ value,
+ caption,
+ tone,
+}: {
+ icon: ReactNode;
+ label: string;
+ value: string;
+ caption: string;
+ tone: StatTone;
+}) {
+ const colors = statToneClasses[tone];
+ return (
+
+
+
+
+
+ {label}
+
+
{value}
+
+
{icon}
+
+
{caption}
+
+ );
+}
+
export default function GitIssuesPage() {
const searchParams = useSearchParams();
const initialStaleOnly = searchParams.get("stale") === "1";
@@ -208,6 +286,38 @@ export default function GitIssuesPage() {
const isClientFiltered = staleOnly || recentClosedOnly;
const visibleTotal = isClientFiltered ? visibleIssues.length : total;
const totalPages = isClientFiltered ? 1 : Math.ceil(total / limit);
+ const openIssueCount = useMemo(
+ () => visibleIssues.filter((issue) => issue.state === "open").length,
+ [visibleIssues],
+ );
+ const closedIssueCount = useMemo(
+ () => visibleIssues.filter((issue) => issue.state === "closed").length,
+ [visibleIssues],
+ );
+ const staleIssueCount = useMemo(
+ () =>
+ visibleIssues.filter((issue) => isStaleOpenIssue(issue, staleCutoffMs))
+ .length,
+ [staleCutoffMs, visibleIssues],
+ );
+ const pullRequestCount = useMemo(
+ () => issues.filter((issue) => issue.is_pull_request).length,
+ [issues],
+ );
+ const activeRepositoryName = useMemo(() => {
+ if (repoFilter === "all") return "All repositories";
+ const repo = repos.find((candidate) => candidate.id === repoFilter);
+ return repo
+ ? repo.display_name || `${repo.owner}/${repo.repo}`
+ : "Selected repository";
+ }, [repoFilter, repos]);
+ const activeModeLabel = staleOnly
+ ? "Stale review"
+ : recentClosedOnly
+ ? "Recently closed"
+ : stateFilter === "all"
+ ? "All issue states"
+ : `${stateFilter[0]?.toUpperCase() ?? ""}${stateFilter.slice(1)} issues`;
return (
+
+ {isLoadingIssues ? (
+
+ ) : (
+
+ )}
+ Refresh
+
+ {canCreateIssues && repos.length > 0 ? (
+ setCreateDialogOpen(true)}>
+
+ Create Issue
+
+ ) : null}
+
+ }
>
-
+
+
+
+
+
+
0 ? "warning" : "success"}
+ className="w-fit shadow-[0_0_24px_rgba(96,165,250,0.16)]"
+ >
+ {staleIssueCount > 0 ? `${staleIssueCount} stale` : "Current"}
+
+
+ Issue Operations
+
+
+ {activeModeLabel} across {activeRepositoryName}. Pull requests
+ are hidden from the issue workflow unless a filter explicitly
+ requests upstream totals.
+
+
+
+ {
+ setStaleOnly((value) => !value);
+ setRecentClosedOnly(false);
+ setStateFilter("open");
+ setPage(1);
+ }}
+ >
+
+ Stale
+
+ {
+ setRecentClosedOnly((value) => !value);
+ setStaleOnly(false);
+ setStateFilter("closed");
+ setPage(1);
+ }}
+ >
+
+ Recent
+
+
+
+
+ }
+ label="Open"
+ value={String(openIssueCount)}
+ caption="Visible issues still active."
+ tone="green"
+ />
+ }
+ label="Closed"
+ value={String(closedIssueCount)}
+ caption={`Closed in this result set.`}
+ tone="violet"
+ />
+ }
+ label="Stale"
+ value={String(staleIssueCount)}
+ caption={`${STALE_ISSUE_DAYS}+ days without updates.`}
+ tone={staleIssueCount > 0 ? "amber" : "blue"}
+ />
+ }
+ label="Scope"
+ value={repoFilter === "all" ? String(repos.length) : "1"}
+ caption={`${pullRequestCount} pull requests excluded.`}
+ tone="blue"
+ />
+
+
+
{
@@ -241,96 +455,88 @@ export default function GitIssuesPage() {
}}
repos={repos}
/>
- {canCreateIssues && repos.length > 0 ? (
- setCreateDialogOpen(true)}
- >
- Create Issue
-
- ) : null}
-
- {staleOnly ? (
-
-
- Showing open issues not updated in {STALE_ISSUE_DAYS}+ days.
-
- {
- setStaleOnly(false);
- setPage(1);
- }}
- >
- Show All Open Issues
-
-
- ) : null}
-
- {recentClosedOnly ? (
-
-
- Showing issues closed in the last {RECENT_CLOSED_DAYS} days.
-
- {
- setRecentClosedOnly(false);
- setPage(1);
- }}
- >
- Show All Closed Issues
-
-
- ) : null}
-
- {error ? (
-
- ) : null}
-
-
-
- {totalPages > 1 && (
-
-
- Page {page} of {totalPages} ({visibleTotal} total)
-
-
+ {staleOnly ? (
+
+
+ Showing open issues not updated in {STALE_ISSUE_DAYS}+ days.
+
setPage((p) => Math.max(1, p - 1))}
- disabled={page <= 1}
+ className="w-full text-[color:var(--warning)] hover:bg-[color:rgba(251,191,36,0.14)] sm:w-auto"
+ onClick={() => {
+ setStaleOnly(false);
+ setPage(1);
+ }}
>
- Previous
-
- setPage((p) => Math.min(totalPages, p + 1))}
- disabled={page >= totalPages}
- >
- Next
+ Show All Open Issues
-
- )}
+ ) : null}
+
+ {recentClosedOnly ? (
+
+
+ Showing issues closed in the last {RECENT_CLOSED_DAYS} days.
+
+ {
+ setRecentClosedOnly(false);
+ setPage(1);
+ }}
+ >
+ Show All Closed Issues
+
+
+ ) : null}
+
+ {error ? (
+
+ ) : null}
+
+
+
+ {totalPages > 1 && (
+
+
+ Page {page} of {totalPages} ({visibleTotal} total)
+
+
+ setPage((p) => Math.max(1, p - 1))}
+ disabled={page <= 1}
+ >
+ Previous
+
+ setPage((p) => Math.min(totalPages, p + 1))}
+ disabled={page >= totalPages}
+ >
+ Next
+
+
+
+ )}
+
{ cancelled = true; };
+ return () => {
+ cancelled = true;
+ };
}, [open, issue]);
const agentsQuery = useListAgentsApiV1AgentsGet<
@@ -138,13 +140,9 @@ export function AssignIssueAgentDialog({
);
const agents =
- agentsQuery.data?.status === 200
- ? (agentsQuery.data.data.items ?? [])
- : [];
+ agentsQuery.data?.status === 200 ? (agentsQuery.data.data.items ?? []) : [];
const allBoards =
- boardsQuery.data?.status === 200
- ? (boardsQuery.data.data.items ?? [])
- : [];
+ boardsQuery.data?.status === 200 ? (boardsQuery.data.data.items ?? []) : [];
useEffect(() => {
if (!open) {
@@ -236,7 +234,7 @@ export function AssignIssueAgentDialog({
disabled={disabled}
className={`mt-0.5 inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition ${
value
- ? "border-emerald-600 bg-emerald-600"
+ ? "border-[color:var(--success)] bg-[color:var(--success)]"
: "border-[color:var(--border)] bg-[color:var(--surface-muted)]"
} ${disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer"}`}
>
@@ -255,8 +253,12 @@ export function AssignIssueAgentDialog({
if (!isSubmitting) onOpenChange(next);
}}
>
-
-
+
+
+
+
+
+
Assign to agent
Create a Pipeline task for{" "}
@@ -268,36 +270,41 @@ export function AssignIssueAgentDialog({
-
+
{noLinkedBoards ? (
-
+
Repository not linked to any board
Before you can assign an agent, link{" "}
- {repositoryName} {" "}
- to the board that should own this task. Open the target board and
- use its Git Project repositories panel, or manage tracked
+
+ {repositoryName}
+ {" "}
+ to the board that should own this task. Open the target board
+ and use its Git Project repositories panel, or manage tracked
repositories first.
- Link to board *
+ Link to board{" "}
+ *
setLinkBoardId(e.target.value)}
disabled={boardsQuery.isLoading || isLinkingRepository}
- className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
+ className="w-full rounded-xl border border-[color:rgba(251,191,36,0.22)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
>
- {boardsQuery.isLoading ? "Loading boards..." : "Select a board..."}
+ {boardsQuery.isLoading
+ ? "Loading boards..."
+ : "Select a board..."}
{allBoards.map((board) => (
@@ -341,7 +348,7 @@ export function AssignIssueAgentDialog({
value={boardId}
onChange={(e) => setBoardId(e.target.value)}
disabled={isSubmitting || boardsLoading}
- className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
+ className="w-full rounded-xl border border-[color:rgba(168,85,247,0.2)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
>
{boardsLoading ? (
Loading boards…
@@ -368,7 +375,7 @@ export function AssignIssueAgentDialog({
value={agentId}
onChange={(e) => setAgentId(e.target.value)}
disabled={isSubmitting || !boardId || agentsQuery.isLoading}
- className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
+ className="w-full rounded-xl border border-[color:rgba(168,85,247,0.2)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
>
Select an agent…
{agentsQuery.isLoading ? (
@@ -390,12 +397,14 @@ export function AssignIssueAgentDialog({
-
Priority
+
+ Priority
+
setPriority(e.target.value)}
disabled={isSubmitting}
- className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
+ className="w-full rounded-xl border border-[color:rgba(168,85,247,0.2)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
>
Low
Medium
@@ -406,7 +415,8 @@ export function AssignIssueAgentDialog({
- Agent prompt *
+ Agent prompt{" "}
+ *
This prompt is copied into the Pipeline task and sent with the
@@ -423,7 +433,11 @@ export function AssignIssueAgentDialog({
- {toggleSwitch(startImmediately, setStartImmediately, isSubmitting)}
+ {toggleSwitch(
+ startImmediately,
+ setStartImmediately,
+ isSubmitting,
+ )}
Start immediately
diff --git a/frontend/src/components/git/CloseForgejoIssueDialog.tsx b/frontend/src/components/git/CloseForgejoIssueDialog.tsx
index ffea045..f16f5a0 100644
--- a/frontend/src/components/git/CloseForgejoIssueDialog.tsx
+++ b/frontend/src/components/git/CloseForgejoIssueDialog.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { XCircle } from "lucide-react";
import {
Dialog,
@@ -52,8 +53,12 @@ export function CloseForgejoIssueDialog({
return (
-
-
+
+
+
+
+
+
Close Git Project issue
Confirm closing{" "}
@@ -64,7 +69,7 @@ export function CloseForgejoIssueDialog({
the local issue cache.
-
+
{issue.title}
diff --git a/frontend/src/components/git/CreateForgejoIssueDialog.tsx b/frontend/src/components/git/CreateForgejoIssueDialog.tsx
index 4ee3669..6947e35 100644
--- a/frontend/src/components/git/CreateForgejoIssueDialog.tsx
+++ b/frontend/src/components/git/CreateForgejoIssueDialog.tsx
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
+import { CircleDot, GitBranch } from "lucide-react";
import {
Dialog,
@@ -90,8 +91,12 @@ export function CreateForgejoIssueDialog({
return (
-
-
+
+
+
+
+
+
Create issue
Open a new issue on the connected Git repository. The body is
@@ -119,9 +124,12 @@ export function CreateForgejoIssueDialog({
) : (
-
- Repository:{" "}
-
{repoLabel}
+
+
+
+ Repository:{" "}
+ {repoLabel}
+
)}
@@ -147,7 +155,7 @@ export function CreateForgejoIssueDialog({
onChange={(e) => setBody(e.target.value)}
rows={22}
disabled={isSubmitting}
- className="resize-y font-mono text-xs"
+ className="resize-y border-[color:rgba(96,165,250,0.18)] bg-[color:var(--surface-muted)] font-mono text-xs"
/>
diff --git a/frontend/src/components/git/EditForgejoIssueDialog.tsx b/frontend/src/components/git/EditForgejoIssueDialog.tsx
index 61bf21f..5751ef8 100644
--- a/frontend/src/components/git/EditForgejoIssueDialog.tsx
+++ b/frontend/src/components/git/EditForgejoIssueDialog.tsx
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
+import { Pencil } from "lucide-react";
import {
Dialog,
@@ -21,7 +22,11 @@ type EditForgejoIssueDialogProps = {
repositoryName: string;
open: boolean;
onOpenChange: (open: boolean) => void;
- onSuccess: (updated: { title: string; body: string | null; state: string }) => void;
+ onSuccess: (updated: {
+ title: string;
+ body: string | null;
+ state: string;
+ }) => void;
};
export function EditForgejoIssueDialog({
@@ -47,7 +52,8 @@ export function EditForgejoIssueDialog({
if (!issue) return null;
const isDirty =
- title.trim() !== issue.title || body !== (issue.body ?? issue.body_preview ?? "");
+ title.trim() !== issue.title ||
+ body !== (issue.body ?? issue.body_preview ?? "");
const handleSubmit = async () => {
if (!title.trim()) return;
@@ -59,7 +65,11 @@ export function EditForgejoIssueDialog({
if (body !== (issue.body ?? issue.body_preview ?? "")) patch.body = body;
const result = await editForgejoIssue(issue.id, patch);
- onSuccess({ title: result.title, body: result.body, state: result.state });
+ onSuccess({
+ title: result.title,
+ body: result.body,
+ state: result.state,
+ });
onOpenChange(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to edit issue");
@@ -78,8 +88,12 @@ export function EditForgejoIssueDialog({
}
}}
>
-
-
+
+
+
+
Edit issue
Editing{" "}
@@ -92,7 +106,12 @@ export function EditForgejoIssueDialog({
-
Title
+
+ Title
+
- Body
+
+ Body
+
diff --git a/frontend/src/components/git/ForgejoIssueDetailDialog.tsx b/frontend/src/components/git/ForgejoIssueDetailDialog.tsx
index 62bee22..6f5fdb5 100644
--- a/frontend/src/components/git/ForgejoIssueDetailDialog.tsx
+++ b/frontend/src/components/git/ForgejoIssueDetailDialog.tsx
@@ -3,6 +3,9 @@
import { useEffect, useMemo, useState } from "react";
import {
+ CheckCircle2,
+ CircleDot,
+ Clock,
ExternalLink,
Loader2,
MessageSquarePlus,
@@ -129,6 +132,7 @@ export function ForgejoIssueDetailDialog({
const body = detail?.body ?? issue.body ?? issue.body_preview ?? "";
const stateVariant = active.state === "open" ? "success" : "default";
const showCloseIssue = canClose && active.state === "open";
+ const isOpenIssue = active.state === "open";
const handleCloseIssueSuccess = () => {
if (detail) setDetail({ ...detail, state: "closed" });
@@ -180,7 +184,20 @@ export function ForgejoIssueDetailDialog({
return (
<>
-
+
+
@@ -281,6 +298,40 @@ export function ForgejoIssueDetailDialog({
) : null}
+
+
+
+ {isOpenIssue ? (
+
+ ) : (
+
+ )}
+ State
+
+
+ {active.state}
+
+
+
+
+
+ Comments
+
+
+ {comments.length}
+
+
+
+
+
+ Updated
+
+
+ {formatDateTime(active.forgejo_updated_at)}
+
+
+
+
diff --git a/frontend/src/components/git/ForgejoIssueFilters.tsx b/frontend/src/components/git/ForgejoIssueFilters.tsx
index ab0c2a8..4688a8d 100644
--- a/frontend/src/components/git/ForgejoIssueFilters.tsx
+++ b/frontend/src/components/git/ForgejoIssueFilters.tsx
@@ -1,5 +1,7 @@
"use client";
+import { Filter, GitBranch, Search } from "lucide-react";
+
import {
Select,
SelectContent,
@@ -30,38 +32,47 @@ export function ForgejoIssueFilters({
repos,
}: ForgejoIssueFiltersProps) {
return (
-
-
-
-
-
-
- Open
- Closed
- All
-
-
+
+
+
+
+
+
+
+
+ Open
+ Closed
+ All
+
+
+
-
-
-
-
-
- All repositories
- {repos.map((r) => (
-
- {r.display_name || `${r.owner}/${r.repo}`}
-
- ))}
-
-
+
+
+
+
+
+
+
+ All repositories
+ {repos.map((r) => (
+
+ {r.display_name || `${r.owner}/${r.repo}`}
+
+ ))}
+
+
+
-
onSearchChange(e.target.value)}
- className="min-w-0"
- />
+
+
+ onSearchChange(e.target.value)}
+ className="min-w-0 pl-9"
+ />
+
);
}
diff --git a/frontend/src/components/git/ForgejoIssuesTable.tsx b/frontend/src/components/git/ForgejoIssuesTable.tsx
index 9d68933..e02e74e 100644
--- a/frontend/src/components/git/ForgejoIssuesTable.tsx
+++ b/frontend/src/components/git/ForgejoIssuesTable.tsx
@@ -3,9 +3,11 @@
import { useMemo, useState } from "react";
import {
+ AlertCircle,
CheckCircle2,
CircleDot,
ExternalLink,
+ GitBranch,
Loader2,
Milestone,
Pencil,
@@ -40,6 +42,41 @@ function labelTextColor(hex: string): string {
return L > 0.179 ? "#1a1a1a" : "#ffffff";
}
+function issueTone(issue: ForgejoIssue) {
+ if (issue.state === "closed") return "closed";
+ const updatedAt = new Date(issue.forgejo_updated_at || issue.updated_at);
+ if (!Number.isNaN(updatedAt.getTime())) {
+ const daysSinceUpdate =
+ (Date.now() - updatedAt.getTime()) / (24 * 60 * 60 * 1000);
+ if (daysSinceUpdate >= 14) return "stale";
+ }
+ return "open";
+}
+
+const issueToneClasses = {
+ open: {
+ rail: "border-l-[color:var(--success)]",
+ row: "bg-[color:rgba(52,211,153,0.018)] hover:bg-[color:rgba(52,211,153,0.07)]",
+ icon: "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.13)] text-[color:var(--success)]",
+ pill: "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.1)] text-[color:var(--success)]",
+ dot: "bg-[color:var(--success)]",
+ },
+ stale: {
+ rail: "border-l-[color:var(--warning)]",
+ row: "bg-[color:rgba(251,191,36,0.022)] hover:bg-[color:rgba(251,191,36,0.08)]",
+ icon: "border-[color:rgba(251,191,36,0.3)] bg-[color:rgba(251,191,36,0.13)] text-[color:var(--warning)]",
+ pill: "border-[color:rgba(251,191,36,0.3)] bg-[color:rgba(251,191,36,0.11)] text-[color:var(--warning)]",
+ dot: "bg-[color:var(--warning)]",
+ },
+ closed: {
+ rail: "border-l-[color:rgb(196,181,253)]",
+ row: "bg-[color:rgba(168,85,247,0.018)] hover:bg-[color:rgba(168,85,247,0.07)]",
+ icon: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.12)] text-[color:rgb(196,181,253)]",
+ pill: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.1)] text-[color:rgb(196,181,253)]",
+ dot: "bg-[color:rgb(196,181,253)]",
+ },
+} as const;
+
function LabelChip({ label }: { label: ForgejoIssueLabel }) {
const bg = normalizeLabelColor(label.color);
return (
@@ -133,7 +170,7 @@ function AssigneeStack({ issue }: { issue: ForgejoIssue }) {
{visible.map((login) => (
{getInitials(login)}
@@ -154,8 +191,10 @@ function IssueStateIcon({ state }: { state: string }) {
return (
@@ -233,15 +272,15 @@ export function ForgejoIssuesTable({
return (
<>
-
-
+
+
{issueCounts.open} Open
-
+
{issueCounts.closed} Closed
@@ -254,7 +293,7 @@ export function ForgejoIssuesTable({
) : issues.length === 0 ? (
-
+
@@ -271,6 +310,8 @@ export function ForgejoIssuesTable({
const repositoryName =
repositoryNameById.get(issue.repository_id) ??
issue.repository_id;
+ const tone = issueTone(issue);
+ const toneClass = issueToneClasses[tone];
const visibleLabels = issue.labels.slice(0, 4);
const hiddenLabelCount =
issue.labels.length - visibleLabels.length;
@@ -286,7 +327,11 @@ export function ForgejoIssuesTable({
return (
@@ -303,6 +348,21 @@ export function ForgejoIssuesTable({
>
{issue.title}
+
+ {tone === "stale" ? (
+
+ ) : issue.state === "open" ? (
+
+ ) : (
+
+ )}
+ {tone === "stale" ? "Stale" : issue.state}
+
{visibleLabels.map((label, i) => (
))}
@@ -314,13 +374,20 @@ export function ForgejoIssuesTable({
-
+
+
{repositoryName}
-
+
#{issue.forgejo_issue_number}
+
{stateVerb} {formatRelativeTime(updatedAt)}
{issue.author ? by {issue.author} : null}
diff --git a/frontend/src/components/git/PostForgejoCommentDialog.tsx b/frontend/src/components/git/PostForgejoCommentDialog.tsx
index 7e0a4ed..0635aca 100644
--- a/frontend/src/components/git/PostForgejoCommentDialog.tsx
+++ b/frontend/src/components/git/PostForgejoCommentDialog.tsx
@@ -1,6 +1,7 @@
"use client";
import { useRef, useState } from "react";
+import { MessageSquarePlus } from "lucide-react";
import {
Dialog,
@@ -64,8 +65,12 @@ export function PostForgejoCommentDialog({
}
}}
>
-
-
+
+
+
+
+
+
Post comment
Comment on{" "}
@@ -76,7 +81,7 @@ export function PostForgejoCommentDialog({
-
+
{issue.title}
@@ -89,7 +94,7 @@ export function PostForgejoCommentDialog({
onChange={(e) => setBody(e.target.value)}
rows={5}
disabled={isSubmitting}
- className="resize-none"
+ className="resize-none border-[color:rgba(52,211,153,0.18)] bg-[color:var(--surface-muted)]"
/>
{error ? (
diff --git a/frontend/src/components/organisms/AgentActivityTicker.tsx b/frontend/src/components/organisms/AgentActivityTicker.tsx
index cb39ade..18618ae 100644
--- a/frontend/src/components/organisms/AgentActivityTicker.tsx
+++ b/frontend/src/components/organisms/AgentActivityTicker.tsx
@@ -61,8 +61,8 @@ export function AgentActivityTicker() {
const display = [...items, ...items];
return (
-
-
+
+
Live
@@ -70,7 +70,7 @@ export function AgentActivityTicker() {
{display.map((item, idx) => (
{item.source}
@@ -80,7 +80,7 @@ export function AgentActivityTicker() {
{fmtRelative(item.created_at)}
-
+
│