Support issue identifiers (PAP-39) in URLs and prefer them throughout

Backend:
- Add router.param middleware in issues, activity, and agents routes to
  resolve identifiers (e.g. PAP-39) to UUIDs before handlers run
- Simplify GET /issues/:id now that param middleware handles resolution
- Include identifier in getAncestors response and issuesForRun query
- Add identifier field to IssueAncestor shared type

Frontend:
- Update all issue navigation links across 15+ files to use
  issue.identifier ?? issue.id instead of bare UUIDs
- Add URL redirect in IssueDetail: navigating via UUID automatically
  replaces the URL with the human-readable identifier
- Fix childIssues filter to use issue.id (UUID) instead of URL param
  so it works correctly with identifier-based URLs
- Add issueUrl() utility in lib/utils.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-20 16:04:05 -06:00
parent 0c0c308594
commit 9906a5ba06
21 changed files with 80 additions and 34 deletions

View File

@@ -189,11 +189,11 @@ export function IssueDetail() {
}, [agents]);
const childIssues = useMemo(() => {
if (!allIssues || !issueId) return [];
if (!allIssues || !issue) return [];
return allIssues
.filter((i) => i.parentId === issueId)
.filter((i) => i.parentId === issue.id)
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
}, [allIssues, issueId]);
}, [allIssues, issue]);
const commentsWithRunMeta = useMemo(() => {
const runMetaByCommentId = new Map<string, { runId: string; runAgentId: string | null }>();
@@ -281,7 +281,7 @@ export function IssueDetail() {
title: `${issueRef} updated`,
body: truncate(updated.title, 96),
tone: "success",
action: { label: `View ${issueRef}`, href: `/issues/${updated.id}` },
action: { label: `View ${issueRef}`, href: `/issues/${updated.identifier ?? updated.id}` },
});
},
});
@@ -298,7 +298,7 @@ export function IssueDetail() {
title: `Comment posted on ${issueRef}`,
body: issue?.title ? truncate(issue.title, 96) : undefined,
tone: "success",
action: issueId ? { label: `View ${issueRef}`, href: `/issues/${issueId}` } : undefined,
action: issueId ? { label: `View ${issueRef}`, href: `/issues/${issue?.identifier ?? issueId}` } : undefined,
});
},
});
@@ -337,6 +337,13 @@ export function IssueDetail() {
]);
}, [setBreadcrumbs, issue, issueId]);
// Redirect to identifier-based URL if navigated via UUID
useEffect(() => {
if (issue?.identifier && issueId !== issue.identifier) {
navigate(`/issues/${issue.identifier}`, { replace: true });
}
}, [issue, issueId, navigate]);
useEffect(() => {
if (issue) {
openPanel(
@@ -373,7 +380,7 @@ export function IssueDetail() {
<span key={ancestor.id} className="flex items-center gap-1">
{i > 0 && <ChevronRight className="h-3 w-3 shrink-0" />}
<Link
to={`/issues/${ancestor.id}`}
to={`/issues/${ancestor.identifier ?? ancestor.id}`}
className="hover:text-foreground transition-colors truncate max-w-[200px]"
title={ancestor.title}
>
@@ -595,7 +602,7 @@ export function IssueDetail() {
{childIssues.map((child) => (
<Link
key={child.id}
to={`/issues/${child.id}`}
to={`/issues/${child.identifier ?? child.id}`}
className="flex items-center justify-between px-3 py-2 text-xs hover:bg-accent/20 transition-colors"
>
<div className="flex items-center gap-2 min-w-0">