@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.
- package/README.md +20 -0
- package/package.json +1 -1
- package/src/cli/commands/command.ts +10 -1
- package/src/cli/commands/context-command.ts +40 -2
- package/src/cli/commands/hunt-command.ts +28 -2
- package/src/cli/commands/review-command.ts +30 -2
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +14 -4
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +1 -0
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +17 -6
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +4 -2
- package/src/core/config.ts +13 -0
- package/src/core/defaults.ts +8 -1
- package/src/core/engine/houndsman-engine.ts +26 -3
- package/src/core/engine/review-scope.ts +17 -22
- package/src/core/engine/review-session.ts +25 -2
- package/src/core/engine/review-workspace.ts +204 -0
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +62 -13
- package/src/core/project-config.ts +12 -0
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/git-repository.ts +46 -0
- package/src/core/services/pr-resolver.ts +147 -0
- package/src/core/services/worktree-manager.ts +269 -0
- package/src/core/storage/file/file-storage.ts +14 -1
- package/src/core/storage/paths.ts +30 -1
- package/src/core/storage/sqlite/mappers.ts +4 -0
- package/src/core/storage/sqlite/schema.ts +13 -3
- package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
- package/src/core/storage/types.ts +12 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/types.ts +2 -0
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +31 -0
- package/src/server/client/App.tsx +86 -22
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/ResumePanel.tsx +8 -8
- package/src/server/client/Sidebar.tsx +452 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +29 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +2 -0
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +108 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +205 -3
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +213 -0
- package/src/server/run-repository.ts +136 -3
- package/src/skill/README.md +11 -3
- package/src/skill/SKILL.md +44 -10
- package/src/skill/hunt.md +33 -7
- package/src/tests/__snapshots__/baseline.test.ts.snap +94 -24
- package/src/tests/ai-core.test.ts +2 -2
- package/src/tests/cli.test.ts +35 -1
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +333 -2
- package/src/tests/worktree.test.ts +369 -0
- 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(
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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 {
|
|
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
|
|
193
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
}
|