| 69 | 69 | | sessionTree, |
| 70 | 70 | | titleHint, |
| 71 | 71 | | titleIsProvisional, |
| 72 | + | type PullHref, |
| 72 | 73 | | } from "../lib/coordination"; |
| 73 | | − | import { sessionPath } from "../lib/work-items"; |
| 74 | + | import { sessionPath, sessionsPath } from "../lib/work-items"; |
| 74 | 75 | | import { AgentSetup } from "./agent-setup"; |
| 75 | 76 | | import { Input, SubmitButton, TimeAgo } from "./ui"; |
| 76 | 77 | | import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; |
| ⋯ |
| 119 | 120 | | } |
| 120 | 121 | | |
| 121 | 122 | | /** Text with each `#N` linked to what it names, read in the repository at `base` (lib/refs.ts). */ |
| 122 | | − | export function NumberedText({ text, base }: { text: string; base: string }) { |
| 123 | + | export function NumberedText({ text, base, pullHref }: { text: string; base: string; pullHref?: PullHref }) { |
| 123 | 124 | | return ( |
| 124 | 125 | | <> |
| 125 | 126 | | {numberedParts(text).map((part, index) => |
| 126 | 127 | | typeof part === "number" ? ( |
| 127 | | − | <Link key={index} to={`${base}/ref/${part}`} className="font-medium text-fg hover:text-accent"> |
| 128 | + | <NumberLink key={index} to={pullHref ? pullHref(part) : `${base}/ref/${part}`} className="font-medium text-fg hover:text-accent"> |
| 128 | 129 | | #{part} |
| 129 | | − | </Link> |
| 130 | + | </NumberLink> |
| 130 | 131 | | ) : ( |
| 131 | 132 | | <Fragment key={index}>{part}</Fragment> |
| 132 | 133 | | ), |
| ⋯ |
| 571 | 572 | | note: <MessageSquare size={14} />, |
| 572 | 573 | | }; |
| 573 | 574 | | |
| 575 | + | /** A pull request's number, linked where it is known; plain text where it is not (`pullHref` said null). */ |
| 576 | + | function NumberLink({ to, className, children }: { to: string | null; className?: string; children: ReactNode }) { |
| 577 | + | if (!to) return <span className={className}>{children}</span>; |
| 578 | + | return ( |
| 579 | + | <Link to={to} className={className}> |
| 580 | + | {children} |
| 581 | + | </Link> |
| 582 | + | ); |
| 583 | + | } |
| 584 | + | |
| 585 | + | /** Where pull request `number` is: `pullHref`'s answer when given (a work item's, across its repositories), else under `base`. */ |
| 586 | + | function pullAt(base: string, pullHref: PullHref | undefined, number: number): string | null { |
| 587 | + | return pullHref ? pullHref(number) : `${base}/pull/${number}`; |
| 588 | + | } |
| 589 | + | |
| 574 | 590 | | /** The pull requests tested together, in queue order: `#41 + #44`. */ |
| 575 | | − | export function Combination({ numbers, base, culprit }: { numbers: number[]; base: string; culprit?: number | null }) { |
| 591 | + | export function Combination({ numbers, base, culprit, pullHref }: { numbers: number[]; base: string; culprit?: number | null; pullHref?: PullHref }) { |
| 576 | 592 | | return ( |
| 577 | 593 | | <span className="inline-flex flex-wrap items-center gap-1"> |
| 578 | 594 | | {numbers.map((number, index) => ( |
| 579 | 595 | | <Fragment key={number}> |
| 580 | 596 | | {index > 0 && <span className="text-faint">+</span>} |
| 581 | | − | <Link to={`${base}/pull/${number}`}> |
| 597 | + | <NumberLink to={pullAt(base, pullHref, number)}> |
| 582 | 598 | | <Badge shape="tag" tone={number === culprit ? "danger" : "neutral"} className="font-mono hover:border-line-strong"> |
| 583 | 599 | | #{number} |
| 584 | 600 | | </Badge> |
| 585 | | − | </Link> |
| 601 | + | </NumberLink> |
| 586 | 602 | | </Fragment> |
| 587 | 603 | | ))} |
| 588 | 604 | | </span> |
| ⋯ |
| 610 | 626 | | } |
| 611 | 627 | | |
| 612 | 628 | | /** One decision record: its kind, how sure it is, the sentence, who and when, and what it rests on. */ |
| 613 | | − | export function DecisionLine({ decision, base }: { decision: Decision; base: string }) { |
| 629 | + | export function DecisionLine({ decision, base, pullHref }: { decision: Decision; base: string; pullHref?: PullHref }) { |
| 614 | 630 | | const evidence = evidenceOf(decision); |
| 615 | 631 | | return ( |
| 616 | 632 | | <div className="flex items-start gap-2.5"> |
| ⋯ |
| 620 | 636 | | <span className="font-medium text-fg">{DECISION_KINDS[decision.kind]}</span> |
| 621 | 637 | | <ConfidenceBadge confidence={decision.confidence} /> |
| 622 | 638 | | {decision.pull != null && ( |
| 623 | | − | <Link to={`${base}/pull/${decision.pull}`} className="text-muted hover:text-fg"> |
| 639 | + | <NumberLink to={pullAt(base, pullHref, decision.pull)} className="text-muted hover:text-fg"> |
| 624 | 640 | | #{decision.pull} |
| 625 | | − | </Link> |
| 641 | + | </NumberLink> |
| 626 | 642 | | )} |
| 627 | 643 | | </p> |
| 628 | 644 | | <p className="mt-1 text-sm wrap-break-word"> |
| 629 | | − | <NumberedText text={decision.summary} base={base} /> |
| 645 | + | <NumberedText text={decision.summary} base={base} pullHref={pullHref} /> |
| 630 | 646 | | </p> |
| 631 | 647 | | <p className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted"> |
| 632 | 648 | | <span>{decision.actor ?? "g1t"}</span> |
| ⋯ |
| 635 | 651 | | </span> |
| 636 | 652 | | {evidence.combination.length > 1 && ( |
| 637 | 653 | | <span className="inline-flex items-center gap-1.5"> |
| 638 | | − | · tested as <Combination numbers={evidence.combination} base={base} culprit={decision.kind === "integration_failure" ? decision.pull : null} /> |
| 654 | + | · tested as{" "} |
| 655 | + | <Combination numbers={evidence.combination} base={base} pullHref={pullHref} culprit={decision.kind === "integration_failure" ? decision.pull : null} /> |
| 639 | 656 | | </span> |
| 640 | 657 | | )} |
| 641 | 658 | | {evidence.check && decision.kind === "integration_failure" && ( |
| ⋯ |
| 656 | 673 | | } |
| 657 | 674 | | |
| 658 | 675 | | /** Decision records, as a list in a card. */ |
| 659 | | − | export function DecisionList({ decisions, base }: { decisions: Decision[]; base: string }) { |
| 676 | + | export function DecisionList({ decisions, base, pullHref }: { decisions: Decision[]; base: string; pullHref?: PullHref }) { |
| 660 | 677 | | return ( |
| 661 | 678 | | <Card asChild divided> |
| 662 | 679 | | <ol> |
| 663 | 680 | | {decisions.map((decision) => ( |
| 664 | 681 | | <li key={decision.id} className="px-4 py-3"> |
| 665 | | − | <DecisionLine decision={decision} base={base} /> |
| 682 | + | <DecisionLine decision={decision} base={base} pullHref={pullHref} /> |
| 666 | 683 | | </li> |
| 667 | 684 | | ))} |
| 668 | 685 | | </ol> |
| ⋯ |
| 681 | 698 | | fixed, |
| 682 | 699 | | branch = "main", |
| 683 | 700 | | className, |
| 701 | + | pullHref, |
| 684 | 702 | | }: { |
| 685 | 703 | | failure: Decision; |
| 686 | 704 | | base: string; |
| 705 | + | /** Where each pull request is, for a work item's, whose pull requests are in several repositories. */ |
| 706 | + | pullHref?: PullHref; |
| 687 | 707 | | /** The branch the combination was built on. */ |
| 688 | 708 | | branch?: string; |
| 689 | 709 | | /** A fix or a passing combination since, when there is one. */ |
| ⋯ |
| 697 | 717 | | {failure.pull != null ? ( |
| 698 | 718 | | <span> |
| 699 | 719 | | <TriangleAlert size={15} className={`mr-2 inline align-[-3px] ${fixed ? "text-info" : "text-danger"}`} /> |
| 700 | | − | <Link to={`${base}/pull/${failure.pull}`} className="hover:underline"> |
| 720 | + | <NumberLink to={pullAt(base, pullHref, failure.pull)} className="hover:underline"> |
| 701 | 721 | | #{failure.pull} |
| 702 | | − | </Link>{" "} |
| 722 | + | </NumberLink>{" "} |
| 703 | 723 | | failed in the merge queue {evidence.combination.length > 1 ? "together with others" : "on its own"} |
| 704 | 724 | | </span> |
| 705 | 725 | | ) : ( |
| ⋯ |
| 714 | 734 | | {evidence.combination.length > 0 && ( |
| 715 | 735 | | <p className="flex flex-wrap items-center gap-1.5 text-xs"> |
| 716 | 736 | | Tested as <span className="font-mono text-fg">{branch}</span> <span className="text-faint">+</span> |
| 717 | | − | <Combination numbers={evidence.combination} base={base} culprit={failure.pull} /> |
| 737 | + | <Combination numbers={evidence.combination} base={base} pullHref={pullHref} culprit={failure.pull} /> |
| 718 | 738 | | </p> |
| 719 | 739 | | )} |
| 720 | 740 | | {evidence.check && ( |
| ⋯ |
| 726 | 746 | | {evidence.suspects.length > 0 ? ( |
| 727 | 747 | | <> |
| 728 | 748 | | Fails with{" "} |
| 729 | | − | <NumberedText text={evidence.suspects.map((n) => `#${n}`).join(", ")} base={base} /> |
| 749 | + | <NumberedText text={evidence.suspects.map((n) => `#${n}`).join(", ")} base={base} pullHref={pullHref} /> |
| 730 | 750 | | {evidence.bisected ? ", narrowed down by building smaller combinations." : "."} |
| 731 | 751 | | </> |
| 732 | 752 | | ) : ( |
| ⋯ |
| 738 | 758 | | <CircleCheck size={13} className="mr-1.5 inline align-[-2px] text-success" /> |
| 739 | 759 | | {fixed.kind === "integration_passed" ? "Passed together since" : "Fixed since"}:{" "} |
| 740 | 760 | | <span className="text-muted"> |
| 741 | | − | <NumberedText text={fixed.summary} base={base} /> |
| 761 | + | <NumberedText text={fixed.summary} base={base} pullHref={pullHref} /> |
| 742 | 762 | | </span> |
| 743 | 763 | | </p> |
| 744 | 764 | | )} |
| ⋯ |
| 917 | 937 | | )} |
| 918 | 938 | | {trees.length > sessions.length && ( |
| 919 | 939 | | <p className="mt-2 text-xs text-muted"> |
| 920 | | − | {trees.length - sessions.length} more on its repositories' Sessions pages |
| 940 | + | <Link to={sessionsPath(work.item.workspace, { state: "recent", item: String(work.item.number) })} className="hover:text-fg hover:underline"> |
| 941 | + | {trees.length - sessions.length} more in Work's Sessions |
| 942 | + | </Link> |
| 921 | 943 | | </p> |
| 922 | 944 | | )} |
| 923 | 945 | | </PanelPart> |