Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

78 linesCodeBlameRaw
1import { ArrowLeft } from "lucide-react";
2import { Link, useSearchParams } from "react-router";
3
4import type { Route } from "./+types/session";
5import { page } from "../../lib/meta";
6import { Avatar } from "../../components/ui/avatar";
7import { Button } from "../../components/ui/button";
8import { SessionEntries } from "../../components/session-entries";
9import { agents } from "../../lib/services.server";
10import { getViewer, unwrap } from "../../lib/session.server";
11
12export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
13 return page(args, {
14 title: `Session of #${params.number}${loaderData ? ` ${loaderData.session.pull.title}` : ""} · ${params.owner}/${params.repo} · g1t`,
15 });
16}
17
18export async function loader({ params, context }: Route.LoaderArgs) {
19 const viewer = getViewer(context);
20 const session = await agents.getSession(viewer, { namespace: params.owner, name: params.repo }, Number(params.number));
21 return { session: unwrap(session) };
22}
23
24export default function SessionPage({ loaderData, params }: Route.ComponentProps) {
25 const { session } = loaderData;
26 const { pull, entries } = session;
27 const base = `/${params.owner}/${params.repo}`;
28 const [search, setSearch] = useSearchParams();
29 const quiet = search.get("results") !== "1";
30 const shown = quiet ? entries.filter((entry) => entry.kind !== "tool_result") : entries;
31 const tools = entries.filter((entry) => entry.kind === "tool_call").length;
32 return (
33 <div className="max-w-4xl">
34 <Link to={`${base}/sessions`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
35 <ArrowLeft size={14} />
36 Sessions
37 </Link>
38 <div className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-2">
39 <Avatar name={pull.agent} image={null} size={22} />
40 <h2 className="min-w-0 grow text-lg font-semibold tracking-tight">
41 <Link to={`${base}/pull/${pull.number}`} className="hover:text-accent">
42 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
43 </Link>
44 </h2>
45 </div>
46 <p className="mt-1.5 text-sm text-muted">
47 {pull.agent} · {pull.status} · {entries.length} entries · {tools} tool calls
48 </p>
49
50 <section className="mt-8">
51 <div className="flex items-baseline justify-between">
52 <h3 className="text-sm font-medium">What happened</h3>
53 <Button
54 type="button"
55 onClick={() => setSearch(quiet ? { results: "1" } : {}, { replace: true, preventScrollReset: true })}
56 variant="link"
57 size="inline"
58 className="text-xs text-muted hover:text-fg font-normal"
59 >
60 {quiet ? "Show tool results" : "Hide tool results"}
61 </Button>
62 </div>
63 {shown.length === 0 ? (
64 <p className="mt-3 text-sm text-muted">Nothing recorded yet.</p>
65 ) : (
66 <div className="mt-3">
67 <SessionEntries entries={shown} />
68 </div>
69 )}
70 {entries.length >= 2000 && (
71 <p className="mt-3 text-xs text-muted">
72 The first 2000 entries. The rest are in the API's read_session, page by page.
73 </p>
74 )}
75 </section>
76 </div>
77 );
78}