@payconductor/houndsman 0.8.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/package.json +1 -1
- 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/brief.ts +25 -4
- package/src/core/commands/scaffold.ts +5 -0
- package/src/core/config.ts +17 -1
- package/src/core/git-runner.ts +8 -1
- package/src/core/orchestrator.ts +114 -2
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +9 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- package/src/core/services/worktree-manager.ts +29 -5
- package/src/core/storage/types.ts +1 -0
- package/src/core/technical-summary.ts +398 -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/index.ts +2 -0
- package/src/server/client/App.tsx +2 -1
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +10 -13
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -413
- 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 +1 -0
- package/src/server/index.ts +23 -409
- package/src/server/styles.css +454 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +5 -0
- 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/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 +16 -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 +39 -4
- package/assets/logo-legacy.png +0 -0
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type AiProviderOptions,
|
|
13
|
+
type AiUsage,
|
|
14
|
+
addUsage,
|
|
15
|
+
emptyUsage,
|
|
16
|
+
type ModelProvider,
|
|
17
|
+
} from "../../core/ai";
|
|
12
18
|
import type { AstService } from "../../core/ast/service";
|
|
13
19
|
import type { WatchdogReport } from "../../core/report-builder";
|
|
14
20
|
import type { Severity } from "../../core/types";
|
|
@@ -49,6 +55,7 @@ export type ReviewAgentDeps = {
|
|
|
49
55
|
reporter: AgentReporter;
|
|
50
56
|
maxOutputTokens: number;
|
|
51
57
|
temperature: number;
|
|
58
|
+
providerOptions?: AiProviderOptions;
|
|
52
59
|
timeoutMs: number;
|
|
53
60
|
ast: AstService;
|
|
54
61
|
readFile?: (path: string) => string | null;
|
|
@@ -338,6 +345,9 @@ export class ReviewAgent {
|
|
|
338
345
|
maxSteps,
|
|
339
346
|
maxOutputTokens: this.deps.maxOutputTokens,
|
|
340
347
|
temperature: this.deps.temperature,
|
|
348
|
+
...(this.deps.providerOptions
|
|
349
|
+
? { providerOptions: this.deps.providerOptions }
|
|
350
|
+
: {}),
|
|
341
351
|
signal: AbortSignal.timeout(this.deps.timeoutMs),
|
|
342
352
|
});
|
|
343
353
|
this.usage = addUsage(this.usage, response.usage);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* aplica apenas overrides de execução (`hunt --model`, `--max-calls`, etc.).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { AiProviderKind } from "../../core/config";
|
|
8
|
+
import type { AiProviderKind, AiProviderOptions } from "../../core/config";
|
|
9
9
|
import { ProjectConfig } from "../../core/project-config";
|
|
10
10
|
|
|
11
11
|
/** Overrides de execução do harness. */
|
|
@@ -76,6 +76,11 @@ export class HarnessConfig {
|
|
|
76
76
|
get temperature(): number {
|
|
77
77
|
return this.project.aiTemperature;
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/** Opções do AI SDK por provider (config do projeto, sem override de flag). */
|
|
81
|
+
get providerOptions(): AiProviderOptions | undefined {
|
|
82
|
+
return this.project.aiProviderOptions;
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/** Inteiro positivo ou o default. */
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
|
|
|
49
49
|
export { ProjectScaffolder } from "./core/commands/scaffold";
|
|
50
50
|
export type {
|
|
51
51
|
AiProviderKind,
|
|
52
|
+
AiProviderOptions,
|
|
52
53
|
HoundsmanAi,
|
|
53
54
|
HoundsmanAiFeatures,
|
|
54
55
|
HoundsmanAst,
|
|
@@ -195,6 +196,7 @@ export type {
|
|
|
195
196
|
WorktreeManagerOptions,
|
|
196
197
|
} from "./core/services/worktree-manager";
|
|
197
198
|
export {
|
|
199
|
+
branchSlug,
|
|
198
200
|
parseWorktreeList,
|
|
199
201
|
repoSlug,
|
|
200
202
|
WorktreeManager,
|
|
@@ -259,11 +259,12 @@ export function App() {
|
|
|
259
259
|
type="button"
|
|
260
260
|
key={item.id}
|
|
261
261
|
title={item.hint}
|
|
262
|
+
aria-label={item.label}
|
|
262
263
|
class={`seg-btn${tab === item.id ? " active" : ""}`}
|
|
263
264
|
onClick={() => setTab(item.id)}
|
|
264
265
|
>
|
|
265
266
|
<TabIcon id={item.id} />
|
|
266
|
-
{item.label}
|
|
267
|
+
<span class="seg-label">{item.label}</span>
|
|
267
268
|
</button>
|
|
268
269
|
))}
|
|
269
270
|
</div>
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -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>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seção `Execuções` da sidebar: lista das runs revisadas, com status, data e
|
|
3
|
+
* contagem de findings. Selecionar uma run troca apenas o conteúdo principal.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BranchIcon, Clock, Sparkles } from "./icons";
|
|
7
|
+
import type { SidebarSectionContext } from "./sidebar-sections";
|
|
8
|
+
import { describeRunStatus } from "./utils/constants";
|
|
9
|
+
import { formatRunTime } from "./utils/format";
|
|
10
|
+
import type { RunSummary } from "./utils/types";
|
|
11
|
+
|
|
12
|
+
function RunItem({
|
|
13
|
+
run,
|
|
14
|
+
active,
|
|
15
|
+
onSelect,
|
|
16
|
+
}: {
|
|
17
|
+
run: RunSummary;
|
|
18
|
+
active: boolean;
|
|
19
|
+
onSelect: (slug: string) => void;
|
|
20
|
+
}) {
|
|
21
|
+
const status = describeRunStatus(run);
|
|
22
|
+
const fixReady = status?.kind === "fix_done";
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
title={`Abrir a run da branch ${run.branch}`}
|
|
27
|
+
class={`run${active ? " active" : ""}`}
|
|
28
|
+
onClick={() => onSelect(run.slug)}
|
|
29
|
+
>
|
|
30
|
+
<span class="run-top">
|
|
31
|
+
<BranchIcon size={14} class="run-icon" />
|
|
32
|
+
<span class="run-branch">{run.branch}</span>
|
|
33
|
+
</span>
|
|
34
|
+
<span class="run-meta">
|
|
35
|
+
<span class="run-when">
|
|
36
|
+
<Clock size={12} />
|
|
37
|
+
{formatRunTime(run.updatedAt) || "sem data"}
|
|
38
|
+
</span>
|
|
39
|
+
<span
|
|
40
|
+
class="run-count"
|
|
41
|
+
title={`${run.summary.present} de ${run.summary.total} findings presentes`}
|
|
42
|
+
>
|
|
43
|
+
{run.summary.present}/{run.summary.total}
|
|
44
|
+
</span>
|
|
45
|
+
</span>
|
|
46
|
+
{fixReady ? (
|
|
47
|
+
<span class="run-badge">
|
|
48
|
+
<Sparkles size={12} /> Correção pronta
|
|
49
|
+
</span>
|
|
50
|
+
) : (
|
|
51
|
+
status && (
|
|
52
|
+
<span class={`run-status status-${status.kind}`}>{status.label}</span>
|
|
53
|
+
)
|
|
54
|
+
)}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function RunsSection({ runs, slug, onSelect }: SidebarSectionContext) {
|
|
60
|
+
return (
|
|
61
|
+
<nav class="runs" aria-label="Execuções">
|
|
62
|
+
{runs.length === 0 && <p class="empty">Nenhuma run encontrada.</p>}
|
|
63
|
+
{runs.map((run) => (
|
|
64
|
+
<RunItem
|
|
65
|
+
key={run.slug}
|
|
66
|
+
run={run}
|
|
67
|
+
active={run.slug === slug}
|
|
68
|
+
onSelect={onSelect}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</nav>
|
|
72
|
+
);
|
|
73
|
+
}
|