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
173 files+889−307125 files not counted0/173 viewed
| 5 | 5 | * without its end). One renderer for every place g1t answers: the top | |
| 6 | 6 | * bar's Ask and its direct message in Chat. | |
| 7 | 7 | * | |
| 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, | |
| 10 | 10 | * a link to one, a full link to this site), people as their card, code in | |
| 11 | 11 | * chat's own block (highlighted, with its language and Copy), tables that | |
| 12 | 12 | * scroll inside the message, and task lists. Raw HTML is never drawn, and | |
| ⋯ | |||
| 26 | 26 | import { type MarkdownRepo, rehypeReferences } from "../../lib/markdown-plugins"; | |
| 27 | 27 | import { CodeBlock } from "../chat/text"; | |
| 28 | 28 | import { MemberCard } from "../chat/profile-card"; | |
| 29 | − | import { CommitChip, IssueChip, PageChip } from "../chat/ref-chip"; | |
| 29 | + | import { CommitChip, PageChip, RefChip } from "../chat/ref-chip"; | |
| 30 | 30 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; | |
| 31 | 31 | ||
| 32 | 32 | /** The text inside a React tree: a code block's source. */ | |
| ⋯ | |||
| 59 | 59 | ); | |
| 60 | 60 | } | |
| 61 | 61 | const target = refTarget(href, origin); | |
| 62 | − | if (target?.kind === "issue") { | |
| 62 | + | if (target?.kind === "item" || target?.kind === "pull" || target?.kind === "ref") { | |
| 63 | 63 | const label = textOf(children); | |
| 64 | + | const where = target.kind === "item" ? target.workspace : target.repo; | |
| 64 | 65 | // 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} />; | |
| 67 | 70 | } | |
| 68 | 71 | if (target?.kind === "commit") return <CommitChip href={target.href} sha={target.sha} />; | |
| 69 | 72 | if (target?.kind === "page") { | |
| ⋯ | |||
| 165 | 168 | } | |
| 166 | 169 | }, [site]); | |
| 167 | 170 | 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]); | |
| 169 | 172 | return ( | |
| 170 | 173 | <Streamdown | |
| 171 | 174 | mode={streaming ? "streaming" : "static"} | |
| 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"; | |
| 2 | 2 | import { type ReactNode, useEffect, useMemo, useState } from "react"; | |
| 3 | 3 | import { Link, useFetcher, useFetchers } from "react-router"; | |
| 4 | 4 | ||
| ⋯ | |||
| 13 | 13 | export function appIcon(key: BuiltinApp, size = 18): ReactNode { | |
| 14 | 14 | const icons: Record<BuiltinApp, ReactNode> = { | |
| 15 | 15 | home: <House size={size} />, | |
| 16 | + | work: <ListTodo size={size} />, | |
| 16 | 17 | chat: <MessagesSquare size={size} />, | |
| 17 | 18 | notifications: <Bell size={size} />, | |
| 18 | 19 | code: <Code2 size={size} />, | |
| 371 | 371 | ||
| 372 | 372 | /** | |
| 373 | 373 | * 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 | |
| 375 | 375 | * the session the coding agent that wrote it recorded. | |
| 376 | 376 | */ | |
| 377 | 377 | export function WhyArt({ className }: { className?: string }) { | |
| 378 | 378 | const D = 7; | |
| 379 | − | const layers = ["COMMIT", "PULL REQUEST", "ISSUE", "SESSION"]; | |
| 379 | + | const layers = ["COMMIT", "PULL REQUEST", "WORK ITEM", "SESSION"]; | |
| 380 | 380 | 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"> | |
| 382 | 382 | <FadedGrid id="why" size={170} step={17} box={[-112, -36, 252, 176]} /> | |
| 383 | 383 | <Plate x={30} y={30} w={90} d={110} stroke={LINE} opacity={0.4} /> | |
| 384 | 384 | {[45, 58, 71, 97, 110, 123].map((y) => ( |
| 463 | 463 | <div className="space-y-1"> | |
| 464 | 464 | <h3 className="text-base font-semibold text-fg">Ask about your work</h3> | |
| 465 | 465 | <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."} | |
| 469 | 471 | </p> | |
| 470 | 472 | </div> | |
| 471 | 473 | <Suggestions className="mt-3 max-w-md justify-center"> |
| 145 | 145 | * Work g1t proposed to hand to a session in the person's own harness. Ask | |
| 146 | 146 | * never starts it: Start runs it on the workspace's Anthropic account (when | |
| 147 | 147 | * 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. | |
| 149 | 149 | */ | |
| 150 | 150 | export function SessionProposal({ | |
| 151 | 151 | proposal, |
| 1 | 1 | /** | |
| 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 | |
| 3 | 3 | * 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. | |
| 5 | 5 | */ | |
| 6 | 6 | import { CircleCheck, CircleDashed, CircleSlash, GitPullRequestArrow, MessageSquare } from "lucide-react"; | |
| 7 | 7 | import { Suspense } from "react"; | |
| ⋯ | |||
| 71 | 71 | } | |
| 72 | 72 | ||
| 73 | 73 | /** | |
| 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 | |
| 75 | 75 | * page shows before the others are read. Nothing while there are no others. | |
| 76 | 76 | */ | |
| 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 }) { | |
| 78 | 78 | return ( | |
| 79 | 79 | <Suspense | |
| 80 | 80 | fallback={ | |
| ⋯ | |||
| 87 | 87 | {(found) => | |
| 88 | 88 | found.filter((attempt) => !attempt.current).length > 0 && ( | |
| 89 | 89 | <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}`}> | |
| 91 | 91 | <h3 className="flex items-center gap-2.5 px-4 pt-3 font-medium"> | |
| 92 | 92 | <GitPullRequestArrow size={16} className="shrink-0 text-info" /> | |
| 93 | − | Other attempts at #{issue} | |
| 93 | + | Other attempts at #{item} | |
| 94 | 94 | </h3> | |
| 95 | 95 | <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. | |
| 97 | 97 | </p> | |
| 98 | 98 | <ul className="divide-y divide-line border-t border-line"> | |
| 99 | 99 | {found.map((attempt) => ( | |
| 180 | 180 | about={ | |
| 181 | 181 | on | |
| 182 | 182 | ? `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." | |
| 184 | 184 | } | |
| 185 | 185 | aside={ | |
| 186 | 186 | <p className="shrink-0 sm:text-right"> |
| 24 | 24 | import { TimeAgo } from "./ui"; | |
| 25 | 25 | import { Button } from "./ui/button"; | |
| 26 | 26 | import { Skeleton } from "./ui/skeleton"; | |
| 27 | + | import { itemPath } from "../lib/refs"; | |
| 27 | 28 | ||
| 28 | 29 | /** | |
| 29 | 30 | * A file with, beside each run of lines, the commit that last changed it. | |
| 30 | 31 | * 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. | |
| 32 | 33 | */ | |
| 33 | 34 | export function BlameView({ | |
| 34 | 35 | base, | |
| ⋯ | |||
| 99 | 100 | <div> | |
| 100 | 101 | <p className="flex items-center gap-2 border-b border-line px-4 py-2 text-xs text-muted"> | |
| 101 | 102 | <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 | |
| 103 | 104 | came from, and what the agent was thinking. | |
| 104 | 105 | </p> | |
| 105 | 106 | <div className="overflow-x-auto"> | |
| ⋯ | |||
| 282 | 283 | </section> | |
| 283 | 284 | )} | |
| 284 | 285 | ||
| 285 | − | {current.issue && ( | |
| 286 | + | {current.item && ( | |
| 286 | 287 | <section className="p-4"> | |
| 287 | 288 | <p className="flex items-center gap-1.5 text-xs text-faint"> | |
| 288 | 289 | <CircleDot size={13} /> | |
| 289 | 290 | What was asked for | |
| 290 | 291 | </p> | |
| 291 | 292 | <Link | |
| 292 | − | to={`${base}/issues/${current.issue.number}`} | |
| 293 | + | to={itemPath(current.item.workspace, current.item.number)} | |
| 293 | 294 | className="mt-1.5 block font-medium hover:text-accent" | |
| 294 | 295 | > | |
| 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> | |
| 296 | 297 | </Link> | |
| 297 | − | {current.issue.body && ( | |
| 298 | + | {current.item.body && ( | |
| 298 | 299 | <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} /> | |
| 300 | 301 | </div> | |
| 301 | 302 | )} | |
| 302 | 303 | </section> | |
| 1 | 1 | /** | |
| 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). | |
| 8 | 9 | */ | |
| 9 | − | import { Bot, CircleDot, GitCommitHorizontal, GitPullRequest, Link2 } from "lucide-react"; | |
| 10 | + | import { Bot, GitCommitHorizontal, GitPullRequest, Link2, ListTodo } from "lucide-react"; | |
| 10 | 11 | import { type ReactNode, useEffect, useState } from "react"; | |
| 11 | 12 | import { Link } from "react-router"; | |
| 12 | 13 | ||
| 13 | 14 | import type { PullStatus } from "@g1t/contracts"; | |
| 14 | 15 | ||
| 15 | − | import { PullIcon, IssueIcon } from "../work-icons"; | |
| 16 | + | import { ItemIcon, PullIcon } from "../work-icons"; | |
| 16 | 17 | import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card"; | |
| 17 | 18 | import { Skeleton } from "../ui/skeleton"; | |
| 18 | 19 | import { cn } from "../../lib/cn"; | |
| 20 | + | import type { RefContext } from "../../lib/refs"; | |
| 19 | 21 | import type { RefCard } from "../../routes/workspace/chat/ref"; | |
| 20 | 22 | ||
| 21 | 23 | /** The chip's frame: a quiet pill that reads as part of the sentence. */ | |
| ⋯ | |||
| 38 | 40 | return found; | |
| 39 | 41 | } | |
| 40 | 42 | ||
| 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 | + | ||
| 41 | 54 | /** | |
| 42 | 55 | * The card itself, asked for from `url` once it opens: chat's | |
| 43 | 56 | * (routes/workspace/chat/ref.ts) for chips there, a repository's | |
| 44 | 57 | * (routes/repo/ref-card.ts) for references in its commits and timelines. | |
| 58 | + | * `where` names the repository or workspace it was read in. | |
| 45 | 59 | */ | |
| 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 }) { | |
| 47 | 61 | const [card, setCard] = useState<RefCard | null | "loading">("loading"); | |
| 48 | 62 | useEffect(() => { | |
| 49 | 63 | let live = true; | |
| ⋯ | |||
| 60 | 74 | </div> | |
| 61 | 75 | ); | |
| 62 | 76 | } | |
| 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>; | |
| 64 | 78 | return ( | |
| 65 | 79 | <div className="flex gap-2.5"> | |
| 66 | 80 | <span className="mt-0.5"> | |
| 67 | 81 | {card.kind === "pull" ? ( | |
| 68 | 82 | <PullIcon status={PULL_STATUSES.find((s) => s === card.state) ?? "open"} size={16} /> | |
| 69 | 83 | ) : ( | |
| 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} /> | |
| 71 | 85 | )} | |
| 72 | 86 | </span> | |
| 73 | 87 | <span className="min-w-0"> | |
| 74 | 88 | <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} | |
| 76 | 90 | </span> | |
| 77 | 91 | <span className="mt-0.5 block font-medium leading-snug text-fg">{card.title}</span> | |
| 78 | 92 | <span className="mt-1 block text-xs text-faint capitalize">{card.state}</span> | |
| ⋯ | |||
| 81 | 95 | ); | |
| 82 | 96 | } | |
| 83 | 97 | ||
| 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 | + | }) { | |
| 87 | 118 | return ( | |
| 88 | 119 | <HoverCard> | |
| 89 | 120 | <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> | |
| 93 | 124 | </Link> | |
| 94 | 125 | </HoverCardTrigger> | |
| 95 | 126 | <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} /> | |
| 97 | 128 | </HoverCardContent> | |
| 98 | 129 | </HoverCard> | |
| 99 | 130 | ); | |
| 3 | 3 | import { Link } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import { MemberCard } from "./profile-card"; | |
| 6 | − | import { CommitChip, IssueChip } from "./ref-chip"; | |
| 6 | + | import { CommitChip, RefChip } from "./ref-chip"; | |
| 7 | 7 | 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 | + | } | |
| 8 | 15 | import { Button } from "../ui/button"; | |
| 9 | 16 | import { Hint } from "../ui/hint"; | |
| 10 | 17 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; | |
| ⋯ | |||
| 48 | 55 | case "del": | |
| 49 | 56 | return <del className="text-muted">{spans(span.c, context)}</del>; | |
| 50 | 57 | 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). | |
| 52 | 59 | 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" />; | |
| 55 | 62 | } | |
| 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 | + | } | |
| 56 | 66 | if (target?.kind === "commit" && !context.codeLink) return <CommitChip href={target.href} sha={target.sha} />; | |
| 57 | 67 | if (span.href.startsWith("/")) { | |
| 58 | 68 | return ( | |
| ⋯ | |||
| 96 | 106 | `#${span.name}` | |
| 97 | 107 | ); | |
| 98 | 108 | 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 }; | |
| 100 | 118 | 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} />; | |
| 105 | 122 | 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"> | |
| 107 | 124 | {label} | |
| 108 | 125 | </Link> | |
| 109 | 126 | ); | |
| 7 | 7 | /** | |
| 8 | 8 | * What a pull request's own check status says. A pull request's checks are | |
| 9 | 9 | * 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 | |
| 11 | 11 | * once had run. | |
| 12 | 12 | */ | |
| 13 | 13 | const LABELS: Record<CheckStatus, string> = { |
| 39 | 39 | <TabsContent value="agent"> | |
| 40 | 40 | <AgentSetup /> | |
| 41 | 41 | <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>.{" "} | |
| 43 | 43 | <Link to="https://docs.g1t.sh/guides/bring-your-own-agent/" className="text-fg underline underline-offset-4"> | |
| 44 | 44 | More | |
| 45 | 45 | </Link> |
| 19 | 19 | switch (hit.kind) { | |
| 20 | 20 | case "repository": | |
| 21 | 21 | return "Repository"; | |
| 22 | − | case "issue": | |
| 23 | − | return `${hit.repo} #${hit.number}`; | |
| 22 | + | case "item": | |
| 23 | + | return `${hit.workspace ?? hit.repo} #${hit.number}`; | |
| 24 | 24 | case "pull": | |
| 25 | 25 | return `${hit.repo} #${hit.number}`; | |
| 26 | 26 | case "user": | |
| ⋯ | |||
| 34 | 34 | ||
| 35 | 35 | /** | |
| 36 | 36 | * ⌘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, | |
| 38 | 38 | * public ones for anyone and private ones for their workspaces' members. | |
| 39 | 39 | * The first row always searches all of g1t for what was typed. | |
| 40 | 40 | */ | |
| 52 | 52 | ||
| 53 | 53 | /** | |
| 54 | 54 | * ⌘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 | |
| 56 | 56 | * (command-palette-dialog.tsx). | |
| 57 | 57 | */ | |
| 58 | 58 | export function CommandPalette(props: { |
| 1 | 1 | /** | |
| 2 | 2 | * 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. | |
| 7 | 8 | */ | |
| 8 | 9 | import type { ReactNode } from "react"; | |
| 9 | 10 | import { Link } from "react-router"; | |
| ⋯ | |||
| 12 | 13 | import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card"; | |
| 13 | 14 | import { cn } from "../lib/cn"; | |
| 14 | 15 | import { messageParts } from "../lib/commit-message"; | |
| 16 | + | import { type RefContext, itemPath, repoRefPath } from "../lib/refs"; | |
| 15 | 17 | ||
| 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; | |
| 18 | 28 | return ( | |
| 19 | 29 | <HoverCard> | |
| 20 | 30 | <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"> | |
| 23 | 32 | {children} | |
| 24 | 33 | </Link> | |
| 25 | 34 | </HoverCardTrigger> | |
| 26 | 35 | <HoverCardContent className="w-80"> | |
| 27 | − | <RefCardBody url={`/${repo}/ref/${number}`} repo={repo} number={number} /> | |
| 36 | + | <RefCardBody url={card} where={where} number={number} /> | |
| 28 | 37 | </HoverCardContent> | |
| 29 | 38 | </HoverCard> | |
| 30 | 39 | ); | |
| 31 | 40 | } | |
| 32 | 41 | ||
| 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 | + | ||
| 33 | 48 | /** | |
| 34 | 49 | * `text` with its references linked, in the repository at `base` | |
| 35 | 50 | * (`/owner/name`). With `to`, the text between them links there, styled | |
| ⋯ | |||
| 54 | 69 | {messageParts(text).map((part, index) => { | |
| 55 | 70 | if (part.kind === "ref") { | |
| 56 | 71 | 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}> | |
| 58 | 73 | {part.text} | |
| 59 | 74 | </RefLink> | |
| 60 | 75 | ); | |
| 1 | 1 | /** | |
| 2 | 2 | * A person's last year on their profile: a square a day, a column a week, | |
| 3 | 3 | * 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 | |
| 5 | 5 | * the viewer can see. The layout is | |
| 6 | 6 | * lib/contribution-calendar.ts; the counts come from the work service's | |
| 7 | 7 | * `contributions`, which never counts work the viewer could not open. |
| 1 | 1 | /** | |
| 2 | 2 | * 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 | |
| 5 | 5 | * show them. What each says | |
| 6 | 6 | * is worked out in lib/coordination.ts. | |
| 7 | 7 | */ | |
| ⋯ | |||
| 58 | 58 | expiresIn, | |
| 59 | 59 | harnessName, | |
| 60 | 60 | type LiveGroup, | |
| 61 | − | type LiveIssue, | |
| 61 | + | type LiveItem, | |
| 62 | 62 | awakeAndIdle, | |
| 63 | − | liveByIssue, | |
| 63 | + | liveByItem, | |
| 64 | 64 | mayEndSession, | |
| 65 | 65 | mergeSessions, | |
| 66 | 66 | numberedParts, | |
| ⋯ | |||
| 96 | 96 | import { Markdown } from "./markdown"; | |
| 97 | 97 | import { PersonLink, TimelineItem } from "./work"; | |
| 98 | 98 | 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 | + | } | |
| 100 | 106 | ||
| 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). */ | |
| 102 | 119 | export function NumberedText({ text, base }: { text: string; base: string }) { | |
| 103 | 120 | return ( | |
| 104 | 121 | <> | |
| 105 | 122 | {numberedParts(text).map((part, index) => | |
| 106 | 123 | 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"> | |
| 108 | 125 | #{part} | |
| 109 | 126 | </Link> | |
| 110 | 127 | ) : ( | |
| ⋯ | |||
| 153 | 170 | } | |
| 154 | 171 | ||
| 155 | 172 | /** 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 }) { | |
| 157 | 174 | const active = claim.state === "active"; | |
| 158 | 175 | const left = expiresIn(claim, now); | |
| 159 | 176 | return ( | |
| ⋯ | |||
| 162 | 179 | <div className="min-w-0 grow"> | |
| 163 | 180 | <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm"> | |
| 164 | 181 | <PersonLink name={claim.actor.username} className="font-medium hover:underline" /> | |
| 165 | − | {showIssue ? ( | |
| 182 | + | {showItem ? ( | |
| 166 | 183 | <span className="text-muted"> | |
| 167 | 184 | 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} | |
| 170 | 187 | </Link> | |
| 171 | 188 | </span> | |
| 172 | 189 | ) : null} | |
| ⋯ | |||
| 217 | 234 | return ( | |
| 218 | 235 | <AlertDialog open={open} onOpenChange={onOpenChange}> | |
| 219 | 236 | <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"> | |
| 221 | 238 | <input type="hidden" name="action" value="end" /> | |
| 222 | 239 | <AlertDialogHeader> | |
| 223 | 240 | <AlertDialogTitle>End {sessionTitle(session)}?</AlertDialogTitle> | |
| ⋯ | |||
| 292 | 309 | <Actor username={session.actor.username} harness={session.harness} /> | |
| 293 | 310 | <div className="min-w-0 grow"> | |
| 294 | 311 | <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"> | |
| 296 | 313 | {sessionTitle(session)} | |
| 297 | 314 | </Link> | |
| 298 | 315 | <HarnessBadge harness={session.harness} /> | |
| ⋯ | |||
| 329 | 346 | } | |
| 330 | 347 | ||
| 331 | 348 | /** | |
| 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 | |
| 333 | 350 | * started, as a card like a comment or a review: g1t working for someone | |
| 334 | 351 | * (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 | |
| 336 | 353 | * harness. What it is about, where it stands now, its branch, and what it | |
| 337 | 354 | * reported once it ended, linked to its page. | |
| 338 | 355 | */ | |
| ⋯ | |||
| 363 | 380 | } | |
| 364 | 381 | aside={ | |
| 365 | 382 | <Link | |
| 366 | − | to={`${base}/work/sessions/${session.id}`} | |
| 383 | + | to={`${base}/sessions/${session.id}`} | |
| 367 | 384 | prefetch="intent" | |
| 368 | 385 | className="inline-flex items-center gap-1 text-xs text-faint hover:text-fg" | |
| 369 | 386 | > | |
| ⋯ | |||
| 372 | 389 | </Link> | |
| 373 | 390 | } | |
| 374 | 391 | > | |
| 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"> | |
| 376 | 393 | {sessionTitle(session)} | |
| 377 | 394 | </Link> | |
| 378 | 395 | <p className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted"> | |
| ⋯ | |||
| 419 | 436 | return ( | |
| 420 | 437 | <div className="flex min-w-0 items-center gap-1.5 text-xs"> | |
| 421 | 438 | <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"> | |
| 423 | 440 | {session.agentType ?? "Subagent"} | |
| 424 | 441 | </Link> | |
| 425 | 442 | {session.task && ( | |
| ⋯ | |||
| 456 | 473 | ender, | |
| 457 | 474 | }: { | |
| 458 | 475 | nodes: SessionNode[]; | |
| 459 | − | base: string; | |
| 476 | + | base: SessionBase; | |
| 460 | 477 | showWhere?: boolean; | |
| 461 | 478 | showBranch?: boolean; | |
| 462 | 479 | className?: string; | |
| ⋯ | |||
| 466 | 483 | <ul className={className ?? "space-y-3"}> | |
| 467 | 484 | {nodes.map((node) => ( | |
| 468 | 485 | <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} /> | |
| 470 | 487 | {node.children.length > 0 && ( | |
| 471 | 488 | <div className="pl-7.5"> | |
| 472 | − | <SubagentBranch nodes={node.children} base={base} /> | |
| 489 | + | <SubagentBranch nodes={node.children} base={baseOf(base, node.session)} /> | |
| 473 | 490 | </div> | |
| 474 | 491 | )} | |
| 475 | 492 | </li> | |
| ⋯ | |||
| 479 | 496 | } | |
| 480 | 497 | ||
| 481 | 498 | /** | |
| 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 | |
| 483 | 500 | * 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. | |
| 485 | 502 | */ | |
| 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 }) { | |
| 487 | 504 | const [open, setOpen] = useState(Boolean(error)); | |
| 488 | 505 | return ( | |
| 489 | 506 | <Dialog open={open} onOpenChange={setOpen}> | |
| ⋯ | |||
| 496 | 513 | <DialogContent> | |
| 497 | 514 | <Form method="post" className="grid gap-5"> | |
| 498 | 515 | <input type="hidden" name="action" value="start-session" /> | |
| 516 | + | {repo && <input type="hidden" name="repo" value={repo} />} | |
| 499 | 517 | <DialogHeader> | |
| 500 | − | <DialogTitle>Start a session on this issue</DialogTitle> | |
| 518 | + | <DialogTitle>Start a session on this work item</DialogTitle> | |
| 501 | 519 | <DialogDescription> | |
| 502 | 520 | 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. | |
| 504 | 522 | </DialogDescription> | |
| 505 | 523 | </DialogHeader> | |
| 506 | 524 | <FieldSet> | |
| ⋯ | |||
| 755 | 773 | {line.session && ( | |
| 756 | 774 | <> | |
| 757 | 775 | {" · "} | |
| 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"> | |
| 759 | 777 | the session | |
| 760 | 778 | </Link> | |
| 761 | 779 | </> | |
| ⋯ | |||
| 771 | 789 | ); | |
| 772 | 790 | } | |
| 773 | 791 | ||
| 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 }) { | |
| 776 | 794 | return ( | |
| 777 | 795 | <li className="flex items-start gap-2"> | |
| 778 | 796 | <span className="mt-0.5"> | |
| 779 | − | <IssueIcon issue={{ state, reason: null }} size={14} /> | |
| 797 | + | <ItemIcon item={{ state, reason: null }} size={14} /> | |
| 780 | 798 | </span> | |
| 781 | 799 | <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"> | |
| 783 | 801 | {title} <span className="text-faint">#{number}</span> | |
| 784 | 802 | </Link> | |
| 785 | 803 | {detail && <span className="block text-xs text-muted">{detail}</span>} | |
| ⋯ | |||
| 816 | 834 | } | |
| 817 | 835 | ||
| 818 | 836 | /** | |
| 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 | |
| 820 | 838 | * it, what it waits on and what waits on it, and for those who may, the | |
| 821 | 839 | * blockers to change. | |
| 822 | 840 | */ | |
| 823 | − | export function IssueWorkPanel({ | |
| 841 | + | export function ItemWorkPanel({ | |
| 824 | 842 | work, | |
| 825 | 843 | ended, | |
| 826 | 844 | base, | |
| ⋯ | |||
| 833 | 851 | work: WorkContext; | |
| 834 | 852 | /** Claims on it that ended lately, shown muted. */ | |
| 835 | 853 | ended: Claim[]; | |
| 836 | − | base: string; | |
| 854 | + | /** Where each of its sessions' pages is: under the repository it was recorded in. */ | |
| 855 | + | base: SessionBase; | |
| 837 | 856 | now: number; | |
| 838 | 857 | canEdit: boolean; | |
| 839 | 858 | error?: string | null; | |
| ⋯ | |||
| 842 | 861 | /** Starting a session on it, for those who may. */ | |
| 843 | 862 | start?: ReactNode; | |
| 844 | 863 | }) { | |
| 845 | − | const open = work.issue.state === "open"; | |
| 864 | + | const open = work.item.state === "open"; | |
| 846 | 865 | const trees = sessionTree(mergeSessions(work.sessions, all)); | |
| 847 | 866 | const sessions = trees.slice(0, 5); | |
| 848 | 867 | const waiting = work.blockedBy.filter((blocker) => blocker.state === "open").length; | |
| ⋯ | |||
| 859 | 878 | <p className="text-xs text-muted"> | |
| 860 | 879 | {open ? ( | |
| 861 | 880 | <> | |
| 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>. | |
| 863 | 882 | </> | |
| 864 | 883 | ) : ( | |
| 865 | 884 | "Nobody is working on it." | |
| 866 | 885 | )} | |
| 867 | 886 | </p> | |
| 868 | 887 | ) : ( | |
| 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} />) | |
| 870 | 889 | )} | |
| 871 | 890 | {work.claims.length > 1 && ( | |
| 872 | 891 | <Alert tone="warn" className="flex items-start gap-1.5 px-2.5 py-1.5 text-xs"> | |
| ⋯ | |||
| 884 | 903 | <SessionTreeList nodes={sessions} base={base} showBranch={false} /> | |
| 885 | 904 | )} | |
| 886 | 905 | {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> | |
| 890 | 909 | )} | |
| 891 | 910 | </PanelPart> | |
| 892 | 911 | )} | |
| ⋯ | |||
| 898 | 917 | {waiting > 0 && <span className="text-warn"> · {waiting} open</span>} | |
| 899 | 918 | </> | |
| 900 | 919 | } | |
| 901 | − | action={canEdit ? <BlockedByEditor current={work.issue.blockedBy} /> : undefined} | |
| 920 | + | action={canEdit ? <BlockedByEditor current={work.item.blockedBy} /> : undefined} | |
| 902 | 921 | > | |
| 903 | 922 | {work.blockedBy.length === 0 ? ( | |
| 904 | 923 | <p className="text-xs text-faint">Nothing. It can start now.</p> | |
| 905 | 924 | ) : ( | |
| 906 | 925 | <ul className="space-y-2"> | |
| 907 | 926 | {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)} /> | |
| 909 | 928 | ))} | |
| 910 | 929 | </ul> | |
| 911 | 930 | )} | |
| ⋯ | |||
| 915 | 934 | {work.blocks.length > 0 && ( | |
| 916 | 935 | <PanelPart title="Blocks"> | |
| 917 | 936 | <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} /> | |
| 920 | 939 | ))} | |
| 921 | 940 | </ul> | |
| 922 | 941 | </PanelPart> | |
| ⋯ | |||
| 926 | 945 | ); | |
| 927 | 946 | } | |
| 928 | 947 | ||
| 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. */ | |
| 930 | 949 | function BlockedByEditor({ current }: { current: number[] }) { | |
| 931 | 950 | return ( | |
| 932 | 951 | <Popover> | |
| ⋯ | |||
| 941 | 960 | <input type="hidden" name="action" value="blocked-by" /> | |
| 942 | 961 | <Label htmlFor="blocked-by">Blocked by</Label> | |
| 943 | 962 | <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> | |
| 945 | 964 | <SubmitButton size="sm" match={{ action: "blocked-by" }} pending="Saving…"> | |
| 946 | 965 | Save | |
| 947 | 966 | </SubmitButton> | |
| ⋯ | |||
| 951 | 970 | ); | |
| 952 | 971 | } | |
| 953 | 972 | ||
| 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({ | |
| 956 | 975 | group, | |
| 957 | 976 | title, | |
| 958 | 977 | base, | |
| 959 | 978 | now, | |
| 960 | 979 | ender, | |
| 961 | 980 | }: { | |
| 962 | − | group: LiveIssue; | |
| 981 | + | group: LiveItem; | |
| 963 | 982 | title: string | undefined; | |
| 964 | 983 | base: string; | |
| 965 | 984 | now: number; | |
| ⋯ | |||
| 970 | 989 | <li> | |
| 971 | 990 | <div className="flex items-start gap-2.5 px-4 py-3"> | |
| 972 | 991 | <span className="mt-1"> | |
| 973 | − | <IssueIcon issue={{ state: "open", reason: null }} /> | |
| 992 | + | <ItemIcon item={{ state: "open", reason: null }} /> | |
| 974 | 993 | </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> | |
| 977 | 996 | </Link> | |
| 978 | 997 | {group.claims.length > 1 && <Badge tone="warn">{group.claims.length} claims</Badge>} | |
| 979 | 998 | </div> | |
| ⋯ | |||
| 995 | 1014 | /** A pull request a session is on, as Live now names it. */ | |
| 996 | 1015 | export type LivePull = { title: string; status: Pull["status"] }; | |
| 997 | 1016 | ||
| 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. */ | |
| 999 | 1018 | function LiveGroupHeading({ | |
| 1000 | 1019 | group, | |
| 1001 | 1020 | pulls, | |
| ⋯ | |||
| 1017 | 1036 | <Link to={`${base}/pull/${group.pull}`} prefetch="intent" className="min-w-0 grow font-medium hover:text-accent"> | |
| 1018 | 1037 | {pull?.title ?? `Pull request #${group.pull}`} <span className="font-normal text-faint">#{group.pull}</span> | |
| 1019 | 1038 | </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> | |
| 1021 | 1040 | </div> | |
| 1022 | 1041 | ); | |
| 1023 | 1042 | } | |
| ⋯ | |||
| 1028 | 1047 | <Link to={`${base}/tree/${encodeURIComponent(group.branch)}`} prefetch="intent" className="min-w-0 truncate font-mono text-sm font-medium hover:text-accent"> | |
| 1029 | 1048 | {group.branch} | |
| 1030 | 1049 | </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> | |
| 1032 | 1051 | </div> | |
| 1033 | 1052 | ); | |
| 1034 | 1053 | } | |
| ⋯ | |||
| 1037 | 1056 | return ( | |
| 1038 | 1057 | <div className="flex items-center gap-2.5 px-4 py-3"> | |
| 1039 | 1058 | {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."> | |
| 1041 | 1060 | <span className="text-sm text-muted"> | |
| 1042 | 1061 | {onDefault ? ( | |
| 1043 | 1062 | <> | |
| ⋯ | |||
| 1052 | 1071 | ); | |
| 1053 | 1072 | } | |
| 1054 | 1073 | ||
| 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. */ | |
| 1056 | 1075 | export function LiveGroupCard({ | |
| 1057 | 1076 | group, | |
| 1058 | 1077 | pulls, | |
| ⋯ | |||
| 1088 | 1107 | } | |
| 1089 | 1108 | ||
| 1090 | 1109 | /** | |
| 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 | |
| 1094 | 1113 | * shown, for a page that links to the rest. | |
| 1095 | 1114 | */ | |
| 1096 | 1115 | export function LiveNowList({ | |
| ⋯ | |||
| 1107 | 1126 | claims: Claim[]; | |
| 1108 | 1127 | sessions: WorkSession[]; | |
| 1109 | 1128 | 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. */ | |
| 1111 | 1130 | pulls?: Record<number, LivePull>; | |
| 1112 | 1131 | /** Sessions on it have no branch of their own. */ | |
| 1113 | 1132 | defaultBranch?: string | null; | |
| ⋯ | |||
| 1118 | 1137 | ender?: SessionEnder; | |
| 1119 | 1138 | }) { | |
| 1120 | 1139 | 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); | |
| 1123 | 1142 | return ( | |
| 1124 | 1143 | <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} /> | |
| 1127 | 1146 | ))} | |
| 1128 | 1147 | {live.groups.map((group) => ( | |
| 1129 | 1148 | <LiveGroupCard | |
| ⋯ | |||
| 1176 | 1195 | <Card className="flex flex-wrap items-center justify-between gap-4 p-5"> | |
| 1177 | 1196 | <div> | |
| 1178 | 1197 | <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> | |
| 1180 | 1199 | </div> | |
| 1181 | 1200 | <Dialog> | |
| 1182 | 1201 | <DialogTrigger asChild> | |
| ⋯ | |||
| 1188 | 1207 | <DialogDescription>Choose your client, connect it to g1t, then ask it to work on this project.</DialogDescription> | |
| 1189 | 1208 | </DialogHeader> | |
| 1190 | 1209 | <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> | |
| 1192 | 1211 | <a href="https://docs.g1t.sh/guides/bring-your-own-agent/" className="text-sm text-accent hover:underline">Connection guide →</a> | |
| 1193 | 1212 | </DialogContent> | |
| 1194 | 1213 | </Dialog> | |
| 133 | 133 | that your addresses, keys, profile and settings are removed for good. | |
| 134 | 134 | </li> | |
| 135 | 135 | <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{" "} | |
| 137 | 137 | <span className="font-mono text-fg">ghost</span> once it is removed. | |
| 138 | 138 | </li> | |
| 139 | 139 | <li> |
| 1 | 1 | /** | |
| 2 | 2 | * 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 | |
| 4 | 4 | * request, a channel, a project, another page); and inline mentions of | |
| 5 | 5 | * people, agents and pages, dates, and citations of code (a file, folder | |
| 6 | 6 | * or pattern in a repository, and the symbol, endpoint or environment | |
| ⋯ | |||
| 21 | 21 | import { Card } from "../../ui/card"; | |
| 22 | 22 | ||
| 23 | 23 | /** 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 }; | |
| 25 | 25 | ||
| 26 | 26 | const CALLOUTS = { | |
| 27 | 27 | info: { icon: <Info size={16} />, ring: "border-info/30 bg-info/8", text: "text-info" }, | |
| ⋯ | |||
| 178 | 178 | ); | |
| 179 | 179 | ||
| 180 | 180 | const EMBED_ICONS: Record<EmbedCard["kind"], ReactNode> = { | |
| 181 | − | issue: <CircleDot size={16} />, | |
| 181 | + | item: <CircleDot size={16} />, | |
| 182 | 182 | pull: <GitPullRequest size={16} />, | |
| 183 | 183 | channel: <Hash size={16} />, | |
| 184 | 184 | project: <Package size={16} />, | |
| ⋯ | |||
| 241 | 241 | value={draft} | |
| 242 | 242 | onChange={(e) => setDraft(e.target.value)} | |
| 243 | 243 | 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" | |
| 245 | 245 | className="min-w-0 grow bg-transparent px-1.5 text-sm text-fg outline-none placeholder:text-faint" | |
| 246 | 246 | /> | |
| 247 | 247 | <Button type="submit" variant="secondary" size="inline" className="px-2.5 py-1 text-xs"> | |
| 231 | 231 | { 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" } }) }, | |
| 232 | 232 | { 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" }) }, | |
| 233 | 233 | { 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" }) }, | |
| 235 | 235 | { | |
| 236 | 236 | title: "Mention", | |
| 237 | 237 | subtext: "A person", |
| 320 | 320 | const WORK_FILL: Record<WorkKind, string> = { | |
| 321 | 321 | pushes: "bg-series-1", | |
| 322 | 322 | pulls: "bg-series-2", | |
| 323 | − | issues: "bg-series-3", | |
| 323 | + | items: "bg-series-3", | |
| 324 | 324 | shipped: "bg-series-4", | |
| 325 | 325 | docs: "bg-series-5", | |
| 326 | 326 | chat: "bg-series-6", | |
| ⋯ | |||
| 602 | 602 | <div className="min-w-0 grow"> | |
| 603 | 603 | <div className="flex min-w-0 items-center gap-1.5"> | |
| 604 | 604 | {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. | |
| 606 | 606 | <span className="line-clamp-2 text-sm text-fg sm:truncate"> | |
| 607 | 607 | <CommitText text={change.title} base={`/${change.refsIn}`} to={change.to} linkClassName="hover:underline" /> | |
| 608 | 608 | </span> | |
| 233 | 233 | icon: <Code2 size={18} />, | |
| 234 | 234 | name: "Code", | |
| 235 | 235 | 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.", | |
| 237 | 237 | to: `${DOCS}/concepts/overview/`, | |
| 238 | 238 | }, | |
| 239 | 239 | { | |
| ⋯ | |||
| 293 | 293 | const FLOW: [string, string][] = [ | |
| 294 | 294 | [ | |
| 295 | 295 | "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.", | |
| 297 | 297 | ], | |
| 298 | 298 | [ | |
| 299 | 299 | "Change", | |
| ⋯ | |||
| 317 | 317 | const FORGE = [ | |
| 318 | 318 | "Git over HTTPS", | |
| 319 | 319 | "Public and private repositories", | |
| 320 | − | "Issues and labels", | |
| 320 | + | "Work items and labels", | |
| 321 | 321 | "Pull requests and forks", | |
| 322 | 322 | "Line comments and reviews", | |
| 323 | 323 | "Protected branches", | |
| ⋯ | |||
| 605 | 605 | "Sessions recorded onto pull requests", | |
| 606 | 606 | "Why-blame on any line", | |
| 607 | 607 | "Decisions recorded with the reasons behind them", | |
| 608 | − | "Search across code, issues and people", | |
| 608 | + | "Search across code, work items and people", | |
| 609 | 609 | ]} | |
| 610 | 610 | more={["Sessions and why-blame", `${DOCS}/guides/why-blame/`]} | |
| 611 | 611 | > | |
| 612 | 612 | 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 | |
| 614 | 614 | recorded as it worked: what it read, ran and decided. The reasoning | |
| 615 | 615 | stays with the code, for people and for the next agent. | |
| 616 | 616 | </Pillar> | |
| ⋯ | |||
| 646 | 646 | A complete forge for the people who build | |
| 647 | 647 | </h2> | |
| 648 | 648 | <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 | |
| 650 | 650 | already works. Write a change by hand, have your coding agent | |
| 651 | 651 | write it, or both on the same branch. | |
| 652 | 652 | </p> | |
| ⋯ | |||
| 716 | 716 | ||
| 717 | 717 | <div className="mt-10 grid gap-4 lg:grid-cols-2"> | |
| 718 | 718 | <div className="rounded-3xl bg-surface p-7 ring-1 ring-line sm:p-8"> | |
| 719 | − | <Eyebrow>Issues</Eyebrow> | |
| 719 | + | <Eyebrow>Work</Eyebrow> | |
| 720 | 720 | <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 | |
| 722 | 722 | </h3> | |
| 723 | 723 | <p className="mt-3 text-sm leading-6 text-muted"> | |
| 724 | 724 | 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 | |
| 726 | 726 | records its session onto it, and the pull request goes through | |
| 727 | 727 | the same checks and reviews as anyone's. | |
| 728 | 728 | </p> | |
| 729 | 729 | <ol className="mt-6 space-y-3 text-sm"> | |
| 730 | 730 | {[ | |
| 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", | |
| 732 | 732 | "Work it yourself, or hand it to your coding agent", | |
| 733 | 733 | "The pull request runs your checks and lands through the queue", | |
| 734 | 734 | ].map((step, index) => ( | |
| ⋯ | |||
| 750 | 750 | Many agents, no collisions | |
| 751 | 751 | </h3> | |
| 752 | 752 | <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 | |
| 754 | 754 | takes, and every session shows live as it works, so everyone sees | |
| 755 | 755 | who is on what. Two that start changing the same files are told | |
| 756 | 756 | while they work, before either opens a pull request. The merge | |
| ⋯ | |||
| 775 | 775 | </h2> | |
| 776 | 776 | <p className="mt-4 max-w-md leading-7 text-muted"> | |
| 777 | 777 | 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, | |
| 779 | 779 | opens pull requests, and records its session and its decisions | |
| 780 | 780 | onto them. | |
| 781 | 781 | </p> | |
| ⋯ | |||
| 895 | 895 | <div className="mx-auto max-w-6xl px-4 py-24"> | |
| 896 | 896 | <Eyebrow>How it flows</Eyebrow> | |
| 897 | 897 | <h2 className="mt-3 text-3xl font-semibold tracking-tight"> | |
| 898 | − | From an issue to main | |
| 898 | + | From a work item to main | |
| 899 | 899 | </h2> | |
| 900 | 900 | <Live> | |
| 901 | 901 | <ol className="relative mt-12 grid gap-6 md:grid-cols-5"> | |
| ⋯ | |||
| 967 | 967 | </h2> | |
| 968 | 968 | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| 969 | 969 | 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 | |
| 971 | 971 | them. The copy on g1t is then your own. | |
| 972 | 972 | </p> | |
| 973 | 973 | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| ⋯ | |||
| 1059 | 1059 | </p> | |
| 1060 | 1060 | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| 1061 | 1061 | 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 | |
| 1063 | 1063 | early version, and chat and deployments still run only on g1t.sh. | |
| 1064 | 1064 | </p> | |
| 1065 | 1065 | <div className="mt-7 flex flex-wrap gap-3"> | |
| 62 | 62 | */ | |
| 63 | 63 | const trees = new WeightedLru<{ tree: Root; weight: number }>(400_000, (entry) => entry.weight); | |
| 64 | 64 | ||
| 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}`; | |
| 67 | 67 | const kept = trees.get(key); | |
| 68 | 68 | if (kept) return kept.tree; | |
| 69 | 69 | const tree = markdownTree(source, { | |
| 70 | 70 | remarkPlugins: [remarkGfm], | |
| 71 | − | rehypePlugins: [rehypeRaw, [rehypeSanitize, SCHEMA], rehypeAlerts, [rehypeReferences, { repo }]], | |
| 71 | + | rehypePlugins: [rehypeRaw, [rehypeSanitize, SCHEMA], rehypeAlerts, [rehypeReferences, { repo, workspace }]], | |
| 72 | 72 | }); | |
| 73 | 73 | trees.set(key, { tree, weight: markdownWeight(source) }); | |
| 74 | 74 | return tree; | |
| ⋯ | |||
| 182 | 182 | * Renders markdown the way people expect from a forge: GitHub flavoured | |
| 183 | 183 | * markdown (tables, task lists, footnotes, strikethrough, autolinks), the | |
| 184 | 184 | * 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 | |
| 187 | 188 | * content is safe to pass in. | |
| 188 | 189 | */ | |
| 189 | 190 | export function Markdown({ | |
| 190 | 191 | source, | |
| 191 | 192 | repo, | |
| 193 | + | workspace, | |
| 192 | 194 | base, | |
| 193 | 195 | rawBase, | |
| 194 | 196 | }: { | |
| 195 | 197 | source: string; | |
| 196 | 198 | /** The repository the text belongs to, for its references. */ | |
| 197 | 199 | 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; | |
| 198 | 202 | /** Where relative links point, e.g. `/acme/web/blob/main/docs` for a file's own folder. */ | |
| 199 | 203 | base?: string; | |
| 200 | 204 | /** | |
| ⋯ | |||
| 210 | 214 | // components never change: where links and pictures point comes from | |
| 211 | 215 | // context, so a new commit (a README's pictures are that commit's files) | |
| 212 | 216 | // 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]); | |
| 214 | 218 | const rendered = useMemo(() => renderMarkdownTree(tree, COMPONENTS), [tree]); | |
| 215 | 219 | const bases = useMemo(() => ({ base, rawBase }), [base, rawBase]); | |
| 216 | 220 | return ( | |
| 22 | 22 | import { Input } from "./ui/input"; | |
| 23 | 23 | import { Textarea } from "./ui/textarea"; | |
| 24 | 24 | import { PersonLink } from "./work"; | |
| 25 | + | import { itemPath } from "../lib/refs"; | |
| 25 | 26 | ||
| 26 | 27 | /** The trigger half of a split button: the chevron that opens its menu. */ | |
| 27 | 28 | function SplitTrigger({ label, variant, disabled }: { label: string; variant: "accent" | "outline"; disabled?: boolean }) { | |
| ⋯ | |||
| 100 | 101 | blocked, | |
| 101 | 102 | queue, | |
| 102 | 103 | canAutoMerge, | |
| 103 | − | issue, | |
| 104 | + | item, | |
| 104 | 105 | options, | |
| 105 | 106 | error, | |
| 106 | 107 | }: { | |
| ⋯ | |||
| 115 | 116 | queue: boolean; | |
| 116 | 117 | /** The viewer may turn auto-merge on: they may merge it. */ | |
| 117 | 118 | 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; | |
| 120 | 121 | options?: ReactNode; | |
| 121 | 122 | error?: string | null; | |
| 122 | 123 | }) { | |
| ⋯ | |||
| 167 | 168 | pull={pull} | |
| 168 | 169 | base={base} | |
| 169 | 170 | repo={repo} | |
| 170 | − | issue={issue} | |
| 171 | + | item={item} | |
| 171 | 172 | blocked={confirming === "merge" ? blocked : null} | |
| 172 | 173 | options={confirming === "merge" ? options : null} | |
| 173 | 174 | error={error} | |
| ⋯ | |||
| 257 | 258 | ); | |
| 258 | 259 | } | |
| 259 | 260 | ||
| 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. */ | |
| 261 | 262 | function ConfirmMerge({ | |
| 262 | 263 | mode, | |
| 263 | 264 | method: initial, | |
| ⋯ | |||
| 265 | 266 | pull, | |
| 266 | 267 | base, | |
| 267 | 268 | repo, | |
| 268 | − | issue, | |
| 269 | + | item, | |
| 269 | 270 | blocked, | |
| 270 | 271 | options, | |
| 271 | 272 | error, | |
| ⋯ | |||
| 277 | 278 | pull: Pull; | |
| 278 | 279 | base: string; | |
| 279 | 280 | repo: string; | |
| 280 | − | issue: { number: number } | null; | |
| 281 | + | item: { number: number } | null; | |
| 281 | 282 | blocked: string | null; | |
| 282 | 283 | options?: ReactNode; | |
| 283 | 284 | error?: string | null; | |
| ⋯ | |||
| 287 | 288 | const [method, setMethod] = useState(initial); | |
| 288 | 289 | const message = choices.messages[method]; | |
| 289 | 290 | 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 ?? []); | |
| 291 | 292 | const deletable = choices.canDeleteBranch ?? (pull.branch != null && !pull.fork); | |
| 292 | 293 | return ( | |
| 293 | 294 | <div className="space-y-4"> | |
| ⋯ | |||
| 354 | 355 | /> | |
| 355 | 356 | </> | |
| 356 | 357 | )} | |
| 357 | − | {mode === "merge" && issue && ( | |
| 358 | + | {mode === "merge" && item && ( | |
| 358 | 359 | <CheckboxOption | |
| 359 | − | name="keepIssueOpen" | |
| 360 | + | name="keepItemOpen" | |
| 360 | 361 | checked={keepOpen} | |
| 361 | 362 | 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.`} | |
| 363 | 364 | labelClassName="text-xs text-muted" | |
| 364 | 365 | /> | |
| 365 | 366 | )} | |
| ⋯ | |||
| 370 | 371 | <CircleDot size={13} className="text-success" /> | |
| 371 | 372 | {mode === "auto" ? "Once merged, closes" : "Closes"} | |
| 372 | 373 | {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"> | |
| 374 | 375 | #{number} | |
| 375 | 376 | </Link> | |
| 376 | 377 | ))} | |
| 555 | 555 | {run && ( | |
| 556 | 556 | <section aria-label={queueFailed ? "Merge queue" : "Earlier commands"} className="py-1"> | |
| 557 | 557 | <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> | |
| 559 | 559 | <span>· on</span> | |
| 560 | 560 | <Link to={commitPath(run.headCommit)} className="inline-flex items-center gap-1 font-mono hover:text-fg"> | |
| 561 | 561 | <GitCommitHorizontal size={12} /> |
| 1 | 1 | /** | |
| 2 | 2 | * The merge queue as a train: the default branch, then each entry in | |
| 3 | 3 | * 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. | |
| 5 | 5 | */ | |
| 6 | 6 | import { Check, CircleDashed, GitCommitHorizontal, GitMerge, Loader2, Minus, X } from "lucide-react"; | |
| 7 | 7 | import { type ReactNode, Suspense } from "react"; |
| 122 | 122 | disabled?: boolean; | |
| 123 | 123 | }) { | |
| 124 | 124 | 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}"); | |
| 126 | 126 | const box = useRef<HTMLTextAreaElement>(null); | |
| 127 | 127 | const name = method === "merge" ? "mergeCommit" : "squashCommit"; | |
| 128 | 128 | const preview = defaultMessage(method, choice, template, SAMPLE_PULL); |
| 138 | 138 | <AlertDialogDescription> | |
| 139 | 139 | {items === 0 | |
| 140 | 140 | ? "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. `} | |
| 142 | 142 | This cannot be undone. | |
| 143 | 143 | </AlertDialogDescription> | |
| 144 | 144 | </AlertDialogHeader> |
| 35 | 35 | import { Hint } from "./ui/hint"; | |
| 36 | 36 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu"; | |
| 37 | 37 | import type { Loaded } from "../lib/mission-control.server"; | |
| 38 | + | import { itemPath, newItemPath } from "../lib/work-items"; | |
| 38 | 39 | ||
| 39 | 40 | /* | |
| 40 | 41 | * Code's Overview: where the viewer is needed in the workspace's code, | |
| ⋯ | |||
| 273 | 274 | )} | |
| 274 | 275 | {row.open !== row.to && ( | |
| 275 | 276 | <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"} | |
| 277 | 278 | </Link> | |
| 278 | 279 | )} | |
| 279 | 280 | </div> | |
| ⋯ | |||
| 469 | 470 | ||
| 470 | 471 | const VERB: Record<Verb, string> = { | |
| 471 | 472 | landed: "landed", | |
| 472 | − | opened_issue: "opened", | |
| 473 | − | closed_issue: "closed", | |
| 473 | + | opened_item: "opened", | |
| 474 | + | closed_item: "closed", | |
| 474 | 475 | started: "started on", | |
| 475 | 476 | ready: "marked ready", | |
| 476 | 477 | checks_passed: "checks passed on", | |
| ⋯ | |||
| 502 | 503 | )}{" "} | |
| 503 | 504 | {VERB[part.verb]}{" "} | |
| 504 | 505 | {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"> | |
| 506 | 507 | <span className="font-mono text-xs">#{number}</span> | |
| 507 | 508 | {title && <> {title}</>} | |
| 508 | 509 | </Link> | |
| ⋯ | |||
| 601 | 602 | const delta = change(week.total, week.previous); | |
| 602 | 603 | const feed = everyActivity ? groups : groups.slice(0, 8); | |
| 603 | 604 | ||
| 604 | − | const newIssue = | |
| 605 | + | const newItem = | |
| 605 | 606 | repos.length > 0 ? ( | |
| 606 | 607 | <DropdownMenu> | |
| 607 | 608 | <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 | |
| 609 | 610 | </DropdownMenuTrigger> | |
| 610 | 611 | <DropdownMenuContent align="start" className="max-h-80 overflow-y-auto"> | |
| 611 | − | <DropdownMenuLabel>In which project?</DropdownMenuLabel> | |
| 612 | + | <DropdownMenuLabel>Filed from which project?</DropdownMenuLabel> | |
| 612 | 613 | {repos.map((repo) => ( | |
| 613 | 614 | <DropdownMenuItem key={repo.name} asChild> | |
| 614 | − | <Link to={`/${repo.namespace}/${repo.name}/issues/new`}> | |
| 615 | + | <Link to={newItemPath(repo.namespace, repo.name)}> | |
| 615 | 616 | <Avatar name={repo.name} size={18} square /> | |
| 616 | 617 | {repo.name} | |
| 617 | 618 | </Link> | |
| ⋯ | |||
| 640 | 641 | </header> | |
| 641 | 642 | ||
| 642 | 643 | <section aria-label="Start work" className="flex flex-wrap items-center gap-2"> | |
| 643 | − | {newIssue} | |
| 644 | + | {newItem} | |
| 644 | 645 | {needs.length > 0 && ( | |
| 645 | 646 | <Link | |
| 646 | 647 | to={`${link({ tab: "needs", all: null })}#work`} | |
| 29 | 29 | import { NavTab, NavTabs } from "./ui/nav-tabs"; | |
| 30 | 30 | import { StatusDialog } from "./presence"; | |
| 31 | 31 | 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"; | |
| 33 | 33 | import { setPresence, useOwnPresence } from "../lib/notify-client"; | |
| 34 | 34 | import { dndOn, liveStatus, pauseUntil, untilLabel } from "../lib/presence"; | |
| 35 | 35 | import { STATUS_URL } from "../lib/status"; | |
| 36 | 36 | import { type ModeKey, modeHome, modeOf } from "../lib/workspace-nav"; | |
| 37 | 37 | import { PROJECT_PAGE_LINKS, type ProjectPage, projectPageAt, projectPages } from "../lib/chrome"; | |
| 38 | 38 | import { ROADMAP, type RoadmapItem } from "../lib/roadmap"; | |
| 39 | + | import { repoWorkPath } from "../lib/work-items"; | |
| 39 | 40 | ||
| 40 | 41 | /** | |
| 41 | 42 | * The phone's layout (below 768px): a bottom bar of apps with More for | |
| ⋯ | |||
| 121 | 122 | ||
| 122 | 123 | /** | |
| 123 | 124 | * 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 | |
| 127 | 129 | * mark: its icon on the sidebar's accent. It steps aside while the keyboard is up | |
| 128 | 130 | * 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). | |
| 130 | 132 | */ | |
| 131 | 133 | export function BottomBar({ | |
| 132 | 134 | user, | |
| ⋯ | |||
| 150 | 152 | const [more, setMore] = useState(false); | |
| 151 | 153 | useEffect(() => setMore(false), [pathname]); | |
| 152 | 154 | const slug = workspace.slug; | |
| 153 | − | const code = hasCodeAccess(workspace); | |
| 154 | 155 | 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] })); | |
| 161 | 161 | const inTabs = tabs.some((tab) => tab.key === mode); | |
| 162 | 162 | if (isConversation(pathname)) return null; | |
| 163 | 163 | return ( | |
| ⋯ | |||
| 259 | 259 | ||
| 260 | 260 | /** | |
| 261 | 261 | * 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 | |
| 264 | 265 | * help", which opens your account's sheet. Browsing beyond your | |
| 265 | 266 | * workspaces, Explore opens Explore's menu (`onExplore`) rather than the | |
| 266 | 267 | * page. | |
| ⋯ | |||
| 283 | 284 | const slug = workspace.slug; | |
| 284 | 285 | const [account, setAccount] = useState(false); | |
| 285 | 286 | const shown = pins.map((key) => pinnedApp(key, slug)).filter((app) => app != null); | |
| 287 | + | const apps = phoneMoreApps(hasCodeAccess(workspace)); | |
| 286 | 288 | return ( | |
| 287 | 289 | <> | |
| 288 | 290 | <Primitive.Root open={open} onOpenChange={onOpenChange}> | |
| ⋯ | |||
| 300 | 302 | <span className="ml-auto truncate font-mono text-xs text-faint">{slug}</span> | |
| 301 | 303 | </div> | |
| 302 | 304 | <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" />} | |
| 303 | 307 | <MoreTile to={`/${slug}/-/apps`} icon={<LayoutGrid size={20} />} label="All apps" /> | |
| 304 | 308 | {shown.map((app) => ( | |
| 305 | 309 | <MoreTile key={app.key} to={app.to} icon={app.icon(26)} label={app.name} /> | |
| 306 | 310 | ))} | |
| 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 | + | ))} | |
| 311 | 316 | {onExplore ? ( | |
| 312 | 317 | <MoreTile | |
| 313 | 318 | icon={<Compass size={20} />} | |
| ⋯ | |||
| 540 | 545 | /** Which project page a roadmap page (`soon/<key>`) sits under, by its section. */ | |
| 541 | 546 | const SOON_UNDER: Partial<Record<RoadmapItem["section"], ProjectPage>> = { | |
| 542 | 547 | Code: "code", | |
| 543 | − | Issues: "issues", | |
| 544 | 548 | Deployments: "deployments", | |
| 545 | 549 | Observability: "observability", | |
| 546 | 550 | Security: "security", | |
| ⋯ | |||
| 557 | 561 | /** | |
| 558 | 562 | * A project's pages along the bottom of its header: the sidebar's own list | |
| 559 | 563 | * (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 | |
| 562 | 567 | * them; a visitor, who has none, always sees the row. | |
| 563 | 568 | */ | |
| 564 | 569 | export function ProjectStrip({ | |
| ⋯ | |||
| 568 | 573 | settings, | |
| 569 | 574 | counts, | |
| 570 | 575 | always = false, | |
| 571 | − | issues = true, | |
| 572 | 576 | }: { | |
| 573 | 577 | /** `/<namespace>/<name>`. */ | |
| 574 | 578 | base: string; | |
| ⋯ | |||
| 576 | 580 | can?: Partial<Abilities>; | |
| 577 | 581 | /** Whether they see its settings (lib/access.ts `seesSettings`), as the sidebar decides. */ | |
| 578 | 582 | settings?: boolean; | |
| 579 | − | counts?: { issues: number; pulls: number }; | |
| 583 | + | counts?: { items: number; pulls: number }; | |
| 580 | 584 | /** On a computer too: for a visitor, who has no sidebar to list them. */ | |
| 581 | 585 | always?: boolean; | |
| 582 | − | /** Whether issues can be opened in it; Issues leaves the row when not. */ | |
| 583 | − | issues?: boolean; | |
| 584 | 586 | }) { | |
| 585 | 587 | const { pathname } = useLocation(); | |
| 586 | 588 | const rest = pathname.toLowerCase().startsWith(base.toLowerCase()) ? pathname.slice(base.length) : ""; | |
| 587 | 589 | 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"), | |
| 589 | 591 | ...(settings ?? member ? (["settings"] as const) : []), | |
| 590 | 592 | ]; | |
| 591 | 593 | return ( | |
| 592 | 594 | <NavTabs label="Project" className={always ? undefined : "md:hidden"}> | |
| 593 | 595 | {pages.map((page) => { | |
| 594 | 596 | 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; | |
| 595 | 599 | return ( | |
| 596 | 600 | <NavTab | |
| 597 | 601 | key={page} | |
| 598 | − | to={link.path ? `${base}/${link.path}` : base} | |
| 602 | + | to={to} | |
| 599 | 603 | current={at === page} | |
| 600 | 604 | 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} | |
| 602 | 606 | > | |
| 603 | 607 | {link.label} | |
| 604 | 608 | </NavTab> | |
| 1 | 1 | /** | |
| 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 | |
| 3 | 3 | * its sidebar, and watching a repository, from its header. Both post to | |
| 4 | 4 | * the repository's `notifications` route (routes/repo/notifications.ts); | |
| 5 | 5 | * the events service keeps the choice and the inbox follows it. | |
| ⋯ | |||
| 16 | 16 | import { WATCH_CHOICES, WATCH_EVENT_LABEL, subscriptionLine, watchLabel } from "../lib/inbox"; | |
| 17 | 17 | ||
| 18 | 18 | /** | |
| 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 | |
| 20 | 20 | * to subscribe or unsubscribe (or stop ignoring it), and a line saying | |
| 21 | 21 | * whether you hear of it, and why. While the choice is on its way, it | |
| 22 | 22 | * shows as made. | |
| ⋯ | |||
| 27 | 27 | kind, | |
| 28 | 28 | subscription, | |
| 29 | 29 | }: { | |
| 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. */ | |
| 31 | 31 | action: string; | |
| 32 | 32 | number: number; | |
| 33 | − | kind: "issue" | "pull"; | |
| 33 | + | kind: "item" | "pull"; | |
| 34 | 34 | subscription: ThreadSubscription | null; | |
| 35 | 35 | }) { | |
| 36 | 36 | const fetcher = useFetcher<{ error?: string }>(); | |
| 112 | 112 | function ToastCard({ notification, paused }: { notification: FeedNotification; paused: boolean }) { | |
| 113 | 113 | const kind = KIND[notification.kind] ?? { label: "Notifications", icon: <Bell /> }; | |
| 114 | 114 | 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. | |
| 116 | 116 | const card = notificationActions(notification).length > 0; | |
| 117 | 117 | const open = () => { | |
| 118 | 118 | dismiss(notification.id); |
| 67 | 67 | const ABOUT: Record<OnboardingStep, { icon: ReactNode; lead: string }> = { | |
| 68 | 68 | ai: { | |
| 69 | 69 | 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.", | |
| 71 | 71 | }, | |
| 72 | 72 | ask: { | |
| 73 | 73 | icon: <Bot size={16} />, | |
| ⋯ | |||
| 75 | 75 | }, | |
| 76 | 76 | code: { | |
| 77 | 77 | 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.", | |
| 79 | 79 | }, | |
| 80 | 80 | harness: { | |
| 81 | 81 | icon: <Laptop size={16} />, | |
| 36 | 36 | CheckCheck, | |
| 37 | 37 | ChevronDown, | |
| 38 | 38 | ChevronLeft, | |
| 39 | − | CircleDot, | |
| 40 | 39 | CircleHelp, | |
| 40 | + | Ellipsis, | |
| 41 | 41 | Code2, | |
| 42 | 42 | Compass, | |
| 43 | 43 | FileDiff, | |
| ⋯ | |||
| 47 | 47 | GitPullRequest, | |
| 48 | 48 | Hash, | |
| 49 | 49 | House, | |
| 50 | + | ListTodo, | |
| 50 | 51 | Loader, | |
| 51 | 52 | MessageSquare, | |
| 52 | 53 | MessagesSquare, | |
| ⋯ | |||
| 273 | 274 | /** The rail's modes, in the app's order (components/rail.tsx), with its icons. */ | |
| 274 | 275 | const RAIL: { key: string; icon: ReactNode; label: string; target?: CursorTarget; scenes?: Scene[]; badge?: number }[] = [ | |
| 275 | 276 | { key: "home", icon: <House size={15} />, label: "Home", scenes: ["home"] }, | |
| 277 | + | { key: "work", icon: <ListTodo size={15} />, label: "Work" }, | |
| 276 | 278 | { key: "explore", icon: <Compass size={15} />, label: "Explore" }, | |
| 277 | 279 | { key: "chat", icon: <MessagesSquare size={15} />, label: "Chat", target: "rail-chat", scenes: ["chat"], badge: 3 }, | |
| 278 | 280 | { key: "code", icon: <Code2 size={15} />, label: "Code", target: "rail-code", scenes: ["code", "session"] }, | |
| ⋯ | |||
| 556 | 558 | </div> | |
| 557 | 559 | <div className="mx-2 my-2 h-px bg-line" /> | |
| 558 | 560 | {row(<Code2 size={13} />, "Code")} | |
| 559 | − | {row(<CircleDot size={13} />, "Issues", "12")} | |
| 561 | + | {row(<ListTodo size={13} />, "Work", "12")} | |
| 560 | 562 | {row(<GitPullRequest size={13} />, "Pull requests", "3", true)} | |
| 561 | 563 | {row(<PlayCircle size={13} />, "Workflows")} | |
| 562 | 564 | <div className="mx-2 my-2 h-px bg-line" /> | |
| ⋯ | |||
| 962 | 964 | const TOUR_FILL: Record<WorkKind, string> = { | |
| 963 | 965 | pushes: "bg-series-1", | |
| 964 | 966 | pulls: "bg-series-2", | |
| 965 | − | issues: "bg-series-3", | |
| 967 | + | items: "bg-series-3", | |
| 966 | 968 | shipped: "bg-series-4", | |
| 967 | 969 | docs: "bg-series-5", | |
| 968 | 970 | chat: "bg-series-6", | |
| ⋯ | |||
| 975 | 977 | */ | |
| 976 | 978 | function PeopleCardMock() { | |
| 977 | 979 | 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 } }, | |
| 980 | 982 | { name: "claude", agent: true, parts: { pushes: 6, pulls: 2 } }, | |
| 981 | 983 | { name: "g1t", agent: true, parts: { shipped: 3, chat: 4 } }, | |
| 982 | 984 | ]; | |
| 983 | 985 | const legend: [WorkKind, string][] = [ | |
| 984 | 986 | ["pushes", "23 commits to 2 projects"], | |
| 985 | 987 | ["pulls", "4 opened, 3 merged"], | |
| 986 | − | ["issues", "3 opened, 2 closed"], | |
| 988 | + | ["items", "3 opened, 2 closed"], | |
| 987 | 989 | ["shipped", "3 deploys"], | |
| 988 | 990 | ["docs", "2 docs edited"], | |
| 989 | 991 | ["chat", "19 messages in 4 conversations"], | |
| ⋯ | |||
| 1268 | 1270 | /** The phone's tab bar, as the app has it (components/mobile.tsx). */ | |
| 1269 | 1271 | const TABS: { scenes: Scene[]; icon: ReactNode; label: string }[] = [ | |
| 1270 | 1272 | { scenes: ["home"], icon: <House size={16} />, label: "Home" }, | |
| 1273 | + | { scenes: [], icon: <ListTodo size={16} />, label: "Work" }, | |
| 1271 | 1274 | { scenes: ["chat"], icon: <MessagesSquare size={16} />, label: "Chat" }, | |
| 1272 | 1275 | { 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" }, | |
| 1274 | 1278 | ]; | |
| 1275 | 1279 | ||
| 1276 | 1280 | function Phone({ frame }: { frame: Frame }) { | |
| ⋯ | |||
| 1303 | 1307 | </div> | |
| 1304 | 1308 | {/* Inside a conversation the tab bar steps aside, as it does on a phone. */} | |
| 1305 | 1309 | {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]"> | |
| 1307 | 1311 | {TABS.map((tab) => { | |
| 1308 | 1312 | const on = tab.scenes.includes(frame.scene); | |
| 1309 | 1313 | return ( | |
| 89 | 89 | {archived ? ( | |
| 90 | 90 | <> | |
| 91 | 91 | <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> | |
| 93 | 93 | <li>Workflows run again.</li> | |
| 94 | 94 | </> | |
| 95 | 95 | ) : ( | |
| 96 | 96 | <> | |
| 97 | 97 | <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> | |
| 99 | 99 | <li>Workflows do not run.</li> | |
| 100 | 100 | <li>Its deployments keep serving, and anyone who could see it still can.</li> | |
| 101 | 101 | </> | |
| ⋯ | |||
| 191 | 191 | <AlertDialogHeader> | |
| 192 | 192 | <AlertDialogTitle>Transfer {repo}</AlertDialogTitle> | |
| 193 | 193 | <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, | |
| 195 | 195 | deployments, and its own secrets, variables and webhooks. | |
| 196 | 196 | </AlertDialogDescription> | |
| 197 | 197 | </AlertDialogHeader> | |
| 46 | 46 | const PUBLIC_COMMANDS: PaletteCommand[] = [ | |
| 47 | 47 | { label: "Features", to: "/features", icon: <BookOpen size={15} /> }, | |
| 48 | 48 | { 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} /> }, | |
| 50 | 50 | { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> }, | |
| 51 | 51 | { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> }, | |
| 52 | 52 | { label: "Sign in", to: "/login", icon: <LogIn size={15} /> }, |
| 200 | 200 | /** | |
| 201 | 201 | * The rail down the window's left edge: a narrow column of icons, each | |
| 202 | 202 | * 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 | |
| 204 | 204 | * unread on them (Notifications is the bell in the top bar, not a rail | |
| 205 | 205 | * app); Explore, lit anywhere beyond your workspaces; the apps you | |
| 206 | 206 | * pinned; the Apps launcher; | |
| ⋯ | |||
| 232 | 232 | const { pins, toggle } = useAppPins(slug, saved); | |
| 233 | 233 | const builtins: Item[] = [ | |
| 234 | 234 | { key: "home", label: "Home" }, | |
| 235 | + | // Every member's, Code access or not. | |
| 236 | + | { key: "work", label: "Work" }, | |
| 235 | 237 | { | |
| 236 | 238 | key: "chat", | |
| 237 | 239 | label: "Chat", | |
| ⋯ | |||
| 270 | 272 | current={here === item.key && !shown.some((app) => at(app.to))} | |
| 271 | 273 | badge={item.badge} | |
| 272 | 274 | /> | |
| 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" && ( | |
| 275 | 277 | <RailLink to="/explore" label="Explore" icon={<Compass size={20} />} current={here === "browse" || at("/explore")} /> | |
| 276 | 278 | )} | |
| 277 | 279 | </Fragment> | |
| 34 | 34 | } | |
| 35 | 35 | ||
| 36 | 36 | /** | |
| 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 | |
| 38 | 38 | * (no `commentId`) or to one of its comments: a pill per emoji with how | |
| 39 | 39 | * many, lit when the viewer is one of them, and who when hovered. Signed | |
| 40 | 40 | * in, a pill adds or takes back the viewer's own and the smiley offers the |
| 127 | 127 | <span className="text-muted"> | |
| 128 | 128 | {settings | |
| 129 | 129 | ? "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."} | |
| 131 | 131 | </span> | |
| 132 | 132 | </span> | |
| 133 | 133 | </p> |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | 30 | function stateIcon(hit: SiteHit): ReactNode { | |
| 31 | − | if (hit.kind === "issue") { | |
| 31 | + | if (hit.kind === "item") { | |
| 32 | 32 | return hit.state === "open" ? ( | |
| 33 | 33 | <CircleDot size={15} className="text-success" aria-label="Open" /> | |
| 34 | 34 | ) : ( | |
| ⋯ | |||
| 54 | 54 | return hit.private ? <Lock size={size} /> : <BookMarked size={size} />; | |
| 55 | 55 | case "code": | |
| 56 | 56 | return <FileCode2 size={size} />; | |
| 57 | − | case "issue": | |
| 57 | + | case "item": | |
| 58 | 58 | return hit.state === "open" ? <CircleDot size={size} /> : <CircleSlash size={size} />; | |
| 59 | 59 | case "pull": | |
| 60 | 60 | return <GitPullRequest size={size} />; | |
| ⋯ | |||
| 214 | 214 | return <Repository hit={hit} />; | |
| 215 | 215 | case "code": | |
| 216 | 216 | return <Code hit={hit} />; | |
| 217 | − | case "issue": | |
| 217 | + | case "item": | |
| 218 | 218 | case "pull": | |
| 219 | 219 | return <Item hit={hit} />; | |
| 220 | 220 | case "user": | |
| 31 | 31 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| 32 | 32 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"; | |
| 33 | 33 | import { wholeDollars } from "../lib/money"; | |
| 34 | + | import { itemPath } from "../lib/refs"; | |
| 34 | 35 | ||
| 35 | 36 | type Done = { ok: boolean; error?: string } | undefined; | |
| 36 | 37 | ||
| ⋯ | |||
| 148 | 149 | {alert.ruleName && alert.ruleName !== alert.ruleId ? alert.ruleName : shortRule(alert.ruleId)} | |
| 149 | 150 | </Link> | |
| 150 | 151 | <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)}> | |
| 153 | 154 | <Badge tone="accent"> | |
| 154 | − | <Bot size={11} /> g1t on #{alert.issue} | |
| 155 | + | <Bot size={11} /> g1t on #{alert.item} | |
| 155 | 156 | </Badge> | |
| 156 | 157 | </Link> | |
| 157 | 158 | )} | |
| 25 | 25 | import { type ReactNode, useEffect, useRef, useState } from "react"; | |
| 26 | 26 | import { Link, useFetcher } from "react-router"; | |
| 27 | 27 | ||
| 28 | + | import { itemPath } from "../lib/refs"; | |
| 29 | + | ||
| 28 | 30 | import { | |
| 29 | 31 | type AlertActivity, | |
| 30 | 32 | type AlertState, | |
| ⋯ | |||
| 272 | 274 | <span>{entry.text}</span> | |
| 273 | 275 | {entry.ref && ( | |
| 274 | 276 | <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)} | |
| 276 | 278 | className="font-medium text-fg-soft hover:text-fg hover:underline" | |
| 277 | 279 | > | |
| 278 | 280 | #{entry.ref.number} | |
| ⋯ | |||
| 504 | 506 | ); | |
| 505 | 507 | } | |
| 506 | 508 | ||
| 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. */ | |
| 508 | 510 | export type UpgradeFix = { | |
| 509 | 511 | number: number; | |
| 510 | 512 | state: "open" | "closed"; | |
| 511 | − | /** The newest pull request for the issue, if any. */ | |
| 513 | + | /** The newest pull request for the work item, if any. */ | |
| 512 | 514 | pull: { number: number; status: PullStatus; agent: string | null } | null; | |
| 513 | 515 | resolvedBy: number | null; | |
| 514 | 516 | }; | |
| ⋯ | |||
| 516 | 518 | /** A security update's pull request as the page loads it. */ | |
| 517 | 519 | export type PullInfo = { number: number; status: PullStatus; title: string }; | |
| 518 | 520 | ||
| 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 }) { | |
| 520 | 522 | const pull = fix?.pull; | |
| 521 | 523 | return ( | |
| 522 | 524 | <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} | |
| 526 | 528 | </Link> | |
| 527 | 529 | {pull && ( | |
| 528 | 530 | <Link to={`${base}/pull/${pull.number}`} className="inline-flex items-center gap-1 text-muted hover:text-fg"> | |
| ⋯ | |||
| 557 | 559 | superseded: <>A newer update replaced it, or the package is no longer vulnerable, so g1t closed it.</>, | |
| 558 | 560 | needs_code: ( | |
| 559 | 561 | <> | |
| 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 && ( | |
| 562 | 564 | <> | |
| 563 | 565 | :{" "} | |
| 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} | |
| 566 | 568 | </Link> | |
| 567 | 569 | </> | |
| 568 | 570 | )} | |
| ⋯ | |||
| 688 | 690 | const manifests = [...new Set(group.vulns.map((vuln) => vuln.manifest))]; | |
| 689 | 691 | const target = highestFix(group.vulns); | |
| 690 | 692 | 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); | |
| 692 | 694 | return ( | |
| 693 | 695 | <li className="px-4 py-3"> | |
| 694 | 696 | <div className="flex items-start gap-3"> | |
| ⋯ | |||
| 707 | 709 | </div> | |
| 708 | 710 | </div> | |
| 709 | 711 | {update && <UpdateStatus update={update} name={group.name} pulls={pulls} base={base} />} | |
| 710 | − | {issue != null && ( | |
| 712 | + | {item != null && ( | |
| 711 | 713 | <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} /> | |
| 713 | 715 | </div> | |
| 714 | 716 | )} | |
| 715 | 717 | <Card asChild tone="plain" radius="lg" className="mt-2.5 space-y-px overflow-hidden sm:ml-7"> | |
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…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Comments
Sign in to comment.