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

Commit

g1t-runner is 0.3.5 and the desktop app 0.3.9: guided runner setup (#50).

syntaqxcommitted Parent4fe201eBrowse files
7 files+159−720/7 viewed
+1−2
3131 useEffect(() => {
3232 const onAsk = (event: Event) => {
3333 const text = (event as CustomEvent<{ text?: string }>).detail?.text?.trim();
34− if (!text) return;
35− setQueued({ text, at: Date.now() });
34+ if (text) setQueued({ text, at: Date.now() });
3635 setWanted(true);
3736 setOpen(true);
3837 };
+111−28
11 /**
22 * Home's way into Ask (components/ask-composer): the box at the top of the
3− * page, Summarize on what happened in the span, and Explain on each change
4− * that landed. Each hands its question to the top bar's Ask panel
5− * (lib/ask.ts askG1t), which asks g1t in the viewer's direct message with
6− * it, so the answer streams there and stays in Chat.
3+ * page, What changed (g1t's plain-English summary of the span, streamed
4+ * onto the page by components/home-summary), and Explain on each change
5+ * that landed. The box and Explain hand their question to the top bar's Ask
6+ * panel (lib/ask.ts askG1t). Every question goes to the viewer's direct
7+ * message with @g1t, so the answers stay in Chat and Ask carries on from them.
78 */
8−import { ArrowUp, Sparkles } from "lucide-react";
9−import { type FormEvent, type KeyboardEvent, useState } from "react";
9+import { ArrowUp, MessageSquare, RotateCcw, Sparkles } from "lucide-react";
10+import { type FormEvent, type KeyboardEvent, Suspense, lazy, useCallback, useEffect, useRef, useState } from "react";
11+import { Link } from "react-router";
1012
11−import { askG1t } from "../lib/ask";
12−import { type LandedChange, type Span, explainQuestion, homeStarters, summarizeQuestion } from "../lib/home";
13+import { askG1t, openAsk } from "../lib/ask";
14+import { HOME_STARTERS, type LandedChange, type Span, explainQuestion, summarizeQuestion } from "../lib/home";
1315 import { Suggestion, Suggestions } from "./ai-elements/suggestion";
16+import { Card } from "./home";
1417 import { MarkGlyph } from "./logo";
1518 import { Button } from "./ui/button";
1619 import { Hint } from "./ui/hint";
20+import { Skeleton } from "./ui/skeleton";
1721 import { Textarea } from "./ui/textarea";
1822
19−type AskSpan = Pick<Span, "key" | "note" | "words" | "from">;
23+const loadSummary = () => import("./home-summary");
24+const HomeSummary = lazy(loadSummary);
2025
26+type AskSpan = Pick<Span, "key" | "note" | "words" | "from" | "now">;
27+
28+/** The window event Home's box sends to have What changed summarize the span. */
29+const SUMMARIZE_EVENT = "g1t:home-summarize";
30+
31+/** What the span is called on a button: "since I was last here", "in the last 24 hours". */
32+function spanLabel(span: AskSpan): string {
33+ return span.key === "last" && span.note == null ? "since I was last here" : `${span.words.charAt(0).toLowerCase()}${span.words.slice(1)}`;
34+}
35+
2136 /** The top of Home: what are we building today, a box that asks g1t, and a few things to ask first. */
22−export function HomeAsk({ span, tz }: { span: AskSpan; tz: string | null }) {
37+export function HomeAsk({ span }: { span: AskSpan }) {
2338 const [draft, setDraft] = useState("");
2439 const send = () => {
2540 if (!draft.trim()) return;
6580 </div>
6681 </form>
6782 <Suggestions className="justify-center">
68− {homeStarters(span, tz).map((starter) => (
83+ <Suggestion
84+ suggestion={`Summarize what changed ${spanLabel(span)}`}
85+ onPick={() => window.dispatchEvent(new CustomEvent(SUMMARIZE_EVENT))}
86+ onPointerEnter={() => void loadSummary()}
87+ className="border-accent-dim text-fg"
88+ />
89+ {HOME_STARTERS.map((starter) => (
6990 <Suggestion key={starter.label} suggestion={starter.label} onPick={() => askG1t(starter.question)} />
7091 ))}
7192 </Suggestions>
7394 );
7495 }
7596
76−/** Summarize what happened in the span, in plain English. */
77−export function SummarizeButton({ span, tz }: { span: AskSpan; tz: string | null }) {
97+/**
98+ * What changed: g1t's summary of the span in plain English, on the page.
99+ * Asked only when the viewer presses Summarize (each answer is a call to
100+ * the workspace's own AI provider); asked again for another span.
101+ */
102+export function WhatChangedCard({ slug, span, tz }: { slug: string; span: AskSpan; tz: string | null }) {
103+ // Each ask is its own: Summarize again asks anew.
104+ const [asked, setAsked] = useState<{ question: string; at: number } | null>(null);
105+ const [done, setDone] = useState<{ channelId: string | null } | null>(null);
106+ const card = useRef<HTMLDivElement>(null);
107+ const label = spanLabel(span);
108+
109+ const summarize = useCallback(() => {
110+ setDone(null);
111+ setAsked({ question: summarizeQuestion(span, tz), at: Date.now() });
112+ }, [span, tz]);
113+
114+ // Home's box asks for it: bring the card into view and start.
115+ useEffect(() => {
116+ const onAsk = () => {
117+ card.current?.scrollIntoView({ behavior: "smooth", block: "start" });
118+ summarize();
119+ };
120+ window.addEventListener(SUMMARIZE_EVENT, onAsk);
121+ return () => window.removeEventListener(SUMMARIZE_EVENT, onAsk);
122+ }, [summarize]);
123+
124+ const onDone = useCallback((channelId: string | null) => setDone({ channelId }), []);
125+
78126 return (
79− <Hint label="g1t reads everything that happened in this span and tells you what changed">
80− <Button type="button" variant="soft" size="sm" onClick={() => askG1t(summarizeQuestion(span, tz))}>
81− <Sparkles size={14} />
82− Summarize
83− </Button>
84− </Hint>
127+ <div ref={card} className="scroll-mt-4">
128+ <Card title="What changed" hint={`In plain English, ${label}`}>
129+ <div className="px-5 pb-5">
130+ {!asked ? (
131+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
132+ <p className="grow text-sm text-muted">g1t reads the commits, pull requests, deploys and conversations {label} and tells you what changed, why, and what is live.</p>
133+ <Button type="button" variant="accent" size="sm" onClick={summarize} onPointerEnter={() => void loadSummary()} className="shrink-0 self-start sm:self-auto">
134+ <Sparkles size={14} />
135+ Summarize
136+ </Button>
137+ </div>
138+ ) : (
139+ <>
140+ <Suspense
141+ fallback={
142+ <div className="space-y-2.5">
143+ <Skeleton className="h-3.5 w-11/12" />
144+ <Skeleton className="h-3.5 w-3/4" />
145+ </div>
146+ }
147+ >
148+ <HomeSummary key={asked.at} slug={slug} question={asked.question} onDone={onDone} />
149+ </Suspense>
150+ {done && (
151+ <div className="mt-4 flex flex-wrap items-center gap-2 border-t border-line pt-3">
152+ <Button type="button" variant="soft" size="sm" onClick={openAsk}>
153+ <MarkGlyph className="h-3" />
154+ Ask a follow-up
155+ </Button>
156+ <Button type="button" variant="ghost" size="sm" onClick={summarize}>
157+ <RotateCcw size={13} />
158+ Summarize again
159+ </Button>
160+ {done.channelId && (
161+ <Button asChild variant="ghost" size="sm" className="sm:ml-auto">
162+ <Link to={`/${slug}/-/chat/dm/${done.channelId}`}>
163+ <MessageSquare size={13} />
164+ In your direct message with g1t
165+ </Link>
166+ </Button>
167+ )}
168+ </div>
169+ )}
170+ </>
171+ )}
172+ </div>
173+ </Card>
174+ </div>
85175 );
86176 }
87177
88178 /** Explain one change that landed: what it did and why, in plain English. */
89179 export function ExplainButton({ change }: { change: Pick<LandedChange, "kind" | "title" | "to"> }) {
90180 return (
91− <Hint label="Explain this change">
92− <Button
93− type="button"
94− variant="ghost"
95− size="icon-xs"
96− aria-label={`Explain ${change.title}`}
97− onClick={() => askG1t(explainQuestion(change))}
98− className="shrink-0 text-muted hover:text-fg sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100"
99− >
181+ <Hint label="Explain this change in plain English">
182+ <Button type="button" variant="ghost" size="icon-xs" aria-label={`Explain ${change.title}`} onClick={() => askG1t(explainQuestion(change))} className="shrink-0 text-muted hover:text-accent">
100183 <Sparkles size={13} />
101184 </Button>
102185 </Hint>
+1−1
546546
547547 function LandedItem({ change, now, tz, timeOnly }: { change: LandedChange; now: number; tz: string | null; timeOnly?: boolean }) {
548548 return (
549− <li className={cn(rowClass, "group items-start")}>
549+ <li className={cn(rowClass, "items-start")}>
550550 <span className="mt-0.5 shrink-0">{LANDED_ICON[change.kind]}</span>
551551 <div className="min-w-0 grow">
552552 <div className="flex min-w-0 items-center gap-1.5">
+5−0
4040 window.dispatchEvent(new CustomEvent<{ text: string }>(ASK_EVENT, { detail: { text: question } }));
4141 }
4242
43+/** Opens the top bar's Ask panel on the conversation as it is: a follow-up to an answer shown on a page. */
44+export function openAsk() {
45+ window.dispatchEvent(new CustomEvent<{ text: string }>(ASK_EVENT, { detail: { text: "" } }));
46+}
47+
4348 /** What the composer says about where the question is asked from. */
4449 export function askWhere(page: AskPage): string | null {
4550 if (page.issue) return `On issue #${page.issue}`;
+10−15
1919 digestOf,
2020 duration,
2121 explainQuestion,
22− homeStarters,
2322 landed,
2423 landedByDay,
2524 landedDeploys,
784783 assert.deepEqual({ ...away, settled: away.settled.length }, { settled: 2, shipped: 1, waiting: 1, deploying: 0, failed: 0, needs: 3 });
785784 });
786785
787−test("Summarize asks about the span the page shows, from its start in the viewer's zone", () => {
788− const from = Date.UTC(2026, 9, 13, 18, 0);
789− const last = summarizeQuestion({ key: "last", note: null, words: "Since yesterday evening", from }, "UTC");
790− assert.equal(last, "Summarize what changed since I was last here (Oct 13, 6:00 PM) in plain English: what shipped, what's still deploying or failed, what's in flight, and what needs me.");
791− assert.match(summarizeQuestion({ key: "24h", note: null, words: "In the last 24 hours", from }, "America/Chicago"), /in the last 24 hours \(since Oct 13, 1:00 PM\)/);
786+test("Summarize asks about the span the page shows, as a person would", () => {
787+ const now = Date.UTC(2026, 9, 14, 22, 0);
788+ assert.equal(
789+ summarizeQuestion({ key: "last", note: null, words: "Since this afternoon", from: Date.UTC(2026, 9, 14, 18, 0), now }, "UTC"),
790+ "Summarize what changed since I was last here (6:00 PM) in plain English.",
791+ );
792+ assert.equal(summarizeQuestion({ key: "24h", note: null, words: "In the last 24 hours", from: now - DAY, now }, "UTC"), "Summarize what changed in the last 24 hours in plain English.");
792793 // A first visit shows the last 24 hours, so it is not asked as "since I was last here".
793− assert.doesNotMatch(summarizeQuestion({ key: "last", note: "first", words: "In the last 24 hours", from }, "UTC"), /last here/);
794+ assert.doesNotMatch(summarizeQuestion({ key: "last", note: "first", words: "In the last 24 hours", from: now - DAY, now }, "UTC"), /last here/);
794795 });
795796
796797 test("Explain asks about the one change, by what it is", () => {
797− assert.equal(explainQuestion({ kind: "merge", title: "Faster pushes", to: "/acme/web/pull/12" }), 'Explain "Faster pushes" (/acme/web/pull/12) in plain English: what changed, why, and is it live?');
798+ assert.equal(explainQuestion({ kind: "merge", title: "Faster pushes", to: "/acme/web/pull/12" }), "Explain pull request #12 in acme/web in plain English: what changed, why, and is it live?");
799+ assert.equal(explainQuestion({ kind: "push", title: "Fix login", to: "/acme/web/commit/abc1234def" }), "Explain commit abc1234 in acme/web in plain English: what changed, why, and is it live?");
798800 assert.match(explainQuestion({ kind: "deploy", title: "web", to: "/acme/web/deployments/1" }), /went out in this deployment/);
799801 assert.match(explainQuestion({ kind: "release", title: "v1.2.0", to: "/acme/web/releases/v1.2.0" }), /What's new in v1\.2\.0/);
800−});
801−
802−test("Home's starters lead with what changed in the span", () => {
803− const starters = homeStarters({ key: "7d", note: null, words: "In the last 7 days", from: 0 }, "UTC");
804− assert.equal(starters.length, 4);
805− assert.equal(starters[0]!.label, "What changed in the last 7 days?");
806− assert.equal(homeStarters({ key: "last", note: null, words: "Since Tuesday", from: 0 }, "UTC")[0]!.label, "What changed since I was last here?");
807802 });
+25−20
13551355
13561356 // --- Asking g1t from Home ------------------------------------------------------------
13571357
1358−type AskedSpan = Pick<Span, "key" | "note" | "words" | "from">;
1358+type AskedSpan = Pick<Span, "key" | "note" | "words" | "from" | "now">;
13591359
1360−/** The span as part of a question, with its start in the viewer's zone (g1t is told the zone): "since I was last here (Oct 14, 2:10 PM)". */
1361−function spanAsked(span: AskedSpan, timeZone: string | null): string {
1362− const since = formatIn(span.from, timeZone, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
1363− if (span.key === "last" && span.note == null) return `since I was last here (${since})`;
1364− return `${span.words.charAt(0).toLowerCase()}${span.words.slice(1)} (since ${since})`;
1360+/** The span as a person would say it: "since I was last here (2:12 PM)", "in the last 24 hours". */
1361+function spanSaid(span: AskedSpan, timeZone: string | null): string {
1362+ if (span.key === "last" && span.note == null) return `since I was last here (${whenShort(span.from, span.now, timeZone)})`;
1363+ return `${span.words.charAt(0).toLowerCase()}${span.words.slice(1)}`;
13651364 }
13661365
13671366 /**
1368− * Home's Summarize: what changed in the span, in plain English, asked of
1369− * g1t with the span's start, so the answer covers what the page shows.
1367+ * Home's Summarize: what changed in the span, in plain English, as a person
1368+ * would ask it. g1t is told the page and the viewer's zone, and its prompt
1369+ * says how to read what shipped (services/integrations ask.rs).
13701370 */
13711371 export function summarizeQuestion(span: AskedSpan, timeZone: string | null): string {
1372− return `Summarize what changed ${spanAsked(span, timeZone)} in plain English: what shipped, what's still deploying or failed, what's in flight, and what needs me.`;
1372+ return `Summarize what changed ${spanSaid(span, timeZone)} in plain English.`;
13731373 }
13741374
1375+/** What a change's page is, in words: "pull request #47 in acme/web", "commit f2c1aa0 in acme/web"; else its path. */
1376+export function changeRef(to: string): string {
1377+ const pull = to.match(/^\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
1378+ if (pull) return `pull request #${pull[3]} in ${pull[1]}/${pull[2]}`;
1379+ const commit = to.match(/^\/([^/]+)\/([^/]+)\/commit\/([0-9a-f]{7,40})/i);
1380+ if (commit) return `commit ${commit[3]!.slice(0, 7)} in ${commit[1]}/${commit[2]}`;
1381+ return to;
1382+}
1383+
13751384 /** A Landed row's Explain: what one change did and why, in plain English. */
13761385 export function explainQuestion(change: Pick<LandedChange, "kind" | "title" | "to">): string {
13771386 if (change.kind === "deploy") return `What went out in this deployment (${change.to}), in plain English?`;
13781387 if (change.kind === "release" || change.kind === "package") return `What's new in ${change.title} (${change.to}), in plain English?`;
1379− return `Explain "${change.title}" (${change.to}) in plain English: what changed, why, and is it live?`;
1388+ return `Explain ${changeRef(change.to)} in plain English: what changed, why, and is it live?`;
13801389 }
13811390
1382−/** What Home's Ask box offers to ask first: a label, and the question it asks. */
1383−export function homeStarters(span: AskedSpan, timeZone: string | null): { label: string; question: string }[] {
1384− const changed = span.key === "last" && span.note == null ? "since I was last here" : `${span.words.charAt(0).toLowerCase()}${span.words.slice(1)}`;
1385− return [
1386− { label: `What changed ${changed}?`, question: summarizeQuestion(span, timeZone) },
1387− { label: "What needs me first?", question: "What needs me first? Most urgent first, one line each on what to do." },
1388− { label: "What shipped today?", question: "What shipped today? What's live, what's still deploying, and what failed." },
1389− { label: "Who's working on what?", question: "Who's working on what right now, and is anything stuck?" },
1390− ];
1391−}
1391+/** What Home's Ask box offers to ask first, after Summarize: a label, and the question it asks. */
1392+export const HOME_STARTERS: { label: string; question: string }[] = [
1393+ { label: "What needs me first?", question: "What needs me first? Most urgent first, one line each on what to do." },
1394+ { label: "What shipped today?", question: "What shipped today? What's live, what's still deploying, and what failed." },
1395+ { label: "Who's working on what?", question: "Who's working on what right now, and is anything stuck?" },
1396+];
+6−6
1919 StartHere,
2020 WindowSwitch,
2121 } from "../../components/home";
22−import { HomeAsk, SummarizeButton } from "../../components/home-ask";
22+import { HomeAsk, WhatChangedCard } from "../../components/home-ask";
2323 import { Skeleton } from "../../components/ui/skeleton";
2424 import { sidebarOrNull } from "../../lib/chat.server";
2525 import {
8383
8484 /**
8585 * Home: the workspace's front page. First a box that asks g1t
86− * (components/home-ask: Summarize on the span and Explain on each change
87− * that landed ask it too); then where you're needed now, with no time
86+ * (components/home-ask; What changed summarizes the span on the page, and
87+ * Explain on each change that landed asks too); then where you're needed now, with no time
8888 * window; what happened since you were last here (or over the last 24
8989 * hours or 7 days, `?window=`): what people did, from the events
9090 * service's digest of the span and chat's, then what landed; what is
247247 <OnboardingOnPage slug={slug} view={loaderData.onboarding} asked={loaderData.setupAsked} />
248248 {/* 0. Ask g1t anything: the answer opens in the top bar's Ask panel. */}
249249 <header className="space-y-3">
250− <HomeAsk span={span} tz={tz} />
250+ <HomeAsk span={span} />
251251 <p className="mx-auto max-w-2xl text-center text-sm leading-relaxed text-muted">
252252 <Suspense fallback={<Skeleton className="inline-block h-3.5 w-72 align-middle" />}>
253253 <Await resolve={head}>
298298 {span.key === "last" && span.note == null ? `${span.words} · ${span.length}` : spanNote(span.note)}
299299 </p>
300300 </div>
301− <div className="flex flex-wrap items-center gap-2 sm:ml-auto">
302− <SummarizeButton span={span} tz={tz} />
301+ <div className="sm:ml-auto">
303302 <WindowSwitch span={span} />
304303 </div>
305304 </div>
305+ <WhatChangedCard key={span.key} slug={slug} span={span} tz={tz} />
306306 <Suspense
307307 fallback={
308308 <Card title={span.key === "last" && span.note == null ? "While you were away" : "Settled for you"}>

Comments

Sign in to comment.