Merge pull request #11621 from cli/kw/1004-gh-agent-task-list-respects--w--web

`gh agent-task list`: implement `-w`/`--web` to open global agent tasks page in a browser
This commit is contained in:
Kynan Ware 2025-09-02 13:57:31 -06:00 committed by GitHub
commit fca5e0cd63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 28 deletions

View file

@ -5,9 +5,11 @@ import (
"fmt"
"time"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
@ -25,14 +27,17 @@ type ListOptions struct {
Limit int
CapiClient func() (*capi.CAPIClient, error)
BaseRepo func() (ghrepo.Interface, error)
Web bool
Browser browser.Browser
}
// NewCmdList creates the list command
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
opts := &ListOptions{
IO: f.IOStreams,
Config: f.Config,
Limit: defaultLimit,
IO: f.IOStreams,
Config: f.Config,
Limit: defaultLimit,
Browser: f.Browser,
}
cmd := &cobra.Command{
@ -41,9 +46,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// Support -R/--repo override
if f != nil {
opts.BaseRepo = f.BaseRepo
}
opts.BaseRepo = f.BaseRepo
if opts.Limit < 1 {
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
}
@ -59,6 +64,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", defaultLimit, fmt.Sprintf("Maximum number of agent tasks to fetch (default %d)", defaultLimit))
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open agent tasks in the browser")
opts.CapiClient = func() (*capi.CAPIClient, error) {
cfg, err := opts.Config()
@ -77,6 +83,18 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
func listRun(opts *ListOptions) error {
if opts.Web {
// Currently the web GUI does not have a page that supports filtering
// based on repo, so we just open the agents dashboard with no args.
// If that page is ever added in the future, we should route to that
// page instead of the global one when --repo is set.
const webURL = "https://github.com/copilot/agents"
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(webURL))
}
return opts.Browser.Browse(webURL)
}
if opts.Limit <= 0 {
opts.Limit = defaultLimit
}
@ -93,6 +111,8 @@ func listRun(opts *ListOptions) error {
var repo ghrepo.Interface
if opts.BaseRepo != nil {
// We swallow this error because when CWD is not a repo and
// the --repo flag is not set, we use the global/user session listing.
repo, _ = opts.BaseRepo()
}
@ -110,8 +130,7 @@ func listRun(opts *ListOptions) error {
opts.IO.StopProgressIndicator()
if len(sessions) == 0 {
fmt.Fprintln(opts.IO.Out, "no agent tasks found")
return nil
return cmdutil.NewNoResultsError("no agent tasks found")
}
cs := opts.IO.ColorScheme()

View file

@ -8,6 +8,7 @@ import (
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
@ -44,6 +45,19 @@ func TestNewCmdList(t *testing.T) {
args: "--limit 0",
wantErr: "invalid limit: 0",
},
{
name: "negative limit",
args: "--limit -5",
wantErr: "invalid limit: -5",
},
{
name: "web flag",
args: "--web",
wantOpts: ListOptions{
Limit: defaultLimit,
Web: true,
},
},
}
for _, tt := range tests {
@ -62,30 +76,32 @@ func TestNewCmdList(t *testing.T) {
}
require.NoError(t, err)
assert.Equal(t, tt.wantOpts.Limit, gotOpts.Limit)
assert.Equal(t, tt.wantOpts.Web, gotOpts.Web)
})
}
}
func Test_listRun(t *testing.T) {
sixHours, _ := time.ParseDuration("6h")
sixHoursAgo := time.Now().Add(-sixHours)
createdAt := sixHoursAgo.Format(time.RFC3339)
createdAt := time.Now().Add(-6 * time.Hour).Format(time.RFC3339) // 6h ago
tests := []struct {
name string
tty bool
stubs func(*httpmock.Registry)
baseRepo ghrepo.Interface
baseRepoErr error
limit int
wantOut string
wantErr error
name string
tty bool
stubs func(*httpmock.Registry)
baseRepo ghrepo.Interface
baseRepoErr error
limit int
web bool
wantOut string
wantErr error
wantStderr string
wantBrowserURL string
}{
{
name: "no sessions",
tty: true,
stubs: func(reg *httpmock.Registry) { registerEmptySessionsMock(reg) },
wantOut: "no agent tasks found\n",
wantErr: cmdutil.NewNoResultsError("no agent tasks found"),
},
{
name: "limit truncates sessions",
@ -143,15 +159,14 @@ func Test_listRun(t *testing.T) {
tty: true,
stubs: func(reg *httpmock.Registry) { registerRepoEmptySessionsMock(reg, "OWNER", "REPO") },
baseRepo: ghrepo.New("OWNER", "REPO"),
wantOut: "no agent tasks found\n",
wantErr: cmdutil.NewNoResultsError("no agent tasks found"),
},
{
name: "repo resolution error does not surface",
tty: true,
baseRepoErr: errors.New("ambiguous repo"),
wantErr: nil,
wantErr: cmdutil.NewNoResultsError("no agent tasks found"),
stubs: func(reg *httpmock.Registry) { registerEmptySessionsMock(reg) },
wantOut: "no agent tasks found\n",
},
{
name: "repo scoped many sessions (tty)",
@ -168,6 +183,23 @@ func Test_listRun(t *testing.T) {
r6 #306 OWNER/REPO mystery about 6 hours ago
`),
},
{
name: "web mode",
tty: true,
web: true,
wantOut: "",
wantStderr: "Opening https://github.com/copilot/agents in your browser.\n",
wantBrowserURL: "https://github.com/copilot/agents",
},
{
name: "web mode with repo still uses global URL, even when --repo is set",
tty: true,
web: true,
baseRepo: ghrepo.New("OWNER", "REPO"),
wantOut: "",
wantStderr: "Opening https://github.com/copilot/agents in your browser.\n",
wantBrowserURL: "https://github.com/copilot/agents",
},
}
for _, tt := range tests {
@ -181,16 +213,28 @@ func Test_listRun(t *testing.T) {
cfg.Set("github.com", "oauth_token", "OTOKEN")
authCfg := cfg.Authentication()
ios, _, stdout, _ := iostreams.Test()
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(tt.tty)
var br *browser.Stub
if tt.web {
br = &browser.Stub{}
}
httpClient := &http.Client{Transport: reg}
capiClient := capi.NewCAPIClient(httpClient, authCfg)
opts := &ListOptions{
IO: ios,
Config: func() (gh.Config, error) { return cfg, nil },
Limit: tt.limit,
CapiClient: func() (*capi.CAPIClient, error) { return capiClient, nil },
IO: ios,
Config: func() (gh.Config, error) { return cfg, nil },
Limit: tt.limit,
Web: tt.web,
Browser: br,
CapiClient: func() (*capi.CAPIClient, error) {
if tt.web {
require.FailNow(t, "CapiClient was called with --web")
}
return capiClient, nil
},
}
if tt.baseRepo != nil || tt.baseRepoErr != nil {
baseRepo := tt.baseRepo
@ -207,6 +251,10 @@ func Test_listRun(t *testing.T) {
}
got := stdout.String()
require.Equal(t, tt.wantOut, got)
require.Equal(t, tt.wantStderr, stderr.String())
if tt.web {
br.Verify(t, tt.wantBrowserURL)
}
reg.Verify(t)
})
}