@diegopetrucci/pi-extensions 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/extensions/oracle/README.md +4 -4
- package/extensions/oracle/index.ts +78 -13
- package/extensions/oracle/package.json +1 -1
- package/extensions/triage-comments/README.md +8 -4
- package/extensions/triage-comments/index.ts +387 -31
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,11 +11,11 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
|
|
|
11
11
|
- [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch/repo on the first line, context/model on the second, optional `DUMB ZONE`, plus OpenAI Codex 5-hour and 7-day usage when available.
|
|
12
12
|
- [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
|
|
13
13
|
- [`openai-fast`](./extensions/openai-fast): Adds `/fast` to enable OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.
|
|
14
|
-
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers with hardcoded rankings,
|
|
14
|
+
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers (including Together) with hardcoded rankings, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
|
|
15
15
|
- [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
|
|
16
16
|
- [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as a one-line invocation plus an expand hint without changing model-visible tool results; toggle temporarily with `/quiet-tools`.
|
|
17
17
|
- [`todo`](./extensions/todo): Adds a branch-aware `todo` tool for the agent and a `/todos` viewer for users.
|
|
18
|
-
- [`triage-comments`](./extensions/triage-comments): Adds `/triage-comments` and a read-only `triage_comments` subagent tool that
|
|
18
|
+
- [`triage-comments`](./extensions/triage-comments): Adds `/triage-comments` and a read-only `triage_comments` subagent tool that can auto-detect the current branch's PR, filter resolved/outdated inline comments, classify selected review comments with evidence, and suggest handling options without implementing changes.
|
|
19
19
|
|
|
20
20
|
(For the full list of pi extensions I use, [check out my dotfiles](https://github.com/diegopetrucci/dot/blob/main/.pi/agent/settings.json).)
|
|
21
21
|
|
|
@@ -11,7 +11,7 @@ It adds an `oracle` tool that spins up a separate read-only pi subprocess and se
|
|
|
11
11
|
- creates an isolated read-only subprocess
|
|
12
12
|
- auto-picks the strongest reasoning model on the current provider
|
|
13
13
|
- uses provider-specific hardcoded rankings first, then a heuristic fallback
|
|
14
|
-
-
|
|
14
|
+
- requests `xhigh` by default for reasoning models, then clamps to the model-supported thinking level
|
|
15
15
|
- defaults to `read,grep,find,ls`
|
|
16
16
|
- can optionally allow non-mutating `bash` inspection
|
|
17
17
|
- shows a live oracle status line and widget while the subprocess is running
|
|
@@ -27,7 +27,7 @@ By default, the extension:
|
|
|
27
27
|
4. tries a provider-specific hardcoded priority list first
|
|
28
28
|
5. falls back to a heuristic that favors stronger tiers like `opus`, `pro`, newer versions, and penalizes `mini`, `flash`, `haiku`, `spark`, etc.
|
|
29
29
|
|
|
30
|
-
The hardcoded rankings now cover pi's built-in provider set, including
|
|
30
|
+
The hardcoded rankings now cover pi's built-in provider set, including Together; see the provider matrix for the current provider-by-provider top picks.
|
|
31
31
|
|
|
32
32
|
If no reasoning model exists on the current provider, it falls back to the best available model on that provider.
|
|
33
33
|
|
|
@@ -35,9 +35,9 @@ If no reasoning model exists on the current provider, it falls back to the best
|
|
|
35
35
|
|
|
36
36
|
Yes — the extension explicitly sets the oracle reasoning level.
|
|
37
37
|
|
|
38
|
-
- reasoning models
|
|
38
|
+
- reasoning models request `xhigh` by default, then use the Pi-compatible effective thinking level supported by the matched model
|
|
39
39
|
- non-reasoning models default to `off`
|
|
40
|
-
- you can override it with the tool's optional `thinkingLevel` parameter
|
|
40
|
+
- you can override it with the tool's optional `thinkingLevel` parameter; matched models still clamp unsupported overrides and report the effective level
|
|
41
41
|
|
|
42
42
|
Use `/oracle-model` inside pi to see what it would pick right now.
|
|
43
43
|
|
|
@@ -7,6 +7,7 @@ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
|
|
9
9
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
10
|
+
type ThinkingLevelMap = Partial<Record<ThinkingLevel, unknown | null>>;
|
|
10
11
|
|
|
11
12
|
type PiModel = {
|
|
12
13
|
provider: string;
|
|
@@ -15,6 +16,7 @@ type PiModel = {
|
|
|
15
16
|
reasoning?: boolean;
|
|
16
17
|
contextWindow?: number;
|
|
17
18
|
maxTokens?: number;
|
|
19
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
interface UsageStats {
|
|
@@ -33,6 +35,8 @@ interface OracleSelection {
|
|
|
33
35
|
modelId: string;
|
|
34
36
|
modelName?: string;
|
|
35
37
|
thinkingLevel: ThinkingLevel;
|
|
38
|
+
requestedThinkingLevel?: ThinkingLevel;
|
|
39
|
+
thinkingLevelClamped?: boolean;
|
|
36
40
|
autoSelected: boolean;
|
|
37
41
|
selectionReason: string;
|
|
38
42
|
}
|
|
@@ -294,6 +298,20 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
294
298
|
"minimax/minimax-m2.1",
|
|
295
299
|
"z-ai/glm-5.1",
|
|
296
300
|
],
|
|
301
|
+
together: [
|
|
302
|
+
"deepseek-ai/DeepSeek-V4-Pro",
|
|
303
|
+
"zai-org/GLM-5.1",
|
|
304
|
+
"moonshotai/Kimi-K2.6",
|
|
305
|
+
"Qwen/Qwen3.6-Plus",
|
|
306
|
+
"MiniMaxAI/MiniMax-M2.7",
|
|
307
|
+
"Qwen/Qwen3.5-397B-A17B",
|
|
308
|
+
"Qwen/Qwen3-Coder-Next-FP8",
|
|
309
|
+
"Qwen/Qwen3-235B-A22B-Instruct-2507-tput",
|
|
310
|
+
"openai/gpt-oss-120b",
|
|
311
|
+
"moonshotai/Kimi-K2.5",
|
|
312
|
+
"deepseek-ai/DeepSeek-V3-1",
|
|
313
|
+
"MiniMaxAI/MiniMax-M2.5",
|
|
314
|
+
],
|
|
297
315
|
"vercel-ai-gateway": [
|
|
298
316
|
"anthropic/claude-opus-4.7",
|
|
299
317
|
"anthropic/claude-opus-4.6",
|
|
@@ -393,7 +411,7 @@ const OracleParams = Type.Object({
|
|
|
393
411
|
thinkingLevel: Type.Optional(
|
|
394
412
|
StringEnum(THINKING_LEVELS, {
|
|
395
413
|
description:
|
|
396
|
-
"Optional reasoning level override for the oracle subprocess.
|
|
414
|
+
"Optional reasoning level override for the oracle subprocess. Defaults request xhigh for reasoning models and off for non-reasoning models, then clamp to matched model capabilities.",
|
|
397
415
|
}),
|
|
398
416
|
),
|
|
399
417
|
cwd: Type.Optional(Type.String({ description: "Optional working directory for the oracle subprocess." })),
|
|
@@ -530,9 +548,48 @@ function withThinking(modelRef: string, thinkingLevel: ThinkingLevel): string {
|
|
|
530
548
|
return `${modelRef}:${thinkingLevel}`;
|
|
531
549
|
}
|
|
532
550
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
551
|
+
// Keep these local so the extension stays compatible with older pi peer installs that do not export clamp helpers.
|
|
552
|
+
function isThinkingLevelSupported(model: PiModel, level: ThinkingLevel): boolean {
|
|
553
|
+
if (!model.reasoning) return level === "off";
|
|
554
|
+
|
|
555
|
+
const map = model.thinkingLevelMap;
|
|
556
|
+
if (level === "xhigh") {
|
|
557
|
+
return !!map && Object.prototype.hasOwnProperty.call(map, "xhigh") && map.xhigh != null;
|
|
558
|
+
}
|
|
559
|
+
return map?.[level] !== null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingLevel {
|
|
563
|
+
if (isThinkingLevelSupported(model, requested)) return requested;
|
|
564
|
+
|
|
565
|
+
const requestedIndex = THINKING_LEVELS.indexOf(requested);
|
|
566
|
+
for (let index = requestedIndex + 1; index < THINKING_LEVELS.length; index++) {
|
|
567
|
+
const level = THINKING_LEVELS[index];
|
|
568
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
569
|
+
}
|
|
570
|
+
for (let index = requestedIndex - 1; index >= 0; index--) {
|
|
571
|
+
const level = THINKING_LEVELS[index];
|
|
572
|
+
if (isThinkingLevelSupported(model, level)) return level;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return "off";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function resolveThinkingLevel(
|
|
579
|
+
model: PiModel | undefined,
|
|
580
|
+
override: ThinkingLevel | undefined,
|
|
581
|
+
): { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean } {
|
|
582
|
+
const requested = override ?? (model?.reasoning ? DEFAULT_THINKING_LEVEL : "off");
|
|
583
|
+
const effective = model ? clampThinkingLevel(model, requested) : requested;
|
|
584
|
+
return { requested, effective, clamped: effective !== requested };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function appendThinkingLevelClampReason(
|
|
588
|
+
reason: string,
|
|
589
|
+
resolution: { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean },
|
|
590
|
+
): string {
|
|
591
|
+
if (!resolution.clamped) return reason;
|
|
592
|
+
return `${reason} Requested thinking level ${resolution.requested} was clamped to ${resolution.effective} based on the matched model's capabilities.`;
|
|
536
593
|
}
|
|
537
594
|
|
|
538
595
|
async function findAvailableModel(
|
|
@@ -599,9 +656,11 @@ async function selectOracleModel(
|
|
|
599
656
|
|
|
600
657
|
const preferred = selectPreferredModel(candidates, providerForPreferences);
|
|
601
658
|
const winner = preferred ?? [...candidates].sort((a, b) => rankModel(b) - rankModel(a))[0];
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
: reason
|
|
659
|
+
const thinking = resolveThinkingLevel(winner, thinkingLevelOverride);
|
|
660
|
+
const selectionReason = appendThinkingLevelClampReason(
|
|
661
|
+
preferred ? `Selected ${winner.id} via the hardcoded preference list for ${winner.provider}.` : reason,
|
|
662
|
+
thinking,
|
|
663
|
+
);
|
|
605
664
|
|
|
606
665
|
return {
|
|
607
666
|
ok: true,
|
|
@@ -610,7 +669,8 @@ async function selectOracleModel(
|
|
|
610
669
|
provider: winner.provider,
|
|
611
670
|
modelId: winner.id,
|
|
612
671
|
modelName: winner.name,
|
|
613
|
-
thinkingLevel:
|
|
672
|
+
thinkingLevel: thinking.effective,
|
|
673
|
+
...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
|
|
614
674
|
autoSelected: true,
|
|
615
675
|
selectionReason,
|
|
616
676
|
},
|
|
@@ -867,7 +927,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
867
927
|
"Use this tool sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
|
|
868
928
|
"Do not use it for routine low-value work; it is slower than the main agent.",
|
|
869
929
|
"The oracle is read-only by default. Set includeBash only when shell-based inspection is genuinely useful.",
|
|
870
|
-
"The oracle
|
|
930
|
+
"The oracle requests xhigh by default for reasoning models; defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
|
|
871
931
|
],
|
|
872
932
|
parameters: OracleParams,
|
|
873
933
|
|
|
@@ -888,16 +948,21 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
888
948
|
const provider =
|
|
889
949
|
matched?.provider ?? (modelRef.includes("/") ? modelRef.split("/")[0] : ctx.model?.provider ?? "unknown");
|
|
890
950
|
const modelId = matched?.id ?? (modelRef.includes("/") ? modelRef.split("/").slice(1).join("/") : modelRef);
|
|
951
|
+
const thinking = resolveThinkingLevel(matched, params.thinkingLevel);
|
|
952
|
+
const selectionReason = matched
|
|
953
|
+
? appendThinkingLevelClampReason("Used the explicit model override provided in the tool call.", thinking)
|
|
954
|
+
: "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
|
|
891
955
|
selection = {
|
|
892
956
|
modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
|
|
893
957
|
provider,
|
|
894
958
|
modelId,
|
|
895
959
|
modelName: matched?.name,
|
|
896
|
-
thinkingLevel:
|
|
960
|
+
thinkingLevel: thinking.effective,
|
|
961
|
+
...(matched && thinking.clamped
|
|
962
|
+
? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
|
|
963
|
+
: {}),
|
|
897
964
|
autoSelected: false,
|
|
898
|
-
selectionReason
|
|
899
|
-
? "Used the explicit model override provided in the tool call."
|
|
900
|
-
: "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.",
|
|
965
|
+
selectionReason,
|
|
901
966
|
};
|
|
902
967
|
} else {
|
|
903
968
|
const selectionResult = await selectOracleModel(ctx, params.thinkingLevel);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-oracle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "oracle", "reasoning", "subagent"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@ The extension registers `/triage-comments` as an interactive intake flow.
|
|
|
37
37
|
```text
|
|
38
38
|
/triage-comments
|
|
39
39
|
/triage-comments paste
|
|
40
|
+
/triage-comments pr
|
|
40
41
|
/triage-comments pr 123
|
|
41
42
|
/triage-comments pr https://github.com/owner/repo/pull/123
|
|
42
43
|
/triage-comments 123
|
|
@@ -44,8 +45,10 @@ The extension registers `/triage-comments` as an interactive intake flow.
|
|
|
44
45
|
|
|
45
46
|
- With no arguments, The Last Harness asks whether to paste feedback or fetch PR comments.
|
|
46
47
|
- `paste` opens an editor for multiline reviewer feedback, then sends one selected feedback item to the main agent.
|
|
47
|
-
- `pr
|
|
48
|
-
-
|
|
48
|
+
- `pr` with no explicit target first tries to detect an existing PR for the current named non-`main` git branch using read-only `git` and `gh pr view` calls. If the branch is `main`, detached, outside a git repository, `gh` is unavailable or unauthenticated, or no PR is found, it falls back to the PR URL/number prompt.
|
|
49
|
+
- `pr <PR URL or number>` and a bare PR URL/number fetch that explicit PR directly, display PR review comments, PR issue comments, and review bodies with `gh` as numbered items with stable IDs, and ask whether to investigate all displayed comments or an explicit subset such as `1,3-5`.
|
|
50
|
+
- Before displaying fetched PR comments, PR mode asks whether to show all comments or hide resolved inline review comments, outdated inline review comments, or both. This filter applies only to inline review comments because GitHub exposes resolved/outdated state at the review-thread level; PR issue comments and review bodies always remain visible, and inline comments without thread metadata remain visible.
|
|
51
|
+
- If more than 50 comments are displayed after filtering, you must choose a subset of at most 50 comments.
|
|
49
52
|
- The command sends a normal user message instructing the main agent to call `triage_comments` with the selected payload. It does not directly edit files or post GitHub replies.
|
|
50
53
|
|
|
51
54
|
The slash command requires interactive UI mode for the editor, PR comment display, and all/subset confirmation. In non-UI modes it prints usage instead of running the intake flow.
|
|
@@ -57,9 +60,9 @@ PR mode requires:
|
|
|
57
60
|
- running inside a git checkout;
|
|
58
61
|
- GitHub CLI `gh` installed and on `PATH`;
|
|
59
62
|
- `gh auth login` completed for the target host/repository, including private repositories;
|
|
60
|
-
- a PR number that `gh pr view` can resolve from the current checkout,
|
|
63
|
+
- a PR number that `gh pr view` can resolve from the current checkout, a full GitHub PR URL, or a current non-`main` branch with an existing PR that `gh pr view` can resolve.
|
|
61
64
|
|
|
62
|
-
The command uses read-only `gh` calls to fetch PR metadata, review comments, issue comments, and review
|
|
65
|
+
The command uses read-only `git`/`gh` calls to detect the current branch PR when no target is supplied, then read-only `gh` calls to fetch PR metadata, review comments, PR issue comments, review bodies, and best-effort review-thread resolved/outdated metadata. It does not post comments, submit reviews, checkout branches, or mutate GitHub.
|
|
63
66
|
|
|
64
67
|
## `triage_comments` tool behavior
|
|
65
68
|
|
|
@@ -131,5 +134,6 @@ Do not implement changes from this triage automatically; ask the parent/user whi
|
|
|
131
134
|
- At most 50 comments can be triaged in one tool call.
|
|
132
135
|
- The subagent has an 8-turn and 8-minute budget.
|
|
133
136
|
- PR mode depends on the GitHub API data available to `gh`; authentication, permissions, host configuration, and API availability can affect what is fetched.
|
|
137
|
+
- Resolved/outdated filtering is best effort and only applies to inline review comments. If GitHub does not return review-thread metadata for an inline comment, `/triage-comments` keeps it visible and labels the thread state as unavailable.
|
|
134
138
|
- The tool validates against the current local checkout. If the checkout does not match the PR head/base or supplied diff context, the result may be `needs clarification` or call out stale/missing evidence.
|
|
135
139
|
- Paste mode treats the editor contents as one feedback item; use PR mode or direct tool calls for multiple separately numbered comments.
|
|
@@ -20,9 +20,34 @@ const MAX_RUN_MS = 8 * 60 * 1000;
|
|
|
20
20
|
const DEFAULT_BASH_TIMEOUT_SECONDS = 30;
|
|
21
21
|
const COLLAPSED_PREVIEW_LINES = 18;
|
|
22
22
|
const GH_COMMAND_TIMEOUT_MS = 30_000;
|
|
23
|
+
const GH_PR_METADATA_FIELDS = 'number,title,url,headRefName,headRefOid,baseRefName,baseRefOid';
|
|
24
|
+
const GH_REVIEW_THREADS_GRAPHQL_QUERY = `
|
|
25
|
+
query TriageCommentsReviewThreads($owner: String!, $name: String!, $number: Int!, $after: String) {
|
|
26
|
+
repository(owner: $owner, name: $name) {
|
|
27
|
+
pullRequest(number: $number) {
|
|
28
|
+
reviewThreads(first: 100, after: $after) {
|
|
29
|
+
pageInfo {
|
|
30
|
+
hasNextPage
|
|
31
|
+
endCursor
|
|
32
|
+
}
|
|
33
|
+
nodes {
|
|
34
|
+
id
|
|
35
|
+
isResolved
|
|
36
|
+
isOutdated
|
|
37
|
+
comments(first: 100) {
|
|
38
|
+
nodes {
|
|
39
|
+
id
|
|
40
|
+
databaseId
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}`;
|
|
23
48
|
const COMMENT_DISPLAY_BODY_LIMIT = 1200;
|
|
24
49
|
const TRIAGE_COMMAND_USAGE =
|
|
25
|
-
"Usage: /triage-comments [paste | pr <PR URL or number>]\nInteractive UI mode lets you paste feedback or fetch PR comments, then confirm all comments or choose a subset such as 1,3-5.";
|
|
50
|
+
"Usage: /triage-comments [paste | pr [<PR URL or number>] | <PR URL or number>]\nInteractive UI mode lets you paste feedback or fetch PR comments, optionally hide resolved/outdated inline review comments, then confirm all displayed comments or choose a subset such as 1,3-5. PR mode without a target first tries to detect an open PR for the current non-main branch.";
|
|
26
51
|
const IMPLEMENTATION_NOTE =
|
|
27
52
|
"Do not implement changes from this triage automatically; ask the parent/user which option to take before implementation.";
|
|
28
53
|
|
|
@@ -185,6 +210,10 @@ function asFiniteNumber(value: unknown): number | undefined {
|
|
|
185
210
|
return undefined;
|
|
186
211
|
}
|
|
187
212
|
|
|
213
|
+
function asBoolean(value: unknown): boolean | undefined {
|
|
214
|
+
return typeof value === "boolean" ? value : undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
188
217
|
function normalizeComment(raw: unknown, index: number): NormalizedComment | undefined {
|
|
189
218
|
if (typeof raw === "string") {
|
|
190
219
|
const body = raw.trim();
|
|
@@ -855,6 +884,34 @@ type PullRequestContext = {
|
|
|
855
884
|
baseSha?: string;
|
|
856
885
|
};
|
|
857
886
|
|
|
887
|
+
type ReviewThreadState = {
|
|
888
|
+
threadId?: string;
|
|
889
|
+
isResolved?: boolean;
|
|
890
|
+
isOutdated?: boolean;
|
|
891
|
+
metadataAvailable: boolean;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
type ReviewThreadStateLookup = {
|
|
895
|
+
byDatabaseId: Map<number, ReviewThreadState>;
|
|
896
|
+
byNodeId: Map<string, ReviewThreadState>;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
type InlineCommentFilter = {
|
|
900
|
+
hideResolved: boolean;
|
|
901
|
+
hideOutdated: boolean;
|
|
902
|
+
label: string;
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
type AppliedInlineCommentFilter = {
|
|
906
|
+
filter: InlineCommentFilter;
|
|
907
|
+
originalCount: number;
|
|
908
|
+
displayedCount: number;
|
|
909
|
+
hiddenInlineCount: number;
|
|
910
|
+
hiddenResolvedInlineCount: number;
|
|
911
|
+
hiddenOutdatedInlineCount: number;
|
|
912
|
+
keptInlineWithoutThreadMetadataCount: number;
|
|
913
|
+
};
|
|
914
|
+
|
|
858
915
|
type CommandComment = {
|
|
859
916
|
id: string;
|
|
860
917
|
body: string;
|
|
@@ -866,6 +923,7 @@ type CommandComment = {
|
|
|
866
923
|
author?: string;
|
|
867
924
|
url?: string;
|
|
868
925
|
createdAt?: string;
|
|
926
|
+
reviewThread?: ReviewThreadState;
|
|
869
927
|
metadata: Record<string, unknown>;
|
|
870
928
|
sourceLabel: string;
|
|
871
929
|
displayNumber?: number;
|
|
@@ -1097,7 +1155,107 @@ function reviewHtmlUrl(record: Record<string, unknown>): string | undefined {
|
|
|
1097
1155
|
return asTrimmedString(record.html_url ?? record.htmlUrl ?? html?.href);
|
|
1098
1156
|
}
|
|
1099
1157
|
|
|
1100
|
-
function
|
|
1158
|
+
function createReviewThreadStateLookup(): ReviewThreadStateLookup {
|
|
1159
|
+
return { byDatabaseId: new Map(), byNodeId: new Map() };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function reviewThreadStateForComment(raw: unknown, lookup: ReviewThreadStateLookup): ReviewThreadState {
|
|
1163
|
+
const record = asRecord(raw);
|
|
1164
|
+
const databaseId = asFiniteNumber(record?.id);
|
|
1165
|
+
const nodeId = asTrimmedString(record?.node_id ?? record?.nodeId);
|
|
1166
|
+
return (
|
|
1167
|
+
(databaseId !== undefined ? lookup.byDatabaseId.get(databaseId) : undefined) ??
|
|
1168
|
+
(nodeId ? lookup.byNodeId.get(nodeId) : undefined) ??
|
|
1169
|
+
{ metadataAvailable: false }
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function fetchReviewThreadStateLookup(
|
|
1174
|
+
pi: ExtensionAPI,
|
|
1175
|
+
ctx: ExtensionCommandContext,
|
|
1176
|
+
pr: PullRequestContext,
|
|
1177
|
+
): Promise<ReviewThreadStateLookup> {
|
|
1178
|
+
const [owner, repo] = pr.repository.split('/');
|
|
1179
|
+
if (!owner || !repo) throw new Error(`Could not build GitHub GraphQL variables from repository ${pr.repository}.`);
|
|
1180
|
+
|
|
1181
|
+
const lookup = createReviewThreadStateLookup();
|
|
1182
|
+
let after: string | undefined;
|
|
1183
|
+
|
|
1184
|
+
while (true) {
|
|
1185
|
+
const args = ['api'];
|
|
1186
|
+
if (pr.host) args.push('--hostname', pr.host);
|
|
1187
|
+
args.push(
|
|
1188
|
+
'graphql',
|
|
1189
|
+
'-f',
|
|
1190
|
+
`owner=${owner}`,
|
|
1191
|
+
'-f',
|
|
1192
|
+
`name=${repo}`,
|
|
1193
|
+
'-F',
|
|
1194
|
+
`number=${pr.number}`,
|
|
1195
|
+
'-f',
|
|
1196
|
+
`query=${GH_REVIEW_THREADS_GRAPHQL_QUERY}`,
|
|
1197
|
+
);
|
|
1198
|
+
if (after) args.push('-f', `after=${after}`);
|
|
1199
|
+
|
|
1200
|
+
const label = 'Fetching review thread resolved/outdated metadata with gh';
|
|
1201
|
+
const stdout = await execChecked(pi, ctx, 'gh', args, label);
|
|
1202
|
+
const raw = parseJsonOutput(stdout, label);
|
|
1203
|
+
const data = asRecord(raw)?.data;
|
|
1204
|
+
const repository = asRecord(asRecord(data)?.repository);
|
|
1205
|
+
const pullRequest = asRecord(repository?.pullRequest);
|
|
1206
|
+
const reviewThreads = asRecord(pullRequest?.reviewThreads);
|
|
1207
|
+
if (!reviewThreads) throw new Error('GitHub GraphQL response did not include reviewThreads.');
|
|
1208
|
+
|
|
1209
|
+
const nodes = Array.isArray(reviewThreads.nodes) ? reviewThreads.nodes : [];
|
|
1210
|
+
for (const rawThread of nodes) {
|
|
1211
|
+
const thread = asRecord(rawThread);
|
|
1212
|
+
if (!thread) continue;
|
|
1213
|
+
const state: ReviewThreadState = {
|
|
1214
|
+
threadId: asTrimmedString(thread.id),
|
|
1215
|
+
isResolved: asBoolean(thread.isResolved),
|
|
1216
|
+
isOutdated: asBoolean(thread.isOutdated),
|
|
1217
|
+
metadataAvailable: true,
|
|
1218
|
+
};
|
|
1219
|
+
const comments = asRecord(thread.comments);
|
|
1220
|
+
const commentNodes = Array.isArray(comments?.nodes) ? comments.nodes : [];
|
|
1221
|
+
for (const rawComment of commentNodes) {
|
|
1222
|
+
const comment = asRecord(rawComment);
|
|
1223
|
+
if (!comment) continue;
|
|
1224
|
+
const databaseId = asFiniteNumber(comment.databaseId);
|
|
1225
|
+
const nodeId = asTrimmedString(comment.id);
|
|
1226
|
+
if (databaseId !== undefined) lookup.byDatabaseId.set(databaseId, state);
|
|
1227
|
+
if (nodeId) lookup.byNodeId.set(nodeId, state);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const pageInfo = asRecord(reviewThreads.pageInfo);
|
|
1232
|
+
if (!asBoolean(pageInfo?.hasNextPage)) break;
|
|
1233
|
+
const endCursor = asTrimmedString(pageInfo?.endCursor);
|
|
1234
|
+
if (!endCursor) break;
|
|
1235
|
+
after = endCursor;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return lookup;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
async function fetchReviewThreadStateLookupBestEffort(
|
|
1242
|
+
pi: ExtensionAPI,
|
|
1243
|
+
ctx: ExtensionCommandContext,
|
|
1244
|
+
pr: PullRequestContext,
|
|
1245
|
+
): Promise<ReviewThreadStateLookup> {
|
|
1246
|
+
try {
|
|
1247
|
+
return await fetchReviewThreadStateLookup(pi, ctx, pr);
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
notifyCommand(
|
|
1250
|
+
ctx,
|
|
1251
|
+
`Could not fetch inline review-thread resolved/outdated metadata; inline review comments without thread metadata will stay visible. ${formatErrorMessage(error)}`,
|
|
1252
|
+
'warning',
|
|
1253
|
+
);
|
|
1254
|
+
return createReviewThreadStateLookup();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function normalizeReviewComment(raw: unknown, pr: PullRequestContext, sortIndex: number, reviewThread: ReviewThreadState): CommandComment | undefined {
|
|
1101
1259
|
const record = asRecord(raw);
|
|
1102
1260
|
if (!record) return undefined;
|
|
1103
1261
|
const body = asTrimmedString(record.body);
|
|
@@ -1123,6 +1281,7 @@ function normalizeReviewComment(raw: unknown, pr: PullRequestContext, sortIndex:
|
|
|
1123
1281
|
author: githubLogin(record.user),
|
|
1124
1282
|
url: asTrimmedString(record.html_url ?? record.htmlUrl),
|
|
1125
1283
|
createdAt,
|
|
1284
|
+
reviewThread,
|
|
1126
1285
|
metadata: compactRecord({
|
|
1127
1286
|
source: 'pull_request_review_comment',
|
|
1128
1287
|
repository: pr.repository,
|
|
@@ -1130,6 +1289,12 @@ function normalizeReviewComment(raw: unknown, pr: PullRequestContext, sortIndex:
|
|
|
1130
1289
|
host: pr.host,
|
|
1131
1290
|
databaseId,
|
|
1132
1291
|
nodeId,
|
|
1292
|
+
reviewThread: compactRecord({
|
|
1293
|
+
threadId: reviewThread.threadId,
|
|
1294
|
+
isResolved: reviewThread.isResolved,
|
|
1295
|
+
isOutdated: reviewThread.isOutdated,
|
|
1296
|
+
metadataAvailable: reviewThread.metadataAvailable,
|
|
1297
|
+
}),
|
|
1133
1298
|
pullRequestReviewId: asFiniteNumber(record.pull_request_review_id ?? record.pullRequestReviewId),
|
|
1134
1299
|
commitId: asTrimmedString(record.commit_id ?? record.commitId),
|
|
1135
1300
|
originalCommitId: asTrimmedString(record.original_commit_id ?? record.originalCommitId),
|
|
@@ -1234,35 +1399,64 @@ function assignDisplayNumbers(comments: CommandComment[]): CommandComment[] {
|
|
|
1234
1399
|
}));
|
|
1235
1400
|
}
|
|
1236
1401
|
|
|
1237
|
-
|
|
1402
|
+
function prContextFallbackTarget(raw: unknown, explicitTarget?: string): string {
|
|
1403
|
+
const record = asRecord(raw);
|
|
1404
|
+
const number = asFiniteNumber(record?.number);
|
|
1405
|
+
return explicitTarget ?? asTrimmedString(record?.url) ?? (number ? String(number) : '');
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
async function fetchPullRequestMetadata(
|
|
1238
1409
|
pi: ExtensionAPI,
|
|
1239
1410
|
ctx: ExtensionCommandContext,
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1411
|
+
label: string,
|
|
1412
|
+
target?: string,
|
|
1413
|
+
): Promise<PullRequestContext> {
|
|
1414
|
+
const args = ['pr', 'view'];
|
|
1415
|
+
if (target) args.push(target);
|
|
1416
|
+
args.push('--json', GH_PR_METADATA_FIELDS);
|
|
1246
1417
|
|
|
1247
|
-
await
|
|
1248
|
-
|
|
1418
|
+
const stdout = await execChecked(pi, ctx, 'gh', args, label);
|
|
1419
|
+
const raw = parseJsonOutput(stdout, label);
|
|
1420
|
+
return normalizePullRequestContext(raw, prContextFallbackTarget(raw, target));
|
|
1421
|
+
}
|
|
1249
1422
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1423
|
+
async function detectCurrentBranchPullRequest(
|
|
1424
|
+
pi: ExtensionAPI,
|
|
1425
|
+
ctx: ExtensionCommandContext,
|
|
1426
|
+
): Promise<PullRequestContext | undefined> {
|
|
1427
|
+
try {
|
|
1428
|
+
const branch = await pi.exec('git', ['branch', '--show-current'], {
|
|
1429
|
+
cwd: ctx.cwd,
|
|
1430
|
+
signal: ctx.signal,
|
|
1431
|
+
timeout: GH_COMMAND_TIMEOUT_MS,
|
|
1432
|
+
});
|
|
1433
|
+
if (branch.killed || branch.code !== 0) return undefined;
|
|
1434
|
+
|
|
1435
|
+
const branchName = branch.stdout.trim();
|
|
1436
|
+
if (!branchName || branchName === 'main') return undefined;
|
|
1437
|
+
|
|
1438
|
+
return await fetchPullRequestMetadata(pi, ctx, 'Detecting current branch PR with gh');
|
|
1439
|
+
} catch {
|
|
1440
|
+
return undefined;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function fetchCommentsForPullRequest(
|
|
1445
|
+
pi: ExtensionAPI,
|
|
1446
|
+
ctx: ExtensionCommandContext,
|
|
1447
|
+
pr: PullRequestContext,
|
|
1448
|
+
): Promise<CommandComment[]> {
|
|
1258
1449
|
const reviewComments = await ghApiArray(pi, ctx, pr, `pulls/${pr.number}/comments?per_page=100`, 'review comments');
|
|
1450
|
+
const reviewThreadLookup = reviewComments.length > 0
|
|
1451
|
+
? await fetchReviewThreadStateLookupBestEffort(pi, ctx, pr)
|
|
1452
|
+
: createReviewThreadStateLookup();
|
|
1259
1453
|
const issueComments = await ghApiArray(pi, ctx, pr, `issues/${pr.number}/comments?per_page=100`, 'issue comments');
|
|
1260
1454
|
const reviews = await ghApiArray(pi, ctx, pr, `pulls/${pr.number}/reviews?per_page=100`, 'review bodies');
|
|
1261
1455
|
|
|
1262
1456
|
let sortIndex = 0;
|
|
1263
1457
|
const comments: CommandComment[] = [];
|
|
1264
1458
|
for (const raw of reviewComments) {
|
|
1265
|
-
const comment = normalizeReviewComment(raw, pr, ++sortIndex);
|
|
1459
|
+
const comment = normalizeReviewComment(raw, pr, ++sortIndex, reviewThreadStateForComment(raw, reviewThreadLookup));
|
|
1266
1460
|
if (comment) comments.push(comment);
|
|
1267
1461
|
}
|
|
1268
1462
|
for (const raw of issueComments) {
|
|
@@ -1274,7 +1468,25 @@ async function fetchPrCommentsForTriage(
|
|
|
1274
1468
|
if (comment) comments.push(comment);
|
|
1275
1469
|
}
|
|
1276
1470
|
|
|
1277
|
-
return
|
|
1471
|
+
return assignDisplayNumbers(comments.sort(compareCommandComments));
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
async function fetchPrCommentsForTriage(
|
|
1475
|
+
pi: ExtensionAPI,
|
|
1476
|
+
ctx: ExtensionCommandContext,
|
|
1477
|
+
target: string,
|
|
1478
|
+
): Promise<{ pr: PullRequestContext; comments: CommandComment[] }> {
|
|
1479
|
+
const normalizedTarget = normalizePrTarget(target);
|
|
1480
|
+
if (!normalizedTarget) {
|
|
1481
|
+
throw new Error('PR input must be a PR number (for example, 123 or #123) or a GitHub pull request URL.');
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
await assertGitRepo(pi, ctx);
|
|
1485
|
+
await assertGhReady(pi, ctx);
|
|
1486
|
+
|
|
1487
|
+
const pr = await fetchPullRequestMetadata(pi, ctx, 'Fetching PR metadata with gh', normalizedTarget);
|
|
1488
|
+
const comments = await fetchCommentsForPullRequest(pi, ctx, pr);
|
|
1489
|
+
return { pr, comments };
|
|
1278
1490
|
}
|
|
1279
1491
|
|
|
1280
1492
|
function truncateForDisplay(text: string, max: number): string {
|
|
@@ -1294,21 +1506,139 @@ function formatCommentLocation(comment: CommandComment): string {
|
|
|
1294
1506
|
return lineRange ? `${comment.path}:${lineRange}` : comment.path;
|
|
1295
1507
|
}
|
|
1296
1508
|
|
|
1297
|
-
function
|
|
1509
|
+
function isInlineReviewComment(comment: CommandComment): boolean {
|
|
1510
|
+
return asTrimmedString(comment.metadata.source) === 'pull_request_review_comment';
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function formatReviewThreadState(comment: CommandComment): string | undefined {
|
|
1514
|
+
if (!isInlineReviewComment(comment)) return undefined;
|
|
1515
|
+
const reviewThread = comment.reviewThread;
|
|
1516
|
+
if (!reviewThread?.metadataAvailable) return 'thread: resolved/outdated state unavailable (kept visible)';
|
|
1517
|
+
const resolved = reviewThread.isResolved === true
|
|
1518
|
+
? 'resolved'
|
|
1519
|
+
: reviewThread.isResolved === false
|
|
1520
|
+
? 'unresolved'
|
|
1521
|
+
: 'resolved state unavailable';
|
|
1522
|
+
const outdated = reviewThread.isOutdated === true
|
|
1523
|
+
? 'outdated'
|
|
1524
|
+
: reviewThread.isOutdated === false
|
|
1525
|
+
? 'current'
|
|
1526
|
+
: 'outdated state unavailable';
|
|
1527
|
+
return `thread: ${resolved}, ${outdated}`;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function formatInlineFilterNotice(summary: AppliedInlineCommentFilter): string {
|
|
1531
|
+
const scope = 'Only inline review comments can be filtered; PR issue comments, review bodies, and inline comments without thread metadata remain visible.';
|
|
1532
|
+
if (!summary.filter.hideResolved && !summary.filter.hideOutdated) {
|
|
1533
|
+
return `Filter: showing all fetched comments. ${scope}`;
|
|
1534
|
+
}
|
|
1535
|
+
const hiddenParts = [
|
|
1536
|
+
summary.hiddenResolvedInlineCount > 0 ? `${summary.hiddenResolvedInlineCount} resolved` : undefined,
|
|
1537
|
+
summary.hiddenOutdatedInlineCount > 0 ? `${summary.hiddenOutdatedInlineCount} outdated` : undefined,
|
|
1538
|
+
]
|
|
1539
|
+
.filter(Boolean)
|
|
1540
|
+
.join(', ');
|
|
1541
|
+
const hiddenDetail = hiddenParts ? ` (${hiddenParts})` : '';
|
|
1542
|
+
return `Filter: ${summary.filter.label}. Hidden ${summary.hiddenInlineCount} inline review comment(s)${hiddenDetail}; displaying ${summary.displayedCount} of ${summary.originalCount} fetched comment(s). ${scope}`;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function formatInlineFilterContext(summary: AppliedInlineCommentFilter): string {
|
|
1546
|
+
return `${formatInlineFilterNotice(summary)} Inline comments kept without thread metadata: ${summary.keptInlineWithoutThreadMetadataCount}.`;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function formatFetchedCommentsForSelection(pr: PullRequestContext, comments: CommandComment[], filterSummary?: AppliedInlineCommentFilter): string {
|
|
1298
1550
|
const title = pr.title ? ` — ${pr.title}` : '';
|
|
1299
1551
|
const limitNotice = comments.length > MAX_COMMENTS
|
|
1300
1552
|
? `\n\ntriage_comments can investigate at most ${MAX_COMMENTS} comments per run, so choose a subset.`
|
|
1301
1553
|
: '\n\nChoose whether to investigate all displayed comments or select a subset.';
|
|
1302
|
-
const
|
|
1554
|
+
const fetchedCount = filterSummary?.originalCount ?? comments.length;
|
|
1555
|
+
const displayedCount = comments.length;
|
|
1556
|
+
const countSummary = filterSummary
|
|
1557
|
+
? `Fetched ${fetchedCount} comment(s) from ${pr.repository}#${pr.number}${title}; displaying ${displayedCount} numbered comment(s) after filtering.`
|
|
1558
|
+
: `Fetched ${displayedCount} numbered comment(s) from ${pr.repository}#${pr.number}${title}.`;
|
|
1559
|
+
const filterNotice = filterSummary ? `\n\n${formatInlineFilterNotice(filterSummary)}` : '';
|
|
1560
|
+
const header = `${countSummary}${filterNotice}${limitNotice}`;
|
|
1303
1561
|
const body = comments.map((comment) => {
|
|
1304
1562
|
const author = comment.author ? ` by @${comment.author}` : '';
|
|
1305
1563
|
const url = comment.url ? `\n url: ${comment.url}` : '';
|
|
1564
|
+
const threadState = formatReviewThreadState(comment);
|
|
1565
|
+
const threadLine = threadState ? `\n ${threadState}` : '';
|
|
1306
1566
|
const preview = indentLines(truncateForDisplay(comment.body, COMMENT_DISPLAY_BODY_LIMIT), ' ');
|
|
1307
|
-
return `${comment.displayNumber ?? '?'}. ${comment.sourceLabel}${author} — ${formatCommentLocation(comment)}\n id: ${comment.id}${url}\n${preview}`;
|
|
1567
|
+
return `${comment.displayNumber ?? '?'}. ${comment.sourceLabel}${author} — ${formatCommentLocation(comment)}\n id: ${comment.id}${url}${threadLine}\n${preview}`;
|
|
1308
1568
|
});
|
|
1309
1569
|
return [header, ...body].join('\n\n');
|
|
1310
1570
|
}
|
|
1311
1571
|
|
|
1572
|
+
async function promptForInlineCommentFilter(ctx: ExtensionCommandContext): Promise<InlineCommentFilter | undefined> {
|
|
1573
|
+
const prompt = [
|
|
1574
|
+
'/triage-comments: filter inline review comments before display',
|
|
1575
|
+
'GitHub exposes resolved/outdated state only for inline review threads. PR issue comments and review bodies will stay visible, and inline comments without thread metadata will stay visible.',
|
|
1576
|
+
].join('\n\n');
|
|
1577
|
+
const showAll = 'Show all fetched comments';
|
|
1578
|
+
const hideResolved = 'Hide resolved inline review comments';
|
|
1579
|
+
const hideOutdated = 'Hide outdated inline review comments';
|
|
1580
|
+
const hideBoth = 'Hide resolved and outdated inline review comments';
|
|
1581
|
+
const cancel = 'Cancel';
|
|
1582
|
+
const decision = await ctx.ui.select(prompt, [showAll, hideResolved, hideOutdated, hideBoth, cancel]);
|
|
1583
|
+
if (!decision || decision === cancel) return undefined;
|
|
1584
|
+
if (decision === hideResolved) return { hideResolved: true, hideOutdated: false, label: 'hiding resolved inline review comments' };
|
|
1585
|
+
if (decision === hideOutdated) return { hideResolved: false, hideOutdated: true, label: 'hiding outdated inline review comments' };
|
|
1586
|
+
if (decision === hideBoth) return { hideResolved: true, hideOutdated: true, label: 'hiding resolved or outdated inline review comments' };
|
|
1587
|
+
return { hideResolved: false, hideOutdated: false, label: 'showing all fetched comments' };
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function applyInlineCommentFilter(
|
|
1591
|
+
comments: CommandComment[],
|
|
1592
|
+
filter: InlineCommentFilter,
|
|
1593
|
+
): { comments: CommandComment[]; summary: AppliedInlineCommentFilter } {
|
|
1594
|
+
const kept: CommandComment[] = [];
|
|
1595
|
+
let hiddenInlineCount = 0;
|
|
1596
|
+
let hiddenResolvedInlineCount = 0;
|
|
1597
|
+
let hiddenOutdatedInlineCount = 0;
|
|
1598
|
+
let keptInlineWithoutThreadMetadataCount = 0;
|
|
1599
|
+
|
|
1600
|
+
for (const comment of comments) {
|
|
1601
|
+
if (!isInlineReviewComment(comment)) {
|
|
1602
|
+
kept.push(comment);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const reviewThread = comment.reviewThread;
|
|
1607
|
+
const hasThreadMetadata = Boolean(reviewThread?.metadataAvailable);
|
|
1608
|
+
const hideForResolved = filter.hideResolved && reviewThread?.isResolved === true;
|
|
1609
|
+
const hideForOutdated = filter.hideOutdated && reviewThread?.isOutdated === true;
|
|
1610
|
+
if (hideForResolved || hideForOutdated) {
|
|
1611
|
+
hiddenInlineCount += 1;
|
|
1612
|
+
if (hideForResolved) hiddenResolvedInlineCount += 1;
|
|
1613
|
+
if (hideForOutdated) hiddenOutdatedInlineCount += 1;
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (!hasThreadMetadata) keptInlineWithoutThreadMetadataCount += 1;
|
|
1618
|
+
kept.push(comment);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const commentsWithFilterMetadata = kept.map((comment) => ({
|
|
1622
|
+
...comment,
|
|
1623
|
+
metadata: compactRecord({
|
|
1624
|
+
...comment.metadata,
|
|
1625
|
+
preFilterDisplayNumber: comment.displayNumber,
|
|
1626
|
+
}),
|
|
1627
|
+
}));
|
|
1628
|
+
|
|
1629
|
+
const summary: AppliedInlineCommentFilter = {
|
|
1630
|
+
filter,
|
|
1631
|
+
originalCount: comments.length,
|
|
1632
|
+
displayedCount: kept.length,
|
|
1633
|
+
hiddenInlineCount,
|
|
1634
|
+
hiddenResolvedInlineCount,
|
|
1635
|
+
hiddenOutdatedInlineCount,
|
|
1636
|
+
keptInlineWithoutThreadMetadataCount,
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
return { comments: assignDisplayNumbers(commentsWithFilterMetadata), summary };
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1312
1642
|
function parseSelectionList(input: string, max: number): SelectionParseResult {
|
|
1313
1643
|
const trimmed = input.trim();
|
|
1314
1644
|
if (!trimmed) return { ok: false, error: 'Enter comment numbers such as 1,3-5.' };
|
|
@@ -1384,11 +1714,12 @@ async function choosePrComments(
|
|
|
1384
1714
|
ctx: ExtensionCommandContext,
|
|
1385
1715
|
pr: PullRequestContext,
|
|
1386
1716
|
comments: CommandComment[],
|
|
1717
|
+
filterSummary?: AppliedInlineCommentFilter,
|
|
1387
1718
|
): Promise<CommandComment[] | undefined> {
|
|
1388
1719
|
const options = comments.length <= MAX_COMMENTS
|
|
1389
1720
|
? ['Investigate all displayed comments', 'Choose a subset', 'Cancel']
|
|
1390
1721
|
: ['Choose a subset', 'Cancel'];
|
|
1391
|
-
const selectionPrompt = formatFetchedCommentsForSelection(pr, comments);
|
|
1722
|
+
const selectionPrompt = formatFetchedCommentsForSelection(pr, comments, filterSummary);
|
|
1392
1723
|
const decision = await ctx.ui.select(selectionPrompt, options);
|
|
1393
1724
|
if (!decision || decision === 'Cancel') return undefined;
|
|
1394
1725
|
if (decision === 'Investigate all displayed comments') return comments;
|
|
@@ -1428,13 +1759,16 @@ function toPayloadComment(comment: CommandComment): Record<string, unknown> {
|
|
|
1428
1759
|
|
|
1429
1760
|
function buildCommandPayload(
|
|
1430
1761
|
comments: CommandComment[],
|
|
1431
|
-
options: { pr?: PullRequestContext; totalDisplayed: number; source: 'paste' | 'pr' },
|
|
1762
|
+
options: { pr?: PullRequestContext; totalDisplayed: number; source: 'paste' | 'pr'; filterSummary?: AppliedInlineCommentFilter },
|
|
1432
1763
|
): TriageCommandPayload {
|
|
1764
|
+
const prSelectionContext = options.filterSummary
|
|
1765
|
+
? `Selected by /triage-comments after fetching ${options.filterSummary.originalCount} PR comment(s), applying the inline review-comment filter, displaying ${options.totalDisplayed} comment(s), and receiving explicit user selection. ${formatInlineFilterContext(options.filterSummary)}`
|
|
1766
|
+
: `Selected by /triage-comments after displaying ${options.totalDisplayed} fetched PR comment(s) and receiving explicit user selection.`;
|
|
1433
1767
|
const payload: TriageCommandPayload = {
|
|
1434
1768
|
comments: comments.map(toPayloadComment),
|
|
1435
1769
|
context:
|
|
1436
1770
|
options.source === 'pr'
|
|
1437
|
-
?
|
|
1771
|
+
? `${prSelectionContext} Do not implement changes until the user chooses a handling option.`
|
|
1438
1772
|
: 'Pasted feedback captured by /triage-comments. Do not implement changes until the user chooses a handling option.',
|
|
1439
1773
|
};
|
|
1440
1774
|
|
|
@@ -1490,6 +1824,18 @@ async function runPasteMode(pi: ExtensionAPI, ctx: ExtensionCommandContext, pref
|
|
|
1490
1824
|
|
|
1491
1825
|
async function runPrMode(pi: ExtensionAPI, ctx: ExtensionCommandContext, target?: string): Promise<void> {
|
|
1492
1826
|
let prTarget = target?.trim();
|
|
1827
|
+
let detectedPr: PullRequestContext | undefined;
|
|
1828
|
+
|
|
1829
|
+
if (!prTarget) {
|
|
1830
|
+
ctx.ui.setStatus('triage-comments', 'triage-comments: checking current branch PR…');
|
|
1831
|
+
detectedPr = await detectCurrentBranchPullRequest(pi, ctx);
|
|
1832
|
+
ctx.ui.setStatus('triage-comments', undefined);
|
|
1833
|
+
if (detectedPr) {
|
|
1834
|
+
prTarget = detectedPr.url ?? String(detectedPr.number);
|
|
1835
|
+
notifyCommand(ctx, `Detected PR ${detectedPr.repository}#${detectedPr.number} from the current branch.`, 'info');
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1493
1839
|
if (!prTarget) {
|
|
1494
1840
|
const input = await ctx.ui.input('PR URL or number', 'For example: 123, #123, or https://github.com/owner/repo/pull/123');
|
|
1495
1841
|
if (input === undefined) return;
|
|
@@ -1504,16 +1850,26 @@ async function runPrMode(pi: ExtensionAPI, ctx: ExtensionCommandContext, target?
|
|
|
1504
1850
|
ctx.ui.setStatus('triage-comments', 'triage-comments: fetching PR comments…');
|
|
1505
1851
|
try {
|
|
1506
1852
|
notifyCommand(ctx, 'Fetching PR comments with gh (read-only)…', 'info');
|
|
1507
|
-
const { pr, comments } =
|
|
1853
|
+
const { pr, comments } = detectedPr
|
|
1854
|
+
? { pr: detectedPr, comments: await fetchCommentsForPullRequest(pi, ctx, detectedPr) }
|
|
1855
|
+
: await fetchPrCommentsForTriage(pi, ctx, prTarget);
|
|
1508
1856
|
if (comments.length === 0) {
|
|
1509
1857
|
notifyCommand(ctx, `No review comments, issue comments, or review bodies were found for ${pr.repository}#${pr.number}.`, 'info');
|
|
1510
1858
|
return;
|
|
1511
1859
|
}
|
|
1512
1860
|
|
|
1513
|
-
const
|
|
1861
|
+
const filter = await promptForInlineCommentFilter(ctx);
|
|
1862
|
+
if (!filter) return;
|
|
1863
|
+
const filtered = applyInlineCommentFilter(comments, filter);
|
|
1864
|
+
if (filtered.comments.length === 0) {
|
|
1865
|
+
notifyCommand(ctx, `${formatInlineFilterNotice(filtered.summary)} No comments remain to send to triage_comments.`, 'info');
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const selected = await choosePrComments(ctx, pr, filtered.comments, filtered.summary);
|
|
1514
1870
|
if (!selected || selected.length === 0) return;
|
|
1515
|
-
const payload = buildCommandPayload(selected, { pr, totalDisplayed: comments.length, source: 'pr' });
|
|
1516
|
-
sendTriageUserMessage(pi, ctx, payload, selected.length, comments.length);
|
|
1871
|
+
const payload = buildCommandPayload(selected, { pr, totalDisplayed: filtered.comments.length, source: 'pr', filterSummary: filtered.summary });
|
|
1872
|
+
sendTriageUserMessage(pi, ctx, payload, selected.length, filtered.comments.length);
|
|
1517
1873
|
} catch (error) {
|
|
1518
1874
|
notifyCommand(ctx, formatErrorMessage(error), 'error');
|
|
1519
1875
|
} finally {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-triage-comments",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A pi extension that adds /triage-comments and a read-only triage_comments subagent tool for review-comment triage.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "triage", "comments", "review", "subagent"],
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "A collection of pi extensions for context management, review-comment triage, notifications, safety guards, GitHub research, todos, tool rendering, and model/provider helpers.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "terminal", "agent"],
|
|
6
6
|
"license": "MIT",
|