Merge pull request #1532 from pikasTech/fix/issue-1531-gh-view-comments

fix: reduce noise for gh issue view comments
This commit is contained in:
Lyon
2026-07-04 20:24:57 +08:00
committed by GitHub
4 changed files with 65 additions and 8 deletions
@@ -4,6 +4,7 @@ Issue writes use `bun scripts/cli.ts gh ...` or the `trans gh:` virtual filesyst
- Body and comments default to Chinese. - Body and comments default to Chinese.
- Recent issue comment progress should prefer `bun scripts/cli.ts gh issue comments <number> --repo owner/name [--limit N] [--full|--raw]`; structured output is stable at `.data.comments`. - Recent issue comment progress should prefer `bun scripts/cli.ts gh issue comments <number> --repo owner/name [--limit N] [--full|--raw]`; structured output is stable at `.data.comments`.
- `gh issue view <number> --json comments` remains the legacy compatibility path at `.data.json.comments`; when comments are requested it should disclose the preferred `gh issue comments <number>` migration path instead of silently pushing operators toward `/tmp` dump recovery.
- New issues include `目标合并分支`. - New issues include `目标合并分支`.
- Multi-stage architecture/API/platform issues begin with `P0 SPEC 先行`. - Multi-stage architecture/API/platform issues begin with `P0 SPEC 先行`.
- Long body text uses `--body-stdin`. - Long body text uses `--body-stdin`.
+1 -1
View File
@@ -73,7 +73,7 @@ export function ghHelp(): unknown {
"issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.", "issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.",
"issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. --title-prefix filters the bounded listed issues locally by exact title startsWith, useful for [FEEDBACK] dedupe, and reports titleFilter input/output counts. Supported --json fields are number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.", "issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. --title-prefix filters the bounded listed issues locally by exact title startsWith, useful for [FEEDBACK] dedupe, and reports titleFilter input/output counts. Supported --json fields are number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.",
"PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.", "PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.",
"issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment view/read/update/edit/delete treat --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection; explicit --json fields limit output even with --raw/--full; full issue body is included only when requested with --json body or when --json is omitted and --full/--raw requests all fields. For recent comment progress, prefer `gh issue comments <number>`: it defaults to a bounded recent-comment table and structured output lives at `.data.comments` instead of `.data.json.comments`. `gh issue view --json comments` remains the compatibility path. Use gh issue comment view <commentId> --full for one full comment body. Unsupported fields fail structurally.", "issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment view/read/update/edit/delete treat --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection; explicit --json fields limit output even with --raw/--full; full issue body is included only when requested with --json body or when --json is omitted and --full/--raw requests all fields. For recent comment progress, prefer `gh issue comments <number>`: it defaults to a bounded recent-comment table and structured output lives at `.data.comments` instead of `.data.json.comments`. `gh issue view --json comments` remains the compatibility path, now with explicit preferredCommand/migrationHint guidance when comments are requested or when large output is compacted. Use gh issue comment view <commentId> --full for one full comment body. Unsupported fields fail structurally.",
"issue attachment list/download scan issue body and comments for GitHub user attachment URLs (`https://github.com/user-attachments/assets/...`). list is read-only and returns bounded attachment metadata. download writes the selected attachment to --output or /tmp/unidesk-gh-attachments, returns bytes/SHA-256/content-type/path, redacts redirected signed URL query parameters, and never prints binary bytes.", "issue attachment list/download scan issue body and comments for GitHub user attachment URLs (`https://github.com/user-attachments/assets/...`). list is read-only and returns bounded attachment metadata. download writes the selected attachment to --output or /tmp/unidesk-gh-attachments, returns bytes/SHA-256/content-type/path, redacts redirected signed URL query parameters, and never prints binary bytes.",
"--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/comments/update/edit/patch/comment view, gh pr list/read/view/comment view, and gh pr diff --file. For issue read/view commands, --full expands issue fields but keeps comment lists bounded; --raw is the explicit all-comment-body escape hatch. For `gh issue comments`, both --full and --raw keep the list bounded to recent comments and include full comment bodies in structured output. Use issue comment view <commentId> --full for a single full comment body. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body only on commands that explicitly support full disclosure.", "--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/comments/update/edit/patch/comment view, gh pr list/read/view/comment view, and gh pr diff --file. For issue read/view commands, --full expands issue fields but keeps comment lists bounded; --raw is the explicit all-comment-body escape hatch. For `gh issue comments`, both --full and --raw keep the list bounded to recent comments and include full comment bodies in structured output. Use issue comment view <commentId> --full for a single full comment body. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body only on commands that explicitly support full disclosure.",
"CLI output larger than config/unidesk-cli.yaml output.maxStdoutBytes is automatically written to /tmp/unidesk-cli-output; stdout stays bounded with outputTruncated=true, warning text, dump file metadata, and drill-down read commands.", "CLI output larger than config/unidesk-cli.yaml output.maxStdoutBytes is automatically written to /tmp/unidesk-cli-output; stdout stays bounded with outputTruncated=true, warning text, dump file metadata, and drill-down read commands.",
+43 -7
View File
@@ -10,6 +10,10 @@ import { commentSummary, compactCommentSummary, compactIssueViewCommentSummary }
import { GITHUB_REST_PAGE_SIZE } from "./types"; import { GITHUB_REST_PAGE_SIZE } from "./types";
import type { GitHubCommandResult, GitHubComment, GitHubErrorPayload, GitHubIssue, GitHubIssueListPage, GitHubIssueListResult, GitHubIssueSearchResponse, IssueListState, IssueViewJsonField } from "./types"; import type { GitHubCommandResult, GitHubComment, GitHubErrorPayload, GitHubIssue, GitHubIssueListPage, GitHubIssueListResult, GitHubIssueSearchResponse, IssueListState, IssueViewJsonField } from "./types";
function preferredIssueCommentsCommand(repo: string, issueNumber: number): string {
return `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo}`;
}
export async function listIssueComments(token: string, repo: string, issueNumber: number, options: { page?: number; perPage?: number } = {}): Promise<GitHubComment[] | GitHubErrorPayload> { export async function listIssueComments(token: string, repo: string, issueNumber: number, options: { page?: number; perPage?: number } = {}): Promise<GitHubComment[] | GitHubErrorPayload> {
const { owner, name } = repoParts(repo); const { owner, name } = repoParts(repo);
const params = new URLSearchParams({ const params = new URLSearchParams({
@@ -133,19 +137,50 @@ export function selectedIssueJson(issue: GitHubIssue, comments: GitHubComment[]
return selected; return selected;
} }
function issueCommentsCompatibility(repo: string, issueNumber: number, comments: GitHubComment[] | null, includeFullCommentBodies: boolean): Record<string, unknown> {
const preferredCommand = preferredIssueCommentsCommand(repo, issueNumber);
return {
commentsCompacted: comments !== null && !includeFullCommentBodies,
commentBodiesOmitted: comments !== null && !includeFullCommentBodies,
fullCommentBodiesIncluded: comments !== null && includeFullCommentBodies,
commentsPath: comments === null ? null : ".data.json.comments",
preferredCommand: comments === null ? null : preferredCommand,
migrationHint: comments === null
? null
: "For bounded recent comment progress, prefer gh issue comments <number>; gh issue view --json comments remains the legacy compatibility path.",
readCommands: {
...(comments !== null && !includeFullCommentBodies ? issueCommentReadCommands(repo, issueNumber) : {}),
...(comments === null ? {} : { preferred: preferredCommand }),
},
};
}
export async function issueRead(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, commandName = "issue read", disclosure: Record<string, unknown> | null = null, options: { includeFullCommentBodies?: boolean } = {}): Promise<GitHubCommandResult> { export async function issueRead(repo: string, token: string, issueNumber: number, jsonFields: IssueViewJsonField[] | undefined, commandName = "issue read", disclosure: Record<string, unknown> | null = null, options: { includeFullCommentBodies?: boolean } = {}): Promise<GitHubCommandResult> {
const issue = await getIssue(token, repo, issueNumber); const issue = await getIssue(token, repo, issueNumber);
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber }); if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
const needsComments = jsonFields === undefined || jsonFields.includes("comments"); const needsComments = jsonFields === undefined || jsonFields.includes("comments");
const includeBody = jsonFields === undefined || jsonFields.includes("body"); const includeBody = jsonFields === undefined || jsonFields.includes("body");
const requestedCommentsField = jsonFields?.includes("comments") === true;
const includeFullCommentBodies = options.includeFullCommentBodies === true; const includeFullCommentBodies = options.includeFullCommentBodies === true;
const comments = needsComments ? await listIssueComments(token, repo, issueNumber) : null; const comments = needsComments ? await listIssueComments(token, repo, issueNumber) : null;
if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue, { includeBody, includePreview: false }) }); if (isGitHubError(comments)) return commandError(commandName, repo, comments, { issueNumber, issue: issueSummary(issue, { includeBody, includePreview: false }) });
const commentsCompatibility = issueCommentsCompatibility(repo, issueNumber, comments, includeFullCommentBodies);
return { return {
ok: true, ok: true,
command: commandName, command: commandName,
repo, repo,
...(disclosure === null ? {} : { disclosure }), ...((disclosure === null && !requestedCommentsField) ? {} : {
disclosure: {
...(disclosure ?? {}),
...(requestedCommentsField ? {
preferredCommand: commentsCompatibility.preferredCommand,
commentsPath: commentsCompatibility.commentsPath,
legacyCompatibilityPath: ".data.json.comments",
migrationHint: commentsCompatibility.migrationHint,
boundedAlternative: "gh issue comments returns a bounded recent-comment summary and is the preferred human/operator path.",
} : {}),
},
}),
issue: issueSummary(issue, { includeBody, includePreview: false }), issue: issueSummary(issue, { includeBody, includePreview: false }),
codeQueueBoardHint: codeQueueBoardCommanderBriefHint(issueNumber, issue.body ?? ""), codeQueueBoardHint: codeQueueBoardCommanderBriefHint(issueNumber, issue.body ?? ""),
...(comments === null || jsonFields !== undefined ? {} : { comments: comments.map(includeFullCommentBodies ? commentSummary : compactCommentSummary) }), ...(comments === null || jsonFields !== undefined ? {} : { comments: comments.map(includeFullCommentBodies ? commentSummary : compactCommentSummary) }),
@@ -155,13 +190,10 @@ export async function issueRead(repo: string, token: string, issueNumber: number
compatibility: { compatibility: {
legacyJsonBodyPath: includeBody ? ".data.issue.body" : null, legacyJsonBodyPath: includeBody ? ".data.issue.body" : null,
bodyOmitted: !includeBody, bodyOmitted: !includeBody,
commentsCompacted: comments !== null && !includeFullCommentBodies, ...commentsCompatibility,
commentBodiesOmitted: comments !== null && !includeFullCommentBodies,
fullCommentBodiesIncluded: comments !== null && includeFullCommentBodies,
commentsPath: comments === null ? null : ".data.json.comments",
readCommands: { readCommands: {
...(includeBody ? {} : issueBodyReadCommands(repo, issueNumber)), ...(includeBody ? {} : issueBodyReadCommands(repo, issueNumber)),
...(comments !== null && !includeFullCommentBodies ? issueCommentReadCommands(repo, issueNumber) : {}), ...recordOrEmpty(commentsCompatibility.readCommands),
}, },
}, },
}), }),
@@ -217,7 +249,7 @@ export async function issueComments(repo: string, token: string, issueNumber: nu
nestedIssueViewCommentsPath: ".data.json.comments", nestedIssueViewCommentsPath: ".data.json.comments",
}, },
readCommands: { readCommands: {
self: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo}`, self: preferredIssueCommentsCommand(repo, issueNumber),
full: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo} --limit ${boundedLimit} --full`, full: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo} --limit ${boundedLimit} --full`,
raw: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo} --limit ${boundedLimit} --raw`, raw: `bun scripts/cli.ts gh issue comments ${issueNumber} --repo ${repo} --limit ${boundedLimit} --raw`,
comment: `bun scripts/cli.ts gh issue comment view <commentId> --repo ${repo} --full`, comment: `bun scripts/cli.ts gh issue comment view <commentId> --repo ${repo} --full`,
@@ -225,3 +257,7 @@ export async function issueComments(repo: string, token: string, issueNumber: nu
}, },
}; };
} }
function recordOrEmpty(value: unknown): Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
+20
View File
@@ -292,6 +292,8 @@ function summarizeEnvelope(envelope: JsonEnvelope<unknown>): Record<string, unkn
const issueRecord = issue as Record<string, unknown>; const issueRecord = issue as Record<string, unknown>;
summary.issue = pickSummary(issueRecord, ["number", "title", "state", "url", "bodyChars", "commentCount"]); summary.issue = pickSummary(issueRecord, ["number", "title", "state", "url", "bodyChars", "commentCount"]);
} }
const issueCommentsMigration = summarizeIssueCommentsMigration(source);
if (issueCommentsMigration !== null) summary.issueCommentsMigration = issueCommentsMigration;
const pullRequest = source.pullRequest; const pullRequest = source.pullRequest;
if (typeof pullRequest === "object" && pullRequest !== null) { if (typeof pullRequest === "object" && pullRequest !== null) {
const prRecord = pullRequest as Record<string, unknown>; const prRecord = pullRequest as Record<string, unknown>;
@@ -319,6 +321,24 @@ function summarizeArrayCounts(source: Record<string, unknown>): Record<string, n
return result; return result;
} }
function summarizeIssueCommentsMigration(source: Record<string, unknown>): Record<string, unknown> | null {
const command = typeof source.command === "string" ? source.command : null;
if (command !== "issue view" && command !== "issue read") return null;
const jsonFields = Array.isArray(source.jsonFields) ? source.jsonFields.filter((value): value is string => typeof value === "string") : [];
if (!jsonFields.includes("comments")) return null;
const compatibility = recordOrNull(source.compatibility);
const readCommands = recordOrNull(compatibility?.readCommands);
const disclosure = recordOrNull(source.disclosure);
const preferredCommand = stringOrNull(compatibility?.preferredCommand)
?? stringOrNull(readCommands?.preferred)
?? stringOrNull(disclosure?.preferredCommand);
return {
legacyCommentsPath: stringOrNull(compatibility?.commentsPath) ?? ".data.json.comments",
preferredCommand,
migrationHint: stringOrNull(compatibility?.migrationHint) ?? stringOrNull(disclosure?.migrationHint),
};
}
function summarizeDebugDispatch(source: Record<string, unknown>): Record<string, unknown> | null { function summarizeDebugDispatch(source: Record<string, unknown>): Record<string, unknown> | null {
const dispatch = recordOrNull(source.dispatch); const dispatch = recordOrNull(source.dispatch);
const wait = recordOrNull(source.wait); const wait = recordOrNull(source.wait);