Skip to content

Commit

Merge branch 'site-fixes' into fast-push

syntaqxcommitted Parents9ef5a0836364a3Browse files
18 files+133−610/18 viewed
+5−0
436436 g1t.page manage their apps under **On g1t.page** on the same page.
437437 - **A deployment's page**, `g1t.sh/<owner>/<project>/deployments/<id>`,
438438 shows its statuses in order, links to its log and run, and its payload.
439+ Only the newest status can show as still under way; a `queued` or
440+ `in_progress` one that a later status followed shows as over.
441+- **Every link to where something runs**, a g1t.page address, an
442+ environment's URL, a pull request's preview or a project's homepage,
443+ opens in a new tab, wherever on the site it appears.
439444 - **The repository's code page** and **the project's overview** show a
440445 **Deployments** panel with how many there are, and each environment's
441446 latest deployment and when. It links to the Deployments page.
+4−1
195195 at most twice, or as often as the repository's **Revisions before asking
196196 you** allows.
197197 4. **Ready to merge.** The required checks passed and it is approved.
198− Merging is yours, unless the repository says otherwise (below).
198+ Merging is yours, unless the repository says otherwise (below). Until
199+ you merge it, Home and Code's overview list it as waiting for you to
200+ merge it, with a link to **Merge automatically when ready** in the
201+ repository's branch settings for a pull request g1t made.
199202
200203 Agents are told to run the same tests and linters the workflows run before
201204 they finish, so most failures are caught in the sandbox. What "done" means
+12−0
11 /** Small pieces every page about deployments shares. */
2+import type { AnchorHTMLAttributes } from "react";
3+
24 import type { DeployStatus } from "@g1t/contracts";
35
46 export const DEPLOY_STATUS: Record<DeployStatus, { label: string; tone: string }> = {
2527 export function host(url: string): string {
2628 return url.replace(/^https?:\/\//, "");
2729 }
30+
31+/**
32+ * A link to where something runs: a g1t.page address, an environment's
33+ * URL, a preview, a project's homepage. It always opens in a new tab, so
34+ * the page about it stays open behind it. `rel` adds to noopener and
35+ * noreferrer (nofollow, say), never replaces them.
36+ */
37+export function DeployLink({ rel, ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) {
38+ return <a {...props} target="_blank" rel={rel ? `noopener noreferrer ${rel}` : "noopener noreferrer"} />;
39+}
+14−2
33 * latest deployment and when, linking to the Deployments page. Also the
44 * state icon and badge every page about deployments shares.
55 */
6−import { Check, CircleDashed, Clock, Loader2, Rocket, X } from "lucide-react";
6+import { Check, CircleDashed, CircleDot, Clock, Loader2, Rocket, X } from "lucide-react";
77 import { Link } from "react-router";
88
99 import type { DeploymentEnvironments, DeploymentSource, DeploymentState } from "@g1t/contracts";
2626 };
2727
2828 /** A deployment's state as an icon: a check, a cross, a spinner, a clock. */
29−export function DeploymentStateIcon({ state, size = 16, className }: { state: DeploymentState; size?: number; className?: string }) {
29+export function DeploymentStateIcon({
30+ state,
31+ size = 16,
32+ className,
33+ over = false,
34+}: {
35+ state: DeploymentState;
36+ size?: number;
37+ className?: string;
38+ /** A past status a later one followed: under way then, over now, so it does not spin. */
39+ over?: boolean;
40+}) {
3041 const label = STATE_WORD[state];
3142 switch (state) {
3243 case "success":
4354 </span>
4455 );
4556 case "in_progress":
57+ if (over) return <CircleDot size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
4658 return <Loader2 size={size} aria-label={label} className={cn("shrink-0 animate-spin text-warn", className)} />;
4759 case "queued":
4860 return <Clock size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
+3−2
22 import { useEffect, useRef, useState } from "react";
33
44 import { cn } from "../lib/cn";
5+import { DeployLink } from "./deploy";
56
67 /**
78 * A thumbnail of production as it is now, linking to it. The screenshot is
5859 );
5960
6061 return href ? (
61− <a href={href} className={cn("group block", className)} aria-label={`Visit ${label}`}>
62+ <DeployLink href={href} className={cn("group block", className)} aria-label={`Visit ${label}`}>
6263 {frame}
63− </a>
64+ </DeployLink>
6465 ) : (
6566 <div className={className}>{frame}</div>
6667 );
+4−3
66
77 import { cn } from "../lib/cn";
88 import { KIND_CHOICES, type KindChoice, type ShownLink, choiceOf, linksToShow } from "../lib/project-kind";
9+import { DeployLink } from "./deploy";
910 import { Input, SubmitButton } from "./ui";
1011 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
1112 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu";
3031 <ul className={cn("space-y-1.5 text-sm", className)}>
3132 {list.map((link) => (
3233 <li key={link.key} className="min-w-0">
33− <a
34+ <DeployLink
3435 href={link.url}
35− rel="noopener noreferrer nofollow"
36+ rel="nofollow"
3637 className="flex min-w-0 items-center gap-2 text-fg-soft hover:text-accent [&_svg]:shrink-0 [&_svg]:text-faint"
3738 >
3839 {LINK_ICON[link.type]}
3940 <span className="truncate">{link.label}</span>
40− </a>
41+ </DeployLink>
4142 </li>
4243 ))}
4344 </ul>
+7−7
1010
1111 import type { DeployStatus, Project } from "@g1t/contracts";
1212
13−import { host, StatusDot } from "./deploy";
13+import { DeployLink, host, StatusDot } from "./deploy";
1414 import { ButtonLink, Input, SubmitButton, TimeAgo } from "./ui";
1515 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
1616 import { Hint } from "./ui/hint";
6666
6767 function BigLink({ url }: { url: string }) {
6868 return (
69− <a href={url} rel="noopener noreferrer" className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent">
69+ <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent">
7070 <span className="truncate">{host(url).replace(/\/$/, "")}</span>
7171 <ArrowUpRight size={16} className="shrink-0 text-faint" />
72− </a>
72+ </DeployLink>
7373 );
7474 }
7575
190190 </div>
191191 {url && (
192192 <div className="flex shrink-0 items-center gap-2">
193− <ButtonLink to={url} variant="accent" reloadDocument>
193+ <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
194194 Visit
195195 <ArrowUpRight size={14} />
196196 </ButtonLink>
291291 </div>
292292 {url && (
293293 <div className="flex shrink-0 items-center gap-2">
294− <ButtonLink to={url} variant="accent" reloadDocument>
294+ <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
295295 Read
296296 <ArrowUpRight size={14} />
297297 </ButtonLink>
329329 if (!url) return null;
330330 return (
331331 <Hint label="Its homepage">
332− <a href={url} rel="noopener noreferrer" className="inline-flex items-center gap-1 text-faint hover:text-fg">
332+ <DeployLink href={url} className="inline-flex items-center gap-1 text-faint hover:text-fg">
333333 <Globe size={12} />
334334 {host(url).replace(/\/$/, "")}
335− </a>
335+ </DeployLink>
336336 </Hint>
337337 );
338338 }
+3−4
33 import { Link } from "react-router";
44
55 import { Duration, StatusIcon } from "./actions";
6+import { DeployLink } from "./deploy";
67 import { GRAPH, type GraphJob, type EdgeState, type PlacedGroup, type PlacedNode, type Standing, layoutRun, sharedUrl, standingOf, unitStanding } from "../lib/run-graph";
78
89 /** A job calling a workflow shows as running while its jobs do. */
4647 /** Where a deployment is, under its node's name. */
4748 function Address({ url }: { url: string }) {
4849 return (
49− <a
50+ <DeployLink
5051 href={url}
51− target="_blank"
52− rel="noreferrer"
5352 className="-mt-2 flex items-center gap-1 truncate pr-3 pl-9 font-mono text-xs text-accent hover:underline"
5453 style={{ height: GRAPH.urlHeight }}
5554 >
5655 <span className="truncate">{url.replace(/^https?:\/\//, "")}</span>
5756 <ExternalLink size={11} className="shrink-0" />
58− </a>
57+ </DeployLink>
5958 );
6059 }
6160
+17−0
1212 groupBuilds,
1313 hasPayload,
1414 isFiltered,
15+ isNewestStatus,
1516 isPageBuild,
1617 orderEnvironments,
1718 pageRange,
195196 assert.equal(hasPayload({ region: "wnam" }), true);
196197 assert.equal(hasPayload(null), false);
197198 });
199+
200+test("only a deployment's newest status can still be under way", () => {
201+ const statuses = [
202+ { created_at: "2026-10-08T10:00:00.000Z" }, // Deploying, building
203+ { created_at: "2026-10-08T10:02:00.000Z" }, // Deployed
204+ ];
205+ assert.equal(isNewestStatus(statuses, 0), false);
206+ assert.equal(isNewestStatus(statuses, 1), true);
207+ // Listed out of order: the newest is still the latest written.
208+ assert.equal(isNewestStatus([...statuses].reverse(), 0), true);
209+ // Written in the same moment: the last listed is the newest.
210+ const tied = [{ created_at: "2026-10-08T10:00:00.000Z" }, { created_at: "2026-10-08T10:00:00.000Z" }];
211+ assert.equal(isNewestStatus(tied, 0), false);
212+ assert.equal(isNewestStatus(tied, 1), true);
213+ assert.equal(isNewestStatus([], 0), false);
214+});
+13−0
99 DeploymentFilter,
1010 DeploymentSource,
1111 DeploymentState,
12+ DeploymentStatus,
1213 RepoDeployment,
1314 } from "@g1t/contracts";
1415
210211 export function hasPayload(payload: Record<string, unknown> | null | undefined): boolean {
211212 return payload != null && typeof payload === "object" && Object.keys(payload).length > 0;
212213 }
214+
215+/**
216+ * Whether a status on a deployment's timeline is its newest: the latest
217+ * one written (the last listed, when two share a time). Only the newest
218+ * can still be under way; one a later status followed is over, whatever
219+ * it said, so it is never shown as running.
220+ */
221+export function isNewestStatus(statuses: readonly Pick<DeploymentStatus, "created_at">[], index: number): boolean {
222+ const at = statuses[index]?.created_at;
223+ if (at == null) return false;
224+ return statuses.every((other, i) => i === index || (i < index ? other.created_at <= at : other.created_at < at));
225+}
+4−1
393393 key,
394394 kind: "ready",
395395 title: pull.title,
396− detail: lowConfidence ? confidenceAsk(lowConfidence) : "Its required checks passed and it was approved. It lands when you merge it.",
396+ // The ask first: a list cuts the sentence short, and "it lands" alone reads as stuck.
397+ detail: lowConfidence ? confidenceAsk(lowConfidence) : "Waiting for you to merge it. It was approved and its required checks passed.",
397398 to,
398399 action: "Merge",
399400 at: Date.parse(pull.updatedAt),
406407 fields: { action: "merge" },
407408 done: "Merging",
408409 }),
410+ // This repository leaves landing an agent's change to a person; say where that is changed.
411+ link: isAgent(pull.agent) ? { label: "Merge automatically when ready", to: `/${repo.namespace}/${repo.name}/settings/branches` } : null,
409412 });
410413 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
411414 // Taken out of the merge queue: its change failed combined with what was ahead.
+4−4
77 import type { Route } from "./+types/explore";
88 import { EmptyState, Pill, TimeAgo, notACredential } from "../components/ui";
99 import { page } from "../lib/meta";
10−import { host } from "../components/deploy";
10+import { DeployLink, host } from "../components/deploy";
1111 import { projects, search } from "../lib/services.server";
1212 import { getViewer } from "../lib/session.server";
1313
6666 </span>
6767 <span className="mt-2 line-clamp-2 grow text-sm text-muted">{repo.description ?? "No description."}</span>
6868 {link && (
69− <a
69+ <DeployLink
7070 href={link}
71− rel="noopener noreferrer nofollow"
71+ rel="nofollow"
7272 className="relative z-10 mt-2 flex min-w-0 items-center gap-1 self-start font-mono text-xs text-muted hover:text-accent"
7373 >
7474 <span className="truncate">{host(link).replace(/\/$/, "")}</span>
7575 <ArrowUpRight size={11} className="shrink-0" />
76− </a>
76+ </DeployLink>
7777 )}
7878 {repo.topics.length > 0 && (
7979 <span className="mt-3 flex flex-wrap gap-1.5">
+17−12
99 import { TimeAgo } from "../../components/ui";
1010 import { Badge } from "../../components/ui/badge";
1111 import { Hint } from "../../components/ui/hint";
12−import { host } from "../../components/deploy";
12+import { DeployLink, host } from "../../components/deploy";
1313 import { DeploymentStateBadge, DeploymentStateIcon, STATE_TONE, sourceLabel } from "../../components/deployments-panel";
1414 import { deployments } from "../../lib/services.server";
1515 import { getViewer } from "../../lib/session.server";
1616 import { requireRepo } from "../../lib/access.server";
17−import { STATE_WORD, buildError, environmentLabel, hasPayload, isPageBuild, shortSha } from "../../lib/deployments";
17+import { STATE_WORD, buildError, environmentLabel, hasPayload, isNewestStatus, isPageBuild, shortSha } from "../../lib/deployments";
1818
1919 export function meta({ params, ...args }: Route.MetaArgs) {
2020 return page(args, { title: `Deployment · ${params.owner}/${params.repo} · g1t` });
8989 </p>
9090 </div>
9191 {url && (
92− <a
92+ <DeployLink
9393 href={url}
9494 className="inline-flex max-w-full items-center gap-1.5 rounded-md bg-accent px-3.5 py-2 text-sm font-medium text-bg hover:bg-accent-hover"
9595 >
9696 <span className="truncate">{host(url)}</span>
9797 <ExternalLink size={14} className="shrink-0" />
98− </a>
98+ </DeployLink>
9999 )}
100100 </header>
101101
169169 ) : (
170170 <ol className="mt-4">
171171 {deployment.statuses.map((status, index) => (
172− <StatusItem key={status.id} status={status} last={index === deployment.statuses.length - 1} />
172+ <StatusItem
173+ key={status.id}
174+ status={status}
175+ last={index === deployment.statuses.length - 1}
176+ over={!isNewestStatus(deployment.statuses, index)}
177+ />
173178 ))}
174179 </ol>
175180 )}
201206 }
202207
203208 /** One status on the timeline, oldest at the top, with a line down to the next. */
204−function StatusItem({ status, last }: { status: DeploymentStatus; last: boolean }) {
209+function StatusItem({ status, last, over }: { status: DeploymentStatus; last: boolean; over: boolean }) {
205210 return (
206211 <li className="relative flex gap-3 pb-5 last:pb-0">
207212 {!last && <span aria-hidden="true" className="absolute top-6 bottom-1 left-[9px] w-px bg-line" />}
208213 <span className="relative mt-0.5 flex size-[19px] shrink-0 items-center justify-center bg-bg">
209− <DeploymentStateIcon state={status.state} size={18} />
214+ <DeploymentStateIcon state={status.state} size={18} over={over} />
210215 </span>
211216 <div className="min-w-0 grow">
212217 <p className="flex flex-wrap items-baseline gap-x-2 text-sm">
213− <span className={`font-medium ${STATE_TONE[status.state]}`}>{STATE_WORD[status.state]}</span>
218+ <span className={`font-medium ${over && status.state === "in_progress" ? "text-muted" : STATE_TONE[status.state]}`}>{STATE_WORD[status.state]}</span>
214219 <span className="text-xs text-faint">
215220 {status.creator} ·{" "}
216221 <Hint label={new Date(status.created_at).toUTCString()}>
224229 {(status.environment_url || status.log_url) && (
225230 <p className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-xs">
226231 {status.environment_url && (
227− <a href={status.environment_url} className="inline-flex min-w-0 items-center gap-1 font-mono text-accent hover:underline">
232+ <DeployLink href={status.environment_url} className="inline-flex min-w-0 items-center gap-1 font-mono text-accent hover:underline">
228233 <span className="truncate">{host(status.environment_url)}</span>
229234 <ExternalLink size={11} className="shrink-0" />
230− </a>
235+ </DeployLink>
231236 )}
232237 {status.log_url && (
233238 <a href={status.log_url} className="inline-flex items-center gap-1 text-muted hover:text-fg">
265270 {BUILD_WORDS[build.status]} · started <TimeAgo at={build.createdAt} /> by {build.createdBy}
266271 </p>
267272 {build.status === "ready" && (
268− <a href={build.url} className="mt-3 inline-flex items-center gap-1.5 font-mono text-sm text-accent hover:underline">
273+ <DeployLink href={build.url} className="mt-3 inline-flex items-center gap-1.5 font-mono text-sm text-accent hover:underline">
269274 {host(build.url)}
270275 <ExternalLink size={12} />
271− </a>
276+ </DeployLink>
272277 )}
273278 </>
274279 );
+9−9
1616
1717 import type { DeploymentEnvironment, FeatureState, RepoDeployment } from "@g1t/contracts";
1818
19−import { host } from "../../components/deploy";
19+import { DeployLink, host } from "../../components/deploy";
2020 import { DeploymentStateBadge, DeploymentStateIcon, sourceLabel } from "../../components/deployments-panel";
2121 import { FilterMenu } from "../../components/labels";
2222
156156 </p>
157157 </div>
158158 {productionUrl && (
159− <a
159+ <DeployLink
160160 href={productionUrl}
161161 className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-line px-3 py-1.5 font-mono text-xs text-muted hover:border-line-strong hover:text-fg"
162162 >
163163 <Globe size={13} className="shrink-0" />
164164 <span className="truncate">{host(productionUrl)}</span>
165− </a>
165+ </DeployLink>
166166 )}
167167 </header>
168168
297297 {latest && <DeploymentStateBadge state={latest.state} />}
298298 </div>
299299 {url ? (
300− <a href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
300+ <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
301301 <span className="truncate">{host(url)}</span>
302302 <ExternalLink size={12} className="shrink-0" />
303− </a>
303+ </DeployLink>
304304 ) : (
305305 <p className="mt-2 text-sm text-faint">No address</p>
306306 )}
652652 </Link>
653653 )}
654654 </span>
655− <a href={app.url} className="hidden min-w-0 truncate font-mono text-xs text-muted hover:text-fg sm:block">
655+ <DeployLink href={app.url} className="hidden min-w-0 truncate font-mono text-xs text-muted hover:text-fg sm:block">
656656 {host(app.url)}
657− </a>
657+ </DeployLink>
658658 <span className="ml-auto shrink-0">{can.run && <AppActions branch={app.branch} up compact />}</span>
659659 </li>
660660 ))}
776776 <p className="mt-0.5 text-xs text-faint">{off ? "Off for this repository." : hint}</p>
777777 {app && href ? (
778778 <>
779− <a href={href} className="mt-3 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
779+ <DeployLink href={href} className="mt-3 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
780780 <span className="truncate">{host(href)}</span>
781781 <ExternalLink size={12} className="shrink-0" />
782− </a>
782+ </DeployLink>
783783 <p className="mt-1 text-xs text-faint">
784784 <span className="font-mono">{app.commit.slice(0, 8)}</span> · deployed <TimeAgo at={app.deployedAt} />
785785 </p>
+8−8
3939
4040 import type { Route } from "./+types/overview";
4141 import { InlineMarkdown } from "../../components/inline-markdown";
42−import { host, StatusDot } from "../../components/deploy";
42+import { DeployLink, host, StatusDot } from "../../components/deploy";
4343 import { CheckBadge } from "../../components/checks";
4444 import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents";
4545 import { ActivityFeed, DeployStrip, Meter, NeedsList, Panel, Quiet, Unavailable, percent } from "../../components/mission";
822822 <div className="flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-line px-5 py-2.5 text-xs sm:px-6">
823823 <KindMenu project={project} label={kindLabel(project)} canChange={canChange} />
824824 {stripLinks.map((link) => (
825− <a key={link.key} href={link.url} rel="noopener noreferrer" className="inline-flex min-w-0 items-center gap-1.5 text-muted hover:text-fg">
825+ <DeployLink key={link.key} href={link.url} className="inline-flex min-w-0 items-center gap-1.5 text-muted hover:text-fg">
826826 {link.type === "docs" ? <BookOpen size={12} className="shrink-0 text-faint" /> : <Globe size={12} className="shrink-0 text-faint" />}
827827 <span className="truncate">{link.type === "docs" ? `Docs · ${bare(link.url)}` : link.label}</span>
828− </a>
828+ </DeployLink>
829829 ))}
830830 </div>
831831 )}
865865 </p>
866866 {production ? (
867867 <>
868− <a
868+ <DeployLink
869869 href={productionUrl ?? production.url}
870870 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
871871 >
872872 {host(productionUrl ?? production.url)}
873873 <ArrowUpRight size={16} className="shrink-0 text-faint" />
874− </a>
874+ </DeployLink>
875875 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
876876 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
877877 <span className="inline-flex items-center gap-1 font-mono">
908908 {member && (
909909 <div className="flex shrink-0 items-center gap-2">
910910 {production && (
911− <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
911+ <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
912912 Visit
913913 <ArrowUpRight size={14} />
914914 </ButtonLink>
12641264 {previews.map((app) => (
12651265 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
12661266 <span className="min-w-0 grow">
1267− <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
1267+ <DeployLink href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
12681268 {host(app.url)}
1269− </a>
1269+ </DeployLink>
12701270 <span className="text-xs text-muted">
12711271 {app.branch}
12721272 {app.number != null && (
+5−4
9494 import { actions, agents, deployments, identity, inbox, repos, work } from "../../lib/services.server";
9595 import { commitChecksFor } from "../../lib/commit-checks.server";
9696 import { CommitChecksBadge } from "../../components/commit-checks";
97+import { DeployLink } from "../../components/deploy";
9798 import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
9899 import { accessTo, refusal, repoFor } from "../../lib/access.server";
99100 import { SubscriptionBox } from "../../components/notifications";
16831684 : "This branch is live"}
16841685 </p>
16851686 {preview ? (
1686− <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1687+ <DeployLink href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
16871688 {preview.url.replace(/^https?:[/][/]/, "")}
1688− </a>
1689+ </DeployLink>
16891690 ) : (
16901691 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
16911692 )}
16971698 </Link>
16981699 )}
16991700 {preview && (
1700− <a
1701+ <DeployLink
17011702 href={preview.url}
17021703 className="inline-flex items-center gap-1.5 rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-bg hover:opacity-90"
17031704 >
17041705 Visit preview
17051706 <ArrowUpRight size={13} />
1706− </a>
1707+ </DeployLink>
17071708 )}
17081709 </div>
17091710 </div>
+1−1
176176 <GitPullRequest size={16} className="mt-0.5 shrink-0 text-warn" />
177177 <span className="min-w-0 grow">
178178 <span className="block truncate text-sm font-medium text-fg">{row.title}</span>
179− <span className="block truncate text-xs text-muted">
179+ <span className="line-clamp-2 text-xs text-muted">
180180 {row.repo ? `${row.repo.name}${row.ref ? ` ${row.ref}` : ""} · ` : ""}
181181 {row.ask}
182182 </span>
+3−3
44 import type { PackageSummary, Project, ProjectDeploys, User } from "@g1t/contracts";
55
66 import type { Route } from "./+types/overview";
7−import { host, StatusDot } from "../../components/deploy";
7+import { DeployLink, host, StatusDot } from "../../components/deploy";
88 import { Avatar, ButtonLink, CopyLine, Pill, TimeAgo } from "../../components/ui";
99 import { UsageCard } from "../../components/usage-card";
1010 import { PullIcon } from "../../components/work-icons";
176176 {project.name}
177177 </Link>
178178 {link && !(library && pkg && !production) ? (
179− <a
179+ <DeployLink
180180 href={link}
181181 className="relative z-10 mt-0.5 flex items-center gap-1 truncate font-mono text-xs text-muted hover:text-accent"
182182 >
183183 {host(link)}
184184 <ArrowUpRight size={11} className="shrink-0" />
185− </a>
185+ </DeployLink>
186186 ) : library && pkg ? (
187187 <Link
188188 to={packagePath(pkg)}