From 7241b42ecff71488673a12dda99a3d1dcf0f23f3 Mon Sep 17 00:00:00 2001 From: Max Beizer Date: Sat, 28 Feb 2026 09:22:28 -0600 Subject: [PATCH 1/4] Add --json support to `gh agent-task view` Add --json, --jq, and --template flags to `gh agent-task view`, consistent with the pattern used by `gh pr view --json`, `gh issue view --json`, etc. This reuses the same ExportData interface and SessionFields defined for list, applying them to the single-session view output. Closes https://github.com/cli/cli/issues/12805 (partial) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/agent-task/capi/sessions.go | 52 ++++++++++++++++++++++++++++ pkg/cmd/agent-task/view/view.go | 7 ++++ pkg/cmd/agent-task/view/view_test.go | 42 ++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/pkg/cmd/agent-task/capi/sessions.go b/pkg/cmd/agent-task/capi/sessions.go index 8e3969d69..c1f7fd846 100644 --- a/pkg/cmd/agent-task/capi/sessions.go +++ b/pkg/cmd/agent-task/capi/sessions.go @@ -102,6 +102,58 @@ type SessionError struct { Message string } +// SessionFields defines the available fields for JSON export of a Session. +var SessionFields = []string{ + "id", + "name", + "status", + "repository", + "createdAt", + "updatedAt", + "pullRequestNumber", + "pullRequestUrl", +} + +// ExportData implements the exportable interface for JSON output. +func (s *Session) ExportData(fields []string) map[string]interface{} { + data := make(map[string]interface{}, len(fields)) + for _, f := range fields { + switch f { + case "id": + data[f] = s.ID + case "name": + data[f] = s.Name + case "status": + data[f] = s.State + case "repository": + if s.PullRequest != nil && s.PullRequest.Repository != nil { + data[f] = s.PullRequest.Repository.NameWithOwner + } else { + data[f] = nil + } + case "createdAt": + data[f] = s.CreatedAt + case "updatedAt": + data[f] = s.LastUpdatedAt + case "pullRequestNumber": + if s.PullRequest != nil { + data[f] = s.PullRequest.Number + } else { + data[f] = nil + } + case "pullRequestUrl": + if s.PullRequest != nil { + data[f] = s.PullRequest.URL + } else { + data[f] = nil + } + default: + data[f] = nil + } + } + return data +} + type resource struct { ID string `json:"id"` UserID uint64 `json:"user_id"` diff --git a/pkg/cmd/agent-task/view/view.go b/pkg/cmd/agent-task/view/view.go index 38c1e0e12..137efdb64 100644 --- a/pkg/cmd/agent-task/view/view.go +++ b/pkg/cmd/agent-task/view/view.go @@ -37,6 +37,7 @@ type ViewOptions struct { Finder prShared.PRFinder Prompter prompter.Prompter Browser browser.Browser + Exporter cmdutil.Exporter LogRenderer func() shared.LogRenderer Sleep func(d time.Duration) @@ -125,6 +126,8 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman cmd.Flags().BoolVar(&opts.Log, "log", false, "Show agent session logs") cmd.Flags().BoolVar(&opts.Follow, "follow", false, "Follow agent session logs") + cmdutil.AddJSONFlags(cmd, &opts.Exporter, capi.SessionFields) + return cmd } @@ -289,6 +292,10 @@ func viewRun(opts *ViewOptions) error { return printLogs(opts, capiClient, session.ID) } + if opts.Exporter != nil { + return opts.Exporter.Write(opts.IO, session) + } + printSession(opts, session) return nil } diff --git a/pkg/cmd/agent-task/view/view_test.go b/pkg/cmd/agent-task/view/view_test.go index 68cc377e3..335569d3a 100644 --- a/pkg/cmd/agent-task/view/view_test.go +++ b/pkg/cmd/agent-task/view/view_test.go @@ -168,6 +168,7 @@ func Test_viewRun(t *testing.T) { promptStubs func(*testing.T, *prompter.MockPrompter) capiStubs func(*testing.T, *capi.CapiClientMock) logRendererStubs func(*testing.T, *shared.LogRendererMock) + jsonFields []string wantOut string wantErr error wantStderr string @@ -1209,6 +1210,41 @@ func Test_viewRun(t *testing.T) { (rendered:) `), }, + { + name: "json output (tty)", + tty: true, + opts: ViewOptions{ + SelectorArg: "some-session-id", + SessionID: "some-session-id", + }, + capiStubs: func(t *testing.T, m *capi.CapiClientMock) { + m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) { + return &capi.Session{ + ID: "some-session-id", + Name: "Fix login bug", + State: "completed", + CreatedAt: sampleDate, + LastUpdatedAt: sampleDate, + CompletedAt: sampleCompletedAt, + ResourceType: "pull", + PullRequest: &api.PullRequest{ + Number: 42, + URL: "https://github.com/OWNER/REPO/pull/42", + Title: "Fix login bug", + State: "OPEN", + Repository: &api.PRRepository{ + NameWithOwner: "OWNER/REPO", + }, + }, + User: &api.GitHubUser{ + Login: "testuser", + }, + }, nil + } + }, + wantOut: "{\"id\":\"some-session-id\",\"name\":\"Fix login bug\",\"pullRequestNumber\":42,\"pullRequestUrl\":\"https://github.com/OWNER/REPO/pull/42\",\"repository\":\"OWNER/REPO\",\"status\":\"completed\"}\n", + jsonFields: []string{"id", "name", "status", "repository", "pullRequestNumber", "pullRequestUrl"}, + }, } for _, tt := range tests { @@ -1244,6 +1280,12 @@ func Test_viewRun(t *testing.T) { return logRenderer } + if tt.jsonFields != nil { + exporter := cmdutil.NewJSONExporter() + exporter.SetFields(tt.jsonFields) + opts.Exporter = exporter + } + err := viewRun(&opts) if tt.wantErr != nil { assert.Error(t, err) From 250d5a850ade0508f1cfdbdef63fbdde67ae29f0 Mon Sep 17 00:00:00 2001 From: Max Beizer Date: Sun, 1 Mar 2026 08:57:53 -0600 Subject: [PATCH 2/4] Fix gofmt alignment in view_test.go Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/agent-task/view/view_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/agent-task/view/view_test.go b/pkg/cmd/agent-task/view/view_test.go index 335569d3a..f2abe4d52 100644 --- a/pkg/cmd/agent-task/view/view_test.go +++ b/pkg/cmd/agent-task/view/view_test.go @@ -1242,7 +1242,7 @@ func Test_viewRun(t *testing.T) { }, nil } }, - wantOut: "{\"id\":\"some-session-id\",\"name\":\"Fix login bug\",\"pullRequestNumber\":42,\"pullRequestUrl\":\"https://github.com/OWNER/REPO/pull/42\",\"repository\":\"OWNER/REPO\",\"status\":\"completed\"}\n", + wantOut: "{\"id\":\"some-session-id\",\"name\":\"Fix login bug\",\"pullRequestNumber\":42,\"pullRequestUrl\":\"https://github.com/OWNER/REPO/pull/42\",\"repository\":\"OWNER/REPO\",\"status\":\"completed\"}\n", jsonFields: []string{"id", "name", "status", "repository", "pullRequestNumber", "pullRequestUrl"}, }, } From ceb8cf2561e7caf1ea272d62e75a1bf7c531e157 Mon Sep 17 00:00:00 2001 From: Max Beizer Date: Sun, 1 Mar 2026 09:09:08 -0600 Subject: [PATCH 3/4] Polish --json support for agent-task view - Rename 'status' field to 'state' for consistency with struct and UI - Add missing JSON fields: completedAt, user, pullRequestTitle, pullRequestState - Add test for nil PullRequest with --json - Expand existing JSON test to cover new fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/agent-task/capi/sessions.go | 32 ++++++++++++++++++++++++++-- pkg/cmd/agent-task/view/view_test.go | 28 +++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/agent-task/capi/sessions.go b/pkg/cmd/agent-task/capi/sessions.go index c1f7fd846..3a74ef3d9 100644 --- a/pkg/cmd/agent-task/capi/sessions.go +++ b/pkg/cmd/agent-task/capi/sessions.go @@ -106,12 +106,16 @@ type SessionError struct { var SessionFields = []string{ "id", "name", - "status", + "state", "repository", + "user", "createdAt", "updatedAt", + "completedAt", "pullRequestNumber", "pullRequestUrl", + "pullRequestTitle", + "pullRequestState", } // ExportData implements the exportable interface for JSON output. @@ -123,7 +127,7 @@ func (s *Session) ExportData(fields []string) map[string]interface{} { data[f] = s.ID case "name": data[f] = s.Name - case "status": + case "state": data[f] = s.State case "repository": if s.PullRequest != nil && s.PullRequest.Repository != nil { @@ -131,10 +135,22 @@ func (s *Session) ExportData(fields []string) map[string]interface{} { } else { data[f] = nil } + case "user": + if s.User != nil { + data[f] = s.User.Login + } else { + data[f] = nil + } case "createdAt": data[f] = s.CreatedAt case "updatedAt": data[f] = s.LastUpdatedAt + case "completedAt": + if s.CompletedAt.IsZero() { + data[f] = nil + } else { + data[f] = s.CompletedAt + } case "pullRequestNumber": if s.PullRequest != nil { data[f] = s.PullRequest.Number @@ -147,6 +163,18 @@ func (s *Session) ExportData(fields []string) map[string]interface{} { } else { data[f] = nil } + case "pullRequestTitle": + if s.PullRequest != nil { + data[f] = s.PullRequest.Title + } else { + data[f] = nil + } + case "pullRequestState": + if s.PullRequest != nil { + data[f] = s.PullRequest.State + } else { + data[f] = nil + } default: data[f] = nil } diff --git a/pkg/cmd/agent-task/view/view_test.go b/pkg/cmd/agent-task/view/view_test.go index f2abe4d52..34036cfa5 100644 --- a/pkg/cmd/agent-task/view/view_test.go +++ b/pkg/cmd/agent-task/view/view_test.go @@ -1231,7 +1231,7 @@ func Test_viewRun(t *testing.T) { Number: 42, URL: "https://github.com/OWNER/REPO/pull/42", Title: "Fix login bug", - State: "OPEN", + State: "MERGED", Repository: &api.PRRepository{ NameWithOwner: "OWNER/REPO", }, @@ -1242,8 +1242,30 @@ func Test_viewRun(t *testing.T) { }, nil } }, - wantOut: "{\"id\":\"some-session-id\",\"name\":\"Fix login bug\",\"pullRequestNumber\":42,\"pullRequestUrl\":\"https://github.com/OWNER/REPO/pull/42\",\"repository\":\"OWNER/REPO\",\"status\":\"completed\"}\n", - jsonFields: []string{"id", "name", "status", "repository", "pullRequestNumber", "pullRequestUrl"}, + wantOut: "{\"id\":\"some-session-id\",\"name\":\"Fix login bug\",\"pullRequestNumber\":42,\"pullRequestState\":\"MERGED\",\"pullRequestTitle\":\"Fix login bug\",\"pullRequestUrl\":\"https://github.com/OWNER/REPO/pull/42\",\"repository\":\"OWNER/REPO\",\"state\":\"completed\",\"user\":\"testuser\"}\n", + jsonFields: []string{"id", "name", "state", "repository", "user", "pullRequestNumber", "pullRequestUrl", "pullRequestTitle", "pullRequestState"}, + }, + { + name: "json output with nil pull request", + tty: false, + opts: ViewOptions{ + SelectorArg: "some-session-id", + SessionID: "some-session-id", + }, + capiStubs: func(t *testing.T, m *capi.CapiClientMock) { + m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) { + return &capi.Session{ + ID: "some-session-id", + Name: "New task", + State: "in_progress", + CreatedAt: sampleDate, + LastUpdatedAt: sampleDate, + ResourceType: "pull", + }, nil + } + }, + wantOut: "{\"id\":\"some-session-id\",\"name\":\"New task\",\"pullRequestNumber\":null,\"pullRequestUrl\":null,\"repository\":null,\"state\":\"in_progress\",\"user\":null}\n", + jsonFields: []string{"id", "name", "state", "repository", "user", "pullRequestNumber", "pullRequestUrl"}, }, } From 0d05a8acca4b4e5d45ee428cb2be4690ffbc6ab7 Mon Sep 17 00:00:00 2001 From: Max Beizer Date: Sun, 1 Mar 2026 10:18:21 -0600 Subject: [PATCH 4/4] Address Copilot review feedback - Prioritize --json output over --log/--follow so JSON is not silently ignored - Emit null for zero createdAt/updatedAt values, consistent with completedAt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/agent-task/capi/sessions.go | 12 ++++++++++-- pkg/cmd/agent-task/view/view.go | 8 ++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/agent-task/capi/sessions.go b/pkg/cmd/agent-task/capi/sessions.go index 3a74ef3d9..4b457d799 100644 --- a/pkg/cmd/agent-task/capi/sessions.go +++ b/pkg/cmd/agent-task/capi/sessions.go @@ -142,9 +142,17 @@ func (s *Session) ExportData(fields []string) map[string]interface{} { data[f] = nil } case "createdAt": - data[f] = s.CreatedAt + if s.CreatedAt.IsZero() { + data[f] = nil + } else { + data[f] = s.CreatedAt + } case "updatedAt": - data[f] = s.LastUpdatedAt + if s.LastUpdatedAt.IsZero() { + data[f] = nil + } else { + data[f] = s.LastUpdatedAt + } case "completedAt": if s.CompletedAt.IsZero() { data[f] = nil diff --git a/pkg/cmd/agent-task/view/view.go b/pkg/cmd/agent-task/view/view.go index 137efdb64..854faa73d 100644 --- a/pkg/cmd/agent-task/view/view.go +++ b/pkg/cmd/agent-task/view/view.go @@ -288,14 +288,14 @@ func viewRun(opts *ViewOptions) error { opts.IO.StopProgressIndicator() } - if opts.Log { - return printLogs(opts, capiClient, session.ID) - } - if opts.Exporter != nil { return opts.Exporter.Write(opts.IO, session) } + if opts.Log { + return printLogs(opts, capiClient, session.ID) + } + printSession(opts, session) return nil }