@payconductor/houndsman 0.6.0 → 0.8.0

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.
Files changed (71) hide show
  1. package/README.md +20 -0
  2. package/package.json +1 -1
  3. package/src/cli/commands/command.ts +10 -1
  4. package/src/cli/commands/context-command.ts +40 -2
  5. package/src/cli/commands/hunt-command.ts +28 -2
  6. package/src/cli/commands/review-command.ts +30 -2
  7. package/src/cli/commands/server-command.ts +7 -1
  8. package/src/cli/commands/worktree-command.ts +118 -0
  9. package/src/cli/help.ts +13 -3
  10. package/src/cli/index.ts +14 -4
  11. package/src/core/ast/service.ts +7 -2
  12. package/src/core/brief.ts +1 -0
  13. package/src/core/commands/context.ts +9 -1
  14. package/src/core/commands/findings.ts +17 -6
  15. package/src/core/commands/review.ts +13 -0
  16. package/src/core/commands/scaffold.ts +4 -2
  17. package/src/core/config.ts +13 -0
  18. package/src/core/defaults.ts +8 -1
  19. package/src/core/engine/houndsman-engine.ts +26 -3
  20. package/src/core/engine/review-scope.ts +17 -22
  21. package/src/core/engine/review-session.ts +25 -2
  22. package/src/core/engine/review-workspace.ts +204 -0
  23. package/src/core/git-runner.ts +25 -13
  24. package/src/core/github-review.ts +10 -7
  25. package/src/core/jev/jev-detector.ts +10 -2
  26. package/src/core/orchestrator.ts +62 -13
  27. package/src/core/project-config.ts +12 -0
  28. package/src/core/services/gh-client.ts +38 -22
  29. package/src/core/services/git-repository.ts +46 -0
  30. package/src/core/services/pr-resolver.ts +147 -0
  31. package/src/core/services/worktree-manager.ts +269 -0
  32. package/src/core/storage/file/file-storage.ts +14 -1
  33. package/src/core/storage/paths.ts +30 -1
  34. package/src/core/storage/sqlite/mappers.ts +4 -0
  35. package/src/core/storage/sqlite/schema.ts +13 -3
  36. package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
  37. package/src/core/storage/types.ts +12 -0
  38. package/src/core/storage/work-area.ts +20 -1
  39. package/src/core/types.ts +2 -0
  40. package/src/harness/hunt/hunt-options.ts +17 -0
  41. package/src/harness/hunt/hunt-session.ts +7 -20
  42. package/src/index.ts +31 -0
  43. package/src/server/client/App.tsx +86 -22
  44. package/src/server/client/CommentsPanel.tsx +255 -0
  45. package/src/server/client/DeleteRunModal.tsx +163 -0
  46. package/src/server/client/FindingCard.tsx +12 -4
  47. package/src/server/client/FindingsList.tsx +4 -1
  48. package/src/server/client/ResumePanel.tsx +8 -8
  49. package/src/server/client/Sidebar.tsx +452 -0
  50. package/src/server/client/SubmitModal.tsx +61 -5
  51. package/src/server/client/icons.tsx +29 -0
  52. package/src/server/client/index.tsx +1 -1
  53. package/src/server/client/utils/constants.ts +2 -0
  54. package/src/server/client/utils/format.ts +47 -2
  55. package/src/server/client/utils/types.ts +108 -0
  56. package/src/server/editor.ts +129 -0
  57. package/src/server/index.ts +205 -3
  58. package/src/server/pr-comments.ts +256 -0
  59. package/src/server/pr-inbox.ts +213 -0
  60. package/src/server/run-repository.ts +136 -3
  61. package/src/skill/README.md +11 -3
  62. package/src/skill/SKILL.md +44 -10
  63. package/src/skill/hunt.md +33 -7
  64. package/src/tests/__snapshots__/baseline.test.ts.snap +94 -24
  65. package/src/tests/ai-core.test.ts +2 -2
  66. package/src/tests/cli.test.ts +35 -1
  67. package/src/tests/github-review.test.ts +9 -0
  68. package/src/tests/server-editor.test.ts +100 -0
  69. package/src/tests/server.test.ts +333 -2
  70. package/src/tests/worktree.test.ts +369 -0
  71. package/src/server/client/RunList.tsx +0 -75
@@ -14,11 +14,9 @@ import {
14
14
  type ModelProvider,
15
15
  } from "../../core/ai";
