@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 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, sets reasoning to xhigh by default, and shows live status while running.
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 classifies selected PR review comments with evidence and handling options without implementing changes.
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
- - sets reasoning/thinking to `xhigh` by default for reasoning models
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 OpenAI/Codex, Anthropic, Google variants, GitHub Copilot, Bedrock, Azure OpenAI Responses, Cloudflare, DeepSeek, Fireworks, Groq, Hugging Face, Kimi/Moonshot, MiniMax, Mistral, OpenCode, OpenRouter, Vercel AI Gateway, xAI, ZAI, and Cerebras.
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 default to `xhigh`
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. Default: xhigh for reasoning models, off for non-reasoning models.",
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
- function resolveThinkingLevel(model: PiModel | undefined, override: ThinkingLevel | undefined): ThinkingLevel {
534
- if (override) return override;
535
- return model?.reasoning ? DEFAULT_THINKING_LEVEL : "off";
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 selectionReason = preferred
603
- ? `Selected ${winner.id} via the hardcoded preference list for ${winner.provider}.`
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: resolveThinkingLevel(winner, thinkingLevelOverride),
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 sets thinking to xhigh by default for reasoning models, unless the tool call explicitly overrides thinkingLevel.",
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: resolveThinkingLevel(matched, params.thinkingLevel),
960
+ thinkingLevel: thinking.effective,
961
+ ...(matched && thinking.clamped
962
+ ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
963
+ : {}),
897
964
  autoSelected: false,
898
- selectionReason: matched
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.7",
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 <PR URL or number>` fetches PR review comments, PR issue comments, and review bodies with `gh`, displays them as numbered items with stable IDs, and asks whether to investigate all displayed comments or an explicit subset such as `1,3-5`.
48
- - If more than 50 comments are fetched, you must choose a subset of at most 50 comments.
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, or a full GitHub PR URL.
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 bodies. It does not post comments, submit reviews, checkout branches, or mutate GitHub.
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 normalizeReviewComment(raw: unknown, pr: PullRequestContext, sortIndex: number): CommandComment | undefined {
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
- async function fetchPrCommentsForTriage(
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
- target: string,
1241
- ): Promise<{ pr: PullRequestContext; comments: CommandComment[] }> {
1242
- const normalizedTarget = normalizePrTarget(target);
1243
- if (!normalizedTarget) {
1244
- throw new Error('PR input must be a PR number (for example, 123 or #123) or a GitHub pull request URL.');
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 assertGitRepo(pi, ctx);
1248
- await assertGhReady(pi, ctx);
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
- const prStdout = await execChecked(
1251
- pi,
1252
- ctx,
1253
- 'gh',
1254
- ['pr', 'view', normalizedTarget, '--json', 'number,title,url,headRefName,headRefOid,baseRefName,baseRefOid'],
1255
- 'Fetching PR metadata with gh',
1256
- );
1257
- const pr = normalizePullRequestContext(parseJsonOutput(prStdout, 'Fetching PR metadata with gh'), normalizedTarget);
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 { pr, comments: assignDisplayNumbers(comments.sort(compareCommandComments)) };
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 formatFetchedCommentsForSelection(pr: PullRequestContext, comments: CommandComment[]): string {
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 header = `Fetched ${comments.length} numbered comment(s) from ${pr.repository}#${pr.number}${title}.${limitNotice}`;
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
- ? `Selected by /triage-comments after displaying ${options.totalDisplayed} fetched PR comment(s) and receiving explicit user selection. Do not implement changes until the user chooses a handling option.`
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 } = await fetchPrCommentsForTriage(pi, ctx, prTarget);
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 selected = await choosePrComments(ctx, pr, comments);
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.0",
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.26",
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",