ui upgrade

This commit is contained in:
null 2026-05-25 12:15:52 -05:00
parent e6c2989c64
commit c71b96421f
11 changed files with 571 additions and 180 deletions

View File

@ -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 (
<div
className={`relative overflow-hidden rounded-xl border p-4 shadow-lush ${colors.card}`}
>
<span
className={`pointer-events-none absolute inset-x-4 top-0 h-px ${colors.glow}`}
/>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-wide text-muted">
{label}
</p>
<p className="mt-2 text-2xl font-semibold text-strong">{value}</p>
</div>
<div className={`rounded-lg border p-2 ${colors.icon}`}>{icon}</div>
</div>
<p className="mt-3 text-sm text-muted">{caption}</p>
</div>
);
}
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 (
<DashboardPageLayout
@ -219,8 +329,112 @@ export default function GitIssuesPage() {
title="Git Project Issues"
description={`${visibleTotal} issue${visibleTotal === 1 ? "" : "s"} from repositories tracked by Pipeline.`}
stickyHeader
headerActions={
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isLoadingIssues}
>
{isLoadingIssues ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
Refresh
</Button>
{canCreateIssues && repos.length > 0 ? (
<Button size="sm" onClick={() => setCreateDialogOpen(true)}>
<Plus className="h-4 w-4" />
Create Issue
</Button>
) : null}
</div>
}
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-5">
<section className="relative overflow-hidden rounded-xl border border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.09)_34%,rgba(251,191,36,0.08)_68%,var(--surface)_100%)] p-4 shadow-lush md:p-5">
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(52,211,153,0.85),rgba(251,191,36,0))]" />
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0">
<Badge
variant={staleIssueCount > 0 ? "warning" : "success"}
className="w-fit shadow-[0_0_24px_rgba(96,165,250,0.16)]"
>
{staleIssueCount > 0 ? `${staleIssueCount} stale` : "Current"}
</Badge>
<h2 className="mt-3 font-heading text-2xl font-semibold text-strong">
Issue Operations
</h2>
<p className="mt-2 max-w-3xl text-sm text-muted">
{activeModeLabel} across {activeRepositoryName}. Pull requests
are hidden from the issue workflow unless a filter explicitly
requests upstream totals.
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant={staleOnly ? "primary" : "outline"}
onClick={() => {
setStaleOnly((value) => !value);
setRecentClosedOnly(false);
setStateFilter("open");
setPage(1);
}}
>
<Timer className="h-4 w-4" />
Stale
</Button>
<Button
type="button"
variant={recentClosedOnly ? "primary" : "outline"}
onClick={() => {
setRecentClosedOnly((value) => !value);
setStaleOnly(false);
setStateFilter("closed");
setPage(1);
}}
>
<CheckCircle2 className="h-4 w-4" />
Recent
</Button>
</div>
</div>
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<IssueStatCard
icon={<CircleDot className="h-4 w-4" />}
label="Open"
value={String(openIssueCount)}
caption="Visible issues still active."
tone="green"
/>
<IssueStatCard
icon={<CheckCircle2 className="h-4 w-4" />}
label="Closed"
value={String(closedIssueCount)}
caption={`Closed in this result set.`}
tone="violet"
/>
<IssueStatCard
icon={<Timer className="h-4 w-4" />}
label="Stale"
value={String(staleIssueCount)}
caption={`${STALE_ISSUE_DAYS}+ days without updates.`}
tone={staleIssueCount > 0 ? "amber" : "blue"}
/>
<IssueStatCard
icon={<GitBranch className="h-4 w-4" />}
label="Scope"
value={repoFilter === "all" ? String(repos.length) : "1"}
caption={`${pullRequestCount} pull requests excluded.`}
tone="blue"
/>
</div>
</section>
<ForgejoIssueFilters
stateFilter={stateFilter}
onStateChange={(v) => {
@ -241,96 +455,88 @@ export default function GitIssuesPage() {
}}
repos={repos}
/>
{canCreateIssues && repos.length > 0 ? (
<Button
size="sm"
onClick={() => setCreateDialogOpen(true)}
>
Create Issue
</Button>
) : null}
</div>
{staleOnly ? (
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--warning)]/35 bg-[color:rgba(251,191,36,0.12)] p-3 text-sm text-[color:var(--warning)] sm:flex-row sm:items-center sm:justify-between">
<span>
Showing open issues not updated in {STALE_ISSUE_DAYS}+ days.
</span>
<Button
type="button"
variant="ghost"
size="sm"
className="w-full text-[color:var(--warning)] hover:bg-[color:rgba(251,191,36,0.14)] sm:w-auto"
onClick={() => {
setStaleOnly(false);
setPage(1);
}}
>
Show All Open Issues
</Button>
</div>
) : null}
{recentClosedOnly ? (
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--success)]/35 bg-[color:rgba(52,211,153,0.12)] p-3 text-sm text-[color:var(--success)] sm:flex-row sm:items-center sm:justify-between">
<span>
Showing issues closed in the last {RECENT_CLOSED_DAYS} days.
</span>
<Button
type="button"
variant="ghost"
size="sm"
className="w-full text-[color:var(--success)] hover:bg-[color:rgba(52,211,153,0.14)] sm:w-auto"
onClick={() => {
setRecentClosedOnly(false);
setPage(1);
}}
>
Show All Closed Issues
</Button>
</div>
) : null}
{error ? (
<div className="mb-4 flex items-start gap-3 rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-sm text-[color:var(--danger)]">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
) : null}
<ForgejoIssuesTable
issues={visibleIssues}
repositories={repos}
isLoading={isLoadingIssues}
canClose={canCloseIssues}
onRefresh={handleRefresh}
/>
{totalPages > 1 && (
<div className="mt-4 flex flex-col gap-3 text-sm text-muted sm:flex-row sm:items-center sm:justify-between">
<span className="break-words">
Page {page} of {totalPages} ({visibleTotal} total)
</span>
<div className="flex gap-2">
{staleOnly ? (
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--warning)]/35 bg-[color:rgba(251,191,36,0.12)] p-3 text-sm text-[color:var(--warning)] sm:flex-row sm:items-center sm:justify-between">
<span>
Showing open issues not updated in {STALE_ISSUE_DAYS}+ days.
</span>
<Button
variant="outline"
type="button"
variant="ghost"
size="sm"
onClick={() => 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
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
>
Next
Show All Open Issues
</Button>
</div>
</div>
)}
) : null}
{recentClosedOnly ? (
<div className="mb-4 flex flex-col gap-3 rounded-xl border border-[color:var(--success)]/35 bg-[color:rgba(52,211,153,0.12)] p-3 text-sm text-[color:var(--success)] sm:flex-row sm:items-center sm:justify-between">
<span>
Showing issues closed in the last {RECENT_CLOSED_DAYS} days.
</span>
<Button
type="button"
variant="ghost"
size="sm"
className="w-full text-[color:var(--success)] hover:bg-[color:rgba(52,211,153,0.14)] sm:w-auto"
onClick={() => {
setRecentClosedOnly(false);
setPage(1);
}}
>
Show All Closed Issues
</Button>
</div>
) : null}
{error ? (
<div className="mb-4 flex items-start gap-3 rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-sm text-[color:var(--danger)]">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
) : null}
<ForgejoIssuesTable
issues={visibleIssues}
repositories={repos}
isLoading={isLoadingIssues}
canClose={canCloseIssues}
onRefresh={handleRefresh}
/>
{totalPages > 1 && (
<div className="mt-4 flex flex-col gap-3 text-sm text-muted sm:flex-row sm:items-center sm:justify-between">
<span className="break-words">
Page {page} of {totalPages} ({visibleTotal} total)
</span>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
>
Next
</Button>
</div>
</div>
)}
</div>
<CreateForgejoIssueDialog
repositories={repos}
open={createDialogOpen}

