@payconductor/houndsman 0.7.0 → 0.10.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/worktree-command.ts +118 -0
- package/src/cli/help.ts +12 -2
- package/src/cli/index.ts +14 -4
- package/src/core/ai/ai-client.ts +21 -0
- package/src/core/ai/ai-sdk-model-provider.ts +43 -18
- package/src/core/ai/features/technical-summarizer.ts +43 -0
- package/src/core/ai/index.ts +4 -1
- package/src/core/ai/prompts/context.ts +3 -3
- package/src/core/ai/prompts/technical.ts +30 -0
- package/src/core/ai/provider-capabilities.ts +45 -0
- package/src/core/ai/schemas.ts +19 -3
- package/src/core/ai/types.ts +3 -1
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +26 -4
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +11 -5
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +9 -2
- package/src/core/config.ts +30 -1
- package/src/core/defaults.ts +7 -0
- 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 +8 -1
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +176 -15
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +21 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- 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 +293 -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 +13 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/technical-summary.ts +398 -0
- package/src/core/types.ts +2 -0
- package/src/harness/agent/agent-factory.ts +1 -0
- package/src/harness/agent/review-agent.ts +11 -1
- package/src/harness/config/harness-config.ts +6 -1
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +33 -0
- package/src/server/client/App.tsx +51 -12
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +17 -20
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -358
- package/src/server/client/icons.tsx +10 -0
- package/src/server/client/sidebar-sections.tsx +73 -0
- package/src/server/client/utils/constants.ts +7 -2
- package/src/server/client/utils/sidebar-prefs.ts +64 -0
- package/src/server/client/utils/types.ts +27 -0
- package/src/server/index.ts +113 -406
- package/src/server/pr-inbox.ts +15 -4
- package/src/server/run-repository.ts +103 -2
- package/src/server/styles.css +454 -0
- 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 +99 -24
- package/src/tests/ai-core.test.ts +71 -0
- package/src/tests/ai-features.test.ts +36 -0
- package/src/tests/baseline.test.ts +4 -1
- package/src/tests/cli.test.ts +2 -1
- package/src/tests/harness-agent.test.ts +19 -0
- package/src/tests/review-resume.test.ts +36 -8
- package/src/tests/review-scope.test.ts +67 -0
- package/src/tests/review-scripts.test.ts +2 -2
- package/src/tests/server.test.ts +54 -1
- package/src/tests/sidebar-sections.test.ts +94 -0
- package/src/tests/technical-summary.test.ts +176 -0
- package/src/tests/worktree.test.ts +404 -0
- package/assets/logo-legacy.png +0 -0
|
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
|
|
|
8
8
|
import { ArtifactsPanel } from "./ArtifactsPanel";
|
|
9
9
|
import { AutoRefresh } from "./AutoRefresh";
|
|
10
10
|
import { CommentsPanel } from "./CommentsPanel";
|
|
11
|
+
import { DeleteRunModal } from "./DeleteRunModal";
|
|
11
12
|
import { Filters, type FiltersState } from "./Filters";
|
|
12
13
|
import { FindingsList } from "./FindingsList";
|
|
13
14
|
import { FileText, Layers, ListIcon, MessageSquare, Refresh, Send } from "./icons";
|
|
@@ -17,7 +18,13 @@ import { SubmitModal } from "./SubmitModal";
|
|
|
17
18
|
import { SummaryBar } from "./SummaryBar";
|
|
18
19
|
import { api, computeSummary } from "./utils/api";
|
|
19
20
|
import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./utils/constants";
|
|
20
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
DeleteRunResponse,
|
|
23
|
+
EditorContext,
|
|
24
|
+
Finding,
|
|
25
|
+
FindingsResponse,
|
|
26
|
+
RunSummary,
|
|
27
|
+
} from "./utils/types";
|
|
21
28
|
|
|
22
29
|
type Tab = "resume" | "findings" | "artifacts" | "comments";
|
|
23
30
|
|
|
@@ -67,6 +74,7 @@ export function App() {
|
|
|
67
74
|
const [refreshing, setRefreshing] = useState(false);
|
|
68
75
|
const [syncing, setSyncing] = useState(false);
|
|
69
76
|
const [submitOpen, setSubmitOpen] = useState(false);
|
|
77
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
70
78
|
/** Editor detectado pelo servidor — base dos links de código. */
|
|
71
79
|
const [editor, setEditor] = useState<EditorContext>({ kind: "vscode" });
|
|
72
80
|
/** Refs espelhando estado volátil — o polling não deve recriar o intervalo. */
|
|
@@ -212,8 +220,28 @@ export function App() {
|
|
|
212
220
|
|
|
213
221
|
const summary = useMemo(() => computeSummary(data?.findings ?? []), [data]);
|
|
214
222
|
|
|
215
|
-
const
|
|
216
|
-
|
|
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
|
+
}, []);
|
|
217
245
|
|
|
218
246
|
return (
|
|
219
247
|
<div class="app">
|
|
@@ -231,11 +259,12 @@ export function App() {
|
|
|
231
259
|
type="button"
|
|
232
260
|
key={item.id}
|
|
233
261
|
title={item.hint}
|
|
262
|
+
aria-label={item.label}
|
|
234
263
|
class={`seg-btn${tab === item.id ? " active" : ""}`}
|
|
235
264
|
onClick={() => setTab(item.id)}
|
|
236
265
|
>
|
|
237
266
|
<TabIcon id={item.id} />
|
|
238
|
-
{item.label}
|
|
267
|
+
<span class="seg-label">{item.label}</span>
|
|
239
268
|
</button>
|
|
240
269
|
))}
|
|
241
270
|
</div>
|
|
@@ -290,7 +319,7 @@ export function App() {
|
|
|
290
319
|
branch={branch}
|
|
291
320
|
findings={data.findings}
|
|
292
321
|
summary={summary}
|
|
293
|
-
|
|
322
|
+
onRequestDelete={() => setDeleteOpen(true)}
|
|
294
323
|
/>
|
|
295
324
|
)}
|
|
296
325
|
{slug && tab === "findings" && data && (
|
|
@@ -312,13 +341,23 @@ export function App() {
|
|
|
312
341
|
{!slug && !error && <p class="empty">Nenhuma execução para triar.</p>}
|
|
313
342
|
</div>
|
|
314
343
|
{slug && (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
344
|
+
<>
|
|
345
|
+
<SubmitModal
|
|
346
|
+
slug={slug}
|
|
347
|
+
branch={branch}
|
|
348
|
+
open={submitOpen}
|
|
349
|
+
onClose={() => setSubmitOpen(false)}
|
|
350
|
+
onSubmitted={afterSubmit}
|
|
351
|
+
/>
|
|
352
|
+
<DeleteRunModal
|
|
353
|
+
slug={slug}
|
|
354
|
+
branch={branch}
|
|
355
|
+
worktreePath={selectedRun?.worktreePath}
|
|
356
|
+
open={deleteOpen}
|
|
357
|
+
onClose={() => setDeleteOpen(false)}
|
|
358
|
+
onDeleted={afterDelete}
|
|
359
|
+
/>
|
|
360
|
+
</>
|
|
322
361
|
)}
|
|
323
362
|
</main>
|
|
324
363
|
</div>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal "Excluir execução": confirma a remoção definitiva da run.
|
|
3
|
+
*
|
|
4
|
+
* Apaga a review, findings, histórico de triagem, artefatos e o staging local.
|
|
5
|
+
* Quando a run tem worktree, oferece removê-lo junto (alterações não commitadas
|
|
6
|
+
* nele são descartadas).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useState } from "preact/hooks";
|
|
10
|
+
import { Spinner, Trash } from "./icons";
|
|
11
|
+
import { api } from "./utils/api";
|
|
12
|
+
import type { DeleteRunResponse } from "./utils/types";
|
|
13
|
+
|
|
14
|
+
export function DeleteRunModal({
|
|
15
|
+
slug,
|
|
16
|
+
branch,
|
|
17
|
+
worktreePath,
|
|
18
|
+
open,
|
|
19
|
+
onClose,
|
|
20
|
+
onDeleted,
|
|
21
|
+
}: {
|
|
22
|
+
slug: string;
|
|
23
|
+
branch: string;
|
|
24
|
+
worktreePath?: string;
|
|
25
|
+
open: boolean;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
onDeleted: (result: DeleteRunResponse) => void;
|
|
28
|
+
}) {
|
|
29
|
+
const [removeWorktree, setRemoveWorktree] = useState(Boolean(worktreePath));
|
|
30
|
+
const [deleting, setDeleting] = useState(false);
|
|
31
|
+
const [error, setError] = useState("");
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!open) return;
|
|
35
|
+
setRemoveWorktree(Boolean(worktreePath));
|
|
36
|
+
setDeleting(false);
|
|
37
|
+
setError("");
|
|
38
|
+
}, [open, worktreePath]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!open) return;
|
|
42
|
+
const onKey = (event: KeyboardEvent) => {
|
|
43
|
+
if (event.key === "Escape" && !deleting) onClose();
|
|
44
|
+
};
|
|
45
|
+
document.addEventListener("keydown", onKey);
|
|
46
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
47
|
+
}, [open, deleting, onClose]);
|
|
48
|
+
|
|
49
|
+
if (!open) return null;
|
|
50
|
+
|
|
51
|
+
const confirm = async () => {
|
|
52
|
+
setDeleting(true);
|
|
53
|
+
setError("");
|
|
54
|
+
try {
|
|
55
|
+
const suffix = removeWorktree && worktreePath ? "?worktree=1" : "";
|
|
56
|
+
const result = await api<DeleteRunResponse>(`/api/runs/${slug}${suffix}`, {
|
|
57
|
+
method: "DELETE",
|
|
58
|
+
});
|
|
59
|
+
if (!result.deleted) throw new Error("A execução não foi encontrada.");
|
|
60
|
+
onDeleted(result);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
63
|
+
} finally {
|
|
64
|
+
setDeleting(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div class="modal-layer">
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class="modal-backdrop"
|
|
73
|
+
aria-label="Fechar"
|
|
74
|
+
title="Fechar"
|
|
75
|
+
onClick={() => {
|
|
76
|
+
if (!deleting) onClose();
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
<div
|
|
80
|
+
class="modal"
|
|
81
|
+
role="dialog"
|
|
82
|
+
aria-modal="true"
|
|
83
|
+
aria-label="Excluir execução"
|
|
84
|
+
>
|
|
85
|
+
<header class="modal-head">
|
|
86
|
+
<div>
|
|
87
|
+
<h2 class="modal-title">Excluir execução</h2>
|
|
88
|
+
<p class="modal-sub">
|
|
89
|
+
Esta ação é permanente e apaga a revisão de{" "}
|
|
90
|
+
<code>{branch}</code>.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class="modal-close"
|
|
96
|
+
title="Fechar"
|
|
97
|
+
disabled={deleting}
|
|
98
|
+
onClick={onClose}
|
|
99
|
+
>
|
|
100
|
+
×
|
|
101
|
+
</button>
|
|
102
|
+
</header>
|
|
103
|
+
|
|
104
|
+
<div class="modal-body">
|
|
105
|
+
<p class="modal-warn">
|
|
106
|
+
Serão removidos os findings, o histórico de triagem, todos os
|
|
107
|
+
artefatos (briefs, classificação, manifesto, relatório, verify) e
|
|
108
|
+
o staging local desta run. O envio já feito ao GitHub não é
|
|
109
|
+
desfeito.
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
{worktreePath && (
|
|
113
|
+
<label class="modal-check">
|
|
114
|
+
<input
|
|
115
|
+
type="checkbox"
|
|
116
|
+
checked={removeWorktree}
|
|
117
|
+
disabled={deleting}
|
|
118
|
+
onChange={(event) =>
|
|
119
|
+
setRemoveWorktree(event.currentTarget.checked)
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
<span>
|
|
123
|
+
Excluir também o worktree desta execução. Alterações não
|
|
124
|
+
commitadas nele serão perdidas.
|
|
125
|
+
<br />
|
|
126
|
+
<code>{worktreePath}</code>
|
|
127
|
+
</span>
|
|
128
|
+
</label>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{error && <p class="error">{error}</p>}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<footer class="modal-foot">
|
|
135
|
+
<span class="submit-label">Run: {branch}</span>
|
|
136
|
+
<div class="submit-actions">
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
class="btn"
|
|
140
|
+
disabled={deleting}
|
|
141
|
+
onClick={onClose}
|
|
142
|
+
>
|
|
143
|
+
Cancelar
|
|
144
|
+
</button>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
class="btn danger"
|
|
148
|
+
disabled={deleting}
|
|
149
|
+
onClick={() => void confirm()}
|
|
150
|
+
>
|
|
151
|
+
{deleting ? (
|
|
152
|
+
<Spinner size={15} class="icon spin" />
|
|
153
|
+
) : (
|
|
154
|
+
<Trash size={15} />
|
|
155
|
+
)}
|
|
156
|
+
{deleting ? "Excluindo…" : "Excluir execução"}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</footer>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seção `PRs` da sidebar: lista as PRs abertas via `gh`, abre a run local da
|
|
3
|
+
* branch, cria worktree dedicado e faz checkout quando a árvore está limpa.
|
|
4
|
+
*
|
|
5
|
+
* A seção fica montada mesmo quando outra está ativa — o fetch só ocorre na
|
|
6
|
+
* primeira ativação (`active`), preservando filtros e estados ao alternar.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
10
|
+
import { BranchIcon, Clock, GitHub, GitPullRequest, Refresh, Spinner } from "./icons";
|
|
11
|
+
import type { SidebarSectionContext } from "./sidebar-sections";
|
|
12
|
+
import { api } from "./utils/api";
|
|
13
|
+
import { formatRunTime } from "./utils/format";
|
|
14
|
+
import type {
|
|
15
|
+
CheckoutResponse,
|
|
16
|
+
PrListItem,
|
|
17
|
+
PrListResponse,
|
|
18
|
+
WorktreeState,
|
|
19
|
+
} from "./utils/types";
|
|
20
|
+
|
|
21
|
+
const REVIEW_LABELS: Record<string, string> = {
|
|
22
|
+
APPROVED: "aprovada",
|
|
23
|
+
CHANGES_REQUESTED: "mudanças",
|
|
24
|
+
REVIEW_REQUIRED: "revisão pendente",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function PrItem({
|
|
28
|
+
pr,
|
|
29
|
+
currentBranch,
|
|
30
|
+
worktree,
|
|
31
|
+
busy,
|
|
32
|
+
confirming,
|
|
33
|
+
onConfirm,
|
|
34
|
+
onCancel,
|
|
35
|
+
onCheckout,
|
|
36
|
+
onSelectRun,
|
|
37
|
+
}: {
|
|
38
|
+
pr: PrListItem;
|
|
39
|
+
currentBranch: string | null;
|
|
40
|
+
worktree: WorktreeState;
|
|
41
|
+
busy: boolean;
|
|
42
|
+
confirming: boolean;
|
|
43
|
+
onConfirm: (number: number) => void;
|
|
44
|
+
onCancel: () => void;
|
|
45
|
+
onCheckout: (pr: PrListItem) => void;
|
|
46
|
+
onSelectRun: (slug: string) => void;
|
|
47
|
+
}) {
|
|
48
|
+
const isCurrent = currentBranch === pr.headRefName;
|
|
49
|
+
const runSlug = pr.runSlug;
|
|
50
|
+
const review = pr.reviewDecision ? REVIEW_LABELS[pr.reviewDecision] : undefined;
|
|
51
|
+
// O título da PR às vezes repete o nome da branch; nesse caso só o
|
|
52
|
+
// indicador `head → base` fica visível.
|
|
53
|
+
const showTitle = pr.title.trim() !== pr.headRefName.trim();
|
|
54
|
+
return (
|
|
55
|
+
<article class={`pr${isCurrent ? " current" : ""}`}>
|
|
56
|
+
<div class="pr-head">
|
|
57
|
+
<GitPullRequest size={14} class="pr-icon" />
|
|
58
|
+
<span class="pr-number">#{pr.number}</span>
|
|
59
|
+
{pr.isDraft && <span class="tag">draft</span>}
|
|
60
|
+
{isCurrent && <span class="pr-current">atual</span>}
|
|
61
|
+
{pr.worktreePath && (
|
|
62
|
+
<span
|
|
63
|
+
class="pr-worktree-badge"
|
|
64
|
+
title={`Worktree: ${pr.worktreePath}`}
|
|
65
|
+
>
|
|
66
|
+
<BranchIcon size={10} />
|
|
67
|
+
worktree
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
<span class="spacer" />
|
|
71
|
+
{pr.updatedAt && (
|
|
72
|
+
<span class="run-when">
|
|
73
|
+
<Clock size={12} />
|
|
74
|
+
{formatRunTime(pr.updatedAt)}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{showTitle && <p class="pr-title">{pr.title}</p>}
|
|
79
|
+
<p class="pr-meta">
|
|
80
|
+
<span class="pr-branch">{pr.headRefName}</span>
|
|
81
|
+
<span class="pr-arrow">→</span>
|
|
82
|
+
<span>{pr.baseRefName || "—"}</span>
|
|
83
|
+
<span>· {pr.author}</span>
|
|
84
|
+
</p>
|
|
85
|
+
{review && <p class={`pr-review pr-review-${pr.reviewDecision}`}>{review}</p>}
|
|
86
|
+
<div class="pr-actions">
|
|
87
|
+
{runSlug && (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
class="pr-btn"
|
|
91
|
+
title="Abrir a run local desta branch"
|
|
92
|
+
onClick={() => onSelectRun(runSlug)}
|
|
93
|
+
>
|
|
94
|
+
Abrir run
|
|
95
|
+
</button>
|
|
96
|
+
)}
|
|
97
|
+
{pr.url && (
|
|
98
|
+
<a
|
|
99
|
+
class="pr-btn"
|
|
100
|
+
href={pr.url}
|
|
101
|
+
target="_blank"
|
|
102
|
+
rel="noreferrer"
|
|
103
|
+
title="Abrir a PR no GitHub"
|
|
104
|
+
>
|
|
105
|
+
<GitHub size={12} /> GitHub
|
|
106
|
+
</a>
|
|
107
|
+
)}
|
|
108
|
+
{confirming ? (
|
|
109
|
+
<>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class="pr-btn danger"
|
|
113
|
+
disabled={busy}
|
|
114
|
+
title="Confirmar checkout da PR"
|
|
115
|
+
onClick={() => onCheckout(pr)}
|
|
116
|
+
>
|
|
117
|
+
{busy ? <Spinner size={12} class="icon spin" /> : "Confirmar"}
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class="pr-btn"
|
|
122
|
+
disabled={busy}
|
|
123
|
+
onClick={onCancel}
|
|
124
|
+
>
|
|
125
|
+
Cancelar
|
|
126
|
+
</button>
|
|
127
|
+
</>
|
|
128
|
+
) : (
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
class="pr-btn"
|
|
132
|
+
disabled={!worktree.clean || busy}
|
|
133
|
+
title={
|
|
134
|
+
worktree.clean
|
|
135
|
+
? `Fazer checkout da branch ${pr.headRefName}`
|
|
136
|
+
: "Há alterações não commitadas — faça commit ou stash antes"
|
|
137
|
+
}
|
|
138
|
+
onClick={() => onConfirm(pr.number)}
|
|
139
|
+
>
|
|
140
|
+
Checkout
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</article>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function PrsSection({ active, onSelect, onRefreshRuns }: SidebarSectionContext) {
|
|
149
|
+
const [data, setData] = useState<PrListResponse | null>(null);
|
|
150
|
+
const [loading, setLoading] = useState(false);
|
|
151
|
+
const [error, setError] = useState("");
|
|
152
|
+
const [notice, setNotice] = useState("");
|
|
153
|
+
const [busy, setBusy] = useState<number | null>(null);
|
|
154
|
+
const [confirming, setConfirming] = useState<number | null>(null);
|
|
155
|
+
const requested = useRef(false);
|
|
156
|
+
|
|
157
|
+
const loadPrs = useCallback(async () => {
|
|
158
|
+
setLoading(true);
|
|
159
|
+
setError("");
|
|
160
|
+
try {
|
|
161
|
+
setData(await api<PrListResponse>("/api/prs"));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
setError(String(err));
|
|
164
|
+
} finally {
|
|
165
|
+
setLoading(false);
|
|
166
|
+
}
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!active || requested.current) return;
|
|
171
|
+
requested.current = true;
|
|
172
|
+
void loadPrs();
|
|
173
|
+
}, [active, loadPrs]);
|
|
174
|
+
|
|
175
|
+
const checkout = useCallback(
|
|
176
|
+
async (pr: PrListItem) => {
|
|
177
|
+
setBusy(pr.number);
|
|
178
|
+
setError("");
|
|
179
|
+
setNotice("");
|
|
180
|
+
try {
|
|
181
|
+
const result = await api<CheckoutResponse>("/api/prs/checkout", {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "content-type": "application/json" },
|
|
184
|
+
body: JSON.stringify({ number: pr.number }),
|
|
185
|
+
});
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
if (result.reason === "dirty") {
|
|
188
|
+
setData((current) =>
|
|
189
|
+
current
|
|
190
|
+
? {
|
|
191
|
+
...current,
|
|
192
|
+
worktree: {
|
|
193
|
+
...current.worktree,
|
|
194
|
+
clean: false,
|
|
195
|
+
changes:
|
|
196
|
+
result.changes?.length ??
|
|
197
|
+
current.worktree.changes,
|
|
198
|
+
sample:
|
|
199
|
+
result.changes ?? current.worktree.sample,
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
: current,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
setError(
|
|
206
|
+
result.reason === "dirty"
|
|
207
|
+
? `Checkout bloqueado: ${result.detail}`
|
|
208
|
+
: result.detail,
|
|
209
|
+
);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
setConfirming(null);
|
|
213
|
+
setNotice(
|
|
214
|
+
`Checkout de #${pr.number} concluído${
|
|
215
|
+
result.branch ? ` — ${result.branch}` : ""
|
|
216
|
+
}.`,
|
|
217
|
+
);
|
|
218
|
+
await loadPrs();
|
|
219
|
+
onRefreshRuns();
|
|
220
|
+
if (pr.runSlug) onSelect(pr.runSlug);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
setError(String(err));
|
|
223
|
+
} finally {
|
|
224
|
+
setBusy(null);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
[loadPrs, onRefreshRuns, onSelect],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const openCount = data?.available ? data.prs.length : null;
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<>
|
|
234
|
+
<div class="pr-toolbar">
|
|
235
|
+
<p class="section-label">
|
|
236
|
+
{openCount === null
|
|
237
|
+
? "PRs abertas"
|
|
238
|
+
: `${openCount} ${openCount === 1 ? "aberta" : "abertas"}`}
|
|
239
|
+
</p>
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
class="icon-btn"
|
|
243
|
+
title="Atualizar lista de PRs"
|
|
244
|
+
disabled={loading}
|
|
245
|
+
onClick={() => void loadPrs()}
|
|
246
|
+
>
|
|
247
|
+
<Refresh size={13} class={loading ? "icon spin" : undefined} />
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{error && <p class="error">{error}</p>}
|
|
252
|
+
{notice && <p class="pr-notice">{notice}</p>}
|
|
253
|
+
{data && !data.available && (
|
|
254
|
+
<p class="empty">
|
|
255
|
+
GitHub indisponível
|
|
256
|
+
{data.reason ? `: ${data.reason}` : "."}
|
|
257
|
+
</p>
|
|
258
|
+
)}
|
|
259
|
+
{data && !data.worktree.available && (
|
|
260
|
+
<p class="empty">Não foi possível ler o estado do git.</p>
|
|
261
|
+
)}
|
|
262
|
+
{data?.worktree.available && !data.worktree.clean && (
|
|
263
|
+
<p class="pr-warning">
|
|
264
|
+
{data.worktree.changes} alteração(ões) não commitada(s) — checkout
|
|
265
|
+
bloqueado até limpar a árvore.
|
|
266
|
+
</p>
|
|
267
|
+
)}
|
|
268
|
+
{data?.currentBranch && (
|
|
269
|
+
<p class="pr-current-branch">
|
|
270
|
+
Branch atual: <code>{data.currentBranch}</code>
|
|
271
|
+
</p>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
<nav class="prs" aria-label="Pull requests abertas">
|
|
275
|
+
{loading && !data && <p class="empty">Carregando PRs…</p>}
|
|
276
|
+
{data?.available && data.prs.length === 0 && (
|
|
277
|
+
<p class="empty">Nenhuma PR aberta.</p>
|
|
278
|
+
)}
|
|
279
|
+
{data?.prs.map((pr) => (
|
|
280
|
+
<PrItem
|
|
281
|
+
key={pr.number}
|
|
282
|
+
pr={pr}
|
|
283
|
+
currentBranch={data.currentBranch}
|
|
284
|
+
worktree={data.worktree}
|
|
285
|
+
busy={busy !== null}
|
|
286
|
+
confirming={confirming === pr.number}
|
|
287
|
+
onConfirm={setConfirming}
|
|
288
|
+
onCancel={() => setConfirming(null)}
|
|
289
|
+
onCheckout={checkout}
|
|
290
|
+
onSelectRun={onSelect}
|
|
291
|
+
/>
|
|
292
|
+
))}
|
|
293
|
+
</nav>
|
|
294
|
+
</>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Aba "Resumo" (inicial): visão principal da revisão.
|
|
3
3
|
*
|
|
4
|
-
* O texto narrativo vem do artefato `review-resume.md`
|
|
5
|
-
* números viram cards a partir do
|
|
6
|
-
* card de domínios resume os achados
|
|
7
|
-
*
|
|
8
|
-
* texto.
|
|
4
|
+
* O texto narrativo vem do artefato `review-resume.md` e é exibido por inteiro
|
|
5
|
+
* (resumo de negócio + pontos técnicos); os números viram cards a partir do
|
|
6
|
+
* `classification.json`/`manifest.json` e o card de domínios resume os achados
|
|
7
|
+
* por severidade em cada watchdog.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import type { ComponentChildren } from "preact";
|
|
12
11
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
13
|
-
import { ExternalLink,
|
|
14
|
-
import {
|
|
12
|
+
import { ExternalLink, Trash } from "./icons";
|
|
13
|
+
import { MarkdownText } from "./MarkdownText";
|
|
15
14
|
import { api } from "./utils/api";
|
|
16
15
|
import { SEVERITY_LABELS, SEVERITY_ORDER } from "./utils/constants";
|
|
17
16
|
import { formatRunTime } from "./utils/format";
|
|
18
|
-
import { sectionBlocks } from "./utils/markdown";
|
|
19
17
|
import type {
|
|
20
18
|
ClassificationArtifact,
|
|
21
19
|
Finding,
|
|
@@ -79,13 +77,13 @@ export function ResumePanel({
|
|
|
79
77
|
branch,
|
|
80
78
|
findings,
|
|
81
79
|
summary,
|
|
82
|
-
|
|
80
|
+
onRequestDelete,
|
|
83
81
|
}: {
|
|
84
82
|
slug: string;
|
|
85
83
|
branch: string;
|
|
86
84
|
findings: Finding[];
|
|
87
85
|
summary: Summary;
|
|
88
|
-
|
|
86
|
+
onRequestDelete: () => void;
|
|
89
87
|
}) {
|
|
90
88
|
const [resume, setResume] = useState("");
|
|
91
89
|
const [detail, setDetail] = useState<RunDetail | null>(null);
|
|
@@ -126,7 +124,6 @@ export function ResumePanel({
|
|
|
126
124
|
}, [slug]);
|
|
127
125
|
|
|
128
126
|
const manifest: RunManifest | null = detail?.manifest ?? null;
|
|
129
|
-
const business = useMemo(() => sectionBlocks(resume, "Resumo de negócio"), [resume]);
|
|
130
127
|
const status =
|
|
131
128
|
summary.bySeverity.blocking > 0
|
|
132
129
|
? STATUS_VIEW.blocking
|
|
@@ -241,12 +238,12 @@ export function ResumePanel({
|
|
|
241
238
|
<span class={`resume-status ${status.tone}`}>{status.label}</span>
|
|
242
239
|
<button
|
|
243
240
|
type="button"
|
|
244
|
-
class="btn"
|
|
245
|
-
title="
|
|
246
|
-
onClick={
|
|
241
|
+
class="btn danger"
|
|
242
|
+
title="Excluir esta execução, seus findings e artefatos locais"
|
|
243
|
+
onClick={onRequestDelete}
|
|
247
244
|
>
|
|
248
|
-
<
|
|
249
|
-
|
|
245
|
+
<Trash size={14} />
|
|
246
|
+
Excluir
|
|
250
247
|
</button>
|
|
251
248
|
</div>
|
|
252
249
|
</section>
|
|
@@ -295,16 +292,16 @@ export function ResumePanel({
|
|
|
295
292
|
|
|
296
293
|
<div class="resume-grid">
|
|
297
294
|
<section class="resume-card">
|
|
298
|
-
<h3 class="resume-card-title">Resumo
|
|
295
|
+
<h3 class="resume-card-title">Resumo da revisão</h3>
|
|
299
296
|
{loading ? (
|
|
300
297
|
<p class="empty">Carregando resumo…</p>
|
|
301
|
-
) :
|
|
298
|
+
) : resume.trim().length > 0 ? (
|
|
302
299
|
<div class="resume-narrative">
|
|
303
|
-
<
|
|
300
|
+
<MarkdownText text={resume} />
|
|
304
301
|
</div>
|
|
305
302
|
) : (
|
|
306
303
|
<p class="empty">
|
|
307
|
-
O resumo
|
|
304
|
+
O resumo ainda não foi gerado para esta run. Rode o{" "}
|
|
308
305
|
<code>--finalize</code> para criar{" "}
|
|
309
306
|
<code>review-resume.md</code>.
|
|
310
307
|
</p>
|