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.

g1t.sh
752 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'worktree-agent-aa68cf6ecc03dffaf'1/**
2 * Coordinating work, drawn: claims, harness sessions, dependencies,
3 * integration failures and decision records, as the issue page, the pull
4 * request page, the Work page and the why panel show them. What each says
5 * is worked out in lib/coordination.ts.
6 */
7import {
8 Activity,
9 ArrowRightLeft,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.10 Bot,
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'11 CircleCheck,
12 CircleDot,
13 CircleX,
14 GitBranch,
15 GitCommitHorizontal,
16 GitMerge,
17 Hand,
18 Layers,
19 Link2,
20 MessageSquare,
21 Pencil,
22 Play,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.23 Sparkles,
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'24 Square,
25 Timer,
26 TriangleAlert,
27} from "lucide-react";
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.28import { Fragment, type ReactNode, useState } from "react";
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'29import { Form, Link } from "react-router";
30
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.31import {
32 AGENT_MODES,
33 type AgentSessionsSetup,
34 type Blocker,
35 type Claim,
36 type Decision,
37 type DecisionConfidence,
38 type Harness,
39 type SessionState,
40 type State,
41 type WorkContext,
42 type WorkSession,
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'43} from "@g1t/contracts";
44
45import {
46 CLAIM_ENDED,
47 CONFIDENCE,
48 DECISION_KINDS,
49 type FeedLine,
50 SESSION_STATES,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.51 type SessionNode,
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'52 checkHref,
53 evidenceOf,
54 expiresIn,
55 harnessName,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.56 mergeSessions,
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'57 numberedParts,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.58 sessionTree,
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'59} from "../lib/coordination";
60import { Input, SubmitButton, TimeAgo } from "./ui";
61import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
62import { Avatar } from "./ui/avatar";
63import { Badge, type BadgeTone } from "./ui/badge";
64import { Button } from "./ui/button";
65import { Card } from "./ui/card";
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.66import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
67import { Field, FieldError, FieldLabel, FieldLegend, FieldSet } from "./ui/field";
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'68import { Hint } from "./ui/hint";
69import { Label } from "./ui/label";
70import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.71import { RadioGroup, RadioOption } from "./ui/radio-group";
72import { Textarea } from "./ui/textarea";
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'73import { PersonLink } from "./work";
74import { IssueIcon } from "./work-icons";
75
76/** Text with each `#N` linked to that issue or pull request (the issue's address leads to a pull request too). */
77export function NumberedText({ text, base }: { text: string; base: string }) {
78 return (
79 <>
80 {numberedParts(text).map((part, index) =>
81 typeof part === "number" ? (
82 <Link key={index} to={`${base}/issues/${part}`} className="font-medium text-fg hover:text-accent">
83 #{part}
84 </Link>
85 ) : (
86 <Fragment key={index}>{part}</Fragment>
87 ),
88 )}
89 </>
90 );
91}
92
93/** The harness a session or claim came from. */
94export function HarnessBadge({ harness }: { harness: Harness | null | undefined }) {
95 const name = harnessName(harness);
96 if (!name) return null;
97 return <Badge>{name}</Badge>;
98}
99
100/** Who did it: an agent's avatar when it came through a harness (a person's token, the harness declared), else theirs. */
101export function Actor({ username, harness, size = 20 }: { username: string; harness?: Harness | null; size?: number }) {
102 return <Avatar name={username} size={size} agent={harness != null} />;
103}
104
105const SESSION_TONE: Record<SessionState, BadgeTone> = { active: "success", idle: "warn", ended: "neutral" };
106
107export function SessionStateBadge({ state }: { state: SessionState }) {
108 const { label, hint } = SESSION_STATES[state];
109 return (
110 <Hint label={hint}>
111 <Badge tone={SESSION_TONE[state]}>{label}</Badge>
112 </Hint>
113 );
114}
115
116/** How sure a decision is: claimed, tested and approved read apart at a glance. */
117export function ConfidenceBadge({ confidence }: { confidence: DecisionConfidence }) {
118 const { label, hint, tone } = CONFIDENCE[confidence];
119 const Icon = confidence === "approved" ? CircleCheck : confidence === "tested" ? Layers : MessageSquare;
120 return (
121 <Hint label={hint}>
122 <Badge tone={tone}>
123 <Icon size={11} />
124 {label}
125 </Badge>
126 </Hint>
127 );
128}
129
130/** One claim: who, from which harness, since when, and when it lapses; one no longer active is muted. */
131export function ClaimLine({ claim, now, showIssue, base }: { claim: Claim; now: number; showIssue?: boolean; base: string }) {
132 const active = claim.state === "active";
133 const left = expiresIn(claim, now);
134 return (
135 <div className={active ? "flex items-start gap-2.5" : "flex items-start gap-2.5 opacity-60"}>
136 <Actor username={claim.actor.username} harness={claim.harness} />
137 <div className="min-w-0 grow">
138 <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm">
139 <PersonLink name={claim.actor.username} className="font-medium hover:underline" />
140 {showIssue ? (
141 <span className="text-muted">
142 claimed{" "}
143 <Link to={`${base}/issues/${claim.issue}`} className="font-medium text-fg hover:text-accent">
144 #{claim.issue}
145 </Link>
146 </span>
147 ) : null}
148 <HarnessBadge harness={claim.harness} />
149 {!active && <Badge>{CLAIM_ENDED[claim.state as Exclude<Claim["state"], "active">]}</Badge>}
150 </p>
151 <p className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
152 <span>
153 since <TimeAgo at={claim.createdAt} />
154 </span>
155 {left && (
156 <span className="inline-flex items-center gap-1" suppressHydrationWarning>
157 · <Timer size={11} />
158 {left}
159 </span>
160 )}
161 {!active && claim.state === "handed_off" && <span>· to {claim.handoffTo ?? "anyone"}</span>}
162 </p>
163 {claim.note && <p className="mt-1 text-xs text-fg-soft">{claim.note}</p>}
164 </div>
165 </div>
166 );
167}
168
169/** One harness session: who, which harness, where it stands, when it was last heard from, linked to its page. */
170export function SessionLine({
171 session,
172 base,
173 showWhere = true,
174 showBranch = true,
175}: {
176 session: WorkSession;
177 base: string;
178 /** Its pull request and branch; not where they are already said. */
179 showWhere?: boolean;
180 /** Its branch; not in a narrow sidebar. */
181 showBranch?: boolean;
182}) {
183 return (
184 <div className="flex items-start gap-2.5">
185 <Actor username={session.actor.username} harness={session.harness} />
186 <div className="min-w-0 grow">
187 <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm">
188 <Link to={`${base}/work/sessions/${session.id}`} prefetch="intent" className="font-medium hover:text-accent">
189 {session.title ?? `${session.actor.username}'s session`}
190 </Link>
191 <HarnessBadge harness={session.harness} />
192 <SessionStateBadge state={session.state} />
193 </p>
194 <p className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
195 <PersonLink name={session.actor.username} className="hover:text-fg" />
196 <span>
197 · {session.state === "ended" ? "ended" : "last seen"} <TimeAgo at={session.endedAt ?? session.lastSeenAt} />
198 </span>
199 {showWhere && session.pull != null && (
200 <span>
201 · on{" "}
202 <Link to={`${base}/pull/${session.pull}`} className="text-fg hover:text-accent">
203 #{session.pull}
204 </Link>
205 </span>
206 )}
207 {showWhere && showBranch && session.branch && (
208 <span className="inline-flex min-w-0 items-center gap-1 font-mono">
209 · <GitBranch size={11} />
210 <span className="truncate">{session.branch}</span>
211 </span>
212 )}
213 </p>
214 </div>
215 </div>
216 );
217}
218
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.219/** A subagent in a line: its kind, its task, where it stands, linked to its page. */
220export function SubagentLine({ session, base }: { session: WorkSession; base: string }) {
221 return (
222 <div className="flex min-w-0 items-center gap-1.5 text-xs">
223 <Bot size={12} className="shrink-0 text-faint" />
224 <Link to={`${base}/work/sessions/${session.id}`} prefetch="intent" className="shrink-0 font-medium text-fg hover:text-accent">
225 {session.agentType ?? "Subagent"}
226 </Link>
227 {session.task && (
228 <Hint label={session.task}>
229 <span className="min-w-0 truncate text-muted">{session.task}</span>
230 </Hint>
231 )}
232 <SessionStateBadge state={session.state} />
233 </div>
234 );
235}
236
237/** Subagents under a session, and theirs under them, indented on a rule. */
238function SubagentBranch({ nodes, base }: { nodes: SessionNode[]; base: string }) {
239 return (
240 <ul className="mt-1.5 space-y-1.5 border-l border-line pl-3">
241 {nodes.map((node) => (
242 <li key={node.session.id}>
243 <SubagentLine session={node.session} base={base} />
244 {node.children.length > 0 && <SubagentBranch nodes={node.children} base={base} />}
245 </li>
246 ))}
247 </ul>
248 );
249}
250
251/** Sessions as trees: each one's line, with its subagents in a compact list under it. */
252export function SessionTreeList({
253 nodes,
254 base,
255 showWhere,
256 showBranch,
257 className,
258}: {
259 nodes: SessionNode[];
260 base: string;
261 showWhere?: boolean;
262 showBranch?: boolean;
263 className?: string;
264}) {
265 return (
266 <ul className={className ?? "space-y-3"}>
267 {nodes.map((node) => (
268 <li key={node.session.id}>
269 <SessionLine session={node.session} base={base} showWhere={showWhere} showBranch={showBranch} />
270 {node.children.length > 0 && (
271 <div className="pl-7.5">
272 <SubagentBranch nodes={node.children} base={base} />
273 </div>
274 )}
275 </li>
276 ))}
277 </ul>
278 );
279}
280
281/**
282 * Starting a session on the issue on the workspace's Anthropic account: a
283 * button opening a dialog that asks what kind of work and for anything to
284 * add, posting `start-session` to the issue's action.
285 */
286export function StartSessionDialog({ setup, error }: { setup: AgentSessionsSetup; error?: string | null }) {
287 const [open, setOpen] = useState(Boolean(error));
288 return (
289 <Dialog open={open} onOpenChange={setOpen}>
290 <DialogTrigger asChild>
291 <Button variant="outline" size="xs">
292 <Sparkles size={12} />
293 Start a session
294 </Button>
295 </DialogTrigger>
296 <DialogContent>
297 <Form method="post" className="grid gap-5">
298 <input type="hidden" name="action" value="start-session" />
299 <DialogHeader>
300 <DialogTitle>Start a session on this issue</DialogTitle>
301 <DialogDescription>
302 Claude works on it on {setup.connectionName ? `the ${setup.connectionName} connection` : "your workspace's Anthropic account"}
303 {setup.model ? `, with ${setup.model}` : ""}. It shows on this issue as it goes, with its subagents.
304 </DialogDescription>
305 </DialogHeader>
306 <FieldSet>
307 <FieldLegend className="mb-0">What to do</FieldLegend>
308 <RadioGroup name="mode" defaultValue="implement">
309 {AGENT_MODES.map((mode) => (
310 <RadioOption key={mode.mode} value={mode.mode} label={mode.label} description={mode.hint} />
311 ))}
312 </RadioGroup>
313 </FieldSet>
314 <Field>
315 <FieldLabel htmlFor="start-session-instruction">Anything to add</FieldLabel>
316 <Textarea id="start-session-instruction" name="instruction" rows={3} maxLength={4000} placeholder="Optional: what to keep in mind" />
317 <FieldError>{error}</FieldError>
318 </Field>
319 <DialogFooter>
320 <DialogClose asChild>
321 <Button variant="ghost">Cancel</Button>
322 </DialogClose>
323 <SubmitButton match={{ action: "start-session" }} pending="Starting…">
324 Start
325 </SubmitButton>
326 </DialogFooter>
327 </Form>
328 </DialogContent>
329 </Dialog>
330 );
331}
332
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'333const KIND_ICON: Record<Decision["kind"], ReactNode> = {
334 proposal: <MessageSquare size={14} />,
335 claim: <MessageSquare size={14} />,
336 integration_failure: <CircleX size={14} className="text-danger" />,
337 integration_passed: <Layers size={14} className="text-success" />,
338 fix: <Link2 size={14} className="text-accent" />,
339 approval: <CircleCheck size={14} className="text-success" />,
340 merge: <GitMerge size={14} className="text-merged" />,
341 note: <MessageSquare size={14} />,
342};
343
344/** The pull requests tested together, in queue order: `#41 + #44`. */
345export function Combination({ numbers, base, culprit }: { numbers: number[]; base: string; culprit?: number | null }) {
346 return (
347 <span className="inline-flex flex-wrap items-center gap-1">
348 {numbers.map((number, index) => (
349 <Fragment key={number}>
350 {index > 0 && <span className="text-faint">+</span>}
351 <Link to={`${base}/pull/${number}`}>
352 <Badge shape="tag" tone={number === culprit ? "danger" : "neutral"} className="font-mono hover:border-line-strong">
353 #{number}
354 </Badge>
355 </Link>
356 </Fragment>
357 ))}
358 </span>
359 );
360}
361
362/** The check a decision rests on, linked where it can be seen. */
363function CheckLink({ check }: { check: { name: string; conclusion: string; url: string | null } }) {
364 const href = checkHref(check.url);
365 const text = (
366 <>
367 <span className="font-mono">{check.name}</span> {check.conclusion}
368 </>
369 );
370 if (!href) return <span>{text}</span>;
371 return href.startsWith("/") ? (
372 <Link to={href} className="text-fg underline-offset-2 hover:underline">
373 {text}
374 </Link>
375 ) : (
376 <a href={href} className="text-fg underline-offset-2 hover:underline" rel="noreferrer" target="_blank">
377 {text}
378 </a>
379 );
380}
381
382/** One decision record: its kind, how sure it is, the sentence, who and when, and what it rests on. */
383export function DecisionLine({ decision, base }: { decision: Decision; base: string }) {
384 const evidence = evidenceOf(decision);
385 return (
386 <div className="flex items-start gap-2.5">
387 <span className="mt-0.5 shrink-0 text-faint">{KIND_ICON[decision.kind]}</span>
388 <div className="min-w-0 grow">
389 <p className="flex flex-wrap items-center gap-1.5 text-xs">
390 <span className="font-medium text-fg">{DECISION_KINDS[decision.kind]}</span>
391 <ConfidenceBadge confidence={decision.confidence} />
392 {decision.pull != null && (
393 <Link to={`${base}/pull/${decision.pull}`} className="text-muted hover:text-fg">
394 #{decision.pull}
395 </Link>
396 )}
397 </p>
398 <p className="mt-1 text-sm wrap-break-word">
399 <NumberedText text={decision.summary} base={base} />
400 </p>
401 <p className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted">
402 <span>{decision.actor ?? "g1t"}</span>
403 <span>
404 · <TimeAgo at={decision.createdAt} />
405 </span>
406 {evidence.combination.length > 1 && (
407 <span className="inline-flex items-center gap-1.5">
408 · tested as <Combination numbers={evidence.combination} base={base} culprit={decision.kind === "integration_failure" ? decision.pull : null} />
409 </span>
410 )}
411 {evidence.check && decision.kind === "integration_failure" && (
412 <span>
413 · <CheckLink check={evidence.check} />
414 </span>
415 )}
416 {evidence.commit && (
417 <span className="inline-flex items-center gap-1 font-mono">
418 · <GitCommitHorizontal size={11} />
419 {evidence.commit.slice(0, 7)}
420 </span>
421 )}
422 </p>
423 </div>
424 </div>
425 );
426}
427
428/** Decision records, as a list in a card. */
429export function DecisionList({ decisions, base }: { decisions: Decision[]; base: string }) {
430 return (
431 <Card asChild divided>
432 <ol>
433 {decisions.map((decision) => (
434 <li key={decision.id} className="px-4 py-3">
435 <DecisionLine decision={decision} base={base} />
436 </li>
437 ))}
438 </ol>
439 </Card>
440 );
441}
442
443/**
444 * A combination that failed in the merge queue: which pull requests were
445 * tested together, the check that failed, which ones it fails with, and
446 * whether it has been answered since.
447 */
448export function IntegrationFailure({
449 failure,
450 base,
451 fixed,
452 branch = "main",
453 className,
454}: {
455 failure: Decision;
456 base: string;
457 /** The branch the combination was built on. */
458 branch?: string;
459 /** A fix or a passing combination since, when there is one. */
460 fixed?: Decision | null;
461 className?: string;
462}) {
463 const evidence = evidenceOf(failure);
464 return (
465 <Alert tone={fixed ? "info" : "danger"} className={className}>
466 <AlertTitle className="flex flex-wrap items-center gap-x-2 gap-y-1 text-fg">
467 {failure.pull != null ? (
468 <span>
469 <TriangleAlert size={15} className={`mr-2 inline align-[-3px] ${fixed ? "text-info" : "text-danger"}`} />
470 <Link to={`${base}/pull/${failure.pull}`} className="hover:underline">
471 #{failure.pull}
472 </Link>{" "}
473 failed in the merge queue {evidence.combination.length > 1 ? "together with others" : "on its own"}
474 </span>
475 ) : (
476 <span>A combination failed in the merge queue</span>
477 )}
478 <ConfidenceBadge confidence={failure.confidence} />
479 <span className="text-xs font-normal text-muted">
480 <TimeAgo at={failure.createdAt} />
481 </span>
482 </AlertTitle>
483 <AlertDescription className="space-y-1.5">
484 {evidence.combination.length > 0 && (
485 <p className="flex flex-wrap items-center gap-1.5 text-xs">
486 Tested as <span className="font-mono text-fg">{branch}</span> <span className="text-faint">+</span>
487 <Combination numbers={evidence.combination} base={base} culprit={failure.pull} />
488 </p>
489 )}
490 {evidence.check && (
491 <p className="text-xs">
492 Failing check: <CheckLink check={evidence.check} />
493 </p>
494 )}
495 <p className="text-xs">
496 {evidence.suspects.length > 0 ? (
497 <>
498 Fails with{" "}
499 <NumberedText text={evidence.suspects.map((n) => `#${n}`).join(", ")} base={base} />
500 {evidence.bisected ? ", narrowed down by building smaller combinations." : "."}
501 </>
502 ) : (
503 "It fails by itself: the change is the cause."
504 )}
505 </p>
506 {fixed && (
507 <p className="text-xs text-fg">
508 <CircleCheck size={13} className="mr-1.5 inline align-[-2px] text-success" />
509 {fixed.kind === "integration_passed" ? "Passed together since" : "Fixed since"}:{" "}
510 <span className="text-muted">
511 <NumberedText text={fixed.summary} base={base} />
512 </span>
513 </p>
514 )}
515 </AlertDescription>
516 </Alert>
517 );
518}
519
520const FEED_ICON: Record<FeedLine["type"], ReactNode> = {
521 "work.claimed": <Hand size={14} />,
522 "work.released": <Hand size={14} />,
523 "work.claim_expired": <Timer size={14} />,
524 "work.handoff_requested": <ArrowRightLeft size={14} />,
525 "session.started": <Play size={14} />,
526 "session.ended": <Square size={14} />,
527 "dependency.changed": <Link2 size={14} />,
528 "overlap.detected": <TriangleAlert size={14} />,
529 "integration.failed": <CircleX size={14} />,
530 "integration.passed": <Layers size={14} />,
531 "decision.recorded": <CircleDot size={14} />,
532};
533
534const FEED_TONE: Record<FeedLine["tone"], string> = {
535 neutral: "text-faint",
536 accent: "text-accent",
537 success: "text-success",
538 warn: "text-warn",
539 danger: "text-danger",
540 info: "text-info",
541};
542
543/** Recent coordination events, newest first, one line each. */
544export function CoordinationFeed({ lines, base }: { lines: FeedLine[]; base: string }) {
545 return (
546 <Card asChild divided>
547 <ol>
548 {lines.map((line) => (
549 <li key={line.id} className="flex items-start gap-3 px-4 py-2.5 text-sm">
550 <span className={`mt-0.5 shrink-0 ${FEED_TONE[line.tone]}`}>{FEED_ICON[line.type]}</span>
551 <p className="min-w-0 grow wrap-break-word text-muted">
552 <PersonLink name={line.who ?? "g1t"} className="font-medium text-fg hover:underline" />
553 {line.harness && <span className="text-faint"> via {harnessName(line.harness)}</span>}{" "}
554 <NumberedText text={line.text} base={base} />
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.555 {line.session && (
556 <>
557 {" · "}
558 <Link to={`${base}/work/sessions/${line.session}`} className="text-fg hover:text-accent">
559 the session
560 </Link>
561 </>
562 )}
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'563 </p>
564 <span className="shrink-0 text-xs text-faint">
565 <TimeAgo at={line.time} />
566 </span>
567 </li>
568 ))}
569 </ol>
570 </Card>
571 );
572}
573
574/** An issue's place among others: one it waits on, or one waiting on it. */
575function DependencyLine({ base, number, title, state, detail }: { base: string; number: number; title: string; state: State; detail?: ReactNode }) {
576 return (
577 <li className="flex items-start gap-2">
578 <span className="mt-0.5">
579 <IssueIcon issue={{ state, reason: null }} size={14} />
580 </span>
581 <span className="min-w-0 grow">
582 <Link to={`${base}/issues/${number}`} className="block truncate text-sm hover:text-accent">
583 {title} <span className="text-faint">#{number}</span>
584 </Link>
585 {detail && <span className="block text-xs text-muted">{detail}</span>}
586 </span>
587 </li>
588 );
589}
590
591/** Where a blocker stands, in a few words: landed, being worked on, or waiting. */
592export function blockerDetail(blocker: Blocker): string {
593 if (blocker.pulls.some((pull) => pull.status === "merged")) return "Merged";
594 if (blocker.state === "closed") return "Closed";
595 const open = blocker.pulls.filter((pull) => pull.status === "open" || pull.status === "draft");
596 const who = blocker.claims.map((claim) => claim.actor.username);
597 return [
598 who.length > 0 ? `Claimed by ${who.join(", ")}` : "Not claimed",
599 open.length > 0 ? `${open.length} pull ${open.length === 1 ? "request" : "requests"} open` : null,
600 ]
601 .filter(Boolean)
602 .join(" · ");
603}
604
605/** A part of the Work panel, under a rule. */
606function PanelPart({ title, action, children }: { title: ReactNode; action?: ReactNode; children: ReactNode }) {
607 return (
608 <div className="border-t border-line pt-3">
609 <div className="flex min-h-7 items-center justify-between gap-2">
610 <p className="text-xs font-medium text-muted">{title}</p>
611 {action}
612 </div>
613 <div className="mt-1.5">{children}</div>
614 </div>
615 );
616}
617
618/**
619 * The Work panel of an issue's sidebar: who has claimed it, the sessions on
620 * it, what it waits on and what waits on it, and for those who may, the
621 * blockers to change.
622 */
623export function IssueWorkPanel({
624 work,
625 ended,
626 base,
627 now,
628 canEdit,
629 error,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.630 sessions: all,
631 start,
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'632}: {
633 work: WorkContext;
634 /** Claims on it that ended lately, shown muted. */
635 ended: Claim[];
636 base: string;
637 now: number;
638 canEdit: boolean;
639 error?: string | null;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.640 /** The sessions tied to it with their subagents, when they could be read; else its own sessions. */
641 sessions?: WorkSession[] | null;
642 /** Starting a session on it, for those who may. */
643 start?: ReactNode;
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'644}) {
645 const open = work.issue.state === "open";
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.646 const trees = sessionTree(mergeSessions(work.sessions, all));
647 const sessions = trees.slice(0, 5);
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'648 const waiting = work.blockedBy.filter((blocker) => blocker.state === "open").length;
649 return (
650 <Card asChild className="space-y-4 p-4">
651 <section aria-label="Work">
652 <h3 className="flex items-center gap-2 text-sm font-medium">
653 <Activity size={14} className="text-accent" />
654 Work
655 </h3>
656
657 <div className="space-y-3">
658 {work.claims.length === 0 && ended.length === 0 ? (
659 <p className="text-xs text-muted">
660 {open ? (
661 <>
662 Nobody has claimed it. Claim it from your harness, or with <code className="font-mono text-fg-soft">g1t work claim {work.issue.number}</code>.
663 </>
664 ) : (
665 "Nobody is working on it."
666 )}
667 </p>
668 ) : (
669 [...work.claims, ...ended.slice(0, 3)].map((claim) => <ClaimLine key={claim.id} claim={claim} now={now} base={base} />)
670 )}
671 {work.claims.length > 1 && (
672 <Alert tone="warn" className="flex items-start gap-1.5 px-2.5 py-1.5 text-xs">
673 <TriangleAlert size={13} className="mt-0.5 shrink-0 text-warn" />
674 {work.claims.length} claims stand on it: they may be doing the same work.
675 </Alert>
676 )}
677 </div>
678
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.679 {(sessions.length > 0 || start) && (
680 <PanelPart title="Sessions" action={start}>
681 {sessions.length === 0 ? (
682 <p className="text-xs text-faint">None yet.</p>
683 ) : (
684 <SessionTreeList nodes={sessions} base={base} showBranch={false} />
685 )}
686 {trees.length > sessions.length && (
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'687 <Link to={`${base}/work`} className="mt-2 inline-block text-xs text-muted hover:text-fg">
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.688 {trees.length - sessions.length} more on the Work page
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'689 </Link>
690 )}
691 </PanelPart>
692 )}
693
694 <PanelPart
695 title={
696 <>
697 Blocked by
698 {waiting > 0 && <span className="text-warn"> · {waiting} open</span>}
699 </>
700 }
701 action={canEdit ? <BlockedByEditor current={work.issue.blockedBy} /> : undefined}
702 >
703 {work.blockedBy.length === 0 ? (
704 <p className="text-xs text-faint">Nothing. It can start now.</p>
705 ) : (
706 <ul className="space-y-2">
707 {work.blockedBy.map((blocker) => (
708 <DependencyLine key={blocker.number} base={base} number={blocker.number} title={blocker.title} state={blocker.state} detail={blockerDetail(blocker)} />
709 ))}
710 </ul>
711 )}
712 {error && <p className="mt-2 text-xs text-danger">{error}</p>}
713 </PanelPart>
714
715 {work.blocks.length > 0 && (
716 <PanelPart title="Blocks">
717 <ul className="space-y-2">
718 {work.blocks.map((issue) => (
719 <DependencyLine key={issue.number} base={base} number={issue.number} title={issue.title} state={issue.state} />
720 ))}
721 </ul>
722 </PanelPart>
723 )}
724 </section>
725 </Card>
726 );
727}
728
729/** Changing what an issue waits on: the whole list, as numbers. */
730function BlockedByEditor({ current }: { current: number[] }) {
731 return (
732 <Popover>
733 <PopoverTrigger asChild>
734 <Button variant="ghost" size="xs">
735 <Pencil size={12} />
736 Edit
737 </Button>
738 </PopoverTrigger>
739 <PopoverContent align="end">
740 <Form method="post" className="space-y-2.5">
741 <input type="hidden" name="action" value="blocked-by" />
742 <Label htmlFor="blocked-by">Blocked by</Label>
743 <Input id="blocked-by" name="blocked_by" defaultValue={current.map((n) => `#${n}`).join(", ")} placeholder="#12, #14" autoComplete="off" />
744 <p className="text-xs text-muted">Issues of this repository that have to land first. Leave it empty for none.</p>
745 <SubmitButton size="sm" match={{ action: "blocked-by" }} pending="Saving…">
746 Save
747 </SubmitButton>
748 </Form>
749 </PopoverContent>
750 </Popover>
751 );
752}

This file's history is long; its oldest lines are credited to the oldest commit read.