From 7dae882c9de79ca9e94fe0d6472bd6c08033b5d7 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:40:08 -0600 Subject: [PATCH] feat: add Issues 2.0 fields to issue view Display new issue metadata in TTY view: - Issue type on state line (gray, before Open/Closed) - Type, Parent, Blocked by, Blocking metadata rows - Sub-issues section with completion progress (X/Y, Z%) - Cross-repo references show full owner/repo#N format All new fields included in defaultFields and JSON export. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/issue/view/view.go | 72 ++++++++++++++++++++++++++++++++- pkg/cmd/issue/view/view_test.go | 6 +++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index 5add5a71b..993940c4f 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -92,6 +92,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman var defaultFields = []string{ "number", "url", "state", "createdAt", "title", "body", "author", "milestone", "assignees", "labels", "reactionGroups", "lastComment", "stateReason", + "issueType", "parent", "subIssues", "subIssuesSummary", "blockedBy", "blocking", } func viewRun(opts *ViewOptions) error { @@ -219,9 +220,15 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue // Header (Title and State) fmt.Fprintf(out, "%s %s#%d\n", cs.Bold(issue.Title), ghrepo.FullName(baseRepo), issue.Number) + + // State line — include issue type prefix when present + stateLine := issueStateTitleWithColor(cs, issue) + if issue.IssueType != nil { + stateLine = cs.Muted(issue.IssueType.Name) + " · " + stateLine + } fmt.Fprintf(out, - "%s • %s opened %s • %s\n", - issueStateTitleWithColor(cs, issue), + "%s · %s opened %s · %s\n", + stateLine, issue.Author.DisplayName(), text.FuzzyAgo(opts.Now(), issue.CreatedAt), text.Pluralize(issue.Comments.TotalCount, "comment"), @@ -242,6 +249,22 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue fmt.Fprint(out, cs.Bold("Labels: ")) fmt.Fprintln(out, labels) } + if issue.IssueType != nil { + fmt.Fprint(out, cs.Bold("Type: ")) + fmt.Fprintln(out, issue.IssueType.Name) + } + if issue.Parent != nil { + fmt.Fprint(out, cs.Bold("Parent: ")) + fmt.Fprintln(out, formatLinkedIssueRef(baseRepo, issue.Parent)+" "+issue.Parent.Title) + } + if blockedBy := formatLinkedIssueList(baseRepo, issue.BlockedBy.Nodes); blockedBy != "" { + fmt.Fprint(out, cs.Bold("Blocked by: ")) + fmt.Fprintln(out, blockedBy) + } + if blocking := formatLinkedIssueList(baseRepo, issue.Blocking.Nodes); blocking != "" { + fmt.Fprint(out, cs.Bold("Blocking: ")) + fmt.Fprintln(out, blocking) + } if projects := issueProjectList(*issue); projects != "" { fmt.Fprint(out, cs.Bold("Projects: ")) fmt.Fprintln(out, projects) @@ -266,6 +289,30 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue } fmt.Fprintf(out, "\n%s\n", md) + // Sub-issues section + if issue.SubIssuesSummary.Total > 0 { + fmt.Fprintf(out, "%s · %d/%d (%d%%)\n", + cs.Bold("Sub-issues"), + issue.SubIssuesSummary.Completed, + issue.SubIssuesSummary.Total, + int(issue.SubIssuesSummary.PercentCompleted), + ) + for _, sub := range issue.SubIssues.Nodes { + stateColor := cs.Green + stateLabel := "Open" + if sub.State == "CLOSED" { + stateColor = cs.Magenta + stateLabel = "Closed" + } + fmt.Fprintf(out, "%s %s %s\n", + stateColor(stateLabel), + formatLinkedIssueRef(baseRepo, &sub), + sub.Title, + ) + } + fmt.Fprintln(out) + } + // Comments if issue.Comments.TotalCount > 0 { preview := !opts.Comments @@ -282,6 +329,27 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue return nil } +// formatLinkedIssueRef formats an issue reference, using just #N for same-repo +// or owner/repo#N for cross-repo references. +func formatLinkedIssueRef(baseRepo ghrepo.Interface, issue *api.LinkedIssue) string { + if issue.Repository.NameWithOwner != "" && issue.Repository.NameWithOwner != ghrepo.FullName(baseRepo) { + return fmt.Sprintf("%s#%d", issue.Repository.NameWithOwner, issue.Number) + } + return fmt.Sprintf("%s#%d", ghrepo.FullName(baseRepo), issue.Number) +} + +// formatLinkedIssueList formats a comma-separated list of linked issue references with titles. +func formatLinkedIssueList(baseRepo ghrepo.Interface, issues []api.LinkedIssue) string { + if len(issues) == 0 { + return "" + } + parts := make([]string, len(issues)) + for i, issue := range issues { + parts[i] = formatLinkedIssueRef(baseRepo, &issue) + " " + issue.Title + } + return strings.Join(parts, ", ") +} + func issueStateTitleWithColor(cs *iostreams.ColorScheme, issue *api.Issue) string { colorFunc := cs.ColorFromString(prShared.ColorForIssueState(*issue)) state := "Open" diff --git a/pkg/cmd/issue/view/view_test.go b/pkg/cmd/issue/view/view_test.go index aa6002563..a15a6b9a5 100644 --- a/pkg/cmd/issue/view/view_test.go +++ b/pkg/cmd/issue/view/view_test.go @@ -46,6 +46,12 @@ func TestJSONFields(t *testing.T) { "url", "isPinned", "stateReason", + "issueType", + "parent", + "subIssues", + "subIssuesSummary", + "blockedBy", + "blocking", }) }