From 8653b896128fe3e638b742a25e6db854c0f39879 Mon Sep 17 00:00:00 2001 From: alexion Date: Fri, 17 Jul 2026 18:59:16 -0400 Subject: [PATCH] feat: show issue labels in `issue view` and add its --fields flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `issue view` rendered state but never labels, and offered no way to add them — so reading one issue's labels forced a detour through `issue list --fields labels` and hunting the matching row. The benchmark transcripts showed agents paying this round-trip on every labels/state read. Show labels by default in the detail view (a detail view should be complete), and add a `--fields` flag mirroring `issue list` / `search` to append assignees, closedAt, milestone, updatedAt, url on request. Also strengthen SKILL.md against the two command-discovery round-trips the transcripts exposed: name the required `search issues` / `search prs` subcommand form (a bare `search ""` is invalid), and point agents straight at `issue view ` for a single issue's fields. Verified live: read-issue-labels-and-state dropped from 10 turns to 4 (cache-read ~3.3x lower), the transcript reduced to three clean commands with the search-help and issue-list round-trips gone. --- skills/gitea-axi/SKILL.md | 5 ++++- src/commands/issue.ts | 31 +++++++++++++++++++++++++++--- test/issue-view.test.ts | 40 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/skills/gitea-axi/SKILL.md b/skills/gitea-axi/SKILL.md index 085086d..5e30a48 100644 --- a/skills/gitea-axi/SKILL.md +++ b/skills/gitea-axi/SKILL.md @@ -33,9 +33,12 @@ So outside a checkout with the token in the environment, `gitea-axi -R - `issue` — list, view, create, comment on, edit, close/reopen, pin, and link issues. - `pr` — create, view, comment on, edit, review, merge, check out, diff, and inspect the checks of pull requests. - `label` — list, create, edit, and delete labels. -- `search` — full-text search across issues and pull requests. +- `search` — full-text search; it takes a subcommand, so search issues with `search issues ""` and pull requests with `search prs ""` (a bare `search ""` is not valid). - `setup` — install this skill (`setup`) and, opt-in, the SessionStart dashboard hook (`setup hooks`). +To read one issue's fields, reach straight for `issue view `: it shows labels and state by default, and takes `--fields assignees,milestone,…` for the rest. +You rarely need `issue list` to answer a question about a single issue. + ## Discovery This skill is a pointer, not a command reference — the CLI is the single source of truth for its own interface. diff --git a/src/commands/issue.ts b/src/commands/issue.ts index beacd9d..680f9f4 100644 --- a/src/commands/issue.ts +++ b/src/commands/issue.ts @@ -222,8 +222,11 @@ Show a single issue. Pull request numbers are rejected — use \`pr view\` inste flags: --comments Render every comment in full (bodies truncated at 800 chars) --full Suppress all truncation of the issue body and comment bodies + --fields Append extra fields: assignees, closedAt, milestone, updatedAt, url --help Show this help +Labels are shown by default; use --fields to add assignees, milestone, and more. + global flags: -R, --repo Override the repository detected from the git origin remote --login Select a tea login profile by name @@ -479,19 +482,32 @@ const ISSUE_VIEW_FIELDS: FieldDef[] = [ pluck("number"), pluck("title"), lowercased("state"), + joined("labels", "labels", "name"), pluck("author", "user.login"), relativeTimeField("created", "created_at"), ]; +// Appended to the default view fields on request via `--fields`, never replacing +// them. Labels and body are shown by default, so they are not offered here. +const ISSUE_VIEW_EXTRA_FIELDS: Record> = { + assignees: joined("assignees", "assignees", "login"), + closedAt: relativeTimeField("closedAt", "closed_at"), + milestone: pluck("milestone", "milestone.title"), + updatedAt: relativeTimeField("updatedAt", "updated_at"), + url: pluck("url", "html_url"), +}; + interface IssueDetailOptions { host: string; full: boolean; withComments: boolean; now: Date; + /** Extra fields selected via `--fields`, appended after the defaults. */ + extraFields: FieldDef[]; } function buildIssueDetail(issue: Issue, options: IssueDetailOptions): Record { - const row = extractRow(issue, ISSUE_VIEW_FIELDS, { + const row = extractRow(issue, [...ISSUE_VIEW_FIELDS, ...options.extraFields], { now: options.now, host: options.host, full: options.full, @@ -539,12 +555,21 @@ async function issueView(deps: CliDeps, args: string[]): Promise { } const { flags, positionals } = parseFlags( args, - { "--comments": { takesValue: false }, "--full": { takesValue: false } }, + { + "--comments": { takesValue: false }, + "--full": { takesValue: false }, + "--fields": { takesValue: true }, + }, "issue view", ); const number = parsePositionalNumber(positionals, "issue view", "issue"); const full = flags["--full"] === true; const withComments = flags["--comments"] === true; + const extraFields = selectExtraFields( + flagValue(flags, "--fields"), + ISSUE_VIEW_EXTRA_FIELDS, + "issue view", + ); const context = await resolveRepoContext(deps); const api = createClient(context); @@ -557,7 +582,7 @@ async function issueView(deps: CliDeps, args: string[]): Promise { } const now = new Date(); - const item = buildIssueDetail(issue, { host: context.host, full, withComments, now }); + const item = buildIssueDetail(issue, { host: context.host, full, withComments, now, extraFields }); const blocks: DetailBlock[] = []; if (withComments) { diff --git a/test/issue-view.test.ts b/test/issue-view.test.ts index afdb614..36eed88 100644 --- a/test/issue-view.test.ts +++ b/test/issue-view.test.ts @@ -47,6 +47,46 @@ describe("issue view", () => { expect(stdout).toContain("comment_count: 3 — use --comments to see full comments"); }); + it("renders the issue's labels comma-joined by default, with no flag", async () => { + server = await startFixtureServer([ + { + method: "GET", + path: ISSUE_PATH, + body: issueBody({ labels: [{ name: "bug" }, { name: "regression" }] }), + }, + ]); + const { stdout, exitCode } = await runCliTest(["issue", "view", "42"], { + env: testModeEnv(server.url), + }); + + expect(exitCode).toBe(0); + // TOON-quoted because the joined value contains a comma. + expect(stdout).toContain('labels: "bug, regression"'); + }); + + it("appends named extra fields with --fields on top of the default fields", async () => { + server = await startFixtureServer([ + { + method: "GET", + path: ISSUE_PATH, + body: issueBody({ + assignees: [{ login: "alexion" }], + milestone: { title: "v2.0" }, + }), + }, + ]); + const { stdout, exitCode } = await runCliTest( + ["issue", "view", "42", "--fields", "assignees,milestone"], + { env: testModeEnv(server.url) }, + ); + + expect(exitCode).toBe(0); + // Default fields are still present; the extra fields are appended. + expect(stdout).toContain("state: open"); + expect(stdout).toContain("assignees: alexion"); + expect(stdout).toContain("milestone: v2.0"); + }); + it("renders comment_count: 0 when there are no comments", async () => { server = await startFixtureServer([ { method: "GET", path: ISSUE_PATH, body: issueBody({ comments: 0 }) },