Skip to content

Commit

Previews build from any branch; Agent in place of Ask AI; pin from the sidebar

- Deploy builds fetch the commit before checking it out (clone::ensure), and ensure falls back to fetching every branch when the store will not fetch a commit by name: a preview of a branch other than the default failed with "reference is not a tree". A test reproduces it. - The new-team route imported a server-only helper it did not use, which failed the production build. - "Ask AI" is "Agent": the top bar button and mission control's composer (still disabled, "Coming later"). - The sidebar's Pinned heading shows only when something is pinned; every project row has a pin to pin or unpin it in place (always shown on touch screens). - Runner tests' scratch repositories never use the machine's commit signing.

syntaqxcommitted Parent810cd76Browse files
10 files+116−400/10 viewed
+9−7
1111 */
1212 export function AskComposer({ children }: { children?: ReactNode }) {
1313 return (
14− <section aria-label="Ask g1t" className="space-y-3">
14+ <section aria-label="Agent" className="space-y-3">
1515 <div aria-disabled="true" className="rounded-xl border border-line bg-surface opacity-80">
16+ <p className="flex items-center gap-2 px-4 pt-3 text-sm font-medium text-fg">
17+ <Sparkles size={14} className="text-merged" aria-hidden="true" />
18+ Agent
19+ <span className="rounded-full border border-line px-1.5 text-[0.6875rem] font-normal text-muted">Coming later</span>
20+ </p>
1621 <label htmlFor="ask-g1t" className="sr-only">
17− Ask g1t (coming later)
22+ Ask the agent (coming later)
1823 </label>
1924 <textarea
2025 id="ask-g1t"
3641 <Soon label="Mention" icon>
3742 <AtSign size={14} />
3843 </Soon>
39− <span className="ml-auto inline-flex items-center gap-1.5 text-xs text-faint">
40− <Sparkles size={13} className="text-merged" aria-hidden="true" />
41− Coming later
42− </span>
44+ <span className="ml-auto" />
4345 <Soon label="Send" icon>
4446 <ArrowUp size={15} />
4547 </Soon>
6668 </button>
6769 </span>
6870 </TooltipTrigger>
69− <TooltipContent>Asking g1t is coming later</TooltipContent>
71+ <TooltipContent>Agent is coming later</TooltipContent>
7072 </Tooltip>
7173 );
7274 }
+5−5
339339 }
340340
341341 /**
342− * Ask AI: here to say it is coming, and not yet usable. A disabled button
342+ * Agent: g1t's agent to talk to, here to say it is coming, and not yet usable. A disabled button
343343 * gets no pointer events, so the tooltip hangs on a span around it.
344344 */
345−export function AskAi() {
345+export function AgentButton() {
346346 return (
347347 <Tooltip>
348348 <TooltipTrigger asChild>
350350 <button
351351 type="button"
352352 disabled
353− aria-label="Ask AI"
353+ aria-label="Agent"
354354 className="pointer-events-none flex h-9 items-center gap-1.5 rounded-md border border-line px-2.5 text-sm text-muted opacity-60 sm:px-3"
355355 >
356356 <Sparkles size={15} className="text-merged" />
357− <span className="hidden sm:inline">Ask AI</span>
357+ <span className="hidden sm:inline">Agent</span>
358358 </button>
359359 </span>
360360 </TooltipTrigger>
361− <TooltipContent>Ask AI is coming later</TooltipContent>
361+ <TooltipContent>Agent is coming later</TooltipContent>
362362 </Tooltip>
363363 );
364364 }
+1−1
870870 </div>
871871 </header>
872872
873− {/* Asking g1t is not on yet; the actions under it do the work today. */}
873+ {/* The agent is not on yet; the actions under it do the work today. */}
874874 <AskComposer>
875875 {newIssue}
876876 {repos.length > 0 && (
+9−4
1515 name,
1616 pinned,
1717 compact,
18+ small,
1819 className,
1920 }: {
2021 workspace: string;
2425 pinned: boolean;
2526 /** Only the pin, as on a row; the header says it in words too. */
2627 compact?: boolean;
28+ /** Row-sized, for the sidebar. */
29+ small?: boolean;
2730 className?: string;
2831 }) {
2932 const fetcher = useFetcher<{ error: string | null }>({ key: `pin:${workspace}/${slug}` });
4245 title={error ?? (shown ? "Unpin from the sidebar" : "Pin to the sidebar")}
4346 className={cn(
4447 "inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-[0.8125rem] transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent",
45− compact
48+ small
49+ ? "relative z-10 size-6 hover:bg-raised"
50+ : compact
4651 ? "relative z-10 size-8 hover:bg-raised"
4752 : "h-8 border border-line px-2.5 text-fg/80 hover:border-line-strong hover:bg-surface hover:text-fg",
48− shown ? "text-accent" : compact ? "text-faint hover:text-fg" : "",
53+ shown ? "text-accent" : compact || small ? "text-faint hover:text-fg" : "",
4954 error && "text-danger",
5055 )}
5156 >
52− <Pin size={14} className={shown ? "fill-current" : undefined} />
53− {!compact && <span className="hidden sm:inline">{shown ? "Unpin" : "Pin"}</span>}
57+ <Pin size={small ? 12 : 14} className={shown ? "fill-current" : undefined} />
58+ {!compact && !small && <span className="hidden sm:inline">{shown ? "Unpin" : "Pin"}</span>}
5459 </button>
5560 </fetcher.Form>
5661 );
+26−12
55 import type { Abilities, InboxCounts, Membership, Spike, User } from "@g1t/contracts";
66
77 import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
8−import { AskAi, InboxBell } from "./inbox";
8+import { AgentButton, InboxBell } from "./inbox";
9+import { PinButton } from "./pin-button";
910 import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
1011 import { StatusDot, useSiteStatus } from "./footer";
1112 import { Logo, Mark } from "./logo";
604605 for (const project of next) form.append("slug", project.name);
605606 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
606607 };
607− const row = (project: ShortcutProject) => (
608− <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
609− {project.title ?? project.name}
610− </SidebarLink>
608+ // Each row pins or unpins in place: the pin shows on hover or focus, and
609+ // stays shown on a pinned row's hover so it reads as "unpin".
610+ const row = (project: ShortcutProject, isPinned: boolean) => (
611+ <div className="group/row relative">
612+ <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
613+ <span className="block truncate pr-6">{project.title ?? project.name}</span>
614+ </SidebarLink>
615+ <PinButton
616+ workspace={slug}
617+ slug={project.name}
618+ name={project.title ?? project.name}
619+ pinned={isPinned}
620+ small
621+ className="absolute top-1/2 right-6 -translate-y-1/2 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
622+ />
623+ </div>
611624 );
612625 return (
613626 <SidebarGroup
618631 </Link>
619632 }
620633 >
621− <SidebarSubhead>Pinned</SidebarSubhead>
622− {pinned.length === 0 ? (
623− <p className="px-2 pb-1 text-xs text-faint">Pin projects to keep them here.</p>
624− ) : (
634+ {/* Only there once something is pinned: each row below has its own pin. */}
635+ {pinned.length > 0 && (
636+ <>
637+ <SidebarSubhead>Pinned</SidebarSubhead>
625638 <ul aria-label="Pinned projects" className="space-y-px">
626639 {pinned.map((project, index) => (
627640 <li
648661 title={pinned.length > 1 ? "Drag, or Alt and an arrow key, to reorder" : undefined}
649662 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
650663 >
651− {row(project)}
664+ {row(project, true)}
652665 {pinned.length > 1 && (
653666 <GripVertical
654667 size={12}
659672 </li>
660673 ))}
661674 </ul>
675+ </>
662676 )}
663677 {recent.length > 0 && (
664678 <>
665679 <SidebarSubhead>Recent</SidebarSubhead>
666680 {recent.map((project) => (
667− <div key={`${project.namespace}/${project.name}`}>{row(project)}</div>
681+ <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
668682 ))}
669683 </>
670684 )}
16951709 </a>
16961710 {user && (
16971711 <>
1698− <AskAi />
1712+ <AgentButton />
16991713 <InboxBell counts={shell.inbox ?? null} />
17001714 </>
17011715 )}
+1−1
1212 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
1313 import { newTeamFromForm, parentChoices, teamPath } from "../../lib/teams";
1414 import { identity } from "../../lib/services.server";
15−import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
15+import { assertSameOrigin, requireUser, roleIn, unwrap } from "../../lib/session.server";
1616
1717 /** The parent select's value for "no parent": Radix selects have no empty value. */
1818 const NO_PARENT = "-";
+1−1
366366 fn commit(dir: &Path, file: &str, text: &str) {
367367 std::fs::write(dir.join(file), text).unwrap();
368368 git_in(dir, &["add", "--all"], None).unwrap();
369− git_in(dir, &["-c", "user.name=t", "-c", "user.email=t@example.com", "commit", "--quiet", "-m", text], None).unwrap();
369+ git_in(dir, &["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", "commit", "--quiet", "-m", text], None).unwrap();
370370 }
371371
372372 fn mirror_of(origin: &Path, into: &Path) {
+55−1
118118 }
119119
120120 /// Makes sure `commit` is in the clone: fetched by name, which servers
121−/// allow for commits on their branches, else the whole of `branch`.
121+/// allow for commits on their branches, else the whole of `branch`, else
122+/// every branch (a preview's commit can be on a branch the clone never
123+/// had, from a server that will not fetch a commit by name).
122124 pub(crate) fn ensure(dir: &Path, auth: &str, remote: &str, branch: &str, commit: &str) -> Result<()> {
123125 if has(dir, commit) {
124126 return Ok(());
129131 }
130132 if is_shallow(dir) {
131133 let _ = git(dir, &["-c", auth, "fetch", "--quiet", "--unshallow", remote, branch]);
134+ if has(dir, commit) {
135+ return Ok(());
136+ }
132137 }
138+ let shallow = if is_shallow(dir) { "--unshallow" } else { "--quiet" };
139+ let _ = git(dir, &["-c", auth, "fetch", "--quiet", shallow, remote, "+refs/heads/*:refs/remotes/everything/*"]);
133140 Ok(())
134141 }
135142
162169 run(&origin, &["init", "--quiet", "-b", "main"]);
163170 run(&origin, &["config", "user.name", "t"]);
164171 run(&origin, &["config", "user.email", "t@example.com"]);
172+ // A test repository of its own: never the machine's commit signing.
173+ run(&origin, &["config", "commit.gpgsign", "false"]);
165174 run(&origin, &["config", "uploadpack.allowReachableSHA1InWant", "true"]);
166175 for i in 0..12 {
167176 std::fs::write(origin.join("f.txt"), format!("{i}\n")).unwrap();
189198 assert_eq!(git(&work, &["log", "-1", "--format=%s", "FETCH_HEAD"]).unwrap(), "side");
190199 let _ = std::fs::remove_dir_all(&root);
191200 }
201+
202+ /// A build of a commit that is only on another branch, from a server
203+ /// that will not fetch a commit by name: a shallow clone of the default
204+ /// branch, then `ensure` finds it (a preview of a branch, 2026-10-08:
205+ /// "reference is not a tree").
206+ #[test]
207+ fn a_commit_only_on_another_branch_is_fetched_for_a_build() {
208+ if Command::new("git").arg("--version").output().is_err() {
209+ return;
210+ }
211+ let root = std::env::temp_dir().join(format!("g1t-ensure-test-{}", std::process::id()));
212+ let _ = std::fs::remove_dir_all(&root);
213+ let origin = root.join("origin");
214+ std::fs::create_dir_all(&origin).unwrap();
215+ let run = |dir: &Path, args: &[&str]| git(dir, args).unwrap();
216+ run(&origin, &["init", "--quiet", "-b", "main"]);
217+ run(&origin, &["config", "user.name", "t"]);
218+ run(&origin, &["config", "user.email", "t@example.com"]);
219+ // A test repository of its own: never the machine's commit signing.
220+ run(&origin, &["config", "commit.gpgsign", "false"]);
221+ run(&origin, &["config", "uploadpack.allowReachableSHA1InWant", "false"]);
222+ run(&origin, &["config", "uploadpack.allowAnySHA1InWant", "false"]);
223+ for i in 0..3 {
224+ std::fs::write(origin.join("f.txt"), format!("{i}\n")).unwrap();
225+ run(&origin, &["add", "f.txt"]);
226+ run(&origin, &["commit", "--quiet", "-m", &format!("c{i}")]);
227+ }
228+ run(&origin, &["checkout", "--quiet", "-b", "v2"]);
229+ std::fs::write(origin.join("g.txt"), "v2\n").unwrap();
230+ run(&origin, &["add", "g.txt"]);
231+ run(&origin, &["commit", "--quiet", "-m", "v2"]);
232+ let preview = run(&origin, &["rev-parse", "HEAD"]);
233+ run(&origin, &["checkout", "--quiet", "main"]);
234+
235+ let url = format!("file://{}", origin.display().to_string().replace('\\', "/"));
236+ let auth = "http.extraHeader=X-Test: 1";
237+ clone(&root, auth, &[], &url, "work").unwrap();
238+ let work = root.join("work");
239+ assert!(!has(&work, &preview));
240+ ensure(&work, auth, "origin", "main", &preview).unwrap();
241+ assert!(has(&work, &preview));
242+ git(&work, &["-c", "advice.detachedHead=false", "checkout", "--quiet", &preview]).unwrap();
243+ assert_eq!(std::fs::read_to_string(work.join("g.txt")).unwrap().trim(), "v2");
244+ let _ = std::fs::remove_dir_all(&root);
245+ }
192246 }
+7−6
571571 let commit = env("GIT_COMMIT")?;
572572 let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?);
573573 std::fs::create_dir_all("/work")?;
574− let cloned = crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR).and_then(|_| {
575− git(
576− Path::new(WORKDIR),
577− &["-c", "advice.detachedHead=false", "checkout", "--quiet", &commit],
578− )
579− });
574+ let workdir = Path::new(WORKDIR);
575+ // The clone is shallow and of the default branch; a preview's commit is
576+ // often on another one, so it is fetched before checking out.
577+ let cloned = crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR)
578+ .and_then(|_| git(workdir, &["rev-parse", "--abbrev-ref", "HEAD"]))
579+ .and_then(|branch| crate::clone::ensure(workdir, &auth, "origin", &branch, &commit))
580+ .and_then(|_| git(workdir, &["-c", "advice.detachedHead=false", "checkout", "--quiet", &commit]));
580581 if let Err(error) = cloned {
581582 bail!("{}", redact(&format!("{error:#}"), secrets));
582583 }
+2−2
11301130 Save, Snooze and Mark all read; `/inbox` with Saved, Done and a reason
11311131 filter; reasons and update counts on each card; a Notifications box on
11321132 issue and pull request pages; a Watch menu in the repository header;
1133−Settings → Notifications; a Needs you card on mission control. Ask AI sits
1133+Settings → Notifications; a Needs you card on mission control. Agent sits
11341134 beside the bell, disabled. Still to come: security alerts (the security
1135−service publishes no event yet), email digests and push, Ask AI, and
1135+service publishes no event yet), email digests and push, Agent, and
11361136 channels.
11371137
11381138 **Channels** (working name): workspace channels, direct messages and