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

Web: Work after Home, a workspace's work items, and old issue links

Work is in the rail right after Home and on the phone's bar (Home, Work, Chat, Notifications, More, with Code first in More), for every member. Its pages are the list, with its filters in the address and in a sheet on a phone, a work item with its repositories, landings, missing targets and suggested targets, a new one, and the workspace's labels and milestones. A repository's Issues tab becomes Work, its Work page becomes Sessions, and old issue, label and milestone addresses redirect: /<owner>/<repo>/issues/<n> to the work item or pull request it names. #N, owner/repo#N and workspace#N link by the shared rules.

G1T-Session:
ses_01m560bz6eeqc8v1hrrha03pap
syntaqxcommitted Parent62c526dBrowse files
173 files+889−307125 files not counted0/173 viewed
+10−7
55 * without its end). One renderer for every place g1t answers: the top
66 * bar's Ask and its direct message in Chat.
77 *
8− * What g1t adds: issues, pull requests and commits as chips with their
9− * card on hover (`owner/repo#12`, `#12` in the page's repository, a hash,
8+ * What g1t adds: work items, pull requests and commits as chips with their
9+ * card on hover (`owner/repo#12`, `#12` in the page's repository or the workspace, a hash,
1010 * a link to one, a full link to this site), people as their card, code in
1111 * chat's own block (highlighted, with its language and Copy), tables that
1212 * scroll inside the message, and task lists. Raw HTML is never drawn, and
2626 import { type MarkdownRepo, rehypeReferences } from "../../lib/markdown-plugins";
2727 import { CodeBlock } from "../chat/text";
2828 import { MemberCard } from "../chat/profile-card";
29−import { CommitChip, IssueChip, PageChip } from "../chat/ref-chip";
29+import { CommitChip, PageChip, RefChip } from "../chat/ref-chip";
3030 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
3131
3232 /** The text inside a React tree: a code block's source. */
5959 );
6060 }
6161 const target = refTarget(href, origin);
62− if (target?.kind === "issue") {
62+ if (target?.kind === "item" || target?.kind === "pull" || target?.kind === "ref") {
6363 const label = textOf(children);
64+ const where = target.kind === "item" ? target.workspace : target.repo;
6465 // A bare link (its text is the address) shows as the reference it is.
65− const shown = /^https?:\/\/|^\//.test(label) ? `${target.repo}#${target.number}` : children;
66− return <IssueChip slug={context.slug} repo={target.repo} number={target.number} to={target.href} label={shown} kind={/\/pull\//.test(target.href) ? "pull" : "issue"} />;
66+ const shown = /^https?:\/\/|^\//.test(label) ? `${where}#${target.number}` : children;
67+ const [namespace = "", name = ""] = where.split("/");
68+ const scope = target.kind === "item" ? { workspace: target.workspace } : { repo: { namespace, name } };
69+ return <RefChip slug={context.slug} scope={scope} number={target.number} to={target.href} label={shown} kind={target.kind === "ref" ? undefined : target.kind} />;
6770 }
6871 if (target?.kind === "commit") return <CommitChip href={target.href} sha={target.sha} />;
6972 if (target?.kind === "page") {
165168 }
166169 }, [site]);
167170 const drawn = useMemo(() => components(context, origin, usercontent), [context, origin, usercontent]);
168− const rehype = useMemo<PluggableList>(() => [...Object.values(defaultRehypePlugins), [rehypeReferences, { repo: context.repo ?? undefined }]], [context.repo]);
171+ const rehype = useMemo<PluggableList>(() => [...Object.values(defaultRehypePlugins), [rehypeReferences, { repo: context.repo ?? undefined, workspace: context.slug }]], [context.repo, context.slug]);
169172 return (
170173 <Streamdown
171174 mode={streaming ? "streaming" : "static"}
+2−1
1−import { Bell, Building2, Code2, House, LayoutGrid, Lock, MessagesSquare, Pin, Plug, Repeat, Search, Shapes, Users } from "lucide-react";
1+import { Bell, Building2, Code2, House, LayoutGrid, ListTodo, Lock, MessagesSquare, Pin, Plug, Repeat, Search, Shapes, Users } from "lucide-react";
22 import { type ReactNode, useEffect, useMemo, useState } from "react";
33 import { Link, useFetcher, useFetchers } from "react-router";
44
1313 export function appIcon(key: BuiltinApp, size = 18): ReactNode {
1414 const icons: Record<BuiltinApp, ReactNode> = {
1515 home: <House size={size} />,
16+ work: <ListTodo size={size} />,
1617 chat: <MessagesSquare size={size} />,
1718 notifications: <Bell size={size} />,
1819 code: <Code2 size={size} />,
+3−3
371371
372372 /**
373373 * Why-blame: a file in layers. One line lights up, and its provenance
374− * rises out of it in turn: the commit, the pull request, the issue and
374+ * rises out of it in turn: the commit, the pull request, the work item and
375375 * the session the coding agent that wrote it recorded.
376376 */
377377 export function WhyArt({ className }: { className?: string }) {
378378 const D = 7;
379− const layers = ["COMMIT", "PULL REQUEST", "ISSUE", "SESSION"];
379+ const layers = ["COMMIT", "PULL REQUEST", "WORK ITEM", "SESSION"];
380380 return (
381− <svg viewBox="-112 -36 252 176" className={className} role="img" aria-label="A line in a file tracing back to its commit, pull request, issue and the coding agent's recorded session">
381+ <svg viewBox="-112 -36 252 176" className={className} role="img" aria-label="A line in a file tracing back to its commit, pull request, work item and the coding agent's recorded session">
382382 <FadedGrid id="why" size={170} step={17} box={[-112, -36, 252, 176]} />
383383 <Plate x={30} y={30} w={90} d={110} stroke={LINE} opacity={0.4} />
384384 {[45, 58, 71, 97, 110, 123].map((y) => (
+5−3
463463 <div className="space-y-1">
464464 <h3 className="text-base font-semibold text-fg">Ask about your work</h3>
465465 <p className="max-w-sm text-sm text-muted">
466− {where.repo
467− ? `g1t is told you're in ${where.repo.namespace}/${where.repo.name}${where.issue ? `, on issue #${where.issue}` : where.pull ? `, on pull request #${where.pull}` : ""}.`
468− : "g1t is told which page you're on, and reads the issue or pull request in front of you."}
466+ {where.item && where.workspace
467+ ? `g1t is told you're on work item #${where.item} in ${where.workspace}.`
468+ : where.repo
469+ ? `g1t is told you're in ${where.repo.namespace}/${where.repo.name}${where.pull ? `, on pull request #${where.pull}` : ""}.`
470+ : "g1t is told which page you're on, and reads the work item or pull request in front of you."}
469471 </p>
470472 </div>
471473 <Suggestions className="mt-3 max-w-md justify-center">
+1−1
145145 * Work g1t proposed to hand to a session in the person's own harness. Ask
146146 * never starts it: Start runs it on the workspace's Anthropic account (when
147147 * one is connected), Copy command gives the Claude Code command to run it
148− * with the g1t plugin. Either way the session is tied to the issue.
148+ * with the g1t plugin. Either way the session is tied to the work item.
149149 */
150150 export function SessionProposal({
151151 proposal,
+7−7
11 /**
2− * The pull requests for one issue, side by side, on each of them: how each
2+ * The pull requests for one work item, side by side, on each of them: how each
33 * stands, how its checks went, where its review is and how big it is, so
4− * two agents' attempts at the same issue can be compared and one chosen.
4+ * two agents' attempts at the same work item can be compared and one chosen.
55 */
66 import { CircleCheck, CircleDashed, CircleSlash, GitPullRequestArrow, MessageSquare } from "lucide-react";
77 import { Suspense } from "react";
7171 }
7272
7373 /**
74− * Every pull request for issue `issue`, this one first: streamed, so the
74+ * Every pull request for work item `item`, this one first: streamed, so the
7575 * page shows before the others are read. Nothing while there are no others.
7676 */
77−export function AttemptsBox({ attempts, issue, base }: { attempts: Promise<Attempt[]>; issue: number; base: string }) {
77+export function AttemptsBox({ attempts, item, base }: { attempts: Promise<Attempt[]>; item: number; base: string }) {
7878 return (
7979 <Suspense
8080 fallback={
8787 {(found) =>
8888 found.filter((attempt) => !attempt.current).length > 0 && (
8989 <Card asChild className="mt-4 overflow-hidden text-sm">
90− <section aria-label={`Other attempts at #${issue}`}>
90+ <section aria-label={`Other attempts at #${item}`}>
9191 <h3 className="flex items-center gap-2.5 px-4 pt-3 font-medium">
9292 <GitPullRequestArrow size={16} className="shrink-0 text-info" />
93− Other attempts at #{issue}
93+ Other attempts at #{item}
9494 </h3>
9595 <p className="px-4 pt-0.5 pb-2 text-xs text-faint">
96− Pull requests for the same issue. Merging one closes the others.
96+ Pull requests for the same work item in this repository. Merging one closes the others.
9797 </p>
9898 <ul className="divide-y divide-line border-t border-line">
9999 {found.map((attempt) => (
+1−1
180180 about={
181181 on
182182 ? `Usage month ${period}${subscription?.periodEnd ? `; the plan renews ${endsOn(subscription.periodEnd)}` : ""}. One price for the workspace, never per seat.`
183− : "The forge is free: repositories, issues, pull requests, reviews, search and the audit log. Checks, workflows and deployments need the plan, or the trial."
183+ : "The forge is free: repositories, work items, pull requests, reviews, search and the audit log. Checks, workflows and deployments need the plan, or the trial."
184184 }
185185 aside={
186186 <p className="shrink-0 sm:text-right">
+8−7
2424 import { TimeAgo } from "./ui";
2525 import { Button } from "./ui/button";
2626 import { Skeleton } from "./ui/skeleton";
27+import { itemPath } from "../lib/refs";
2728
2829 /**
2930 * A file with, beside each run of lines, the commit that last changed it.
3031 * Picking a line opens why it is the way it is: the commit, the pull
31− * request and issue it came from, and what the agent was thinking.
32+ * request and work item it came from, and what the agent was thinking.
3233 */
3334 export function BlameView({
3435 base,
99100 <div>
100101 <p className="flex items-center gap-2 border-b border-line px-4 py-2 text-xs text-muted">
101102 <Sparkles size={13} className="text-accent" />
102− Pick any line to see why it is the way it is: the commit, the pull request and issue it
103+ Pick any line to see why it is the way it is: the commit, the pull request and work item it
103104 came from, and what the agent was thinking.
104105 </p>
105106 <div className="overflow-x-auto">
282283 </section>
283284 )}
284285
285− {current.issue && (
286+ {current.item && (
286287 <section className="p-4">
287288 <p className="flex items-center gap-1.5 text-xs text-faint">
288289 <CircleDot size={13} />
289290 What was asked for
290291 </p>
291292 <Link
292− to={`${base}/issues/${current.issue.number}`}
293+ to={itemPath(current.item.workspace, current.item.number)}
293294 className="mt-1.5 block font-medium hover:text-accent"
294295 >
295− {current.issue.title} <span className="font-normal text-faint">#{current.issue.number}</span>
296+ {current.item.title} <span className="font-normal text-faint">#{current.item.number}</span>
296297 </Link>
297− {current.issue.body && (
298+ {current.item.body && (
298299 <div className="mt-2 max-h-48 overflow-hidden text-muted **:text-[0.8125rem]! **:leading-5! mask-[linear-gradient(to_bottom,black_70%,transparent)]">
299− <Markdown source={current.issue.body} repo={repoAt(base)} />
300+ <Markdown source={current.item.body} workspace={current.item.workspace} />
300301 </div>
301302 )}
302303 </section>
+50−19
11 /**
2− * References in chat and in g1t's answers, as compact chips: an issue or
3− * pull request (`acme/web#12`, `#12` in a repository's context, or a link
4− * to one) with its title and state in a hover card, a commit by its short
5− * hash, and any other page of the site by its path. One look everywhere a
6− * conversation shows them (components/chat/text.tsx for people's messages,
7− * components/ai-elements/response.tsx for g1t's answers).
2+ * References in chat and in g1t's answers, as compact chips: a work item
3+ * or pull request (`acme/web#12`, `acme#12`, `#12` read as lib/refs.ts
4+ * says, or a link to one) with its title and state in a hover card, a
5+ * commit by its short hash, and any other page of the site by its path.
6+ * One look everywhere a conversation shows them (components/chat/text.tsx
7+ * for people's messages, components/ai-elements/response.tsx for g1t's
8+ * answers).
89 */
9−import { Bot, CircleDot, GitCommitHorizontal, GitPullRequest, Link2 } from "lucide-react";
10+import { Bot, GitCommitHorizontal, GitPullRequest, Link2, ListTodo } from "lucide-react";
1011 import { type ReactNode, useEffect, useState } from "react";
1112 import { Link } from "react-router";
1213
1314 import type { PullStatus } from "@g1t/contracts";
1415
15−import { PullIcon, IssueIcon } from "../work-icons";
16+import { ItemIcon, PullIcon } from "../work-icons";
1617 import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card";
1718 import { Skeleton } from "../ui/skeleton";
1819 import { cn } from "../../lib/cn";
20+import type { RefContext } from "../../lib/refs";
1921 import type { RefCard } from "../../routes/workspace/chat/ref";
2022
2123 /** The chip's frame: a quiet pill that reads as part of the sentence. */
3840 return found;
3941 }
4042
43+/** Where `#number` is read, in words: `acme/web` or `acme`. */
44+function scopeName(scope: RefContext): string {
45+ return "repo" in scope ? `${scope.repo.namespace}/${scope.repo.name}` : scope.workspace;
46+}
47+
48+/** Chat's card for `#number` read in `scope`, asked for in the workspace `slug` (routes/workspace/chat/ref.ts). */
49+export function chatCardUrl(slug: string, scope: RefContext, number: number): string {
50+ const where = "repo" in scope ? `repo=${encodeURIComponent(scopeName(scope))}` : `workspace=${encodeURIComponent(scope.workspace)}`;
51+ return `/${slug}/-/chat/ref?${where}&number=${number}`;
52+}
53+
4154 /**
4255 * The card itself, asked for from `url` once it opens: chat's
4356 * (routes/workspace/chat/ref.ts) for chips there, a repository's
4457 * (routes/repo/ref-card.ts) for references in its commits and timelines.
58+ * `where` names the repository or workspace it was read in.
4559 */
46−export function RefCardBody({ url, repo, number }: { url: string; repo: string; number: number }) {
60+export function RefCardBody({ url, where, number }: { url: string; where: string; number: number }) {
4761 const [card, setCard] = useState<RefCard | null | "loading">("loading");
4862 useEffect(() => {
4963 let live = true;
6074 </div>
6175 );
6276 }
63− if (!card) return <p className="text-xs text-muted">Nothing you can see is numbered #{number} in {repo}.</p>;
77+ if (!card) return <p className="text-xs text-muted">Nothing you can see is numbered #{number} in {where}.</p>;
6478 return (
6579 <div className="flex gap-2.5">
6680 <span className="mt-0.5">
6781 {card.kind === "pull" ? (
6882 <PullIcon status={PULL_STATUSES.find((s) => s === card.state) ?? "open"} size={16} />
6983 ) : (
70− <IssueIcon issue={{ state: card.state === "open" ? "open" : "closed", reason: null }} size={16} />
84+ <ItemIcon item={{ state: card.state === "open" ? "open" : "closed", reason: card.state === "not planned" ? "not_planned" : null }} size={16} />
7185 )}
7286 </span>
7387 <span className="min-w-0">
7488 <span className="block text-xs text-muted">
75− {card.repo} · {card.kind === "pull" ? "Pull request" : "Issue"} #{card.number}
89+ {card.kind === "pull" ? `${card.repo} · Pull request` : `${card.workspace} · Work item`} #{card.number}
7690 </span>
7791 <span className="mt-0.5 block font-medium leading-snug text-fg">{card.title}</span>
7892 <span className="mt-1 block text-xs text-faint capitalize">{card.state}</span>
8195 );
8296 }
8397
84−/** `acme/web#12`: a chip to the issue or pull request, its card on hover. */
85−export function IssueChip({ slug, repo, number, label, to, kind }: { slug: string; repo: string; number: number; label?: ReactNode; to?: string; kind?: "issue" | "pull" }) {
86− const href = to ?? `/${repo}/issues/${number}`;
98+/**
99+ * `acme/web#12` or `#12`: a chip to the work item or pull request it names
100+ * (`to`), its card on hover, read in `scope` and asked for in the
101+ * workspace `slug`.
102+ */
103+export function RefChip({
104+ slug,
105+ scope,
106+ number,
107+ label,
108+ to,
109+ kind,
110+}: {
111+ slug: string;
112+ scope: RefContext;
113+ number: number;
114+ label?: ReactNode;
115+ to: string;
116+ kind?: "item" | "pull";
117+}) {
87118 return (
88119 <HoverCard>
89120 <HoverCardTrigger asChild>
90− <Link to={href} className={CHIP} data-ref="issue">
91− {kind === "pull" ? <GitPullRequest size={12} className="text-faint" /> : <CircleDot size={12} className="text-faint" />}
92− <span className="truncate">{label ?? `${repo}#${number}`}</span>
121+ <Link to={to} className={CHIP} data-ref="ref">
122+ {kind === "pull" ? <GitPullRequest size={12} className="text-faint" /> : <ListTodo size={12} className="text-faint" />}
123+ <span className="truncate">{label ?? `${"repo" in scope ? scopeName(scope) : ""}#${number}`}</span>
93124 </Link>
94125 </HoverCardTrigger>
95126 <HoverCardContent className="w-80">
96− <RefCardBody url={`/${slug}/-/chat/ref?repo=${encodeURIComponent(repo)}&number=${number}`} repo={repo} number={number} />
127+ <RefCardBody url={chatCardUrl(slug, scope, number)} where={scopeName(scope)} number={number} />
97128 </HoverCardContent>
98129 </HoverCard>
99130 );
+27−10
33 import { Link } from "react-router";
44
55 import { MemberCard } from "./profile-card";
6−import { CommitChip, IssueChip } from "./ref-chip";
6+import { CommitChip, RefChip } from "./ref-chip";
77 import { refTarget } from "../../lib/g1t-chat";
8+import { type RefContext, refHref } from "../../lib/refs";
9+
10+/** `acme/web` as a repository. */
11+function repoOf(path: string): { namespace: string; name: string } {
12+ const [namespace = "", name = ""] = path.split("/");
13+ return { namespace: namespace.toLowerCase(), name };
14+}
815 import { Button } from "../ui/button";
916 import { Hint } from "../ui/hint";
1017 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
4855 case "del":
4956 return <del className="text-muted">{spans(span.c, context)}</del>;
5057 case "link": {
51− // A link to an issue, a pull request or a commit here is its chip (components/chat/ref-chip.tsx).
58+ // A link to a work item, a pull request or a commit here is its chip (components/chat/ref-chip.tsx).
5259 const target = refTarget(span.href);
53− if (target?.kind === "issue" && !context.codeLink) {
54− return <IssueChip slug={context.slug} repo={target.repo} number={target.number} to={target.href} label={spans(span.c, context)} kind={/\/pull\//.test(target.href) ? "pull" : "issue"} />;
60+ if (target?.kind === "item") {
61+ return <RefChip slug={context.slug} scope={{ workspace: target.workspace }} number={target.number} to={target.href} label={spans(span.c, context)} kind="item" />;
5562 }
63+ if ((target?.kind === "pull" || target?.kind === "ref") && !context.codeLink) {
64+ return <RefChip slug={context.slug} scope={{ repo: repoOf(target.repo) }} number={target.number} to={target.href} label={spans(span.c, context)} kind={target.kind === "pull" ? "pull" : undefined} />;
65+ }
5666 if (target?.kind === "commit" && !context.codeLink) return <CommitChip href={target.href} sha={target.sha} />;
5767 if (span.href.startsWith("/")) {
5868 return (
96106 `#${span.name}`
97107 );
98108 case "ref": {
99− const repo = span.repo ? (span.repo.includes("/") ? span.repo : `${context.slug}/${span.repo}`) : context.project ? `${context.slug}/${context.project}` : null;
109+ // `acme/web#12` in that repository, `acme#12` in that workspace, and
110+ // `#12` in the channel's project, else this workspace (lib/refs.ts).
111+ const scope: RefContext = span.repo
112+ ? span.repo.includes("/")
113+ ? { repo: repoOf(span.repo) }
114+ : { workspace: span.repo.toLowerCase() }
115+ : context.project
116+ ? { repo: { namespace: context.slug, name: context.project } }
117+ : { workspace: context.slug };
100118 const label = `${span.repo ? span.repo : ""}#${span.number}`;
101− if (!repo) return <span className="font-medium text-fg-soft">{label}</span>;
102− // Issues and pull requests share numbers; the issue page sends a pull on.
103− const href = `/${repo}/issues/${span.number}`;
104− if (!context.codeLink) return <IssueChip slug={context.slug} repo={repo} number={span.number} label={label} />;
119+ const href = refHref({ scope: "here", number: span.number }, scope)!;
120+ // Read in a repository it may be a pull request, which is Code's.
121+ if (!context.codeLink || !("repo" in scope)) return <RefChip slug={context.slug} scope={scope} number={span.number} to={href} label={label} />;
105122 return (
106− <Link to={context.codeLink ? context.codeLink(href) : href} className="font-medium text-accent hover:underline">
123+ <Link to={context.codeLink(href)} className="font-medium text-accent hover:underline">
107124 {label}
108125 </Link>
109126 );
+1−1
77 /**
88 * What a pull request's own check status says. A pull request's checks are
99 * the workflows run on it (see the merge box); this is set when the merge
10− * queue took it out, and on older pull requests, by commands their issue
10+ * queue took it out, and on older pull requests, by commands their work item
1111 * once had run.
1212 */
1313 const LABELS: Record<CheckStatus, string> = {
+1−1
3939 <TabsContent value="agent">
4040 <AgentSetup />
4141 <p className="mt-2 text-xs text-muted">
42− Then ask it to work on an issue in <span className="font-mono text-fg">{path}</span>.{" "}
42+ Then ask it to work on a work item in <span className="font-mono text-fg">{path}</span>.{" "}
4343 <Link to="https://docs.g1t.sh/guides/bring-your-own-agent/" className="text-fg underline underline-offset-4">
4444 More
4545 </Link>
+3−3
1919 switch (hit.kind) {
2020 case "repository":
2121 return "Repository";
22− case "issue":
23− return `${hit.repo} #${hit.number}`;
22+ case "item":
23+ return `${hit.workspace ?? hit.repo} #${hit.number}`;
2424 case "pull":
2525 return `${hit.repo} #${hit.number}`;
2626 case "user":
3434
3535 /**
3636 * ⌘K: pages and actions from what the page already knows, and, as someone
37− * types, repositories, issues, pull requests and people from search,
37+ * types, repositories, work items, pull requests and people from search,
3838 * public ones for anyone and private ones for their workspaces' members.
3939 * The first row always searches all of g1t for what was typed.
4040 */
+1−1
5252
5353 /**
5454 * ⌘K: pages and actions from what the page already knows, and, as someone
55− * types, repositories, issues, pull requests and people from search
55+ * types, repositories, work items, pull requests and people from search
5656 * (command-palette-dialog.tsx).
5757 */
5858 export function CommandPalette(props: {
+25−10
11 /**
22 * A commit message's text with its references linked: `#12` and
3− * `acme/web#12` to the issue or pull request, with its card on hover, and
4− * a full hash to its commit. Where the whole message links to its commit
5− * (a list of commits), the text between references links there and each
6− * reference links on its own, so no link sits inside another.
3+ * `acme/web#12` to the work item or pull request they name (lib/refs.ts),
4+ * with its card on hover, and a full hash to its commit. Where the whole
5+ * message links to its commit (a list of commits), the text between
6+ * references links there and each reference links on its own, so no link
7+ * sits inside another.
78 */
89 import type { ReactNode } from "react";
910 import { Link } from "react-router";
1213 import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card";
1314 import { cn } from "../lib/cn";
1415 import { messageParts } from "../lib/commit-message";
16+import { type RefContext, itemPath, repoRefPath } from "../lib/refs";
1517
16−/** `#12` in `repo` (`owner/name`): a link to it, its card on hover. */
17−export function RefLink({ repo, number, className, children }: { repo: string; number: number; className?: string; children: ReactNode }) {
18+/**
19+ * `#12` read in `scope`, a repository or a workspace: a link to what it
20+ * names, its card on hover. In a repository both are its `ref` page
21+ * (routes/repo/ref-card.ts); in a workspace, the work item's page and the
22+ * workspace's `-/work/ref` card.
23+ */
24+export function RefLink({ scope, number, className, children }: { scope: RefContext; number: number; className?: string; children: ReactNode }) {
25+ const to = "repo" in scope ? repoRefPath(scope.repo, number) : itemPath(scope.workspace, number);
26+ const card = "repo" in scope ? to : `/${scope.workspace}/-/work/ref/${number}`;
27+ const where = "repo" in scope ? `${scope.repo.namespace}/${scope.repo.name}` : scope.workspace;
1828 return (
1929 <HoverCard>
2030 <HoverCardTrigger asChild>
21− {/* Issues and pull requests share numbers; the issue page forwards. */}
22− <Link to={`/${repo}/issues/${number}`} className={cn("relative z-10", className)} data-ref="issue">
31+ <Link to={to} className={cn("relative z-10", className)} data-ref="ref">
2332 {children}
2433 </Link>
2534 </HoverCardTrigger>
2635 <HoverCardContent className="w-80">
27− <RefCardBody url={`/${repo}/ref/${number}`} repo={repo} number={number} />
36+ <RefCardBody url={card} where={where} number={number} />
2837 </HoverCardContent>
2938 </HoverCard>
3039 );
3140 }
3241
42+/** `owner/name` as a reference's repository. */
43+export function repoScope(path: string): RefContext {
44+ const [namespace = "", name = ""] = path.replace(/^\//, "").split("/");
45+ return { repo: { namespace, name } };
46+}
47+
3348 /**
3449 * `text` with its references linked, in the repository at `base`
3550 * (`/owner/name`). With `to`, the text between them links there, styled
5469 {messageParts(text).map((part, index) => {
5570 if (part.kind === "ref") {
5671 return (
57− <RefLink key={index} repo={part.repo ?? repo} number={part.number} className={refClassName}>
72+ <RefLink key={index} scope={repoScope(part.repo ?? repo)} number={part.number} className={refClassName}>
5873 {part.text}
5974 </RefLink>
6075 );
+1−1
11 /**
22 * A person's last year on their profile: a square a day, a column a week,
33 * shaded in the accent by how much they did (commits pushed to a default
4− * branch, issues and pull requests opened, reviews given) on repositories
4+ * branch, work items and pull requests opened, reviews given) on repositories
55 * the viewer can see. The layout is
66 * lib/contribution-calendar.ts; the counts come from the work service's
77 * `contributions`, which never counts work the viewer could not open.
+86−67
11 /**
22 * 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, a project's overview and the why panel
3+ * integration failures and decision records, as a work item's page, the pull
4+ * request page, the Sessions page, a project's overview and the why panel
55 * show them. What each says
66 * is worked out in lib/coordination.ts.
77 */
5858 expiresIn,
5959 harnessName,
6060 type LiveGroup,
61− type LiveIssue,
61+ type LiveItem,
6262 awakeAndIdle,
63− liveByIssue,
63+ liveByItem,
6464 mayEndSession,
6565 mergeSessions,
6666 numberedParts,
9696 import { Markdown } from "./markdown";
9797 import { PersonLink, TimelineItem } from "./work";
9898 import { repoAt } from "../lib/markdown-plugins";
99−import { IssueIcon, PullIcon } from "./work-icons";
99+import { ItemIcon, PullIcon } from "./work-icons";
100+import { itemPath } from "../lib/refs";
101+
102+/** The workspace of the repository at `base` (`/acme/web`): its work items are numbered there. */
103+function workspaceOf(base: string): string {
104+ return base.split("/").filter(Boolean)[0]?.toLowerCase() ?? "";
105+}
100106
101−/** Text with each `#N` linked to that issue or pull request (the issue's address leads to a pull request too). */
107+/**
108+ * Where a session's pages are: under the repository at `base`, or, where
109+ * sessions from several repositories show together (a work item's page),
110+ * under the one each was recorded in.
111+ */
112+export type SessionBase = string | ((session: WorkSession) => string);
113+
114+function baseOf(base: SessionBase, session: WorkSession): string {
115+ return typeof base === "string" ? base : base(session);
116+}
117+
118+/** Text with each `#N` linked to what it names, read in the repository at `base` (lib/refs.ts). */
102119 export function NumberedText({ text, base }: { text: string; base: string }) {
103120 return (
104121 <>
105122 {numberedParts(text).map((part, index) =>
106123 typeof part === "number" ? (
107− <Link key={index} to={`${base}/issues/${part}`} className="font-medium text-fg hover:text-accent">
124+ <Link key={index} to={`${base}/ref/${part}`} className="font-medium text-fg hover:text-accent">
108125 #{part}
109126 </Link>
110127 ) : (
153170 }
154171
155172 /** One claim: who, from which harness, since when, and when it lapses; one no longer active is muted. */
156−export function ClaimLine({ claim, now, showIssue, base }: { claim: Claim; now: number; showIssue?: boolean; base: string }) {
173+export function ClaimLine({ claim, now, showItem }: { claim: Claim; now: number; showItem?: boolean; base?: string }) {
157174 const active = claim.state === "active";
158175 const left = expiresIn(claim, now);
159176 return (
162179 <div className="min-w-0 grow">
163180 <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm">
164181 <PersonLink name={claim.actor.username} className="font-medium hover:underline" />
165− {showIssue ? (
182+ {showItem ? (
166183 <span className="text-muted">
167184 claimed{" "}
168− <Link to={`${base}/issues/${claim.issue}`} className="font-medium text-fg hover:text-accent">
169− #{claim.issue}
185+ <Link to={itemPath(claim.workspace, claim.item)} className="font-medium text-fg hover:text-accent">
186+ #{claim.item}
170187 </Link>
171188 </span>
172189 ) : null}
217234 return (
218235 <AlertDialog open={open} onOpenChange={onOpenChange}>
219236 <AlertDialogContent>
220− <fetcher.Form method="post" action={`${base}/work/sessions/${session.id}`} className="grid gap-5">
237+ <fetcher.Form method="post" action={`${base}/sessions/${session.id}`} className="grid gap-5">
221238 <input type="hidden" name="action" value="end" />
222239 <AlertDialogHeader>
223240 <AlertDialogTitle>End {sessionTitle(session)}?</AlertDialogTitle>
292309 <Actor username={session.actor.username} harness={session.harness} />
293310 <div className="min-w-0 grow">
294311 <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm">
295− <Link to={`${base}/work/sessions/${session.id}`} prefetch="intent" className="font-medium hover:text-accent">
312+ <Link to={`${base}/sessions/${session.id}`} prefetch="intent" className="font-medium hover:text-accent">
296313 {sessionTitle(session)}
297314 </Link>
298315 <HarnessBadge harness={session.harness} />
329346 }
330347
331348 /**
332− * A session in an issue's or pull request's conversation, where it
349+ * A session in a work item's or pull request's conversation, where it
333350 * started, as a card like a comment or a review: g1t working for someone
334351 * (a session g1t started on the workspace's Claude, when it was assigned
335− * the issue or asked in the thread), or someone working in their own
352+ * the work item or asked in the thread), or someone working in their own
336353 * harness. What it is about, where it stands now, its branch, and what it
337354 * reported once it ended, linked to its page.
338355 */
363380 }
364381 aside={
365382 <Link
366− to={`${base}/work/sessions/${session.id}`}
383+ to={`${base}/sessions/${session.id}`}
367384 prefetch="intent"
368385 className="inline-flex items-center gap-1 text-xs text-faint hover:text-fg"
369386 >
372389 </Link>
373390 }
374391 >
375− <Link to={`${base}/work/sessions/${session.id}`} prefetch="intent" className="font-medium hover:text-accent">
392+ <Link to={`${base}/sessions/${session.id}`} prefetch="intent" className="font-medium hover:text-accent">
376393 {sessionTitle(session)}
377394 </Link>
378395 <p className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted">
419436 return (
420437 <div className="flex min-w-0 items-center gap-1.5 text-xs">
421438 <Bot size={12} className="shrink-0 text-faint" />
422− <Link to={`${base}/work/sessions/${session.id}`} prefetch="intent" className="shrink-0 font-medium text-fg hover:text-accent">
439+ <Link to={`${base}/sessions/${session.id}`} prefetch="intent" className="shrink-0 font-medium text-fg hover:text-accent">
423440 {session.agentType ?? "Subagent"}
424441 </Link>
425442 {session.task && (
456473 ender,
457474 }: {
458475 nodes: SessionNode[];
459− base: string;
476+ base: SessionBase;
460477 showWhere?: boolean;
461478 showBranch?: boolean;
462479 className?: string;
466483 <ul className={className ?? "space-y-3"}>
467484 {nodes.map((node) => (
468485 <li key={node.session.id}>
469− <SessionLine session={node.session} base={base} showWhere={showWhere} showBranch={showBranch} ender={ender} />
486+ <SessionLine session={node.session} base={baseOf(base, node.session)} showWhere={showWhere} showBranch={showBranch} ender={ender} />
470487 {node.children.length > 0 && (
471488 <div className="pl-7.5">
472− <SubagentBranch nodes={node.children} base={base} />
489+ <SubagentBranch nodes={node.children} base={baseOf(base, node.session)} />
473490 </div>
474491 )}
475492 </li>
479496 }
480497
481498 /**
482− * Starting a session on the issue on the workspace's Anthropic account: a
499+ * Starting a session on the work item on the workspace's Anthropic account: a
483500 * button opening a dialog that asks what kind of work and for anything to
484− * add, posting `start-session` to the issue's action.
501+ * add, posting `start-session` to the work item's action.
485502 */
486−export function StartSessionDialog({ setup, error }: { setup: AgentSessionsSetup; error?: string | null }) {
503+export function StartSessionDialog({ setup, repo, error }: { setup: AgentSessionsSetup; /** The repository it works in, `owner/name`. */ repo?: string; error?: string | null }) {
487504 const [open, setOpen] = useState(Boolean(error));
488505 return (
489506 <Dialog open={open} onOpenChange={setOpen}>
496513 <DialogContent>
497514 <Form method="post" className="grid gap-5">
498515 <input type="hidden" name="action" value="start-session" />
516+ {repo && <input type="hidden" name="repo" value={repo} />}
499517 <DialogHeader>
500− <DialogTitle>Start a session on this issue</DialogTitle>
518+ <DialogTitle>Start a session on this work item</DialogTitle>
501519 <DialogDescription>
502520 Claude works on it on {setup.connectionName ? `the ${setup.connectionName} connection` : "your workspace's Anthropic account"}
503− {setup.model ? `, with ${setup.model}` : ""}. It shows on this issue as it goes, with its subagents.
521+ {setup.model ? `, with ${setup.model}` : ""}. It shows on this work item as it goes, with its subagents.
504522 </DialogDescription>
505523 </DialogHeader>
506524 <FieldSet>
755773 {line.session && (
756774 <>
757775 {" · "}
758− <Link to={`${base}/work/sessions/${line.session}`} className="text-fg hover:text-accent">
776+ <Link to={`${base}/sessions/${line.session}`} className="text-fg hover:text-accent">
759777 the session
760778 </Link>
761779 </>
771789 );
772790 }
773791
774−/** An issue's place among others: one it waits on, or one waiting on it. */
775−function DependencyLine({ base, number, title, state, detail }: { base: string; number: number; title: string; state: State; detail?: ReactNode }) {
792+/** A work item's place among others: one it waits on, or one waiting on it. */
793+function DependencyLine({ workspace, number, title, state, detail }: { workspace: string; number: number; title: string; state: State; detail?: ReactNode }) {
776794 return (
777795 <li className="flex items-start gap-2">
778796 <span className="mt-0.5">
779− <IssueIcon issue={{ state, reason: null }} size={14} />
797+ <ItemIcon item={{ state, reason: null }} size={14} />
780798 </span>
781799 <span className="min-w-0 grow">
782− <Link to={`${base}/issues/${number}`} className="block truncate text-sm hover:text-accent">
800+ <Link to={itemPath(workspace, number)} className="block truncate text-sm hover:text-accent">
783801 {title} <span className="text-faint">#{number}</span>
784802 </Link>
785803 {detail && <span className="block text-xs text-muted">{detail}</span>}
816834 }
817835
818836 /**
819− * The Work panel of an issue's sidebar: who has claimed it, the sessions on
837+ * The Work panel of a work item's sidebar: who has claimed it, the sessions on
820838 * it, what it waits on and what waits on it, and for those who may, the
821839 * blockers to change.
822840 */
823−export function IssueWorkPanel({
841+export function ItemWorkPanel({
824842 work,
825843 ended,
826844 base,
833851 work: WorkContext;
834852 /** Claims on it that ended lately, shown muted. */
835853 ended: Claim[];
836− base: string;
854+ /** Where each of its sessions' pages is: under the repository it was recorded in. */
855+ base: SessionBase;
837856 now: number;
838857 canEdit: boolean;
839858 error?: string | null;
842861 /** Starting a session on it, for those who may. */
843862 start?: ReactNode;
844863 }) {
845− const open = work.issue.state === "open";
864+ const open = work.item.state === "open";
846865 const trees = sessionTree(mergeSessions(work.sessions, all));
847866 const sessions = trees.slice(0, 5);
848867 const waiting = work.blockedBy.filter((blocker) => blocker.state === "open").length;
859878 <p className="text-xs text-muted">
860879 {open ? (
861880 <>
862− 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>.
881+ Nobody has claimed it. Claim it from your harness, or with <code className="font-mono text-fg-soft">g1t work claim {work.item.number}</code>.
863882 </>
864883 ) : (
865884 "Nobody is working on it."
866885 )}
867886 </p>
868887 ) : (
869− [...work.claims, ...ended.slice(0, 3)].map((claim) => <ClaimLine key={claim.id} claim={claim} now={now} base={base} />)
888+ [...work.claims, ...ended.slice(0, 3)].map((claim) => <ClaimLine key={claim.id} claim={claim} now={now} />)
870889 )}
871890 {work.claims.length > 1 && (
872891 <Alert tone="warn" className="flex items-start gap-1.5 px-2.5 py-1.5 text-xs">
884903 <SessionTreeList nodes={sessions} base={base} showBranch={false} />
885904 )}
886905 {trees.length > sessions.length && (
887− <Link to={`${base}/work`} className="mt-2 inline-block text-xs text-muted hover:text-fg">
888− {trees.length - sessions.length} more on the Work page
889− </Link>
906+ <p className="mt-2 text-xs text-muted">
907+ {trees.length - sessions.length} more on its repositories' Sessions pages
908+ </p>
890909 )}
891910 </PanelPart>
892911 )}
898917 {waiting > 0 && <span className="text-warn"> · {waiting} open</span>}
899918 </>
900919 }
901− action={canEdit ? <BlockedByEditor current={work.issue.blockedBy} /> : undefined}
920+ action={canEdit ? <BlockedByEditor current={work.item.blockedBy} /> : undefined}
902921 >
903922 {work.blockedBy.length === 0 ? (
904923 <p className="text-xs text-faint">Nothing. It can start now.</p>
905924 ) : (
906925 <ul className="space-y-2">
907926 {work.blockedBy.map((blocker) => (
908− <DependencyLine key={blocker.number} base={base} number={blocker.number} title={blocker.title} state={blocker.state} detail={blockerDetail(blocker)} />
927+ <DependencyLine key={blocker.number} workspace={work.item.workspace} number={blocker.number} title={blocker.title} state={blocker.state} detail={blockerDetail(blocker)} />
909928 ))}
910929 </ul>
911930 )}
915934 {work.blocks.length > 0 && (
916935 <PanelPart title="Blocks">
917936 <ul className="space-y-2">
918− {work.blocks.map((issue) => (
919− <DependencyLine key={issue.number} base={base} number={issue.number} title={issue.title} state={issue.state} />
937+ {work.blocks.map((item) => (
938+ <DependencyLine key={item.number} workspace={work.item.workspace} number={item.number} title={item.title} state={item.state} />
920939 ))}
921940 </ul>
922941 </PanelPart>
926945 );
927946 }
928947
929−/** Changing what an issue waits on: the whole list, as numbers. */
948+/** Changing what a work item waits on: the whole list, as numbers. */
930949 function BlockedByEditor({ current }: { current: number[] }) {
931950 return (
932951 <Popover>
941960 <input type="hidden" name="action" value="blocked-by" />
942961 <Label htmlFor="blocked-by">Blocked by</Label>
943962 <Input id="blocked-by" name="blocked_by" defaultValue={current.map((n) => `#${n}`).join(", ")} placeholder="#12, #14" autoComplete="off" />
944− <p className="text-xs text-muted">Issues of this repository that have to land first. Leave it empty for none.</p>
963+ <p className="text-xs text-muted">Work items of this workspace that have to land first. Leave it empty for none.</p>
945964 <SubmitButton size="sm" match={{ action: "blocked-by" }} pending="Saving…">
946965 Save
947966 </SubmitButton>
951970 );
952971 }
953972
954−/** One issue being worked on: its title, then each claim and session on it. */
955−export function LiveIssueCard({
973+/** One work item being worked on: its title, then each claim and session on it. */
974+export function LiveItemCard({
956975 group,
957976 title,
958977 base,
959978 now,
960979 ender,
961980 }: {
962− group: LiveIssue;
981+ group: LiveItem;
963982 title: string | undefined;
964983 base: string;
965984 now: number;
970989 <li>
971990 <div className="flex items-start gap-2.5 px-4 py-3">
972991 <span className="mt-1">
973− <IssueIcon issue={{ state: "open", reason: null }} />
992+ <ItemIcon item={{ state: "open", reason: null }} />
974993 </span>
975− <Link to={`${base}/issues/${group.issue}`} prefetch="intent" className="min-w-0 grow font-medium hover:text-accent">
976− {title ?? `Issue #${group.issue}`} <span className="font-normal text-faint">#{group.issue}</span>
994+ <Link to={itemPath(workspaceOf(base), group.item)} prefetch="intent" className="min-w-0 grow font-medium hover:text-accent">
995+ {title ?? `Work item #${group.item}`} <span className="font-normal text-faint">#{group.item}</span>
977996 </Link>
978997 {group.claims.length > 1 && <Badge tone="warn">{group.claims.length} claims</Badge>}
979998 </div>
9951014 /** A pull request a session is on, as Live now names it. */
9961015 export type LivePull = { title: string; status: Pull["status"] };
9971016
998−/** What a group of sessions on no issue is on, as its card's heading: the pull request, the branch, or neither. */
1017+/** What a group of sessions on no work item is on, as its card's heading: the pull request, the branch, or neither. */
9991018 function LiveGroupHeading({
10001019 group,
10011020 pulls,
10171036 <Link to={`${base}/pull/${group.pull}`} prefetch="intent" className="min-w-0 grow font-medium hover:text-accent">
10181037 {pull?.title ?? `Pull request #${group.pull}`} <span className="font-normal text-faint">#{group.pull}</span>
10191038 </Link>
1020− <span className="mt-0.5 shrink-0 text-xs text-faint">No issue yet</span>
1039+ <span className="mt-0.5 shrink-0 text-xs text-faint">No work item yet</span>
10211040 </div>
10221041 );
10231042 }
10281047 <Link to={`${base}/tree/${encodeURIComponent(group.branch)}`} prefetch="intent" className="min-w-0 truncate font-mono text-sm font-medium hover:text-accent">
10291048 {group.branch}
10301049 </Link>
1031− <span className="ml-auto shrink-0 text-xs text-faint">No issue yet</span>
1050+ <span className="ml-auto shrink-0 text-xs text-faint">No work item yet</span>
10321051 </div>
10331052 );
10341053 }
10371056 return (
10381057 <div className="flex items-center gap-2.5 px-4 py-3">
10391058 {onDefault ? <GitBranch size={16} className="shrink-0 text-faint" /> : <Activity size={16} className="shrink-0 text-faint" />}
1040− <Hint label="Sessions on no issue, pull request or branch of their own, each titled with what it is for. One opens or claims an issue when its work becomes a change.">
1059+ <Hint label="Sessions on no work item, pull request or branch of their own, each titled with what it is for. One opens or claims a work item when its work becomes a change.">
10411060 <span className="text-sm text-muted">
10421061 {onDefault ? (
10431062 <>
10521071 );
10531072 }
10541073
1055−/** Sessions on no issue yet that share a pull request or a branch, or share neither: a heading, then each session. */
1074+/** Sessions on no work item yet that share a pull request or a branch, or share neither: a heading, then each session. */
10561075 export function LiveGroupCard({
10571076 group,
10581077 pulls,
10881107 }
10891108
10901109 /**
1091− * Who is working right now: each issue with its claims and sessions, the
1092− * most recently active first, then sessions on no issue yet, by pull
1093− * request, then by branch, then those on neither. `limit` caps the issues
1110+ * Who is working right now: each work item with its claims and sessions, the
1111+ * most recently active first, then sessions on no work item yet, by pull
1112+ * request, then by branch, then those on neither. `limit` caps the work items
10941113 * shown, for a page that links to the rest.
10951114 */
10961115 export function LiveNowList({
11071126 claims: Claim[];
11081127 sessions: WorkSession[];
11091128 titles: Record<number, string>;
1110− /** The open pull requests' titles and states, for the sessions on one with no issue. */
1129+ /** The open pull requests' titles and states, for the sessions on one with no work item. */
11111130 pulls?: Record<number, LivePull>;
11121131 /** Sessions on it have no branch of their own. */
11131132 defaultBranch?: string | null;
11181137 ender?: SessionEnder;
11191138 }) {
11201139 const { awake, idle } = awakeAndIdle(sessions);
1121− const live = liveByIssue(claims, awake, defaultBranch);
1122− const issues = limit == null ? live.issues : live.issues.slice(0, limit);
1140+ const live = liveByItem(claims, awake, defaultBranch);
1141+ const items = limit == null ? live.items : live.items.slice(0, limit);
11231142 return (
11241143 <ol className="space-y-3">
1125− {issues.map((group) => (
1126− <LiveIssueCard key={group.issue} group={group} title={titles[group.issue]} base={base} now={now} ender={ender} />
1144+ {items.map((group) => (
1145+ <LiveItemCard key={group.item} group={group} title={titles[group.item]} base={base} now={now} ender={ender} />
11271146 ))}
11281147 {live.groups.map((group) => (
11291148 <LiveGroupCard
11761195 <Card className="flex flex-wrap items-center justify-between gap-4 p-5">
11771196 <div>
11781197 <p className="text-sm font-medium">{title}</p>
1179− <p className="mt-1 text-xs text-muted">Sessions and claimed issues appear here as work happens.</p>
1198+ <p className="mt-1 text-xs text-muted">Sessions and claimed work items appear here as work happens.</p>
11801199 </div>
11811200 <Dialog>
11821201 <DialogTrigger asChild>
11881207 <DialogDescription>Choose your client, connect it to g1t, then ask it to work on this project.</DialogDescription>
11891208 </DialogHeader>
11901209 <AgentSetup />
1191− <p className="text-xs text-muted">Ask your agent to claim an issue and record its session so the team can follow along.</p>
1210+ <p className="text-xs text-muted">Ask your agent to claim a work item and record its session so the team can follow along.</p>
11921211 <a href="https://docs.g1t.sh/guides/bring-your-own-agent/" className="text-sm text-accent hover:underline">Connection guide &rarr;</a>
11931212 </DialogContent>
11941213 </Dialog>
+1−1
133133 that your addresses, keys, profile and settings are removed for good.
134134 </li>
135135 <li>
136− What you wrote stays where it is: issues, pull requests, comments and reviews show as{" "}
136+ What you wrote stays where it is: work items, pull requests, comments and reviews show as{" "}
137137 <span className="font-mono text-fg">ghost</span> once it is removed.
138138 </li>
139139 <li>
+4−4
11 /**
22 * g1t's own blocks in the page editor, beside BlockNote's: callouts,
3− * Mermaid diagrams, math, and cards for g1t things (an issue, a pull
3+ * Mermaid diagrams, math, and cards for g1t things (a work item, a pull
44 * request, a channel, a project, another page); and inline mentions of
55 * people, agents and pages, dates, and citations of code (a file, folder
66 * or pattern in a repository, and the symbol, endpoint or environment
2121 import { Card } from "../../ui/card";
2222
2323 /** What an embed shows, from the site (`-/artifacts/api?embed=`). */
24−export type EmbedCard = { kind: "issue" | "pull" | "channel" | "project" | "page" | "link"; title: string; subtitle: string | null; state: string | null; href: string };
24+export type EmbedCard = { kind: "item" | "pull" | "channel" | "project" | "page" | "link"; title: string; subtitle: string | null; state: string | null; href: string };
2525
2626 const CALLOUTS = {
2727 info: { icon: <Info size={16} />, ring: "border-info/30 bg-info/8", text: "text-info" },
178178 );
179179
180180 const EMBED_ICONS: Record<EmbedCard["kind"], ReactNode> = {
181− issue: <CircleDot size={16} />,
181+ item: <CircleDot size={16} />,
182182 pull: <GitPullRequest size={16} />,
183183 channel: <Hash size={16} />,
184184 project: <Package size={16} />,
241241 value={draft}
242242 onChange={(e) => setDraft(e.target.value)}
243243 onKeyDown={(e) => e.stopPropagation()}
244− placeholder="Paste the address of an issue, pull request, channel, project or artifact"
244+ placeholder="Paste the address of a work item, pull request, channel, project or artifact"
245245 className="min-w-0 grow bg-transparent px-1.5 text-sm text-fg outline-none placeholder:text-faint"
246246 />
247247 <Button type="submit" variant="secondary" size="inline" className="px-2.5 py-1 text-xs">
+1−1
231231 { title: "Success callout", subtext: "A tip, or what good looks like", aliases: ["callout", "success", "tip"], group: "Callouts", icon: <CheckCircle2 size={18} />, onItemClick: insert({ type: "callout", props: { kind: "success" } }) },
232232 { title: "Diagram", subtext: "A Mermaid diagram, drawn as you type", aliases: ["mermaid", "flowchart", "sequence", "chart"], group: "Advanced", icon: <Workflow size={18} />, onItemClick: insert({ type: "mermaid" }) },
233233 { title: "Math", subtext: "A formula, typeset with KaTeX", aliases: ["math", "latex", "katex", "equation", "formula"], group: "Advanced", icon: <Sigma size={18} />, onItemClick: insert({ type: "math" }) },
234− { title: "Embed from g1t", subtext: "An issue, pull request, channel, project or artifact, live", aliases: ["embed", "issue", "pull", "pr", "channel", "project"], group: "g1t", icon: <GitPullRequest size={18} />, onItemClick: insert({ type: "embed" }) },
234+ { title: "Embed from g1t", subtext: "A work item, pull request, channel, project or artifact, live", aliases: ["embed", "item", "work", "pull", "pr", "channel", "project"], group: "g1t", icon: <GitPullRequest size={18} />, onItemClick: insert({ type: "embed" }) },
235235 {
236236 title: "Mention",
237237 subtext: "A person",
+2−2
320320 const WORK_FILL: Record<WorkKind, string> = {
321321 pushes: "bg-series-1",
322322 pulls: "bg-series-2",
323− issues: "bg-series-3",
323+ items: "bg-series-3",
324324 shipped: "bg-series-4",
325325 docs: "bg-series-5",
326326 chat: "bg-series-6",
602602 <div className="min-w-0 grow">
603603 <div className="flex min-w-0 items-center gap-1.5">
604604 {change.refsIn ? (
605− // A commit's subject: its `#12`s link to their issues, the rest to the commit.
605+ // A commit's subject: its `#12`s link to what they name, the rest to the commit.
606606 <span className="line-clamp-2 text-sm text-fg sm:truncate">
607607 <CommitText text={change.title} base={`/${change.refsIn}`} to={change.to} linkClassName="hover:underline" />
608608 </span>
+15−15
233233 icon: <Code2 size={18} />,
234234 name: "Code",
235235 about:
236− "Repositories, issues, pull requests, checks, a merge queue and deployments. For the people who build.",
236+ "Repositories, work items, pull requests, checks, a merge queue and deployments. For the people who build.",
237237 to: `${DOCS}/concepts/overview/`,
238238 },
239239 {
293293 const FLOW: [string, string][] = [
294294 [
295295 "Ask",
296− "Open an issue, or raise it in a channel: what should change, and what done looks like.",
296+ "Open a work item, or raise it in a channel: what should change, and what done looks like.",
297297 ],
298298 [
299299 "Change",
317317 const FORGE = [
318318 "Git over HTTPS",
319319 "Public and private repositories",
320− "Issues and labels",
320+ "Work items and labels",
321321 "Pull requests and forks",
322322 "Line comments and reviews",
323323 "Protected branches",
605605 "Sessions recorded onto pull requests",
606606 "Why-blame on any line",
607607 "Decisions recorded with the reasons behind them",
608− "Search across code, issues and people",
608+ "Search across code, work items and people",
609609 ]}
610610 more={["Sessions and why-blame", `${DOCS}/guides/why-blame/`]}
611611 >
612612 Pick any line. g1t shows the commit that changed it, the pull
613− request and issue it came from, and the session your coding agent
613+ request and work item it came from, and the session your coding agent
614614 recorded as it worked: what it read, ran and decided. The reasoning
615615 stays with the code, for people and for the next agent.
616616 </Pillar>
646646 A complete forge for the people who build
647647 </h2>
648648 <p className="mt-4 max-w-md leading-7 text-muted">
649− Issues, branches, pull requests and reviews, the way your team
649+ Work items, branches, pull requests and reviews, the way your team
650650 already works. Write a change by hand, have your coding agent
651651 write it, or both on the same branch.
652652 </p>
716716
717717 <div className="mt-10 grid gap-4 lg:grid-cols-2">
718718 <div className="rounded-3xl bg-surface p-7 ring-1 ring-line sm:p-8">
719− <Eyebrow>Issues</Eyebrow>
719+ <Eyebrow>Work</Eyebrow>
720720 <h3 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
721− Point your coding agent at an issue
721+ Point your coding agent at a work item
722722 </h3>
723723 <p className="mt-3 text-sm leading-6 text-muted">
724724 Your coding agent connects to g1t through MCP and works as you. It
725− reads the issue, pushes a branch, opens the pull request and
725+ reads the work item, pushes a branch, opens the pull request and
726726 records its session onto it, and the pull request goes through
727727 the same checks and reviews as anyone&apos;s.
728728 </p>
729729 <ol className="mt-6 space-y-3 text-sm">
730730 {[
731− "Open an issue: what should change, and what done looks like",
731+ "Open a work item: what should change, and what done looks like",
732732 "Work it yourself, or hand it to your coding agent",
733733 "The pull request runs your checks and lands through the queue",
734734 ].map((step, index) => (
750750 Many agents, no collisions
751751 </h3>
752752 <p className="mt-3 text-sm leading-6 text-muted">
753− Run as many coding agents as you like. Each claims the issue it
753+ Run as many coding agents as you like. Each claims the work item it
754754 takes, and every session shows live as it works, so everyone sees
755755 who is on what. Two that start changing the same files are told
756756 while they work, before either opens a pull request. The merge
775775 </h2>
776776 <p className="mt-4 max-w-md leading-7 text-muted">
777777 Connect Claude Code, Codex, Cursor or any other MCP client and
778− sign in once. It works as you: it reads issues, pushes branches,
778+ sign in once. It works as you: it reads work items, pushes branches,
779779 opens pull requests, and records its session and its decisions
780780 onto them.
781781 </p>
895895 <div className="mx-auto max-w-6xl px-4 py-24">
896896 <Eyebrow>How it flows</Eyebrow>
897897 <h2 className="mt-3 text-3xl font-semibold tracking-tight">
898− From an issue to main
898+ From a work item to main
899899 </h2>
900900 <Live>
901901 <ol className="relative mt-12 grid gap-6 md:grid-cols-5">
967967 </h2>
968968 <p className="mt-4 max-w-xl leading-7 text-muted">
969969 Bring repositories across from GitHub in a few clicks: every
970− branch and tag with full history, and their issues if you want
970+ branch and tag with full history, and their issues as work items if you want
971971 them. The copy on g1t is then your own.
972972 </p>
973973 <p className="mt-4 max-w-xl leading-7 text-muted">
10591059 </p>
10601060 <p className="mt-4 max-w-xl leading-7 text-muted">
10611061 You can run the core forge yourself with Docker Compose today:
1062− accounts, repositories, issues, pull requests and search. It is an
1062+ accounts, repositories, work items, pull requests and search. It is an
10631063 early version, and chat and deployments still run only on g1t.sh.
10641064 </p>
10651065 <div className="mt-7 flex flex-wrap gap-3">
+10−6
6262 */
6363 const trees = new WeightedLru<{ tree: Root; weight: number }>(400_000, (entry) => entry.weight);
6464
65−function treeOf(source: string, repo: MarkdownRepo | undefined): Root {
66− const key = `${repo ? `${repo.namespace}/${repo.name}` : ""}\n${source}`;
65+function treeOf(source: string, repo: MarkdownRepo | undefined, workspace: string | undefined): Root {
66+ const key = `${repo ? `${repo.namespace}/${repo.name}` : workspace ? `${workspace}/-` : ""}\n${source}`;
6767 const kept = trees.get(key);
6868 if (kept) return kept.tree;
6969 const tree = markdownTree(source, {
7070 remarkPlugins: [remarkGfm],
71− rehypePlugins: [rehypeRaw, [rehypeSanitize, SCHEMA], rehypeAlerts, [rehypeReferences, { repo }]],
71+ rehypePlugins: [rehypeRaw, [rehypeSanitize, SCHEMA], rehypeAlerts, [rehypeReferences, { repo, workspace }]],
7272 });
7373 trees.set(key, { tree, weight: markdownWeight(source) });
7474 return tree;
182182 * Renders markdown the way people expect from a forge: GitHub flavoured
183183 * markdown (tables, task lists, footnotes, strikethrough, autolinks), the
184184 * HTML GitHub allows, alerts, highlighted code, heading anchors, and
185− * references (`#12`, `owner/repo#12`, `@name`, commit hashes) linked
186− * within `repo`. Anything an author writes is sanitized, so untrusted
185+ * references (`#12`, `owner/repo#12`, `workspace#12`, `@name`, commit
186+ * hashes) read in `repo`, or in `workspace` for a workspace's own text
187+ * (lib/refs.ts). Anything an author writes is sanitized, so untrusted
187188 * content is safe to pass in.
188189 */
189190 export function Markdown({
190191 source,
191192 repo,
193+ workspace,
192194 base,
193195 rawBase,
194196 }: {
195197 source: string;
196198 /** The repository the text belongs to, for its references. */
197199 repo?: MarkdownRepo;
200+ /** The workspace the text belongs to when it is no repository's (a work item's), for its references. */
201+ workspace?: string;
198202 /** Where relative links point, e.g. `/acme/web/blob/main/docs` for a file's own folder. */
199203 base?: string;
200204 /**
210214 // components never change: where links and pictures point comes from
211215 // context, so a new commit (a README's pictures are that commit's files)
212216 // updates their addresses in place rather than replacing every element.
213− const tree = useMemo(() => treeOf(source, repo), [source, repo?.namespace, repo?.name]);
217+ const tree = useMemo(() => treeOf(source, repo, workspace), [source, repo?.namespace, repo?.name, workspace]);
214218 const rendered = useMemo(() => renderMarkdownTree(tree, COMPONENTS), [tree]);
215219 const bases = useMemo(() => ({ base, rawBase }), [base, rawBase]);
216220 return (
+13−12
2222 import { Input } from "./ui/input";
2323 import { Textarea } from "./ui/textarea";
2424 import { PersonLink } from "./work";
25+import { itemPath } from "../lib/refs";
2526
2627 /** The trigger half of a split button: the chevron that opens its menu. */
2728 function SplitTrigger({ label, variant, disabled }: { label: string; variant: "accent" | "outline"; disabled?: boolean }) {
100101 blocked,
101102 queue,
102103 canAutoMerge,
103− issue,
104+ item,
104105 options,
105106 error,
106107 }: {
115116 queue: boolean;
116117 /** The viewer may turn auto-merge on: they may merge it. */
117118 canAutoMerge: boolean;
118− /** The open issue it is for, which merging closes unless kept open. */
119− issue: { number: number } | null;
119+ /** The open work item it is for, which merging lands unless kept open. */
120+ item: { number: number } | null;
120121 options?: ReactNode;
121122 error?: string | null;
122123 }) {
167168 pull={pull}
168169 base={base}
169170 repo={repo}
170− issue={issue}
171+ item={item}
171172 blocked={confirming === "merge" ? blocked : null}
172173 options={confirming === "merge" ? options : null}
173174 error={error}
257258 );
258259 }
259260
260−/** The confirm step: the commit's message, the branch, and the issues it closes. */
261+/** The confirm step: the commit's message, the branch, and the work items it closes. */
261262 function ConfirmMerge({
262263 mode,
263264 method: initial,
265266 pull,
266267 base,
267268 repo,
268− issue,
269+ item,
269270 blocked,
270271 options,
271272 error,
277278 pull: Pull;
278279 base: string;
279280 repo: string;
280− issue: { number: number } | null;
281+ item: { number: number } | null;
281282 blocked: string | null;
282283 options?: ReactNode;
283284 error?: string | null;
287288 const [method, setMethod] = useState(initial);
288289 const message = choices.messages[method];
289290 const [keepOpen, setKeepOpen] = useState(false);
290− const closes = keepOpen ? (pull.closes ?? []).filter((number) => number !== issue?.number) : (pull.closes ?? []);
291+ const closes = keepOpen ? (pull.closes ?? []).filter((number) => number !== item?.number) : (pull.closes ?? []);
291292 const deletable = choices.canDeleteBranch ?? (pull.branch != null && !pull.fork);
292293 return (
293294 <div className="space-y-4">
354355 />
355356 </>
356357 )}
357− {mode === "merge" && issue && (
358+ {mode === "merge" && item && (
358359 <CheckboxOption
359− name="keepIssueOpen"
360+ name="keepItemOpen"
360361 checked={keepOpen}
361362 onCheckedChange={(checked) => setKeepOpen(checked === true)}
362− label={`Keep #${issue.number} open. This is only part of the work.`}
363+ label={`Keep #${item.number} open. This is only part of the work.`}
363364 labelClassName="text-xs text-muted"
364365 />
365366 )}
370371 <CircleDot size={13} className="text-success" />
371372 {mode === "auto" ? "Once merged, closes" : "Closes"}
372373 {closes.map((number) => (
373− <Link key={number} to={`/${repo}/issues/${number}`} className="font-medium text-fg hover:underline">
374+ <Link key={number} to={itemPath(repo.split("/")[0]!, number)} className="font-medium text-fg hover:underline">
374375 #{number}
375376 </Link>
376377 ))}
+1−1
555555 {run && (
556556 <section aria-label={queueFailed ? "Merge queue" : "Earlier commands"} className="py-1">
557557 <p className="flex flex-wrap items-center gap-x-1.5 px-4 pt-1.5 pb-1 text-xs text-faint">
558− <span className="font-medium text-muted">{queueFailed ? "Merge queue" : "Commands from the issue"}</span>
558+ <span className="font-medium text-muted">{queueFailed ? "Merge queue" : "Commands from the work item"}</span>
559559 <span>· on</span>
560560 <Link to={commitPath(run.headCommit)} className="inline-flex items-center gap-1 font-mono hover:text-fg">
561561 <GitCommitHorizontal size={12} />
+1−1
11 /**
22 * The merge queue as a train: the default branch, then each entry in
33 * order with the combination it is tested as and how its checks stand.
4− * The Merge queue page shows it whole; the Work page shows what is moving.
4+ * The Merge queue page shows it whole; the Sessions page shows what is moving.
55 */
66 import { Check, CircleDashed, GitCommitHorizontal, GitMerge, Loader2, Minus, X } from "lucide-react";
77 import { type ReactNode, Suspense } from "react";
+1−1
122122 disabled?: boolean;
123123 }) {
124124 const [choice, setChoice] = useState<CommitMessageDefault>(saved);
125− const [template, setTemplate] = useState(savedTemplate ?? "{title} (#{number})\n\n{body}\n\nCloses {issues}\n{co_authors}");
125+ const [template, setTemplate] = useState(savedTemplate ?? "{title} (#{number})\n\n{body}\n\nCloses {items}\n{co_authors}");
126126 const box = useRef<HTMLTextAreaElement>(null);
127127 const name = method === "merge" ? "mergeCommit" : "squashCommit";
128128 const preview = defaultMessage(method, choice, template, SAMPLE_PULL);
+1−1
138138 <AlertDialogDescription>
139139 {items === 0
140140 ? "Nothing is in it. "
141− : `The ${items} ${items === 1 ? "issue or pull request" : "issues and pull requests"} in it stay as they are, in no milestone. `}
141+ : `The ${items} ${items === 1 ? "work item or pull request" : "work items and pull requests"} in it stay as they are, in no milestone. `}
142142 This cannot be undone.
143143 </AlertDialogDescription>
144144 </AlertDialogHeader>
+10−9
3535 import { Hint } from "./ui/hint";
3636 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu";
3737 import type { Loaded } from "../lib/mission-control.server";
38+import { itemPath, newItemPath } from "../lib/work-items";
3839
3940 /*
4041 * Code's Overview: where the viewer is needed in the workspace's code,
273274 )}
274275 {row.open !== row.to && (
275276 <Link to={row.open} prefetch="intent" className="px-2 py-1.5 text-sm text-muted hover:text-fg">
276− {row.open.includes("/issues/") ? "Open issue" : isPull ? "See the changes" : "Open pull request"}
277+ {row.open.includes("/-/work/") ? "Open work item" : isPull ? "See the changes" : "Open pull request"}
277278 </Link>
278279 )}
279280 </div>
469470
470471 const VERB: Record<Verb, string> = {
471472 landed: "landed",
472− opened_issue: "opened",
473− closed_issue: "closed",
473+ opened_item: "opened",
474+ closed_item: "closed",
474475 started: "started on",
475476 ready: "marked ready",
476477 checks_passed: "checks passed on",
502503 )}{" "}
503504 {VERB[part.verb]}{" "}
504505 {number != null ? (
505− <Link to={`${base}/issues/${number}`} prefetch="intent" className="text-fg-soft hover:text-fg">
506+ <Link to={part.verb === "opened_item" || part.verb === "closed_item" ? itemPath(group.repo.namespace, number) : `${base}/pull/${number}`} prefetch="intent" className="text-fg-soft hover:text-fg">
506507 <span className="font-mono text-xs">#{number}</span>
507508 {title && <> {title}</>}
508509 </Link>
601602 const delta = change(week.total, week.previous);
602603 const feed = everyActivity ? groups : groups.slice(0, 8);
603604
604− const newIssue =
605+ const newItem =
605606 repos.length > 0 ? (
606607 <DropdownMenu>
607608 <DropdownMenuTrigger className="inline-flex items-center gap-1.5 rounded-md border border-line-strong px-3 py-2 text-sm font-medium text-fg/90 transition-colors hover:bg-raised hover:text-fg">
608− <Plus size={14} /> New issue
609+ <Plus size={14} /> New work item
609610 </DropdownMenuTrigger>
610611 <DropdownMenuContent align="start" className="max-h-80 overflow-y-auto">
611− <DropdownMenuLabel>In which project?</DropdownMenuLabel>
612+ <DropdownMenuLabel>Filed from which project?</DropdownMenuLabel>
612613 {repos.map((repo) => (
613614 <DropdownMenuItem key={repo.name} asChild>
614− <Link to={`/${repo.namespace}/${repo.name}/issues/new`}>
615+ <Link to={newItemPath(repo.namespace, repo.name)}>
615616 <Avatar name={repo.name} size={18} square />
616617 {repo.name}
617618 </Link>
640641 </header>
641642
642643 <section aria-label="Start work" className="flex flex-wrap items-center gap-2">
643− {newIssue}
644+ {newItem}
644645 {needs.length > 0 && (
645646 <Link
646647 to={`${link({ tab: "needs", all: null })}#work`}
+32−28
2929 import { NavTab, NavTabs } from "./ui/nav-tabs";
3030 import { StatusDialog } from "./presence";
3131 import { ThemeSwitch } from "./theme-switch";
32−import { type BuiltinApp, type PinnableApp, appOf } from "../lib/apps";
32+import { type BuiltinApp, PHONE_TABS, type PinnableApp, appOf, phoneMoreApps } from "../lib/apps";
3333 import { setPresence, useOwnPresence } from "../lib/notify-client";
3434 import { dndOn, liveStatus, pauseUntil, untilLabel } from "../lib/presence";
3535 import { STATUS_URL } from "../lib/status";
3636 import { type ModeKey, modeHome, modeOf } from "../lib/workspace-nav";
3737 import { PROJECT_PAGE_LINKS, type ProjectPage, projectPageAt, projectPages } from "../lib/chrome";
3838 import { ROADMAP, type RoadmapItem } from "../lib/roadmap";
39+import { repoWorkPath } from "../lib/work-items";
3940
4041 /**
4142 * The phone's layout (below 768px): a bottom bar of apps with More for
121122
122123 /**
123124 * The bottom bar of a phone, flat along the bottom edge under a rule, as
124− * the rail stands flat beside the page on a computer: Home, Chat,
125− * Notifications and Code (for a member with Code access), each
126− * with what is unread, and More. The tab you are in has the rail's own
125+ * the rail stands flat beside the page on a computer: Home, Work, Chat and
126+ * Notifications (lib/apps.ts `PHONE_TABS`), each with what is unread, and
127+ * More, whose sheet starts with Code: a phone triages and talks about work
128+ * far more than it browses code. The tab you are in has the rail's own
127129 * mark: its icon on the sidebar's accent. It steps aside while the keyboard is up
128130 * and inside a conversation. The tab you are in, tapped again, opens its
129− * mode's menu (its sidebar).
131+ * mode's menu (its sidebar: Work's saved views, labels and milestones).
130132 */
131133 export function BottomBar({
132134 user,
150152 const [more, setMore] = useState(false);
151153 useEffect(() => setMore(false), [pathname]);
152154 const slug = workspace.slug;
153− const code = hasCodeAccess(workspace);
154155 const mode = shellMode ?? modeOf(pathname, slug);
155− const tabs: Tab[] = [
156− { key: "home", label: "Home" },
157− { key: "chat", label: "Chat", badge: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 } },
158− { key: "notifications", label: "Notifications", badge: { count: unread.notifications, loud: true } },
159− ...(code ? [{ key: "code" as const, label: "Code" }] : []),
160− ];
156+ const badges: Partial<Record<ModeKey, Tab["badge"]>> = {
157+ chat: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 },
158+ notifications: { count: unread.notifications, loud: true },
159+ };
160+ const tabs: Tab[] = PHONE_TABS.map((key) => ({ key, label: appOf(key).name, badge: badges[key] }));
161161 const inTabs = tabs.some((tab) => tab.key === mode);
162162 if (isConversation(pathname)) return null;
163163 return (
259259
260260 /**
261261 * More, from the bottom bar: a flat panel standing on it, with g1t's
262− * mark at its top and a grid of big tiles: all apps,
263− * your pinned apps, Artifacts, People, Workspace, Explore, and "You and
262+ * mark at its top and a grid of big tiles: Code (for a member with Code
263+ * access), all apps, your pinned apps, Artifacts, Routines, People,
264+ * Workspace, Explore, and "You and
264265 * help", which opens your account's sheet. Browsing beyond your
265266 * workspaces, Explore opens Explore's menu (`onExplore`) rather than the
266267 * page.
283284 const slug = workspace.slug;
284285 const [account, setAccount] = useState(false);
285286 const shown = pins.map((key) => pinnedApp(key, slug)).filter((app) => app != null);
287+ const apps = phoneMoreApps(hasCodeAccess(workspace));
286288 return (
287289 <>
288290 <Primitive.Root open={open} onOpenChange={onOpenChange}>
300302 <span className="ml-auto truncate font-mono text-xs text-faint">{slug}</span>
301303 </div>
302304 <div className="grid grid-cols-4 gap-1.5">
305+ {/* Code first, for a member who has it: it left the bar for Work. */}
306+ {apps.includes("code") && <MoreTile to={appOf("code").path(slug)} icon={appIcon("code", 20)} label="Code" />}
303307 <MoreTile to={`/${slug}/-/apps`} icon={<LayoutGrid size={20} />} label="All apps" />
304308 {shown.map((app) => (
305309 <MoreTile key={app.key} to={app.to} icon={app.icon(26)} label={app.name} />
306310 ))}
307− <MoreTile to={appOf("artifacts").path(slug)} icon={appIcon("artifacts", 20)} label="Artifacts" />
308− <MoreTile to={appOf("routines").path(slug)} icon={appIcon("routines", 20)} label="Routines" />
309− <MoreTile to={appOf("people").path(slug)} icon={appIcon("people", 20)} label="People" />
310− <MoreTile to={appOf("workspace").path(slug)} icon={appIcon("workspace", 20)} label="Workspace" />
311+ {apps
312+ .filter((key) => key !== "code")
313+ .map((key) => (
314+ <MoreTile key={key} to={appOf(key).path(slug)} icon={appIcon(key, 20)} label={appOf(key).name} />
315+ ))}
311316 {onExplore ? (
312317 <MoreTile
313318 icon={<Compass size={20} />}
540545 /** Which project page a roadmap page (`soon/<key>`) sits under, by its section. */
541546 const SOON_UNDER: Partial<Record<RoadmapItem["section"], ProjectPage>> = {
542547 Code: "code",
543− Issues: "issues",
544548 Deployments: "deployments",
545549 Observability: "observability",
546550 Security: "security",
557561 /**
558562 * A project's pages along the bottom of its header: the sidebar's own list
559563 * (lib/chrome.ts) as a row of tabs that scrolls sideways on a phone, the
560− * current one underlined on the header's border and kept in view, so Issues
561− * and Pull requests are one tap away. From 768px a member's sidebar has
564+ * current one underlined on the header's border and kept in view, so Work
565+ * and Pull requests are one tap away. Work is the workspace's Work list
566+ * filtered to the project (lib/work-items.ts `repoWorkPath`). From 768px a member's sidebar has
562567 * them; a visitor, who has none, always sees the row.
563568 */
564569 export function ProjectStrip({
568573 settings,
569574 counts,
570575 always = false,
571− issues = true,
572576 }: {
573577 /** `/<namespace>/<name>`. */
574578 base: string;
576580 can?: Partial<Abilities>;
577581 /** Whether they see its settings (lib/access.ts `seesSettings`), as the sidebar decides. */
578582 settings?: boolean;
579− counts?: { issues: number; pulls: number };
583+ counts?: { items: number; pulls: number };
580584 /** On a computer too: for a visitor, who has no sidebar to list them. */
581585 always?: boolean;
582− /** Whether issues can be opened in it; Issues leaves the row when not. */
583− issues?: boolean;
584586 }) {
585587 const { pathname } = useLocation();
586588 const rest = pathname.toLowerCase().startsWith(base.toLowerCase()) ? pathname.slice(base.length) : "";
587589 const at = projectPageAt(rest, soonPagesOf());
588− const pages: (ProjectPage | "overview")[] = ["overview", ...projectPages(member, can).filter((page) => page !== "observability" && page !== "settings" && (issues || page !== "issues")),
590+ const pages: (ProjectPage | "overview")[] = ["overview", ...projectPages(member, can).filter((page) => page !== "observability" && page !== "settings"),
589591 ...(settings ?? member ? (["settings"] as const) : []),
590592 ];
591593 return (
592594 <NavTabs label="Project" className={always ? undefined : "md:hidden"}>
593595 {pages.map((page) => {
594596 const link = page === "overview" ? { label: "Overview", path: "" } : PROJECT_PAGE_LINKS[page];
597+ const [namespace = "", name = ""] = base.split("/").filter(Boolean);
598+ const to = page === "work" ? repoWorkPath(namespace, name) : link.path ? `${base}/${link.path}` : base;
595599 return (
596600 <NavTab
597601 key={page}
598− to={link.path ? `${base}/${link.path}` : base}
602+ to={to}
599603 current={at === page}
600604 soon={page !== "overview" && PROJECT_PAGE_LINKS[page].soon}
601− count={page === "issues" ? counts?.issues : page === "pulls" ? counts?.pulls : undefined}
605+ count={page === "work" ? counts?.items : page === "pulls" ? counts?.pulls : undefined}
602606 >
603607 {link.label}
604608 </NavTab>
+4−4
11 /**
2− * Choosing what you hear of: subscribing to one issue or pull request, from
2+ * Choosing what you hear of: subscribing to one work item or pull request, from
33 * its sidebar, and watching a repository, from its header. Both post to
44 * the repository's `notifications` route (routes/repo/notifications.ts);
55 * the events service keeps the choice and the inbox follows it.
1616 import { WATCH_CHOICES, WATCH_EVENT_LABEL, subscriptionLine, watchLabel } from "../lib/inbox";
1717
1818 /**
19− * The sidebar's Notifications box on an issue or pull request: one button
19+ * The sidebar's Notifications box on a work item or pull request: one button
2020 * to subscribe or unsubscribe (or stop ignoring it), and a line saying
2121 * whether you hear of it, and why. While the choice is on its way, it
2222 * shows as made.
2727 kind,
2828 subscription,
2929 }: {
30− /** The repository's notifications route: `/<owner>/<repo>/notifications`. */
30+ /** Where the choice is posted: a repository's notifications route (`/<owner>/<repo>/notifications`), or a work item's page. */
3131 action: string;
3232 number: number;
33− kind: "issue" | "pull";
33+ kind: "item" | "pull";
3434 subscription: ThreadSubscription | null;
3535 }) {
3636 const fetcher = useFetcher<{ error?: string }>();
+1−1
112112 function ToastCard({ notification, paused }: { notification: FeedNotification; paused: boolean }) {
113113 const kind = KIND[notification.kind] ?? { label: "Notifications", icon: <Bell /> };
114114 const loud = LOUD.has(notification.kind);
115− // A card's actions (Approve more, Stop, File issue…): more time to decide.
115+ // A card's actions (Approve more, Stop, File work item…): more time to decide.
116116 const card = notificationActions(notification).length > 0;
117117 const open = () => {
118118 dismiss(notification.id);
+2−2
6767 const ABOUT: Record<OnboardingStep, { icon: ReactNode; lead: string }> = {
6868 ai: {
6969 icon: <Sparkles size={16} />,
70− lead: "g1t works with models on your own keys: Ask and @g1t in Chat answer with one, and an Anthropic key starts sessions on issues. Add a frontier lab, a platform, or a model you host yourself. g1t bills nothing for it; what it costs is on your provider's bill.",
70+ lead: "g1t works with models on your own keys: Ask and @g1t in Chat answer with one, and an Anthropic key starts sessions on work items. Add a frontier lab, a platform, or a model you host yourself. g1t bills nothing for it; what it costs is on your provider's bill.",
7171 },
7272 ask: {
7373 icon: <Bot size={16} />,
7575 },
7676 code: {
7777 icon: <Code2 size={16} />,
78− lead: "Start a project with a new repository, or bring repositories you already have across from GitHub. Issues, pull requests, checks and deployments live beside the code.",
78+ lead: "Start a project with a new repository, or bring repositories you already have across from GitHub. Work items, pull requests, checks and deployments live beside the code.",
7979 },
8080 harness: {
8181 icon: <Laptop size={16} />,
+12−8
3636 CheckCheck,
3737 ChevronDown,
3838 ChevronLeft,
39− CircleDot,
4039 CircleHelp,
40+ Ellipsis,
4141 Code2,
4242 Compass,
4343 FileDiff,
4747 GitPullRequest,
4848 Hash,
4949 House,
50+ ListTodo,
5051 Loader,
5152 MessageSquare,
5253 MessagesSquare,
273274 /** The rail's modes, in the app's order (components/rail.tsx), with its icons. */
274275 const RAIL: { key: string; icon: ReactNode; label: string; target?: CursorTarget; scenes?: Scene[]; badge?: number }[] = [
275276 { key: "home", icon: <House size={15} />, label: "Home", scenes: ["home"] },
277+ { key: "work", icon: <ListTodo size={15} />, label: "Work" },
276278 { key: "explore", icon: <Compass size={15} />, label: "Explore" },
277279 { key: "chat", icon: <MessagesSquare size={15} />, label: "Chat", target: "rail-chat", scenes: ["chat"], badge: 3 },
278280 { key: "code", icon: <Code2 size={15} />, label: "Code", target: "rail-code", scenes: ["code", "session"] },
556558 </div>
557559 <div className="mx-2 my-2 h-px bg-line" />
558560 {row(<Code2 size={13} />, "Code")}
559− {row(<CircleDot size={13} />, "Issues", "12")}
561+ {row(<ListTodo size={13} />, "Work", "12")}
560562 {row(<GitPullRequest size={13} />, "Pull requests", "3", true)}
561563 {row(<PlayCircle size={13} />, "Workflows")}
562564 <div className="mx-2 my-2 h-px bg-line" />
962964 const TOUR_FILL: Record<WorkKind, string> = {
963965 pushes: "bg-series-1",
964966 pulls: "bg-series-2",
965− issues: "bg-series-3",
967+ items: "bg-series-3",
966968 shipped: "bg-series-4",
967969 docs: "bg-series-5",
968970 chat: "bg-series-6",
975977 */
976978 function PeopleCardMock() {
977979 const rows: { name: string; agent: boolean; parts: Partial<Record<WorkKind, number>> }[] = [
978− { name: "jordan", agent: false, parts: { pushes: 4, pulls: 2, issues: 3, chat: 9 } },
979− { name: "alex", agent: false, parts: { pulls: 3, issues: 2, docs: 2, chat: 6 } },
980+ { name: "jordan", agent: false, parts: { pushes: 4, pulls: 2, items: 3, chat: 9 } },
981+ { name: "alex", agent: false, parts: { pulls: 3, items: 2, docs: 2, chat: 6 } },
980982 { name: "claude", agent: true, parts: { pushes: 6, pulls: 2 } },
981983 { name: "g1t", agent: true, parts: { shipped: 3, chat: 4 } },
982984 ];
983985 const legend: [WorkKind, string][] = [
984986 ["pushes", "23 commits to 2 projects"],
985987 ["pulls", "4 opened, 3 merged"],
986− ["issues", "3 opened, 2 closed"],
988+ ["items", "3 opened, 2 closed"],
987989 ["shipped", "3 deploys"],
988990 ["docs", "2 docs edited"],
989991 ["chat", "19 messages in 4 conversations"],
12681270 /** The phone's tab bar, as the app has it (components/mobile.tsx). */
12691271 const TABS: { scenes: Scene[]; icon: ReactNode; label: string }[] = [
12701272 { scenes: ["home"], icon: <House size={16} />, label: "Home" },
1273+ { scenes: [], icon: <ListTodo size={16} />, label: "Work" },
12711274 { scenes: ["chat"], icon: <MessagesSquare size={16} />, label: "Chat" },
12721275 { scenes: [], icon: <Bell size={16} />, label: "Notifications" },
1273− { scenes: ["code", "session"], icon: <Code2 size={16} />, label: "Code" },
1276+ // Code is the first of More's tiles on a phone.
1277+ { scenes: ["code", "session"], icon: <Ellipsis size={16} />, label: "More" },
12741278 ];
12751279
12761280 function Phone({ frame }: { frame: Frame }) {
13031307 </div>
13041308 {/* Inside a conversation the tab bar steps aside, as it does on a phone. */}
13051309 {frame.scene !== "chat" && (
1306− <div className="grid h-12 shrink-0 grid-cols-4 border-t border-line bg-[#0b0b0d]">
1310+ <div className="grid h-12 shrink-0 grid-cols-5 border-t border-line bg-[#0b0b0d]">
13071311 {TABS.map((tab) => {
13081312 const on = tab.scenes.includes(frame.scene);
13091313 return (
+3−3
8989 {archived ? (
9090 <>
9191 <li>Pushes and merges are accepted again.</li>
92− <li>Issues and pull requests unlock.</li>
92+ <li>Work items and pull requests unlock.</li>
9393 <li>Workflows run again.</li>
9494 </>
9595 ) : (
9696 <>
9797 <li>Pushes and merges are refused, for everyone.</li>
98− <li>Issues and pull requests are locked. They stay readable.</li>
98+ <li>Work items and pull requests are locked. They stay readable.</li>
9999 <li>Workflows do not run.</li>
100100 <li>Its deployments keep serving, and anyone who could see it still can.</li>
101101 </>
191191 <AlertDialogHeader>
192192 <AlertDialogTitle>Transfer {repo}</AlertDialogTitle>
193193 <AlertDialogDescription>
194− It keeps its name and moves with everything in it: code, issues, pull requests, workflow runs,
194+ It keeps its name and moves with everything in it: code, work items, pull requests, workflow runs,
195195 deployments, and its own secrets, variables and webhooks.
196196 </AlertDialogDescription>
197197 </AlertDialogHeader>
+1−1
4646 const PUBLIC_COMMANDS: PaletteCommand[] = [
4747 { label: "Features", to: "/features", icon: <BookOpen size={15} /> },
4848 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
49− { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
49+ { label: "Search g1t", hint: "Repositories, code, work items, people", to: "/search", icon: <Search size={15} /> },
5050 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
5151 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
5252 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
+5−3
200200 /**
201201 * The rail down the window's left edge: a narrow column of icons, each
202202 * named by the hint beside it. g1t's mark, which leads to Home; the
203− * built-in apps (Home, Chat, Code and Artifacts) with what is
203+ * built-in apps (Home, Work, Chat, Code, Artifacts and Routines) with what is
204204 * unread on them (Notifications is the bell in the top bar, not a rail
205205 * app); Explore, lit anywhere beyond your workspaces; the apps you
206206 * pinned; the Apps launcher;
232232 const { pins, toggle } = useAppPins(slug, saved);
233233 const builtins: Item[] = [
234234 { key: "home", label: "Home" },
235+ // Every member's, Code access or not.
236+ { key: "work", label: "Work" },
235237 {
236238 key: "chat",
237239 label: "Chat",
270272 current={here === item.key && !shown.some((app) => at(app.to))}
271273 badge={item.badge}
272274 />
273− {/* Right after Home: g1t beyond your workspaces (Explore, search, profiles, other workspaces and their public projects). */}
274− {item.key === "home" && (
275+ {/* Right after Work: g1t beyond your workspaces (Explore, search, profiles, other workspaces and their public projects). */}
276+ {item.key === "work" && (
275277 <RailLink to="/explore" label="Explore" icon={<Compass size={20} />} current={here === "browse" || at("/explore")} />
276278 )}
277279 </Fragment>
+1−1
3434 }
3535
3636 /**
37− * How people (and g1t) reacted to an issue's or pull request's description
37+ * How people (and g1t) reacted to a work item's or pull request's description
3838 * (no `commentId`) or to one of its comments: a pill per emoji with how
3939 * many, lit when the viewer is one of them, and who when hovered. Signed
4040 * in, a pill adds or takes back the viewer's own and the smiley offers the
+1−1
127127 <span className="text-muted">
128128 {settings
129129 ? "It is read-only, so settings that change it are turned off until it is unarchived."
130− : "It is read-only: pushes and merges are refused, issues and pull requests are locked, and workflows do not run. Its deployments keep serving."}
130+ : "It is read-only: pushes and merges are refused, work items and pull requests are locked, and workflows do not run. Its deployments keep serving."}
131131 </span>
132132 </span>
133133 </p>
+3−3
2828 }
2929
3030 function stateIcon(hit: SiteHit): ReactNode {
31− if (hit.kind === "issue") {
31+ if (hit.kind === "item") {
3232 return hit.state === "open" ? (
3333 <CircleDot size={15} className="text-success" aria-label="Open" />
3434 ) : (
5454 return hit.private ? <Lock size={size} /> : <BookMarked size={size} />;
5555 case "code":
5656 return <FileCode2 size={size} />;
57− case "issue":
57+ case "item":
5858 return hit.state === "open" ? <CircleDot size={size} /> : <CircleSlash size={size} />;
5959 case "pull":
6060 return <GitPullRequest size={size} />;
214214 return <Repository hit={hit} />;
215215 case "code":
216216 return <Code hit={hit} />;
217− case "issue":
217+ case "item":
218218 case "pull":
219219 return <Item hit={hit} />;
220220 case "user":
+4−3
3131 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
3232 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
3333 import { wholeDollars } from "../lib/money";
34+import { itemPath } from "../lib/refs";
3435
3536 type Done = { ok: boolean; error?: string } | undefined;
3637
148149 {alert.ruleName && alert.ruleName !== alert.ruleId ? alert.ruleName : shortRule(alert.ruleId)}
149150 </Link>
150151 <SeverityBadge severity={alert.severity} />
151− {alert.issue != null && (
152− <Link to={`${base}/issues/${alert.issue}`}>
152+ {alert.item != null && (
153+ <Link to={itemPath(base.split("/")[1]!, alert.item)}>
153154 <Badge tone="accent">
154− <Bot size={11} /> g1t on #{alert.issue}
155+ <Bot size={11} /> g1t on #{alert.item}
155156 </Badge>
156157 </Link>
157158 )}
+16−14
2525 import { type ReactNode, useEffect, useRef, useState } from "react";
2626 import { Link, useFetcher } from "react-router";
2727
28+import { itemPath } from "../lib/refs";
29+
2830 import {
2931 type AlertActivity,
3032 type AlertState,
272274 <span>{entry.text}</span>
273275 {entry.ref && (
274276 <Link
275− to={`${base}/${entry.ref.kind === "pull" ? "pull" : "issues"}/${entry.ref.number}`}
277+ to={entry.ref.kind === "pull" ? `${base}/pull/${entry.ref.number}` : itemPath(base.split("/")[1]!, entry.ref.number)}
276278 className="font-medium text-fg-soft hover:text-fg hover:underline"
277279 >
278280 #{entry.ref.number}
504506 );
505507 }
506508
507−/** Where the legacy upgrade issue for a package stands, as the page loads it. */
509+/** Where the legacy upgrade work item for a package stands, as the page loads it. */
508510 export type UpgradeFix = {
509511 number: number;
510512 state: "open" | "closed";
511− /** The newest pull request for the issue, if any. */
513+ /** The newest pull request for the work item, if any. */
512514 pull: { number: number; status: PullStatus; agent: string | null } | null;
513515 resolvedBy: number | null;
514516 };
516518 /** A security update's pull request as the page loads it. */
517519 export type PullInfo = { number: number; status: PullStatus; title: string };
518520
519−function FixLink({ issue, fix, base }: { issue: number; fix: UpgradeFix | undefined; base: string }) {
521+function FixLink({ item, fix, base }: { item: number; fix: UpgradeFix | undefined; base: string }) {
520522 const pull = fix?.pull;
521523 return (
522524 <span className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
523− <span className="text-muted">Upgrade issue</span>
524− <Link to={`${base}/issues/${issue}`} className="inline-flex items-center gap-1 text-fg-soft hover:text-fg">
525− {fix?.state === "closed" ? <CircleCheck size={12} className="text-merged" /> : <CircleDot size={12} className="text-success" />}#{issue}
525+ <span className="text-muted">Upgrade work item</span>
526+ <Link to={itemPath(base.split("/")[1]!, item)} className="inline-flex items-center gap-1 text-fg-soft hover:text-fg">
527+ {fix?.state === "closed" ? <CircleCheck size={12} className="text-merged" /> : <CircleDot size={12} className="text-success" />}#{item}
526528 </Link>
527529 {pull && (
528530 <Link to={`${base}/pull/${pull.number}`} className="inline-flex items-center gap-1 text-muted hover:text-fg">
557559 superseded: <>A newer update replaced it, or the package is no longer vulnerable, so g1t closed it.</>,
558560 needs_code: (
559561 <>
560− Raising {name} to {update.target} needs code changes, so g1t opened an issue to make them
561− {update.issue != null && (
562+ Raising {name} to {update.target} needs code changes, so g1t opened a work item to make them
563+ {update.item != null && (
562564 <>
563565 :{" "}
564− <Link to={`${base}/issues/${update.issue}`} className="font-medium text-fg-soft hover:text-fg hover:underline">
565− #{update.issue}
566+ <Link to={itemPath(base.split("/")[1]!, update.item)} className="font-medium text-fg-soft hover:text-fg hover:underline">
567+ #{update.item}
566568 </Link>
567569 </>
568570 )}
688690 const manifests = [...new Set(group.vulns.map((vuln) => vuln.manifest))];
689691 const target = highestFix(group.vulns);
690692 const update = latestUpdate(group.vulns);
691− const issue = update ? null : (group.vulns.find((vuln) => vuln.issue != null)?.issue ?? null);
693+ const item = update ? null : (group.vulns.find((vuln) => vuln.item != null)?.item ?? null);
692694 return (
693695 <li className="px-4 py-3">
694696 <div className="flex items-start gap-3">
707709 </div>
708710 </div>
709711 {update && <UpdateStatus update={update} name={group.name} pulls={pulls} base={base} />}
710− {issue != null && (
712+ {item != null && (
711713 <div className="mt-2 sm:ml-7">
712− <FixLink issue={issue} fix={fixes[issue]} base={base} />
714+ <FixLink item={item} fix={fixes[item]} base={base} />
713715 </div>
714716 )}
715717 <Card asChild tone="plain" radius="lg" className="mt-2.5 space-y-px overflow-hidden sm:ml-7">
+95−23
Loading the diff…
+1−1
Loading the diff…
+1−1
Loading the diff…
+13−6
Loading the diff…
+7−7
Loading the diff…
+395−0
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…

Comments

Sign in to comment.