From 2e5adaa49b5fed7fa8224e6b81e0694c2de79e4f Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 15 Jun 2026 13:37:39 +0000 Subject: [PATCH] fix: improve feedback issue lookup help --- scripts/src/gh.ts | 45 +++++++++++++++++++++++++++++++-------- scripts/src/help.ts | 3 +++ scripts/src/hwlab-node.ts | 2 +- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/scripts/src/gh.ts b/scripts/src/gh.ts index cdd93667..34788f2e 100644 --- a/scripts/src/gh.ts +++ b/scripts/src/gh.ts @@ -61,7 +61,7 @@ const GH_VALUE_OPTIONS = new Set([ "--expect-updated-at", "--expect-body-sha", "--body-profile", "--label", "--field", "--value", "--section", "--to", "--status", "--row-file", "--category", "--branch", "--tasks", "--summary", "--focus", "--validation", "--progress", "--number", "--pr", - "--search", "--inactive-hours", "--comment", "--comment-file", "--description", + "--search", "--title-prefix", "--inactive-hours", "--comment", "--comment-file", "--description", "--attachment", "--output", ]); const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--body-stdin", "--body-patch-stdin", "--comment-stdin", "--raw", "--full", "--stat", "--merge", "--squash", "--rebase", "--delete-branch", "--private", "--public", "--auto-init"]); @@ -355,6 +355,7 @@ interface GitHubOptions { allowShortBody: boolean; labels: string[]; search?: string; + titlePrefix?: string; title?: string; body?: string; bodyFile?: string; @@ -932,6 +933,7 @@ function parseOptions(args: string[]): GitHubOptions { allowShortBody: hasFlag(args, "--allow-short-body"), labels: labelsOption(args), search: optionValue(args, "--search"), + titlePrefix: optionValue(args, "--title-prefix")?.trim(), title: optionValue(args, "--title"), body: optionValue(args, "--body"), bodyFile: stdinAliasFileOption(args, "--body-file", "--body-stdin"), @@ -6163,7 +6165,7 @@ function shellWord(value: string): string { return JSON.stringify(value); } -function issueListNextCommand(repo: string, state: IssueListState, limit: number, search: string, labels: string[]): string { +function issueListNextCommand(repo: string, state: IssueListState, limit: number, search: string, labels: string[], titlePrefix: string): string { const parts = [ "bun scripts/cli.ts gh issue list", "--repo", shellWord(repo), @@ -6171,15 +6173,21 @@ function issueListNextCommand(repo: string, state: IssueListState, limit: number "--limit", String(Math.min(limit * 2, MAX_ISSUE_LIST_LIMIT)), ]; if (search.length > 0) parts.push("--search", shellWord(search)); + if (titlePrefix.length > 0) parts.push("--title-prefix", shellWord(titlePrefix)); for (const label of labels) parts.push("--label", shellWord(label)); return parts.join(" "); } -async function issueList(repo: string, token: string, state: IssueListState, limit: number, jsonFields: IssueListJsonField[] | undefined, search?: string, labels: string[] = [], noDump = false): Promise { +async function issueList(repo: string, token: string, state: IssueListState, limit: number, jsonFields: IssueListJsonField[] | undefined, search?: string, labels: string[] = [], noDump = false, titlePrefix?: string): Promise { const normalizedSearch = String(search ?? "").trim(); + const normalizedTitlePrefix = String(titlePrefix ?? "").trim(); + if (titlePrefix !== undefined && normalizedTitlePrefix.length === 0) return validationError("issue list", repo, "--title-prefix requires a non-empty value"); const result = await listIssues(token, repo, state, limit, normalizedSearch, labels); - if (isGitHubError(result)) return commandError("issue list", repo, result, { state, limit, search: normalizedSearch || null, labels }); - const issues = result.items; + if (isGitHubError(result)) return commandError("issue list", repo, result, { state, limit, search: normalizedSearch || null, titlePrefix: normalizedTitlePrefix || null, labels }); + const listedIssues = result.items; + const issues = normalizedTitlePrefix.length > 0 + ? listedIssues.filter((issue) => issue.title.startsWith(normalizedTitlePrefix)) + : listedIssues; const fields = jsonFields ?? ISSUE_LIST_JSON_FIELDS.slice(); return { ok: true, @@ -6189,6 +6197,20 @@ async function issueList(repo: string, token: string, state: IssueListState, lim state, limit, search: normalizedSearch || null, + titlePrefix: normalizedTitlePrefix || null, + ...(normalizedTitlePrefix.length > 0 + ? { + titleFilter: { + field: "title", + operator: "startsWith", + prefix: normalizedTitlePrefix, + scope: "bounded-listed-issues", + inputCount: listedIssues.length, + outputCount: issues.length, + filteredOut: listedIssues.length - issues.length, + }, + } + : {}), labels, count: issues.length, rawCount: result.rawCount, @@ -6201,7 +6223,7 @@ async function issueList(repo: string, token: string, state: IssueListState, lim ...(result.hasMore && limit < MAX_ISSUE_LIST_LIMIT ? { next: { - command: issueListNextCommand(repo, state, limit, normalizedSearch, labels), + command: issueListNextCommand(repo, state, limit, normalizedSearch, labels, normalizedTitlePrefix), note: "Increase --limit to continue scanning beyond the current bounded result set.", }, } @@ -6220,6 +6242,7 @@ async function issueList(repo: string, token: string, state: IssueListState, lim query: normalizedSearch ? { q: `${normalizedSearch} repo:${repo} type:issue${state === "all" ? "" : ` state:${state}`}${labels.map((label) => ` ${githubSearchLabelQualifier(label)}`).join("")}`, per_page: GITHUB_REST_PAGE_SIZE } : { state, labels, per_page: GITHUB_REST_PAGE_SIZE }, + localTitlePrefixFilter: normalizedTitlePrefix || null, }, note: "GitHub's issues endpoint may include pull requests; this command filters pull requests from .issues.", }; @@ -7630,7 +7653,7 @@ export function ghHelp(): unknown { "bun scripts/cli.ts gh auth status [--repo owner/name]", "bun scripts/cli.ts gh repo view |--repo owner/name", "bun scripts/cli.ts gh repo create |--repo owner/name [--private|--public] [--description text] [--auto-init] [--dry-run]", - "bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels] [--raw|--full]", + "bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--title-prefix text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels] [--raw|--full]", "bun scripts/cli.ts gh issue view [--repo owner/name] [--number N compat] [--json body,title,state,closed,closedAt,comments,commentCount] [--raw|--full]", "bun scripts/cli.ts gh issue read [--repo owner/name] [--number N compat] [--raw|--full] [compatibility alias for issue view]", "bun scripts/cli.ts gh issue attachment list [--repo owner/name] [--number N compat]", @@ -7683,7 +7706,7 @@ export function ghHelp(): unknown { "repo view/create use GitHub REST through the same token path. repo create defaults to private repositories, preflights existing repos, supports --dry-run, and refuses duplicate creation.", "Token values are never printed; auth status reports only token source and presence.", "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. 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.", "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 delete treats --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; 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.", @@ -7859,6 +7882,10 @@ export async function runGhCommand(args: string[]): Promise value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--search is only supported by gh issue list"); } + if (optionWasProvided(args, "--title-prefix") && !(top === "issue" && sub === "list")) { + const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; + return validationError(command, options.repo, "--title-prefix is only supported by gh issue list"); + } if (optionWasProvided(args, "--inactive-hours") && !(top === "issue" && sub === "stale-close")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--inactive-hours is only supported by gh issue stale-close"); @@ -8054,7 +8081,7 @@ export async function runGhCommand(args: string[]): Promise (await import("./hwlab-test-accounts")).hwlabTestAccountsHelp(), hwlabNodeHelpSummary()); } + if (top === "hwlab" && (sub === "node" || sub === "nodes") && args[2] === "web-probe") { + return loadHelp(async () => (await import("./hwlab-node")).hwlabNodeWebProbeHelp(), hwlabNodeHelpSummary()); + } if (top === "hwlab" && (sub === "node" || sub === "nodes")) return loadHelp(async () => (await import("./hwlab-node")).hwlabNodeHelp(), hwlabNodeHelpSummary()); if (top === "hwlab" && sub === "g14") return loadHelp(async () => (await import("./hwlab-g14")).hwlabG14Help(), hwlabG14HelpSummary()); if (top === "hwlab") return loadHelp(async () => (await import("./hwlab-cd")).hwlabHelp(), hwlabHelpSummary()); diff --git a/scripts/src/hwlab-node.ts b/scripts/src/hwlab-node.ts index 28e543c3..a10bb9c3 100644 --- a/scripts/src/hwlab-node.ts +++ b/scripts/src/hwlab-node.ts @@ -229,7 +229,7 @@ export function hwlabNodeHelp(): Record { }; } -function hwlabNodeWebProbeHelp(): Record { +export function hwlabNodeWebProbeHelp(): Record { return { ok: true, command: "hwlab nodes web-probe",