diff --git a/DEVLOG.md b/DEVLOG.md
index cfae7c5..0aac722 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -2,6 +2,32 @@
> Append-only session log. Read at session start. Update at session end.
+## 2026-06-28 — AXI-90 QuickCreate GitHub issue/PR import
+
+Enhanced the ⇧C QuickCreate issue overlay so a pasted GitHub issue/PR URL (or
+`owner/repo#123`) becomes an import flow instead of creating a Forge issue whose
+title is the URL. The overlay now checks repo linkability, previews the resolved
+GitHub resource, shows issue vs PR state inline, switches the primary action to
+Import, and calls `github.importIssue` with the canonical preview URL. Existing
+project and resolved label chips are carried through to the import where possible.
+
+Backend import path now accepts either `url` or `repoFullName + number`, plus an
+optional `resourceType`; issue-number shorthands auto-resolve to PRs when GitHub
+marks the issue response as `pull_request`. Imported PRs reuse the existing
+`issueCreateInputFromGitHub` path, creating a normal Forge issue with a SOURCE
+link to the PR.
+
+Gates: `pnpm typecheck` clean; `pnpm lint` clean (pre-existing native-select
+warnings only); targeted `vitest` for QuickCreate/GitHub support passed (11/11);
+full suite passed when run with the repo `.env` and `OPENAI_API_KEY` unset
+(1032 pass / 1 skipped). Targeted Playwright coverage for `issue-flow` and
+`mobile-smoke` passed locally (8/8) after updating the Combobox ARIA contract
+and status-picker E2E interactions to match the themed Combobox replacement for
+native selects. Follow-up CI fix updated `sprints-roadmap.spec.ts` to drive the
+new `DatePicker` buttons instead of removed native date inputs; local rerun
+passed (2/2). Initial full-suite attempt without the repo `.env` failed on
+missing `DATABASE_URL`, then was rerun with env loaded.
+
## 2026-06-27 — Subject-label resolver (audit log + webhook deliveries)
Closed the deferral from the gap sweep: raw `subjectId.slice(0,8)` on the
diff --git a/src/components/quick-create.tsx b/src/components/quick-create.tsx
index 527a97a..40222af 100644
--- a/src/components/quick-create.tsx
+++ b/src/components/quick-create.tsx
@@ -10,10 +10,21 @@ import {
} from "react";
import { createPortal } from "react-dom";
import { useRouter, usePathname } from "next/navigation";
-import { Calendar, ChevronDown, CornerDownLeft, Plus, X } from "lucide-react";
+import {
+ AlertCircle,
+ Calendar,
+ ChevronDown,
+ CornerDownLeft,
+ Github,
+ GitPullRequest,
+ Loader2,
+ Plus,
+ X,
+} from "lucide-react";
import { toast } from "sonner";
import { ArtifactType, NotificationSeverity } from "@prisma/client";
import { cn } from "@/lib/utils";
+import { parseGitHubIssueOrPrRef } from "@/lib/github-ref";
import { MOTION } from "@/lib/motion";
import { Kbd } from "@/components/ui/kbd";
import { useHotkey } from "@/lib/keyboard";
@@ -350,11 +361,44 @@ export function QuickCreate() {
{ enabled: open && !!contextIssueId },
);
+ // Pasting a GitHub issue/PR URL (or owner/repo#123) into Issue mode turns
+ // the capture bar into an import path instead of creating a literal URL-title
+ // Forge issue. Sub-issue/comment contexts stay local to the current issue.
+ const githubImportRef = useMemo(
+ () => (mode.kind === "issue" ? parseGitHubIssueOrPrRef(text) : null),
+ [mode.kind, text],
+ );
+ const githubImportRepo = githubImportRef?.repoFullName ?? null;
+ const githubLinkability = trpc.github.linkability.useQuery(
+ { repoFullName: githubImportRepo ?? "" },
+ {
+ enabled: open && !!githubImportRepo && !!githubImportRef?.number,
+ staleTime: 15_000,
+ retry: false,
+ },
+ );
+ const githubReady = githubLinkability.data?.status === "ready" ? githubLinkability.data : null;
+ const githubPreviewInput = githubImportRef?.url
+ ? { url: githubImportRef.url, mappingId: githubReady?.mappingId }
+ : {
+ repoFullName: githubImportRepo ?? "",
+ number: githubImportRef?.number ?? 0,
+ mappingId: githubReady?.mappingId,
+ };
+ const githubPreview = trpc.github.preview.useQuery(githubPreviewInput, {
+ enabled: !!githubReady && !!githubImportRef?.number,
+ staleTime: 15_000,
+ retry: false,
+ });
+
// ----- mutations --------------------------------------------------
const createIssue = trpc.issue.create.useMutation({
onError: (err) => toast.error(err.message),
});
+ const importGitHubIssue = trpc.github.importIssue.useMutation({
+ onError: (err) => toast.error(err.message),
+ });
const createComment = trpc.comment.create.useMutation({
onError: (err) => toast.error(err.message),
});
@@ -390,6 +434,7 @@ export function QuickCreate() {
const busy =
createIssue.isPending ||
+ importGitHubIssue.isPending ||
createComment.isPending ||
createCycle.isPending ||
createInitiative.isPending ||
@@ -671,6 +716,45 @@ export function QuickCreate() {
try {
switch (mode.kind) {
case "issue": {
+ if (githubImportRef) {
+ if (githubLinkability.isLoading) {
+ toast.info("Checking GitHub access…");
+ return;
+ }
+ if (!githubReady) {
+ toast.error("Connect this GitHub repository before importing it.");
+ return;
+ }
+ if (githubPreview.isLoading) {
+ toast.info("Loading GitHub preview…");
+ return;
+ }
+ if (githubPreview.isError || !githubPreview.data) {
+ toast.error(githubPreview.error?.message ?? "Could not load GitHub preview.");
+ return;
+ }
+ const explicitLabelIds = (labels ?? [])
+ .filter((label) =>
+ committed.some(
+ (c) => c.kind === "label" && c.name.toLowerCase() === label.name.toLowerCase(),
+ ),
+ )
+ .map((label) => label.id);
+ const result = await importGitHubIssue.mutateAsync({
+ mappingId: githubReady.mappingId,
+ url: githubPreview.data.url,
+ projectId: projectId || null,
+ labelIds: explicitLabelIds,
+ });
+ await utils.issue.list.invalidate();
+ done(result.created ? "Imported GitHub issue." : "Opened existing imported issue.");
+ if (secondary) {
+ const base = ws ? `/w/${ws.slug}` : "";
+ router.push(`${base}/issues/${result.issueId}`);
+ }
+ return;
+ }
+
// Resolve title + commands. Most commands have already been
// committed into chips (priority/project synced onto the native
// pickers, the rest in `committed`), but the operator may have a
@@ -1321,13 +1405,26 @@ export function QuickCreate() {
MOTION.fast,
)}
>
- {busy ? "…" : "Create"}
+ {busy ? "…" : githubImportRef ? "Import" : "Create"}
⏎
+ {githubImportRef && (
+
+ )}
+
{/* Add pills (mouse path) — issue / sub-issue only. Each primes
the matching slash so it shares the keyboard value picker. */}
{isIssueLike && (
@@ -1527,6 +1624,121 @@ export function QuickCreate() {
);
}
+function GitHubImportStatus({
+ checking,
+ linkabilityError,
+ linkabilityStatus,
+ previewLoading,
+ previewError,
+ snapshot,
+ fallbackRepo,
+ fallbackNumber,
+}: {
+ checking: boolean;
+ linkabilityError: string | null;
+ linkabilityStatus: string | null;
+ previewLoading: boolean;
+ previewError: string | null;
+ snapshot:
+ | {
+ resourceType: string;
+ title: string;
+ state: string;
+ url: string;
+ repoFullName: string;
+ number: number;
+ }
+ | null;
+ fallbackRepo: string;
+ fallbackNumber: number;
+}) {
+ const lineClass = "flex items-center gap-1.5 text-meta";
+ const warningClass = `${lineClass} text-warning`;
+ const mutedClass = `${lineClass} text-muted-foreground`;
+
+ if (checking) {
+ return (
+
+
+
+ Checking access to {fallbackRepo}…
+
+
+ );
+ }
+
+ if (linkabilityError) {
+ return (
+
+
+
+ {linkabilityError}
+
+
+ );
+ }
+
+ if (linkabilityStatus && linkabilityStatus !== "ready") {
+ return (
+
+
+
+ Connect {fallbackRepo} in GitHub settings before importing.
+
+
+ );
+ }
+
+ if (previewLoading) {
+ return (
+
+
+
+ Loading {fallbackRepo}#{fallbackNumber}…
+
+
+ );
+ }
+
+ if (previewError) {
+ return (
+
+ );
+ }
+
+ if (!snapshot) return null;
+ const isPr = snapshot.resourceType === "PULL_REQUEST";
+ return (
+
+
+ {isPr ? (
+
+ ) : (
+
+ )}
+
+
+ {snapshot.title}
+
+
+
+ {snapshot.repoFullName}#{snapshot.number}
+
+ {isPr ? "pull request" : "issue"}
+ {snapshot.state || "unknown"}
+ Press Enter to import
+
+
+
+
+ );
+}
+
type SwitchableMode =
| "issue"
| "cycle"
diff --git a/src/components/ui/anchored-popover.tsx b/src/components/ui/anchored-popover.tsx
index 77498dc..c352599 100644
--- a/src/components/ui/anchored-popover.tsx
+++ b/src/components/ui/anchored-popover.tsx
@@ -37,6 +37,7 @@ export function AnchoredPopover({
minSpaceBelow = 220,
className,
role = "menu",
+ id,
ariaLabel,
dismissOnOutsideClick = true,
dismissOnEscape = true,
@@ -58,6 +59,7 @@ export function AnchoredPopover({
minSpaceBelow?: number;
className?: string;
role?: string;
+ id?: string;
ariaLabel?: string;
dismissOnOutsideClick?: boolean;
dismissOnEscape?: boolean;
@@ -140,6 +142,7 @@ export function AnchoredPopover({
return createPortal(
(null);
const inputRef = useRef(null);
+ const listboxId = useId();
const isAsync = !!onQueryChange;
const showSearch = searchable || isAsync;
@@ -134,9 +135,11 @@ export function Combobox({