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.

106 linesCodeBlameRaw
1import { ArrowLeft, GitBranch, GitCommitHorizontal } from "lucide-react";
2import { Link } from "react-router";
3
4import type { Route } from "./+types/work-session";
5import { Actor, HarnessBadge, SessionStateBadge } from "../../components/coordination";
6import { SessionEntries } from "../../components/session-entries";
7import { EmptyState, TimeAgo } from "../../components/ui";
8import { PersonLink } from "../../components/work";
9import { requireRepo } from "../../lib/access.server";
10import { harnessName } from "../../lib/coordination";
11import { page } from "../../lib/meta";
12import { notFound } from "../../lib/not-found.server";
13import { useRefreshWhile } from "../../lib/refresh";
14import { work } from "../../lib/services.server";
15
16export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
17 const session = loaderData?.session;
18 const name = session ? `${session.title ?? `${session.actor.username}'s ${harnessName(session.harness)} session`} · ` : "";
19 return page(args, { title: `${name}Session · ${params.owner}/${params.repo} · g1t` });
20}
21
22export async function loader({ params, context }: Route.LoaderArgs) {
23 const { viewer } = await requireRepo(context, params, "read");
24 const found = await work.getWorkSession({ namespace: params.owner, name: params.repo }, params.id, viewer).catch(() => null);
25 if (!found?.ok) throw notFound("page");
26 return found.value;
27}
28
29/** One harness session: who ran it, where, what it is on, and what it recorded. */
30export default function WorkSessionPage({ loaderData, params }: Route.ComponentProps) {
31 const { session, entries } = loaderData;
32 const base = `/${params.owner}/${params.repo}`;
33 useRefreshWhile(session.state === "active", 10_000);
34 const tools = entries.filter((entry) => entry.kind === "tool_call").length;
35 return (
36 <div className="max-w-4xl">
37 <Link to={`${base}/work`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
38 <ArrowLeft size={14} />
39 Work
40 </Link>
41 <div className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-2">
42 <Actor username={session.actor.username} harness={session.harness} size={24} />
43 <h2 className="min-w-0 grow text-lg font-semibold tracking-tight">
44 {session.title ?? `${session.actor.username}'s session`}
45 </h2>
46 <HarnessBadge harness={session.harness} />
47 <SessionStateBadge state={session.state} />
48 </div>
49 <p className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted">
50 <PersonLink name={session.actor.username} className="font-medium text-fg hover:underline" />
51 <span>
52 started <TimeAgo at={session.startedAt} />
53 </span>
54 <span>
55 · {session.state === "ended" ? "ended" : "last heard from"} <TimeAgo at={session.endedAt ?? session.lastSeenAt} />
56 {session.endReason && ` (${session.endReason === "timed_out" ? "timed out" : session.endReason})`}
57 </span>
58 {session.issue != null && (
59 <span>
60 · on{" "}
61 <Link to={`${base}/issues/${session.issue}`} className="text-fg hover:text-accent">
62 #{session.issue}
63 </Link>
64 </span>
65 )}
66 {session.pull != null && (
67 <span>
68 · in{" "}
69 <Link to={`${base}/pull/${session.pull}`} className="text-fg hover:text-accent">
70 #{session.pull}
71 </Link>
72 </span>
73 )}
74 {session.branch && (
75 <span className="inline-flex items-center gap-1 font-mono text-xs">
76 · <GitBranch size={12} />
77 {session.branch}
78 </span>
79 )}
80 {session.headCommit && (
81 <span className="inline-flex items-center gap-1 font-mono text-xs">
82 · <GitCommitHorizontal size={12} />
83 {session.headCommit.slice(0, 7)}
84 </span>
85 )}
86 </p>
87 <p className="mt-1 text-xs text-faint">
88 Run by {session.actor.username}'s token in {harnessName(session.harness)}, as the harness declared. What it records is its own
89 account; the commits, checks and merges are the record.
90 </p>
91
92 <section className="mt-8">
93 <h3 className="text-sm font-medium">
94 What it recorded <span className="font-normal text-muted">· {entries.length} entries, {tools} tool calls</span>
95 </h3>
96 <div className="mt-3">
97 {entries.length === 0 ? (
98 <EmptyState title="Nothing recorded yet">Prompts, messages, tool calls and notes show here as the session records them.</EmptyState>
99 ) : (
100 <SessionEntries entries={entries} />
101 )}
102 </div>
103 </section>
104 </div>
105 );
106}