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.

235 lines · 9.0 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 { AGENT_TEXT, NEEDS_TEXT, TASK_TEXT, type AgentItem, type NeedsItem, type Today } from "./today.js";
7import type { TodayPull } from "./today-repo.js";
8import type { MyTask } from "./tasks.js";
9
10/** The Today view's sections on the dashboard (web/account.tsx): what needs you, your agents, and the last day. */
11
12const ChecksIcon: FC<{ p: TodayPull }> = ({ p }) =>
13 p.checks === "none" ? (
14 <span class="st faint" title="No checks">{raw(ICONS.pr)}</span>
15 ) : p.checks === "pending" ? (
16 <Status status="in_progress" />
17 ) : (
18 <Status conclusion={p.checks} />
19 );
20
21const PullLine: FC<{ repo: { fullName: string; url: string }; p: TodayPull; badge: { label: string; tone: string } }> = ({ repo, p, badge }) => (
22 <li>
23 <ChecksIcon p={p} />
24 <div class="grow">
25 <a class="title" href={`${repo.url}/pull/${p.number}`}>
26 {p.title}
27 </a>{" "}
28 <span class={`badge ${badge.tone}`}>{badge.label}</span>
29 <div class="meta">
30 <a href={repo.url} class="muted">
31 {repo.fullName}
32 </a>{" "}
33 #{p.number} by <Actor login={p.author.login} kind={p.author.kind} avatar={false} /> · updated <Time at={p.updatedAt} />
34 {p.failingChecks.length ? <> · failing: {p.failingChecks.slice(0, 3).join(", ")}</> : null}
35 {p.changesRequestedBy.length ? <> · changes requested by {p.changesRequestedBy.join(", ")}</> : null}
36 </div>
37 {p.intent ? <div class="meta">Intent: {p.intent.length > 160 ? `${p.intent.slice(0, 157)}…` : p.intent}</div> : null}
38 </div>
39 </li>
40);
41
42const Tile: FC<{ n: number; label: string; href: string; tone?: string }> = ({ n, label, href, tone }) => (
43 <a class={`tile ${n && tone ? tone : ""}`} href={href}>
44 <span class="num">{n}</span>
45 <span class="lbl">{label}</span>
46 </a>
47);
48
49export const TodayView: FC<{ today: Today; name: string }> = ({ today, name }) => {
50 const working = today.agentsAtWork.filter((x) => x.state !== "draft").length;
51 const landedByAgents = today.landed.filter((x) => x.pull.author.kind === "agent").length;
52 return (
53 <div>
54 <div class="row between mb">
55 <h1 class="m0" style="font-size:22px">Today</h1>
56 <span class="muted small">
57 {name} · {today.repositories} active {today.repositories === 1 ? "repository" : "repositories"}
58 {today.incomplete ? " (some left out)" : ""}
59 </span>
60 </div>
61 <div class="tiles">
62 <Tile n={today.needsYou.length} label="need you" href="#needs-you" tone="purple" />
63 <Tile n={working} label="agent pull requests open" href="#agents" />
64 <Tile n={today.landed.length} label={`merged since yesterday${landedByAgents ? ` (${landedByAgents} by agents)` : ""}`} href="#landed" />
65 <Tile n={today.failures.length} label="failed runs" href="#landed" tone="red" />
66 </div>
67 <NeedsYou items={today.needsYou} />
68 {today.deployments.length ? <Deployments items={today.deployments} /> : null}
69 <Agents items={today.agentsAtWork} />
70 {today.tasks.length ? <Tasks items={today.tasks} /> : null}
71 <SinceYesterday today={today} />
72 </div>
73 );
74};
75
76const Tasks: FC<{ items: MyTask[] }> = ({ items }) => (
77 <div class="box" id="tasks">
78 <div class="head">
79 <h2>{raw(ICONS.zap)} Your agent tasks</h2>
80 <span class="muted small">Running, or changed since yesterday</span>
81 </div>
82 <ul class="list">
83 {items.map((t) => (
84 <li>
85 <div class="grow">
86 <a href={`/${t.repository}/tasks/${t.number}`}>
87 <b>{t.title}</b>
88 </a>{" "}
89 <span class={`badge ${TASK_TEXT[t.state].tone}`}>{TASK_TEXT[t.state].label}</span>
90 <div class="muted small">
91 {t.repository} · task #{t.number} · {t.agent}
92 {t.prNumber ? (
93 <>
94 {" "}
95 · <a href={`/${t.repository}/pull/${t.prNumber}`}>#{t.prNumber}</a>
96 </>
97 ) : null}
98 </div>
99 </div>
100 <span class="muted small">
101 <Time at={t.updatedAt} />
102 </span>
103 </li>
104 ))}
105 </ul>
106 </div>
107);
108
109const Deployments: FC<{ items: Today["deployments"] }> = ({ items }) => (
110 <div class="box" id="deployments">
111 <div class="head">
112 <h2>{raw(ICONS.rocket)} Deployments waiting for you</h2>
113 <span class="muted small">You're a required reviewer for these environments</span>
114 </div>
115 <ul class="list">
116 {items.map(({ repo, deployment: d }) => (
117 <li>
118 <div class="grow">
119 <a href={`${repo.url}/actions/runs/${d.runId}`}>
120 <b>{d.environment}</b>: {d.title || d.workflow}
121 </a>
122 <div class="muted small">
123 {repo.fullName} · {d.workflow} #{d.runNumber} on {d.ref.replace(/^refs\/(heads|tags)\//, "")} · started by {d.actor.login}
124 </div>
125 </div>
126 <span class="muted small">
127 <Time at={d.since} />
128 </span>
129 </li>
130 ))}
131 </ul>
132 </div>
133);
134
135const NeedsYou: FC<{ items: NeedsItem[] }> = ({ items }) => (
136 <div class="box" id="needs-you">
137 <div class="head">
138 <h2>{raw(ICONS.eye)} Needs you</h2>
139 <span class="muted small">Reviews you were asked for, agent work waiting for a person, and your own blocked pull requests</span>
140 </div>
141 {items.length ? (
142 <ul class="list">
143 {items.slice(0, 30).map((x) => (
144 <PullLine repo={x.repo} p={x.pull} badge={NEEDS_TEXT[x.kind]} />
145 ))}
146 </ul>
147 ) : (
148 <div class="empty">Nothing is waiting on you.</div>
149 )}
150 </div>
151);
152
153const Agents: FC<{ items: AgentItem[] }> = ({ items }) => (
154 <div class="box" id="agents">
155 <div class="head">
156 <h2>{raw(ICONS.bot)} Agents at work</h2>
157 <span class="muted small">Open pull requests from agents you and your organizations own</span>
158 <a class="small" href="/settings/agents" style="margin-left:auto">
159 Manage agents
160 </a>
161 </div>
162 {items.length ? (
163 <ul class="list">
164 {items.slice(0, 30).map((x) => (
165 <PullLine repo={x.repo} p={x.pull} badge={AGENT_TEXT[x.state]} />
166 ))}
167 </ul>
168 ) : (
169 <div class="empty">
170 Your agents have no open pull requests. <a href="/docs/agents/overview">Set up a coding agent</a>
171 </div>
172 )}
173 </div>
174);
175
176const SinceYesterday: FC<{ today: Today }> = ({ today }) => {
177 const empty = !today.landed.length && !today.releases.length && !today.failures.length;
178 return (
179 <div class="box" id="landed">
180 <div class="head">
181 <h2>{raw(ICONS.history)} Since yesterday</h2>
182 <span class="muted small">Merged, released and failed in the last 24 hours</span>
183 </div>
184 {empty ? (
185 <div class="empty">Nothing merged, released or failed in the last 24 hours.</div>
186 ) : (
187 <ul class="list">
188 {today.releases.map((x) => (
189 <li>
190 <span class="st ok">{raw(ICONS.tag)}</span>
191 <div class="grow">
192 <a class="title" href={`${x.repo.url}/releases/tag/${encodeURIComponent(x.release.tag)}`}>
193 {x.release.name}
194 </a>{" "}
195 <span class="badge green">{x.release.prerelease ? "Pre-release" : "Released"}</span>
196 <div class="meta">
197 {x.repo.fullName} · by <Actor login={x.release.author.login} kind={x.release.author.kind} avatar={false} /> · <Time at={x.release.publishedAt} />
198 </div>
199 </div>
200 </li>
201 ))}
202 {today.failures.map((x) => (
203 <li>
204 <span class="st bad">{raw(ICONS.failure)}</span>
205 <div class="grow">
206 <a class="title" href={`${x.repo.url}/actions/runs/${x.run.id}`}>
207 {x.run.title || x.run.workflow}
208 </a>{" "}
209 <span class="badge red">{x.run.workflow} failed</span>
210 <div class="meta">
211 {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} />
212 </div>
213 </div>
214 </li>
215 ))}
216 {today.landed.map((x) => (
217 <li>
218 <span class="st" style="color:var(--purple)">{raw(ICONS.prMerged)}</span>
219 <div class="grow">
220 <a class="title" href={`${x.repo.url}/pull/${x.pull.number}`}>
221 {x.pull.title}
222 </a>{" "}
223 <span class="badge purple">Merged</span>
224 <div class="meta">
225 {x.repo.fullName} #{x.pull.number} by <Actor login={x.pull.author.login} kind={x.pull.author.kind} avatar={false} />
226 {x.pull.mergedBy ? <> · merged by {x.pull.mergedBy}</> : null} · <Time at={x.pull.mergedAt} />
227 </div>
228 </div>
229 </li>
230 ))}
231 </ul>
232 )}
233 </div>
234 );
235};