Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

190 lines · 7.8 KBRaw History
1/** @jsxImportSource hono/jsx */
2import { raw } from "hono/html";
3import type { FC } from "hono/jsx";
4import { ICONS } from "../web/assets.js";
5import { Actor, Status, Time } from "../web/ui.js";
6import type { AgentItem, AgentPullState, NeedsItem, NeedsKind, Today } from "./today.js";
7import type { TodayPull } from "./today-repo.js";
8
9/** The Today view's sections on the dashboard (web/account.tsx): what needs you, your agents, and the last day. */
10
11const NEEDS_TEXT: Record<NeedsKind, { label: string; tone: string }> = {
12 review_requested: { label: "Review requested", tone: "blue" },
13 agent_approval: { label: "Agent PR waiting for a person", tone: "purple" },
14 changes_requested: { label: "Changes requested", tone: "amber" },
15 checks_failing: { label: "Checks failing", tone: "red" },
16};
17
18const AGENT_TEXT: Record<AgentPullState, { label: string; tone: string }> = {
19 draft: { label: "Draft", tone: "" },
20 landing: { label: "Landing", tone: "green" },
21 checks_failing: { label: "Checks failing", tone: "red" },
22 changes_requested: { label: "Changes requested", tone: "amber" },
23 approved: { label: "Approved", tone: "green" },
24 checks_running: { label: "Checks running", tone: "blue" },
25 waiting_for_approval: { label: "Waiting for approval", tone: "purple" },
26};
27
28const ChecksIcon: FC<{ p: TodayPull }> = ({ p }) =>
29 p.checks === "none" ? (
30 <span class="st faint" title="No checks">{raw(ICONS.pr)}</span>
31 ) : p.checks === "pending" ? (
32 <Status status="in_progress" />
33 ) : (
34 <Status conclusion={p.checks} />
35 );
36
37const PullLine: FC<{ repo: { fullName: string; url: string }; p: TodayPull; badge: { label: string; tone: string } }> = ({ repo, p, badge }) => (
38 <li>
39 <ChecksIcon p={p} />
40 <div class="grow">
41 <a class="title" href={`${repo.url}/pull/${p.number}`}>
42 {p.title}
43 </a>{" "}
44 <span class={`badge ${badge.tone}`}>{badge.label}</span>
45 <div class="meta">
46 <a href={repo.url} class="muted">
47 {repo.fullName}
48 </a>{" "}
49 #{p.number} by <Actor login={p.author.login} kind={p.author.kind} avatar={false} /> · updated <Time at={p.updatedAt} />
50 {p.failingChecks.length ? <> · failing: {p.failingChecks.slice(0, 3).join(", ")}</> : null}
51 {p.changesRequestedBy.length ? <> · changes requested by {p.changesRequestedBy.join(", ")}</> : null}
52 </div>
53 {p.intent ? <div class="meta">Intent: {p.intent.length > 160 ? `${p.intent.slice(0, 157)}…` : p.intent}</div> : null}
54 </div>
55 </li>
56);
57
58const Tile: FC<{ n: number; label: string; href: string; tone?: string }> = ({ n, label, href, tone }) => (
59 <a class={`tile ${n && tone ? tone : ""}`} href={href}>
60 <span class="num">{n}</span>
61 <span class="lbl">{label}</span>
62 </a>
63);
64
65export const TodayView: FC<{ today: Today; name: string }> = ({ today, name }) => {
66 const working = today.agentsAtWork.filter((x) => x.state !== "draft").length;
67 const landedByAgents = today.landed.filter((x) => x.pull.author.kind === "agent").length;
68 return (
69 <div>
70 <div class="row between mb">
71 <h1 class="m0" style="font-size:22px">Today</h1>
72 <span class="muted small">
73 {name} · {today.repositories} active {today.repositories === 1 ? "repository" : "repositories"}
74 {today.incomplete ? " (some left out)" : ""}
75 </span>
76 </div>
77 <div class="tiles">
78 <Tile n={today.needsYou.length} label="need you" href="#needs-you" tone="purple" />
79 <Tile n={working} label="agent pull requests open" href="#agents" />
80 <Tile n={today.landed.length} label={`merged since yesterday${landedByAgents ? ` (${landedByAgents} by agents)` : ""}`} href="#landed" />
81 <Tile n={today.failures.length} label="failed runs" href="#landed" tone="red" />
82 </div>
83 <NeedsYou items={today.needsYou} />
84 <Agents items={today.agentsAtWork} />
85 <SinceYesterday today={today} />
86 </div>
87 );
88};
89
90const NeedsYou: FC<{ items: NeedsItem[] }> = ({ items }) => (
91 <div class="box" id="needs-you">
92 <div class="head">
93 <h2>{raw(ICONS.eye)} Needs you</h2>
94 <span class="muted small">Reviews you were asked for, agent work waiting for a person, and your own blocked pull requests</span>
95 </div>
96 {items.length ? (
97 <ul class="list">
98 {items.slice(0, 30).map((x) => (
99 <PullLine repo={x.repo} p={x.pull} badge={NEEDS_TEXT[x.kind]} />
100 ))}
101 </ul>
102 ) : (
103 <div class="empty">Nothing is waiting on you.</div>
104 )}
105 </div>
106);
107
108const Agents: FC<{ items: AgentItem[] }> = ({ items }) => (
109 <div class="box" id="agents">
110 <div class="head">
111 <h2>{raw(ICONS.bot)} Agents at work</h2>
112 <span class="muted small">Open pull requests from agents you and your organizations own</span>
113 <a class="small" href="/settings/agents" style="margin-left:auto">
114 Manage agents
115 </a>
116 </div>
117 {items.length ? (
118 <ul class="list">
119 {items.slice(0, 30).map((x) => (
120 <PullLine repo={x.repo} p={x.pull} badge={AGENT_TEXT[x.state]} />
121 ))}
122 </ul>
123 ) : (
124 <div class="empty">
125 Your agents have no open pull requests. <a href="/docs/agents/overview">Set up a coding agent</a>
126 </div>
127 )}
128 </div>
129);
130
131const SinceYesterday: FC<{ today: Today }> = ({ today }) => {
132 const empty = !today.landed.length && !today.releases.length && !today.failures.length;
133 return (
134 <div class="box" id="landed">
135 <div class="head">
136 <h2>{raw(ICONS.history)} Since yesterday</h2>
137 <span class="muted small">Merged, released and failed in the last 24 hours</span>
138 </div>
139 {empty ? (
140 <div class="empty">Nothing merged, released or failed in the last 24 hours.</div>
141 ) : (
142 <ul class="list">
143 {today.releases.map((x) => (
144 <li>
145 <span class="st ok">{raw(ICONS.tag)}</span>
146 <div class="grow">
147 <a class="title" href={`${x.repo.url}/releases/tag/${encodeURIComponent(x.release.tag)}`}>
148 {x.release.name}
149 </a>{" "}
150 <span class="badge green">{x.release.prerelease ? "Pre-release" : "Released"}</span>
151 <div class="meta">
152 {x.repo.fullName} · by <Actor login={x.release.author.login} kind={x.release.author.kind} avatar={false} /> · <Time at={x.release.publishedAt} />
153 </div>
154 </div>
155 </li>
156 ))}
157 {today.failures.map((x) => (
158 <li>
159 <span class="st bad">{raw(ICONS.failure)}</span>
160 <div class="grow">
161 <a class="title" href={`${x.repo.url}/actions/runs/${x.run.id}`}>
162 {x.run.title || x.run.workflow}
163 </a>{" "}
164 <span class="badge red">{x.run.workflow} failed</span>
165 <div class="meta">
166 {x.repo.fullName} · {x.run.ref.replace(/^refs\/heads\//, "")} · <Actor login={x.run.actor.login} kind={x.run.actor.kind} avatar={false} /> · <Time at={x.run.at} />
167 </div>
168 </div>
169 </li>
170 ))}
171 {today.landed.map((x) => (
172 <li>
173 <span class="st" style="color:var(--purple)">{raw(ICONS.prMerged)}</span>
174 <div class="grow">
175 <a class="title" href={`${x.repo.url}/pull/${x.pull.number}`}>
176 {x.pull.title}
177 </a>{" "}
178 <span class="badge purple">Merged</span>
179 <div class="meta">
180 {x.repo.fullName} #{x.pull.number} by <Actor login={x.pull.author.login} kind={x.pull.author.kind} avatar={false} />
181 {x.pull.mergedBy ? <> · merged by {x.pull.mergedBy}</> : null} · <Time at={x.pull.mergedAt} />
182 </div>
183 </div>
184 </li>
185 ))}
186 </ul>
187 )}
188 </div>
189 );
190};