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

Every page loads again: the top bar's Ask loads its panel the first time it is wanted instead of with every page. The shell imported the whole answer stack (AI Elements, Streamdown and its highlighter, the chat helpers) for the Ask panel, and the bundler's shared groups put the chat chunk and the shell's chunk each importing the other; a page that loaded the chat chunk first ran the shell's module before the chat chunk had finished, and its repository tabs read an array still undefined (`Cannot read properties of undefined (reading 'flatMap')`). components/ask-button.tsx is the button alone, in the shell; the panel (components/ask-composer.tsx, now AskSheet) is a lazy chunk, fetched when the pointer comes near the button or it takes focus, shown with a skeleton until it arrives, and kept mounted once loaded so the conversation stays while it is closed. A question asked from the panel also says the browser's time zone. Checked: a production build of the site has no import cycle between those two chunks any more (scripted over the built chunks), the site's typecheck and its 888 tests.

syntaqxcommitted Parent1f5404eBrowse files
4 files+88−290/4 viewed
+57−0
1+/**
2+ * The top bar's Ask: the lavender pixel 1 beside its words. The panel it
3+ * opens (components/ask-composer.tsx: the direct message with @g1t, its
4+ * answers drawn with Markdown, code and tool steps) is loaded the first
5+ * time it is wanted, starting when the pointer comes near, so no page
6+ * carries it until then. Kept mounted once loaded, so the conversation
7+ * stays while the panel is closed.
8+ */
9+import { Suspense, lazy, useState } from "react";
10+
11+import { MarkGlyph } from "./logo";
12+import { Button } from "./ui/button";
13+import { Hint } from "./ui/hint";
14+import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
15+import { Skeleton } from "./ui/skeleton";
16+
17+const load = () => import("./ask-composer");
18+const AskSheet = lazy(() => load().then((module) => ({ default: module.AskSheet })));
19+
20+export function AskButton({ slug }: { slug: string }) {
21+ const [open, setOpen] = useState(false);
22+ const [wanted, setWanted] = useState(false);
23+ const change = (next: boolean) => {
24+ if (next) setWanted(true);
25+ setOpen(next);
26+ };
27+ return (
28+ <Sheet open={open} onOpenChange={change}>
29+ <Hint label="Ask g1t about your work">
30+ <SheetTrigger asChild>
31+ <Button variant="soft" size="bar" aria-label="Ask g1t" className="gap-2 max-md:ring-0 lg:px-3" onPointerEnter={() => void load()} onFocus={() => void load()}>
32+ <MarkGlyph className="h-3.5" />
33+ <span className="max-lg:hidden">Ask</span>
34+ </Button>
35+ </SheetTrigger>
36+ </Hint>
37+ {wanted && (
38+ <Suspense fallback={<Loading />}>
39+ <AskSheet slug={slug} open={open} />
40+ </Suspense>
41+ )}
42+ </Sheet>
43+ );
44+}
45+
46+/** The panel's frame while it loads. */
47+function Loading() {
48+ return (
49+ <SheetContent side="right" className="sm:max-w-xl">
50+ <SheetTitle className="sr-only">Ask g1t</SheetTitle>
51+ <div className="flex flex-col gap-3 p-5" aria-busy="true">
52+ <Skeleton className="h-5 w-32" />
53+ <Skeleton className="h-4 w-56" />
54+ </div>
55+ </SheetContent>
56+ );
57+}
+21−28
44
55 import { type AskSetup, type ChatLiveEvent, type ChatMessage, type Result } from "@g1t/contracts";
66
7−import { askPage, askWhere } from "../lib/ask";
7+import { askPage, askWhere, browserTimeZone } from "../lib/ask";
88 import { anyAnswering, applyLive, fromG1t, suggestions } from "../lib/g1t-chat";
99 import { Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton } from "./ai-elements/conversation";
1010 import { Message, MessageContent } from "./ai-elements/message";
1717 import { Button } from "./ui/button";
1818 import { Hint } from "./ui/hint";
1919 import { Kbd } from "./ui/kbd";
20−import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet";
20+import { SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./ui/sheet";
2121 import { Skeleton } from "./ui/skeleton";
2222 import { Textarea } from "./ui/textarea";
2323
159159 pending: true,
160160 };
161161 setMessages((now) => [...now, pending]);
162− const saved = await chatApi<ChatMessage>(slug, { intent: "post", channel_id: id, body, page: askPage(pathname) });
162+ const saved = await chatApi<ChatMessage>(slug, { intent: "post", channel_id: id, body, page: { ...askPage(pathname), time_zone: browserTimeZone() } });
163163 setMessages((now) => (saved.ok ? byId([...now.filter((m) => m.id !== pendingId), saved.value]) : now.map((m) => (m.id === pendingId ? { ...m, failed: true } : m))));
164164 // The answer arrives over the socket; should it have opened late, ask once more.
165165 if (saved.ok) setTimeout(() => void load(), 1_500);
183183
184184 type G1tConversation = ReturnType<typeof useG1tConversation>;
185185
186−/** The top bar's Ask: the lavender pixel 1 beside its words. Opens the conversation beside the page. */
187−export function AskButton({ slug }: { slug: string }) {
188− const [open, setOpen] = useState(false);
186+/**
187+ * The Ask panel's sheet (components/ask-button.tsx loads it the first time
188+ * the top bar's Ask opens, so pages don't carry it until then): the
189+ * conversation, kept while the panel is closed.
190+ */
191+export function AskSheet({ slug, open }: { slug: string; open: boolean }) {
189192 const conversation = useG1tConversation(slug, open);
190193 const busy = anyAnswering(conversation.messages);
191194 return (
192− <Sheet open={open} onOpenChange={setOpen}>
193− <Hint label="Ask g1t about your work">
194− <SheetTrigger asChild>
195− <Button variant="soft" size="bar" aria-label="Ask g1t" className="gap-2 max-md:ring-0 lg:px-3">
196− <MarkGlyph className="h-3.5" />
197− <span className="max-lg:hidden">Ask</span>
198− </Button>
199− </SheetTrigger>
200− </Hint>
201− <SheetContent
202− side="right"
203− className="sm:max-w-xl"
204− // Esc stops an answer still coming first; pressed again, it closes the panel.
205− onEscapeKeyDown={(event) => {
206− if (!busy) return;
207− event.preventDefault();
208− void conversation.act("g1t_stop");
209− }}
210− >
211− <AskPanel slug={slug} open={open} conversation={conversation} />
212− </SheetContent>
213− </Sheet>
195+ <SheetContent
196+ side="right"
197+ className="sm:max-w-xl"
198+ // Esc stops an answer still coming first; pressed again, it closes the panel.
199+ onEscapeKeyDown={(event) => {
200+ if (!busy) return;
201+ event.preventDefault();
202+ void conversation.act("g1t_stop");
203+ }}
204+ >
205+ <AskPanel slug={slug} open={open} conversation={conversation} />
206+ </SheetContent>
214207 );
215208 }
216209
+1−1
66
77 import { InMain } from "./landmark";
88 import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
9−import { AskButton } from "./ask-composer";
9+import { AskButton } from "./ask-button";
1010 import { NotificationsBell } from "./inbox";
1111 import { desktopBridge, useDesktopApp, useTitleBarSearch } from "../lib/notify-client";
1212 import { PinButton } from "./pin-button";
+9−0
2626 return page;
2727 }
2828
29+/** The browser's time zone, sent with a question so "today" is the asker's today. */
30+export function browserTimeZone(): string | null {
31+ try {
32+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
33+ } catch {
34+ return null;
35+ }
36+}
37+
2938 /** What the composer says about where the question is asked from. */
3039 export function askWhere(page: AskPage): string | null {
3140 if (page.issue) return `On issue #${page.issue}`;