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.

Commit

Home says what changed on the page (#51): What changed, first under "Since you were last here", streams g1t's plain-English summary of the span into the card (components/home-summary, loaded when Summarize is first pressed), with Ask a follow-up, Summarize again and a link to the direct message it is kept in. The box's first starter, "Summarize what changed since I was last here", brings the card into view and starts it. The questions read as a person would ask them ("Summarize what changed since I was last here (2:12 PM) in plain English.", "Explain pull request #47 in flagon-io/g1t in plain English: what changed, why, and is it live?"): what to read and how comes from g1t's own prompt. Explain shows on every Landed row, not on hover. Checked: the site's typecheck and tests (955), and the real Home route rendered with mocked data and a streaming answer at desktop and phone width, dark and light: Summarize, the starter, Ask a follow-up and Explain.

G1t-Session:
ses_01m4ygex20ezftekecszgw2gv5
syntaqxcommitted Parent4491f3aBrowse files
2 files+155−30/2 viewed
+152−0
1+/**
2+ * Home's summary, on the page: the question goes to the viewer's direct
3+ * message with @g1t, as Ask's does, and g1t's answer streams into the card
4+ * over that conversation's live socket. Loaded the first time Summarize is
5+ * pressed (components/home-ask.tsx), so Home doesn't carry the answer's
6+ * Markdown and code until then. The answer stays in the direct message, so
7+ * a follow-up in Ask carries on from it.
8+ */
9+import { useCallback, useEffect, useRef, useState } from "react";
10+import { Link, useLocation } from "react-router";
11+
12+import type { AskSetup, ChatLiveEvent, ChatMessage, Result } from "@g1t/contracts";
13+
14+import { askPage } from "../lib/ask";
15+import { answering, applyLive, fromG1t } from "../lib/g1t-chat";
16+import { browserTimeZone } from "../lib/time-zone";
17+import { G1tAnswer } from "./chat/answer";
18+import { useChatLive } from "./chat/live";
19+import { Alert, AlertDescription } from "./ui/alert";
20+import { Skeleton } from "./ui/skeleton";
21+
22+/** How often the conversation is read again while the answer comes, should the socket miss it. */
23+const POLL_MS = 2_500;
24+/** How long the card waits for an answer before it stops reading. */
25+const GIVE_UP_MS = 4 * 60_000;
26+
27+async function chatApi<T>(slug: string, body: Record<string, unknown>): Promise<Result<T>> {
28+ try {
29+ const response = await fetch(`/${slug}/-/chat/api`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
30+ return (await response.json()) as Result<T>;
31+ } catch {
32+ return { ok: false, error: { code: "conflict", message: "g1t could not be reached. Check your connection and try again." } };
33+ }
34+}
35+
36+function LiveSocket({ slug, channelId, onEvent, onReconnect }: { slug: string; channelId: string; onEvent: (event: ChatLiveEvent) => void; onReconnect: () => void }) {
37+ useChatLive(slug, channelId, onEvent, onReconnect);
38+ return null;
39+}
40+
41+type State =
42+ | { step: "asking" }
43+ | { step: "answering"; channelId: string; questionId: string }
44+ | { step: "problem"; message: string; setup: boolean; owner: boolean };
45+
46+/** Asks g1t `question` once, then shows its answer as it streams. `onDone` runs when the answer is finished. */
47+export default function HomeSummary({ slug, question, onDone }: { slug: string; question: string; onDone?: (channelId: string | null) => void }) {
48+ const { pathname } = useLocation();
49+ const [state, setState] = useState<State>({ step: "asking" });
50+ const [messages, setMessages] = useState<ChatMessage[]>([]);
51+ const asked = useRef(false);
52+
53+ useEffect(() => {
54+ if (asked.current) return;
55+ asked.current = true;
56+ void (async () => {
57+ const setup = await fetch(`/${slug}/-/ask`, { headers: { accept: "application/json" } })
58+ .then((response) => response.json() as Promise<{ ok: boolean; value?: AskSetup; owner: boolean }>)
59+ .catch(() => null);
60+ if (!setup?.ok || !setup.value) return setState({ step: "problem", message: "Ask could not be reached. Try again in a moment.", setup: false, owner: false });
61+ if (setup.value.problem) return setState({ step: "problem", message: setup.value.problem, setup: true, owner: setup.owner });
62+ const opened = await chatApi<{ channel: { id: string } }>(slug, { intent: "g1t_dm" });
63+ if (!opened.ok) return setState({ step: "problem", message: opened.error.message, setup: false, owner: false });
64+ const channelId = opened.value.channel.id;
65+ const saved = await chatApi<ChatMessage>(slug, { intent: "post", channel_id: channelId, body: question, page: { ...askPage(pathname), time_zone: browserTimeZone() } });
66+ if (!saved.ok) return setState({ step: "problem", message: saved.error.message, setup: false, owner: false });
67+ setState({ step: "answering", channelId, questionId: saved.value.id });
68+ })();
69+ }, [slug, question, pathname]);
70+
71+ const channelId = state.step === "answering" ? state.channelId : null;
72+ const questionId = state.step === "answering" ? state.questionId : null;
73+ const answer = questionId ? (messages.find((m) => fromG1t(m) && !m.reset && !m.deleted_at && m.id > questionId) ?? null) : null;
74+ const finished = answer != null && !answering(answer.answer);
75+
76+ const load = useCallback(async () => {
77+ if (!channelId) return;
78+ try {
79+ const response = await fetch(`/${slug}/-/chat/api?channel=${encodeURIComponent(channelId)}&limit=10`, { headers: { accept: "application/json" } });
80+ const page = (await response.json()) as Result<{ messages: ChatMessage[] }>;
81+ if (page.ok) {
82+ // A frame streamed over the socket is newer than a row still being written: keep the longer body.
83+ setMessages((now) =>
84+ page.value.messages.map((m) => {
85+ const known = now.find((k) => k.id === m.id);
86+ return known && answering(m.answer) && known.body.length > m.body.length ? known : m;
87+ }),
88+ );
89+ }
90+ } catch {
91+ // Read again on the next tick.
92+ }
93+ }, [channelId, slug]);
94+
95+ const onEvent = useCallback((event: ChatLiveEvent) => {
96+ if (channelId) setMessages((now) => applyLive(now, event, channelId));
97+ }, [channelId]);
98+
99+ // Read until the answer is finished: the socket brings it as it streams, this makes sure of it.
100+ useEffect(() => {
101+ if (!channelId || finished) return;
102+ void load();
103+ const started = Date.now();
104+ const timer = setInterval(() => {
105+ if (Date.now() - started > GIVE_UP_MS) clearInterval(timer);
106+ else void load();
107+ }, POLL_MS);
108+ return () => clearInterval(timer);
109+ }, [channelId, finished, load]);
110+
111+ const told = useRef(false);
112+ useEffect(() => {
113+ if (finished && !told.current) {
114+ told.current = true;
115+ onDone?.(channelId);
116+ }
117+ }, [finished, channelId, onDone]);
118+
119+ if (state.step === "problem") {
120+ return (
121+ <Alert tone={state.setup ? "info" : "danger"}>
122+ <AlertDescription>
123+ {state.message}{" "}
124+ {state.setup &&
125+ (state.owner ? (
126+ <Link to={`/${slug}/-/ai#ask`} className="text-accent hover:underline">
127+ Connect a provider and choose it for Ask.
128+ </Link>
129+ ) : (
130+ <>Ask an owner of {slug} to connect one.</>
131+ ))}
132+ </AlertDescription>
133+ </Alert>
134+ );
135+ }
136+
137+ return (
138+ <div aria-live="polite" aria-busy={!finished}>
139+ {channelId && <LiveSocket slug={slug} channelId={channelId} onEvent={onEvent} onReconnect={() => void load()} />}
140+ {answer ? (
141+ <G1tAnswer message={answer} context={{ slug, repo: null }} />
142+ ) : (
143+ <div className="space-y-2.5" aria-label="g1t is reading what happened">
144+ <p className="text-sm text-muted">Reading what happened…</p>
145+ <Skeleton className="h-3.5 w-11/12" />
146+ <Skeleton className="h-3.5 w-3/4" />
147+ <Skeleton className="h-3.5 w-5/6" />
148+ </div>
149+ )}
150+ </div>
151+ );
152+}
+3−3
8383
8484 /**
8585 * Home: the workspace's front page. First a box that asks g1t
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
88− * window; what happened since you were last here (or over the last 24
86+ * (components/home-ask: What changed summarizes the span on the page, and
87+ * Explain on each change that landed asks about it); then where you're
88+ * needed now, with no time 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
9191 * running; what the span cost. The last visit is kept by notify, per

Comments

Sign in to comment.