16
16
  import type { AstAnalysis } from "../../core/ast/types";
17
- import { collectDiff } from "../../core/diff-source";
18
17
  import type { HoundsmanEngine } from "../../core/engine/houndsman-engine";
19
18
  import type { RunClassification } from "../../core/engine/review-scope";
20
19
  import { parseEntries } from "../../core/file-classifier";
21
- import { runGit, tryGit } from "../../core/git-runner";
22
20
  import type { LoggerLike } from "../../core/logging";
23
21
  import type { PrepareResult } from "../../core/orchestrator";
24
22
  import { defaultOptions, type ReviewOptions } from "../../core/orchestrator";
@@ -67,6 +65,8 @@ export class HuntSession {
67
65
  clean: options.clean,
68
66
  noAi: true,
69
67
  finalize: false,
68
+ pr: options.pr,
69
+ worktreePath: options.worktreePath,
70
70
  };
71
71
 
72
72
  const session = engine.session(reviewOptions);
@@ -137,7 +137,7 @@ export class HuntSession {
137
137
  reporter,
138
138
  config: this.deps.config,
139
139
  ast: engine.ast,
140
- readFile: defaultFileReader(fileRoot()),
140
+ readFile: defaultFileReader(engine.root),
141
141
  });
142
142
  const runner = new AgentRunner(factory);
