feat: show issue labels in issue view and add its --fields flag

`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 "<query>"` is invalid), and point agents
straight at `issue view <n>` 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.
This commit is contained in:
2026-07-17 18:59:16 -04:00
parent ab59e699c1
commit 8653b89612
3 changed files with 72 additions and 4 deletions

View File

@@ -33,9 +33,12 @@ So outside a checkout with the token in the environment, `gitea-axi <command> -R
- `issue` — list, view, create, comment on, edit, close/reopen, pin, and link issues. - `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. - `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. - `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 "<query>"` and pull requests with `search prs "<query>"` (a bare `search "<query>"` is not valid).
- `setup` — install this skill (`setup`) and, opt-in, the SessionStart dashboard hook (`setup hooks`). - `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 <number>`: 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 ## Discovery
This skill is a pointer, not a command reference — the CLI is the single source of truth for its own interface. This skill is a pointer, not a command reference — the CLI is the single source of truth for its own interface.

View File

@@ -222,8 +222,11 @@ Show a single issue. Pull request numbers are rejected — use \`pr view\` inste
flags: flags:
--comments Render every comment in full (bodies truncated at 800 chars) --comments Render every comment in full (bodies truncated at 800 chars)
--full Suppress all truncation of the issue body and comment bodies --full Suppress all truncation of the issue body and comment bodies
--fields <a,b,c> Append extra fields: assignees, closedAt, milestone, updatedAt, url
--help Show this help --help Show this help
Labels are shown by default; use --fields to add assignees, milestone, and more.
global flags: global flags:
-R, --repo <OWNER/NAME> Override the repository detected from the git origin remote -R, --repo <OWNER/NAME> Override the repository detected from the git origin remote
--login <name> Select a tea login profile by name --login <name> Select a tea login profile by name
@@ -479,19 +482,32 @@ const ISSUE_VIEW_FIELDS: FieldDef<Issue>[] = [
pluck("number"), pluck("number"),
pluck("title"), pluck("title"),
lowercased("state"), lowercased("state"),
joined("labels", "labels", "name"),
pluck("author", "user.login"), pluck("author", "user.login"),
relativeTimeField("created", "created_at"), 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<string, FieldDef<Issue>> = {
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 { interface IssueDetailOptions {
host: string; host: string;
full: boolean; full: boolean;
withComments: boolean; withComments: boolean;
now: Date; now: Date;
/** Extra fields selected via `--fields`, appended after the defaults. */
extraFields: FieldDef<Issue>[];
} }
function buildIssueDetail(issue: Issue, options: IssueDetailOptions): Record<string, unknown> { function buildIssueDetail(issue: Issue, options: IssueDetailOptions): Record<string, unknown> {
const row = extractRow(issue, ISSUE_VIEW_FIELDS, { const row = extractRow(issue, [...ISSUE_VIEW_FIELDS, ...options.extraFields], {
now: options.now, now: options.now,
host: options.host, host: options.host,
full: options.full, full: options.full,
@@ -539,12 +555,21 @@ async function issueView(deps: CliDeps, args: string[]): Promise<string> {
} }
const { flags, positionals } = parseFlags( const { flags, positionals } = parseFlags(
args, args,
{ "--comments": { takesValue: false }, "--full": { takesValue: false } }, {
"--comments": { takesValue: false },
"--full": { takesValue: false },
"--fields": { takesValue: true },
},
"issue view", "issue view",
); );
const number = parsePositionalNumber(positionals, "issue view", "issue"); const number = parsePositionalNumber(positionals, "issue view", "issue");
const full = flags["--full"] === true; const full = flags["--full"] === true;
const withComments = flags["--comments"] === true; const withComments = flags["--comments"] === true;
const extraFields = selectExtraFields(
flagValue(flags, "--fields"),
ISSUE_VIEW_EXTRA_FIELDS,
"issue view",
);
const context = await resolveRepoContext(deps); const context = await resolveRepoContext(deps);
const api = createClient(context); const api = createClient(context);
@@ -557,7 +582,7 @@ async function issueView(deps: CliDeps, args: string[]): Promise<string> {
} }
const now = new Date(); 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[] = []; const blocks: DetailBlock[] = [];
if (withComments) { if (withComments) {

View File

@@ -47,6 +47,46 @@ describe("issue view", () => {
expect(stdout).toContain("comment_count: 3 — use --comments to see full comments"); 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 () => { it("renders comment_count: 0 when there are no comments", async () => {
server = await startFixtureServer([ server = await startFixtureServer([
{ method: "GET", path: ISSUE_PATH, body: issueBody({ comments: 0 }) }, { method: "GET", path: ISSUE_PATH, body: issueBody({ comments: 0 }) },