View File

@ -267,7 +267,7 @@ textarea::placeholder {
animation: progress-shimmer 1.8s linear infinite;
}
.animate-ticker {
animation: ticker-scroll 40s linear infinite;
animation: ticker-scroll 90s linear infinite;
}
.animate-ticker:hover {
animation-play-state: paused;

View File

@ -2,7 +2,7 @@
import Link from "next/link";
import { useEffect, useState } from "react";
import { ExternalLink } from "lucide-react";
import { ExternalLink, UserPlus } from "lucide-react";
import {
useListAgentsApiV1AgentsGet,
@ -109,7 +109,9 @@ export function AssignIssueAgentDialog({
setLinkedBoardsLoaded(true);
}
});
return () => { 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);
}}
>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogContent className="relative max-w-lg overflow-hidden border-[color:rgba(168,85,247,0.28)] bg-[linear-gradient(180deg,rgba(168,85,247,0.08),rgba(96,165,250,0.05)_160px,rgba(255,255,255,0)_240px),var(--surface)]">
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(168,85,247,0),rgba(168,85,247,0.82),rgba(96,165,250,0.7),rgba(168,85,247,0))]" />
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(168,85,247,0.3)] bg-[color:rgba(168,85,247,0.13)] text-[color:rgb(196,181,253)]">
<UserPlus className="h-4 w-4" />
</div>
<DialogTitle>Assign to agent</DialogTitle>
<DialogDescription>
Create a Pipeline task for{" "}
@ -268,36 +270,41 @@ export function AssignIssueAgentDialog({
</DialogHeader>
<div className="space-y-4">
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2">
<div className="rounded-xl border border-[color:rgba(168,85,247,0.22)] bg-[color:rgba(168,85,247,0.07)] px-3 py-2">
<p className="truncate text-sm font-medium text-strong">
{issue.title}
</p>
</div>
{noLinkedBoards ? (
<div className="rounded-xl border border-[color:rgba(234,179,8,0.4)] bg-[color:rgba(234,179,8,0.08)] p-4 text-sm">
<div className="rounded-xl border border-[color:rgba(251,191,36,0.4)] bg-[color:rgba(251,191,36,0.08)] p-4 text-sm">
<p className="font-semibold text-[color:var(--warning,#ca8a04)]">
Repository not linked to any board
</p>
<p className="mt-1 text-muted">
Before you can assign an agent, link{" "}
<span className="font-medium text-strong">{repositoryName}</span>{" "}
to the board that should own this task. Open the target board and
use its Git Project repositories panel, or manage tracked
<span className="font-medium text-strong">
{repositoryName}
</span>{" "}
to the board that should own this task. Open the target board
and use its Git Project repositories panel, or manage tracked
repositories first.
</p>
<div className="mt-4 space-y-2">
<label className="text-sm font-medium text-strong">
Link to board <span className="text-[color:var(--danger)]">*</span>
Link to board{" "}
<span className="text-[color:var(--danger)]">*</span>
</label>
<select
value={linkBoardId}
onChange={(e) => 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)]"
>
<option value="">
{boardsQuery.isLoading ? "Loading boards..." : "Select a board..."}
{boardsQuery.isLoading
? "Loading boards..."
: "Select a board..."}
</option>
{allBoards.map((board) => (
<option key={board.id} value={board.id}>
@ -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 ? (
<option>Loading boards</option>
@ -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)]"
>
<option value="">Select an agent</option>
{agentsQuery.isLoading ? (
@ -390,12 +397,14 @@ export function AssignIssueAgentDialog({
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-strong">Priority</label>
<label className="text-sm font-medium text-strong">
Priority
</label>
<select
value={priority}
onChange={(e) => 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)]"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
@ -406,7 +415,8 @@ export function AssignIssueAgentDialog({
<div className="space-y-1.5">
<label className="text-sm font-medium text-strong">
Agent prompt <span className="text-[color:var(--danger)]">*</span>
Agent prompt{" "}
<span className="text-[color:var(--danger)]">*</span>
</label>
<Textarea
value={instructions}
@ -414,7 +424,7 @@ export function AssignIssueAgentDialog({
disabled={isSubmitting}
rows={9}
placeholder="Prompt the agent with the goal, context, and expected validation…"
className="resize-y text-sm"
className="resize-y border-[color:rgba(168,85,247,0.18)] bg-[color:var(--surface-muted)] text-sm"
/>
<p className="text-xs text-muted">
This prompt is copied into the Pipeline task and sent with the
@ -423,7 +433,11 @@ export function AssignIssueAgentDialog({
</div>
<div className="flex items-center gap-3">
{toggleSwitch(startImmediately, setStartImmediately, isSubmitting)}
{toggleSwitch(
startImmediately,
setStartImmediately,
isSubmitting,
)}
<span className="space-y-0.5">
<span className="block text-sm font-medium text-strong">
Start immediately

View File

@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogContent className="relative max-w-lg overflow-hidden border-[color:rgba(248,113,113,0.32)] bg-[linear-gradient(180deg,rgba(248,113,113,0.09),rgba(255,255,255,0)_190px),var(--surface)]">
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(248,113,113,0),rgba(248,113,113,0.85),rgba(248,113,113,0))]" />
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(248,113,113,0.34)] bg-[color:rgba(248,113,113,0.13)] text-[color:var(--danger)]">
<XCircle className="h-4 w-4" />
</div>
<DialogTitle>Close Git Project issue</DialogTitle>
<DialogDescription>
Confirm closing{" "}
@ -64,7 +69,7 @@ export function CloseForgejoIssueDialog({
the local issue cache.
</DialogDescription>
</DialogHeader>
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-3">
<div className="rounded-xl border border-[color:rgba(248,113,113,0.22)] bg-[color:rgba(248,113,113,0.06)] p-3">
<p className="break-words text-sm font-medium text-strong">
{issue.title}
</p>

View File

@ -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 (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogContent className="relative max-w-3xl overflow-hidden border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(180deg,rgba(96,165,250,0.08),rgba(255,255,255,0)_220px),var(--surface)]">
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(52,211,153,0.75),rgba(96,165,250,0))]" />
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(96,165,250,0.3)] bg-[color:rgba(96,165,250,0.13)] text-[color:var(--accent-strong)]">
<CircleDot className="h-4 w-4" />
</div>
<DialogTitle>Create issue</DialogTitle>
<DialogDescription>
Open a new issue on the connected Git repository. The body is
@ -119,9 +124,12 @@ export function CreateForgejoIssueDialog({
</select>
</div>
) : (
<div className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2 text-sm text-muted">
Repository:{" "}
<span className="font-medium text-strong">{repoLabel}</span>
<div className="flex items-center gap-2 rounded-lg border border-[color:rgba(96,165,250,0.22)] bg-[color:rgba(96,165,250,0.08)] px-3 py-2 text-sm text-muted">
<GitBranch className="h-4 w-4 text-[color:var(--accent)]" />
<span>
Repository:{" "}
<span className="font-medium text-strong">{repoLabel}</span>
</span>
</div>
)}
@ -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"
/>
</div>
</div>

View File

@ -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({
}
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogContent className="relative max-w-2xl overflow-hidden border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(180deg,rgba(96,165,250,0.08),rgba(255,255,255,0)_200px),var(--surface)]">
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(96,165,250,0))]" />
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(96,165,250,0.3)] bg-[color:rgba(96,165,250,0.13)] text-[color:var(--accent-strong)]">
<Pencil className="h-4 w-4" />
</div>
<DialogTitle>Edit issue</DialogTitle>
<DialogDescription>
Editing{" "}
@ -92,7 +106,12 @@ export function EditForgejoIssueDialog({
<div className="space-y-4">
<div className="space-y-1.5">
<label htmlFor="issue-title" className="text-sm font-medium text-strong">Title</label>
<label
htmlFor="issue-title"
className="text-sm font-medium text-strong"
>
Title
</label>
<Input
id="issue-title"
value={title}
@ -103,14 +122,19 @@ export function EditForgejoIssueDialog({
</div>
<div className="space-y-1.5">
<label htmlFor="issue-body" className="text-sm font-medium text-strong">Body</label>
<label
htmlFor="issue-body"
className="text-sm font-medium text-strong"
>
Body
</label>
<Textarea
id="issue-body"
value={body}
onChange={(e) => setBody(e.target.value)}
rows={10}
disabled={isSubmitting}
className="resize-none font-mono text-sm"
className="resize-none border-[color:rgba(96,165,250,0.18)] bg-[color:var(--surface-muted)] font-mono text-sm"
placeholder="Issue body (Markdown supported)"
/>
</div>

View File

@ -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 (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl border-[color:var(--border-strong)] bg-[color:var(--surface)]/95 backdrop-blur-xl">
<DialogContent
className={`relative max-w-4xl overflow-hidden backdrop-blur-xl ${
isOpenIssue
? "border-[color:rgba(52,211,153,0.3)] bg-[linear-gradient(180deg,rgba(52,211,153,0.08),rgba(96,165,250,0.04)_160px,rgba(255,255,255,0)_260px),var(--surface)]"
: "border-[color:rgba(168,85,247,0.3)] bg-[linear-gradient(180deg,rgba(168,85,247,0.08),rgba(96,165,250,0.04)_160px,rgba(255,255,255,0)_260px),var(--surface)]"
}`}
>
<div
className={`pointer-events-none absolute inset-x-4 top-0 h-px ${
isOpenIssue
? "bg-[linear-gradient(90deg,rgba(52,211,153,0),rgba(52,211,153,0.82),rgba(96,165,250,0.66),rgba(52,211,153,0))]"
: "bg-[linear-gradient(90deg,rgba(168,85,247,0),rgba(168,85,247,0.82),rgba(96,165,250,0.66),rgba(168,85,247,0))]"
}`}
/>
<DialogHeader className="gap-5 border-b border-[color:var(--border)] pb-5">
<div className="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
<div className="min-w-0 space-y-2">
@ -281,6 +298,40 @@ export function ForgejoIssueDetailDialog({
</div>
) : null}
<div className="mt-5 grid gap-3 sm:grid-cols-3">
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)]/70 p-3">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted">
{isOpenIssue ? (
<CircleDot className="h-3.5 w-3.5 text-[color:var(--success)]" />
) : (
<CheckCircle2 className="h-3.5 w-3.5 text-[color:rgb(196,181,253)]" />
)}
State
</div>
<p className="mt-2 text-sm font-semibold text-strong">
{active.state}
</p>
</div>
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)]/70 p-3">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted">
<MessageSquarePlus className="h-3.5 w-3.5 text-[color:var(--accent)]" />
Comments
</div>
<p className="mt-2 text-sm font-semibold text-strong">
{comments.length}
</p>
</div>
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)]/70 p-3">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted">
<Clock className="h-3.5 w-3.5 text-[color:var(--warning)]" />
Updated
</div>
<p className="mt-2 text-sm font-semibold text-strong">
{formatDateTime(active.forgejo_updated_at)}
</p>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="mt-5">
<TabsList className="max-w-full overflow-x-auto rounded-2xl bg-[color:var(--surface-muted)]/45">
<TabsTrigger className="shrink-0 rounded-xl" value="overview">

View File

@ -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 (
<div className="mb-4 grid gap-3 rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-3 shadow-lush sm:grid-cols-[140px_minmax(200px,280px)_minmax(220px,1fr)]">
<Select value={stateFilter} onValueChange={onStateChange}>
<SelectTrigger>
<SelectValue placeholder="State" />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
<SelectItem value="all">All</SelectItem>
</SelectContent>
</Select>
<div className="grid gap-3 rounded-xl border border-[color:var(--border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0)_72%),var(--surface)] p-3 shadow-lush sm:grid-cols-[150px_minmax(220px,300px)_minmax(240px,1fr)]">
<div className="relative">
<Filter className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted" />
<Select value={stateFilter} onValueChange={onStateChange}>
<SelectTrigger className="pl-9">
<SelectValue placeholder="State" />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
<SelectItem value="all">All</SelectItem>
</SelectContent>
</Select>
</div>
<Select value={repoFilter} onValueChange={onRepoChange}>
<SelectTrigger>
<SelectValue placeholder="Repository" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All repositories</SelectItem>
{repos.map((r) => (
<SelectItem key={r.id} value={r.id}>
{r.display_name || `${r.owner}/${r.repo}`}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<GitBranch className="pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-muted" />
<Select value={repoFilter} onValueChange={onRepoChange}>
<SelectTrigger className="pl-9">
<SelectValue placeholder="Repository" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All repositories</SelectItem>
{repos.map((r) => (
<SelectItem key={r.id} value={r.id}>
{r.display_name || `${r.owner}/${r.repo}`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Input
placeholder="Search Git Project issues…"
value={search}
onChange={(e) => onSearchChange(e.target.value)}
className="min-w-0"
/>
<div className="relative min-w-0">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" />
<Input
placeholder="Search Git Project issues..."
value={search}
onChange={(e) => onSearchChange(e.target.value)}
className="min-w-0 pl-9"
/>
</div>
</div>
);
}

View File

@ -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) => (
<span
key={login}
className="flex h-7 w-7 items-center justify-center rounded-full border-2 border-[color:var(--surface)] bg-[color:var(--surface-muted)] text-[10px] font-semibold text-strong shadow-sm"
className="flex h-7 w-7 items-center justify-center rounded-full border-2 border-[color:var(--surface)] bg-[linear-gradient(145deg,rgba(96,165,250,0.22),rgba(52,211,153,0.14)),var(--surface-muted)] text-[10px] font-semibold text-strong shadow-sm"
>
{getInitials(login)}
</span>
@ -154,8 +191,10 @@ function IssueStateIcon({ state }: { state: string }) {
return (
<span
className={cn(
"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center",
isOpen ? "text-[color:var(--success)]" : "text-[color:var(--danger)]",
"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border",
isOpen
? "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.13)] text-[color:var(--success)]"
: "border-[color:rgba(168,85,247,0.28)] bg-[color:rgba(168,85,247,0.12)] text-[color:rgb(196,181,253)]",
)}
title={isOpen ? "Open issue" : "Closed issue"}
>
@ -233,15 +272,15 @@ export function ForgejoIssuesTable({
return (
<>
<div className="overflow-hidden rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-lush">
<div className="flex flex-col gap-3 border-b border-[color:var(--border)] bg-[color:var(--surface-muted)] px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
<div className="overflow-hidden rounded-xl border border-[color:rgba(96,165,250,0.2)] bg-[linear-gradient(180deg,rgba(96,165,250,0.07),rgba(15,23,36,0)_160px),var(--surface)] shadow-lush">
<div className="flex flex-col gap-3 border-b border-[color:var(--border)] bg-[linear-gradient(90deg,rgba(96,165,250,0.15),rgba(52,211,153,0.1),rgba(251,191,36,0.08))] px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<span className="inline-flex items-center gap-1.5 font-medium text-strong">
<CircleDot className="h-4 w-4 text-[color:var(--success)]" />
{issueCounts.open} Open
</span>
<span className="inline-flex items-center gap-1.5 text-muted">
<CheckCircle2 className="h-4 w-4" />
<CheckCircle2 className="h-4 w-4 text-[color:rgb(196,181,253)]" />
{issueCounts.closed} Closed
</span>
</div>
@ -254,7 +293,7 @@ export function ForgejoIssuesTable({
<LoadingIssuesList />
) : issues.length === 0 ? (
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
<div className="mb-4 rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-4 text-[color:var(--text-muted)]">
<div className="mb-4 rounded-2xl border border-[color:rgba(96,165,250,0.28)] bg-[color:rgba(96,165,250,0.1)] p-4 text-[color:var(--accent-strong)]">
<CircleDot className="h-12 w-12" />
</div>
<h3 className="mb-2 text-lg font-semibold text-strong">
@ -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 (
<article
key={issue.id}
className="group grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-3 px-4 py-4 transition-colors hover:bg-[color:var(--surface-muted)] sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:gap-x-4"
className={cn(
"group grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-3 border-l-4 px-4 py-4 transition-colors sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:gap-x-4",
toneClass.rail,
toneClass.row,
)}
>
<IssueStateIcon state={issue.state} />
@ -303,6 +348,21 @@ export function ForgejoIssuesTable({
>
{issue.title}
</button>
<span
className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide",
toneClass.pill,
)}
>
{tone === "stale" ? (
<AlertCircle className="h-3 w-3" />
) : issue.state === "open" ? (
<CircleDot className="h-3 w-3" />
) : (
<CheckCircle2 className="h-3 w-3" />
)}
{tone === "stale" ? "Stale" : issue.state}
</span>
{visibleLabels.map((label, i) => (
<LabelChip key={`${label.name}-${i}`} label={label} />
))}
@ -314,13 +374,20 @@ export function ForgejoIssuesTable({
</div>
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted">
<span className="font-medium text-strong">
<span className="inline-flex min-w-0 items-center gap-1 font-medium text-strong">
<GitBranch className="h-3.5 w-3.5 shrink-0 text-[color:var(--accent)]" />
{repositoryName}
</span>
<span className="font-mono">
<span className="rounded-full border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-2 py-0.5 font-mono">
#{issue.forgejo_issue_number}
</span>
<span title={formatDateTime(updatedAt)}>
<span
className={cn(
"mr-1 inline-block h-1.5 w-1.5 rounded-full",
toneClass.dot,
)}
/>
{stateVerb} {formatRelativeTime(updatedAt)}
</span>
{issue.author ? <span>by {issue.author}</span> : null}

View File

@ -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({
}
}}
>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogContent className="relative max-w-lg overflow-hidden border-[color:rgba(52,211,153,0.28)] bg-[linear-gradient(180deg,rgba(52,211,153,0.08),rgba(255,255,255,0)_190px),var(--surface)]">
<div className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(52,211,153,0),rgba(52,211,153,0.82),rgba(96,165,250,0.65),rgba(52,211,153,0))]" />
<DialogHeader className="border-b border-[color:var(--border)] pb-4">
<div className="mb-1 flex h-9 w-9 items-center justify-center rounded-xl border border-[color:rgba(52,211,153,0.3)] bg-[color:rgba(52,211,153,0.13)] text-[color:var(--success)]">
<MessageSquarePlus className="h-4 w-4" />
</div>
<DialogTitle>Post comment</DialogTitle>
<DialogDescription>
Comment on{" "}
@ -76,7 +81,7 @@ export function PostForgejoCommentDialog({
</DialogDescription>
</DialogHeader>
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2">
<div className="rounded-xl border border-[color:rgba(52,211,153,0.22)] bg-[color:rgba(52,211,153,0.07)] px-3 py-2">
<p className="truncate text-sm font-medium text-strong">
{issue.title}
</p>
@ -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 ? (

View File

@ -61,8 +61,8 @@ export function AgentActivityTicker() {
const display = [...items, ...items];
return (
<div className="border-t border-[color:var(--border)] bg-[color:var(--surface-muted)] overflow-hidden h-7 flex items-center">
<span className="shrink-0 px-3 text-[10px] font-semibold uppercase tracking-widest text-[color:var(--text-quiet)] select-none border-r border-[color:var(--border)] h-full flex items-center">
<div className="border-t border-[color:var(--border)] bg-[color:var(--surface-muted)] overflow-hidden h-8 flex items-center">
<span className="shrink-0 px-3 text-[11px] font-semibold uppercase tracking-widest text-[color:var(--text-quiet)] select-none border-r border-[color:var(--border)] h-full flex items-center">
Live
</span>
<div className="flex-1 overflow-hidden h-full flex items-center">
@ -70,7 +70,7 @@ export function AgentActivityTicker() {
{display.map((item, idx) => (
<span
key={`${item.id}-${idx}`}
className="inline-flex items-center gap-1.5 px-4 text-[10px]"
className="inline-flex items-center gap-2 px-6 text-[11px]"
>
<span className="font-semibold text-[color:var(--accent)]">
{item.source}
@ -80,7 +80,7 @@ export function AgentActivityTicker() {
<span className="text-[color:var(--text-quiet)] tabular-nums ml-1">
{fmtRelative(item.created_at)}
</span>
<span className="mx-3 text-[color:var(--border)] select-none">
<span className="mx-5 text-[color:var(--border)] select-none">
</span>
</span>