143
143
  const scheduler = new AgentScheduler(
@@ -265,11 +265,11 @@ export class HuntSession {
265
265
  work: import("../../core/storage/work-area").ReviewWorkArea,
266
266
  ) {
267
267
  const { engine, options } = this.deps;
268
- const diff = collectDiff(options.base, options.head);
268
+ const diff = engine.git.collectDiff(options.base, options.head);
269
269
  const entries = parseEntries(
270
- runGit(["diff", `${options.base}...${options.head}`, "--name-status"]),
270
+ engine.git.diffNameStatus(options.base, options.head),
271
271
  );
272
- const baseRef = mergeBase(options.base, options.head) ?? options.base;
272
+ const baseRef = engine.git.mergeBase(options.base, options.head) ?? options.base;
273
273
  const ast: Map<string, AstAnalysis> = options.noAst
274
274
  ? new Map()
275
275
  : await engine.ast.analyze(entries, diff, baseRef);
@@ -283,7 +283,7 @@ export class HuntSession {
283
283
  context,
284
284
  ast,
285
285
  impact,
286
- repoRoot: process.cwd(),
286
+ repoRoot: engine.root,
287
287
  diff,
288
288
  branch: review.branch,
289
289
  watchdog: options.watchdog,
@@ -357,16 +357,3 @@ function readContextSummary(
357
357
  return null;
358
358
  }
359
359
  }
360
-
361
- /** Merge-base best effort. */
362
- function mergeBase(base: string, head: string): string | null {
363
- const result = tryGit(["merge-base", base, head]);
364
- if (!result.ok) return null;
365
- const sha = result.stdout.trim();
366
- return sha.length > 0 ? sha : null;
367
- }
368
-
369
- /** Raiz do repositório para leitura das tools. */
370
- function fileRoot(): string {
371
- return process.cwd();
372
- }
package/src/index.ts CHANGED
@@ -89,6 +89,14 @@ export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
89
89
  export { HoundsmanEngine } from "./core/engine/houndsman-engine";
90
90
  export { ReviewScopeResolver } from "./core/engine/review-scope";
91
91
  export { ReviewSession } from "./core/engine/review-session";
92
+ export type {
93
+ ResolvedWorkspace,
94
+ WorkspaceRequest,
95
+ } from "./core/engine/review-workspace";
96
+ export {
97
+ mainRootOf,
98
+ resolveReviewWorkspace,
99
+ } from "./core/engine/review-workspace";
92
100
  export type { HoundsmanEnv } from "./core/env";
93
101
  export { ENV_NAMES, env, readEnv } from "./core/env";
94
102
  export {
@@ -171,8 +179,26 @@ export { FixBatchBuilder } from "./core/services/fix-batch-builder";
171
179
  export type { GhResult, GhRunner } from "./core/services/gh-client";
172
180
  export { GhClient } from "./core/services/gh-client";
173
181
  export { GitRepository } from "./core/services/git-repository";
182
+ export type {
183
+ PrRefs,
184
+ PrResolveResult,
185
+ PullRequestInfo,
186
+ } from "./core/services/pr-resolver";
187
+ export { PrResolver } from "./core/services/pr-resolver";
174
188
  export { ProjectFiles } from "./core/services/project-files";
175
189
  export { Verifier } from "./core/services/verifier";
190
+ export type {
191
+ EnsureWorktreeInput,
192
+ EnsureWorktreeResult,
193
+ RemoveWorktreeResult,
194
+ WorktreeEntry,
195
+ WorktreeManagerOptions,
196
+ } from "./core/services/worktree-manager";
197
+ export {
198
+ parseWorktreeList,
199
+ repoSlug,
200
+ WorktreeManager,
201
+ } from "./core/services/worktree-manager";
176
202
  export {
177
203
  createMemoryStorage,
178
204
  createStorage,
@@ -226,4 +252,9 @@ export {
226
252
  } from "./core/watchdog-registry";
227
253
  export type { ServerOptions } from "./server";
228
254
  export { createRequestHandler, HoundsmanServer, startServer } from "./server";
255
+ export type {
256
+ DeleteRunResult,
257
+ RunDetail,
258
+ RunListItem,
259
+ } from "./server/run-repository";
229
260
  export { RunRepository } from "./server/run-repository";
@@ -7,18 +7,26 @@
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
8
8
  import { ArtifactsPanel } from "./ArtifactsPanel";
9
9
  import { AutoRefresh } from "./AutoRefresh";
10
+ import { CommentsPanel } from "./CommentsPanel";
11
+ import { DeleteRunModal } from "./DeleteRunModal";
10
12
  import { Filters, type FiltersState } from "./Filters";
11
13
  import { FindingsList } from "./FindingsList";
12
- import { FileText, Layers, ListIcon, Refresh, Send } from "./icons";
14
+ import { FileText, Layers, ListIcon, MessageSquare, Refresh, Send } from "./icons";
13
15
  import { ResumePanel } from "./ResumePanel";
14
- import { RunList } from "./RunList";
16
+ import { Sidebar } from "./Sidebar";
15
17
  import { SubmitModal } from "./SubmitModal";
16
18
  import { SummaryBar } from "./SummaryBar";
17
19
  import { api, computeSummary } from "./utils/api";
18
20
  import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./utils/constants";
19
- import type { Finding, FindingsResponse, RunSummary } from "./utils/types";
21
+ import type {
22
+ DeleteRunResponse,
23
+ EditorContext,
24
+ Finding,
25
+ FindingsResponse,
26
+ RunSummary,
27
+ } from "./utils/types";
20
28
 
21
- type Tab = "resume" | "findings" | "artifacts";
29
+ type Tab = "resume" | "findings" | "artifacts" | "comments";
22
30
 
23
31
  const TABS: { id: Tab; label: string; hint: string }[] = [
24
32
  {
@@ -36,8 +44,20 @@ const TABS: { id: Tab; label: string; hint: string }[] = [
36
44
  label: "Artefatos",
37
45
  hint: "Arquivos brutos gerados pela revisão (manifesto, classificação, relatório…)",
38
46
  },
47
+ {
48
+ id: "comments",
49
+ label: "Comentários",
50
+ hint: "Comentários da PR: threads de revisão e comentários gerais (carregados ao abrir a aba)",
51
+ },
39
52
  ];
40
53
 
54
+ function TabIcon({ id }: { id: Tab }) {
55
+ if (id === "resume") return <FileText size={15} />;
56
+ if (id === "findings") return <ListIcon size={15} />;
57
+ if (id === "artifacts") return <Layers size={15} />;
58
+ return <MessageSquare size={15} />;
59
+ }
60
+
41
61
  export function App() {
42
62
  const [runs, setRuns] = useState<RunSummary[]>([]);
43
63
  const [slug, setSlug] = useState<string | null>(null);
@@ -54,6 +74,9 @@ export function App() {
54
74
  const [refreshing, setRefreshing] = useState(false);
55
75
  const [syncing, setSyncing] = useState(false);
56
76
  const [submitOpen, setSubmitOpen] = useState(false);
77
+ const [deleteOpen, setDeleteOpen] = useState(false);
78
+ /** Editor detectado pelo servidor — base dos links de código. */
79
+ const [editor, setEditor] = useState<EditorContext>({ kind: "vscode" });
57
80
  /** Refs espelhando estado volátil — o polling não deve recriar o intervalo. */
58
81
  const lastSyncRef = useRef(lastSync);
59
82
  const slugRef = useRef<string | null>(null);
@@ -116,6 +139,14 @@ export function App() {
116
139
  reloadRuns();
117
140
  }, [reloadRuns]);
118
141
 
142
+ useEffect(() => {
143
+ api<EditorContext>("/api/editor")
144
+ .then(setEditor)
145
+ .catch(() => {
146
+ // fail-open: mantém o fallback VS Code.
147
+ });
148
+ }, []);
149
+
119
150
  useEffect(() => {
120
151
  if (slug) loadFindings(slug);
121
152
  }, [slug, loadFindings]);
@@ -189,12 +220,37 @@ export function App() {
189
220
 
190
221
  const summary = useMemo(() => computeSummary(data?.findings ?? []), [data]);
191
222
 
192
- const branch =
193
- data?.branch ?? runs.find((run) => run.slug === slug)?.branch ?? slug ?? "";
223
+ const selectedRun = runs.find((run) => run.slug === slug) ?? null;
224
+ const branch = data?.branch ?? selectedRun?.branch ?? slug ?? "";
225
+
226
+ /** Após excluir: limpa a seleção e escolhe a próxima run da lista. */
227
+ const afterDelete = useCallback((result: DeleteRunResponse) => {
228
+ setDeleteOpen(false);
229
+ setData(null);
230
+ setSlug(null);
231
+ if (result.warning) {
232
+ setError(
233
+ `Execução excluída, mas o worktree não foi removido: ${result.warning}`,
234
+ );
235
+ } else {
236
+ setError("");
237
+ }
238
+ api<{ runs: RunSummary[] }>("/api/runs")
239
+ .then(({ runs: list }) => {
240
+ setRuns(list);
241
+ setSlug(list[0]?.slug ?? null);
242
+ })
243
+ .catch((err) => setError(String(err)));
244
+ }, []);
194
245
 
195
246
  return (
196
247
  <div class="app">
197
- <RunList runs={runs} slug={slug} onSelect={setSlug} />
248
+ <Sidebar
249
+ runs={runs}
250
+ slug={slug}
251
+ onSelect={setSlug}
252
+ onRefreshRuns={() => void reloadRuns()}
253
+ />
198
254
  <main class="main">
199
255
  <header class="topbar">
200
256
  <div class="segmented">
@@ -206,13 +262,7 @@ export function App() {
206
262
  class={`seg-btn${tab === item.id ? " active" : ""}`}
207
263
  onClick={() => setTab(item.id)}
208
264
  >
209
- {item.id === "resume" ? (
210
- <FileText size={15} />
211
- ) : item.id === "findings" ? (
212
- <ListIcon size={15} />
213
- ) : (
214
- <Layers size={15} />
215
- )}
265
+ <TabIcon id={item.id} />
216
266
  {item.label}
217
267
  </button>
218
268
  ))}
@@ -268,7 +318,7 @@ export function App() {
268
318
  branch={branch}
269
319
  findings={data.findings}
270
320
  summary={summary}
271
- onOpenArtifacts={() => setTab("artifacts")}
321
+ onRequestDelete={() => setDeleteOpen(true)}
272
322
  />
273
323
  )}
274
324
  {slug && tab === "findings" && data && (
@@ -278,21 +328,35 @@ export function App() {
278
328
  <FindingsList
279
329
  slug={slug}
280
330
  findings={findings}
331
+ editor={editor}
281
332
  onUpdated={onUpdated}
282
333
  />
283
334
  </>
284
335
  )}
285
336
  {slug && tab === "artifacts" && <ArtifactsPanel slug={slug} />}
337
+ {slug && tab === "comments" && (
338
+ <CommentsPanel slug={slug} editor={editor} />
339
+ )}
286
340
  {!slug && !error && <p class="empty">Nenhuma execução para triar.</p>}
287
341
  </div>
288
342
  {slug && (
289
- <SubmitModal
290
- slug={slug}
291
- branch={branch}
292
- open={submitOpen}
293
- onClose={() => setSubmitOpen(false)}
294
- onSubmitted={afterSubmit}
295
- />
343
+ <>
344
+ <SubmitModal
345
+ slug={slug}
346
+ branch={branch}
347
+ open={submitOpen}
348
+ onClose={() => setSubmitOpen(false)}
349
+ onSubmitted={afterSubmit}
350
+ />
351
+ <DeleteRunModal
352
+ slug={slug}
353
+ branch={branch}
354
+ worktreePath={selectedRun?.worktreePath}
355
+ open={deleteOpen}
356
+ onClose={() => setDeleteOpen(false)}
357
+ onDeleted={afterDelete}
358
+ />
359
+ </>
296
360
  )}
297
361
  </main>
298
362
  </div>
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Aba "Comentários": threads de revisão e comentários gerais da PR da run.
3
+ *
4
+ * O componente só é montado quando a aba é acessada — o carregamento (e a
5
+ * chamada ao `gh`) acontece no mount, nunca no carregamento da página.
6
+ */
7
+
8
+ import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
9
+ import { Clock, CodeIcon, ExternalLink, GitHub, MessageSquare, Refresh } from "./icons";
10
+ import { MarkdownText } from "./MarkdownText";
11
+ import { api } from "./utils/api";
12
+ import { editorFileLink, editorLabel, formatRunTime } from "./utils/format";
13
+ import type {
14
+ EditorContext,
15
+ PrCommentsResponse,
16
+ PrCommentThread,
17
+ PrCommentView,
18
+ } from "./utils/types";
19
+
20
+ function CommentCard({
21
+ comment,
22
+ editor,
23
+ reply,
24
+ }: {
25
+ comment: PrCommentView;
26
+ editor: EditorContext;
27
+ reply: boolean;
28
+ }) {
29
+ const location = comment.path
30
+ ? comment.line
31
+ ? `${comment.path}:${comment.line}`
32
+ : comment.path
33
+ : null;
34
+ const href = comment.absFile
35
+ ? editorFileLink(comment.absFile, comment.line, editor)
36
+ : null;
37
+ return (
38
+ <article class={`pr-comment${reply ? " reply" : ""}`}>
39
+ <header class="pr-comment-head">
40
+ <span class="pr-comment-author">{comment.author}</span>
41
+ {comment.authorType === "Bot" && <span class="tag">bot</span>}
42
+ <span class="run-when">
43
+ <Clock size={12} />
44
+ {formatRunTime(comment.createdAt)}
45
+ </span>
46
+ <span class="spacer" />
47
+ {comment.url && (
48
+ <a
49
+ class="pr-comment-link"
50
+ href={comment.url}
51
+ target="_blank"
52
+ rel="noreferrer"
53
+ title="Abrir o comentário no GitHub"
54
+ >
55
+ <GitHub size={12} /> GitHub
56
+ </a>
57
+ )}
58
+ </header>
59
+ <div class="pr-comment-body">
60
+ <MarkdownText text={comment.body} />
61
+ </div>
62
+ {location &&
63
+ (href ? (
64
+ <a
65
+ class="loc"
66
+ href={href}
67
+ title={`Abrir ${location} no ${editorLabel(editor)}`}
68
+ >
69
+ <CodeIcon size={13} />
70
+ {location}
71
+ <ExternalLink size={12} class="ext" />
72
+ </a>
73
+ ) : (
74
+ <code class="loc static">
75
+ <CodeIcon size={13} />
76
+ {location}
77
+ </code>
78
+ ))}
79
+ </article>
80
+ );
81
+ }
82
+
83
+ function ThreadCard({
84
+ thread,
85
+ editor,
86
+ }: {
87
+ thread: PrCommentThread;
88
+ editor: EditorContext;
89
+ }) {
90
+ const location = thread.line
91
+ ? `${thread.path}:${thread.line}`
92
+ : thread.path || "arquivo";
93
+ const href = thread.absFile
94
+ ? editorFileLink(thread.absFile, thread.line, editor)
95
+ : null;
96
+ return (
97
+ <section class="comment-thread">
98
+ <header class="comment-thread-head">
99
+ <CodeIcon size={13} />
100
+ <span class="comment-file">{location}</span>
101
+ <span class="spacer" />
102
+ {href && (
103
+ <a
104
+ class="icon-btn"
105
+ href={href}
106
+ title={`Abrir ${location} no ${editorLabel(editor)}`}
107
+ >
108
+ <ExternalLink size={13} />
109
+ </a>
110
+ )}
111
+ <span class="count">{thread.comments.length}</span>
112
+ </header>
113
+ {thread.comments.map((comment, index) => (
114
+ <CommentCard
115
+ key={comment.id}
116
+ comment={comment}
117
+ editor={editor}
118
+ reply={index > 0}
119
+ />
120
+ ))}
121
+ </section>
122
+ );
123
+ }
124
+
125
+ export function CommentsPanel({ slug, editor }: { slug: string; editor: EditorContext }) {
126
+ const [data, setData] = useState<PrCommentsResponse | null>(null);
127
+ const [loading, setLoading] = useState(true);
128
+ const [error, setError] = useState("");
129
+
130
+ const load = useCallback(
131
+ async (refresh = false) => {
132
+ setLoading(true);
133
+ setError("");
134
+ try {
135
+ const result = await api<PrCommentsResponse>(
136
+ `/api/runs/${slug}/comments${refresh ? "?refresh=1" : ""}`,
137
+ );
138
+ setData(result);
139
+ } catch (err) {
140
+ setError(String(err));
141
+ } finally {
142
+ setLoading(false);
143
+ }
144
+ },
145
+ [slug],
146
+ );
147
+
148
+ useEffect(() => {
149
+ setData(null);
150
+ void load();
151
+ }, [load]);
152
+
153
+ const groups = useMemo(() => {
154
+ const map = new Map<string, PrCommentThread[]>();
155
+ for (const thread of data?.threads ?? []) {
156
+ const key = thread.path || "(sem arquivo)";
157
+ const list = map.get(key) ?? [];
158
+ list.push(thread);
159
+ map.set(key, list);
160
+ }
161
+ return [...map.entries()];
162
+ }, [data]);
163
+
164
+ const empty =
165
+ data?.available && data.threads.length === 0 && data.issueComments.length === 0;
166
+
167
+ return (
168
+ <div class="comments">
169
+ <div class="comments-toolbar">
170
+ <div class="comments-counts">
171
+ <MessageSquare size={14} />
172
+ {data?.available ? (
173
+ <>
174
+ <span>
175
+ <b>{data.counts.threads}</b> threads
176
+ </span>
177
+ <span>
178
+ <b>{data.counts.inline}</b> inline
179
+ </span>
180
+ <span>
181
+ <b>{data.counts.issue}</b> gerais
182
+ </span>
183
+ </>
184
+ ) : (
185
+ <span>Comentários da PR</span>
186
+ )}
187
+ {data?.target?.url && (
188
+ <a
189
+ class="pr-comment-link"
190
+ href={data.target.url}
191
+ target="_blank"
192
+ rel="noreferrer"
193
+ >
194
+ <GitHub size={12} /> PR #{data.target.number}
195
+ </a>
196
+ )}
197
+ </div>
198
+ <button
199
+ type="button"
200
+ class="btn"
201
+ disabled={loading}
202
+ title="Recarrega os comentários ignorando o cache de 60s"
203
+ onClick={() => void load(true)}
204
+ >
205
+ <Refresh size={14} class={loading ? "icon spin" : undefined} />
206
+ Atualizar
207
+ </button>
208
+ </div>
209
+
210
+ {loading && !data && <p class="empty">Carregando comentários da PR…</p>}
211
+ {error && <p class="error">{error}</p>}
212
+ {data && !data.available && (
213
+ <p class="empty">
214
+ Comentários indisponíveis
215
+ {data.reason ? `: ${data.reason}` : "."}
216
+ </p>
217
+ )}
218
+ {empty && <p class="empty">Nenhum comentário nesta PR.</p>}
219
+
220
+ {data?.available && groups.length > 0 && (
221
+ <section class="comment-group">
222
+ <h2 class="comment-group-title">Comentários de revisão</h2>
223
+ {groups.map(([path, threads]) => (
224
+ <div class="comment-file-group" key={path}>
225
+ <p class="comment-group-label">
226
+ {path} <span class="count">{threads.length}</span>
227
+ </p>
228
+ {threads.map((thread) => (
229
+ <ThreadCard
230
+ key={thread.id}
231
+ thread={thread}
232
+ editor={editor}
233
+ />
234
+ ))}
235
+ </div>
236
+ ))}
237
+ </section>
238
+ )}
239
+
240
+ {data?.available && data.issueComments.length > 0 && (
241
+ <section class="comment-group">
242
+ <h2 class="comment-group-title">Comentários gerais</h2>
243
+ {data.issueComments.map((comment) => (
244
+ <CommentCard
245
+ key={comment.id}
246
+ comment={comment}
247
+ editor={editor}
248
+ reply={false}
249
+ />
250
+ ))}
251
+ </section>
252
+ )}
253
+ </div>
254
+ );
255
+ }