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:
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 }) },
|
||||||
|
|||||||
Reference in New Issue
Block a user