@payconductor/houndsman 0.8.0 → 0.11.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/cli/index.ts +9 -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/base.ts +7 -0
- package/src/core/ai/prompts/context.ts +5 -4
- package/src/core/ai/prompts/technical.ts +31 -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-loader.ts +18 -4
- package/src/core/config.ts +17 -1
- package/src/core/engine/review-workspace.ts +6 -2
- 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 +90 -7
- 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 +47 -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 +110 -5
- package/assets/logo-legacy.png +0 -0
|
@@ -1,221 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidebar:
|
|
2
|
+
* Sidebar do servidor: rail vertical de seções (padrão VS Code) + painel
|
|
3
|
+
* expansível. A navegação vem do registry `sidebar-sections.tsx`; a rail fica
|
|
4
|
+
* sempre visível, inclusive com o painel recolhido.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
* PRs lista as PRs abertas via `gh`, permite abrir a run correspondente e faz
|
|
6
|
-
* checkout somente quando a árvore de trabalho está limpa.
|
|
6
|
+
* Largura, colapso e seção ativa são preferências locais (`sidebar-prefs.ts`).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
10
|
+
import { ChevronRight } from "./icons";
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import type {
|
|
24
|
-
CheckoutResponse,
|
|
25
|
-
PrListItem,
|
|
26
|
-
PrListResponse,
|
|
27
|
-
PrWorktreeResponse,
|
|
28
|
-
RunSummary,
|
|
29
|
-
WorktreeState,
|
|
30
|
-
} from "./utils/types";
|
|
31
|
-
|
|
32
|
-
type SidebarView = "runs" | "prs";
|
|
33
|
-
|
|
34
|
-
const REVIEW_LABELS: Record<string, string> = {
|
|
35
|
-
APPROVED: "aprovada",
|
|
36
|
-
CHANGES_REQUESTED: "mudanças",
|
|
37
|
-
REVIEW_REQUIRED: "revisão pendente",
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
function RunItem({
|
|
41
|
-
run,
|
|
42
|
-
active,
|
|
43
|
-
onSelect,
|
|
44
|
-
}: {
|
|
45
|
-
run: RunSummary;
|
|
46
|
-
active: boolean;
|
|
47
|
-
onSelect: (slug: string) => void;
|
|
48
|
-
}) {
|
|
49
|
-
const status = describeRunStatus(run);
|
|
50
|
-
const fixReady = status?.kind === "fix_done";
|
|
51
|
-
return (
|
|
52
|
-
<button
|
|
53
|
-
type="button"
|
|
54
|
-
title={`Abrir a run da branch ${run.branch}`}
|
|
55
|
-
class={`run${active ? " active" : ""}`}
|
|
56
|
-
onClick={() => onSelect(run.slug)}
|
|
57
|
-
>
|
|
58
|
-
<span class="run-top">
|
|
59
|
-
<BranchIcon size={14} class="run-icon" />
|
|
60
|
-
<span class="run-branch">{run.branch}</span>
|
|
61
|
-
</span>
|
|
62
|
-
<span class="run-meta">
|
|
63
|
-
<span class="run-when">
|
|
64
|
-
<Clock size={12} />
|
|
65
|
-
{formatRunTime(run.updatedAt) || "sem data"}
|
|
66
|
-
</span>
|
|
67
|
-
<span
|
|
68
|
-
class="run-count"
|
|
69
|
-
title={`${run.summary.present} de ${run.summary.total} findings presentes`}
|
|
70
|
-
>
|
|
71
|
-
{run.summary.present}/{run.summary.total}
|
|
72
|
-
</span>
|
|
73
|
-
</span>
|
|
74
|
-
{fixReady ? (
|
|
75
|
-
<span class="run-badge">
|
|
76
|
-
<Sparkles size={12} /> Correção pronta
|
|
77
|
-
</span>
|
|
78
|
-
) : (
|
|
79
|
-
status && (
|
|
80
|
-
<span class={`run-status status-${status.kind}`}>{status.label}</span>
|
|
81
|
-
)
|
|
82
|
-
)}
|
|
83
|
-
</button>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
12
|
+
getSidebarSection,
|
|
13
|
+
resolveSidebarSectionId,
|
|
14
|
+
SIDEBAR_SECTIONS,
|
|
15
|
+
type SidebarSectionContext,
|
|
16
|
+
} from "./sidebar-sections";
|
|
17
|
+
import {
|
|
18
|
+
clampSidebarWidth,
|
|
19
|
+
readSidebarPrefs,
|
|
20
|
+
SIDEBAR_MAX_WIDTH,
|
|
21
|
+
SIDEBAR_MIN_WIDTH,
|
|
22
|
+
saveSidebarPrefs,
|
|
23
|
+
} from "./utils/sidebar-prefs";
|
|
24
|
+
import type { RunSummary } from "./utils/types";
|
|
86
25
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
currentBranch,
|
|
90
|
-
worktree,
|
|
91
|
-
busy,
|
|
92
|
-
confirming,
|
|
93
|
-
worktreeBusy,
|
|
94
|
-
onConfirm,
|
|
95
|
-
onCancel,
|
|
96
|
-
onCheckout,
|
|
97
|
-
onCreateWorktree,
|
|
98
|
-
onSelectRun,
|
|
99
|
-
}: {
|
|
100
|
-
pr: PrListItem;
|
|
101
|
-
currentBranch: string | null;
|
|
102
|
-
worktree: WorktreeState;
|
|
103
|
-
busy: boolean;
|
|
104
|
-
confirming: boolean;
|
|
105
|
-
worktreeBusy: boolean;
|
|
106
|
-
onConfirm: (number: number) => void;
|
|
107
|
-
onCancel: () => void;
|
|
108
|
-
onCheckout: (pr: PrListItem) => void;
|
|
109
|
-
onCreateWorktree: (pr: PrListItem) => void;
|
|
110
|
-
onSelectRun: (slug: string) => void;
|
|
111
|
-
}) {
|
|
112
|
-
const isCurrent = currentBranch === pr.headRefName;
|
|
113
|
-
const runSlug = pr.runSlug;
|
|
114
|
-
const review = pr.reviewDecision ? REVIEW_LABELS[pr.reviewDecision] : undefined;
|
|
115
|
-
return (
|
|
116
|
-
<article class={`pr${isCurrent ? " current" : ""}`}>
|
|
117
|
-
<div class="pr-head">
|
|
118
|
-
<GitPullRequest size={14} class="pr-icon" />
|
|
119
|
-
<span class="pr-number">#{pr.number}</span>
|
|
120
|
-
{pr.isDraft && <span class="tag">draft</span>}
|
|
121
|
-
{isCurrent && <span class="pr-current">atual</span>}
|
|
122
|
-
<span class="spacer" />
|
|
123
|
-
{pr.updatedAt && (
|
|
124
|
-
<span class="run-when">
|
|
125
|
-
<Clock size={12} />
|
|
126
|
-
{formatRunTime(pr.updatedAt)}
|
|
127
|
-
</span>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
|
-
<p class="pr-title">{pr.title}</p>
|
|
131
|
-
<p class="pr-meta">
|
|
132
|
-
<span class="pr-branch">{pr.headRefName}</span>
|
|
133
|
-
<span class="pr-arrow">→</span>
|
|
134
|
-
<span>{pr.baseRefName || "—"}</span>
|
|
135
|
-
<span>· {pr.author}</span>
|
|
136
|
-
</p>
|
|
137
|
-
{review && <p class={`pr-review pr-review-${pr.reviewDecision}`}>{review}</p>}
|
|
138
|
-
{pr.worktreePath && (
|
|
139
|
-
<p class="pr-worktree" title={pr.worktreePath}>
|
|
140
|
-
Worktree: <code>{pr.worktreePath}</code>
|
|
141
|
-
</p>
|
|
142
|
-
)}
|
|
143
|
-
<div class="pr-actions">
|
|
144
|
-
{runSlug && (
|
|
145
|
-
<button
|
|
146
|
-
type="button"
|
|
147
|
-
class="pr-btn"
|
|
148
|
-
title="Abrir a run local desta branch"
|
|
149
|
-
onClick={() => onSelectRun(runSlug)}
|
|
150
|
-
>
|
|
151
|
-
Abrir run
|
|
152
|
-
</button>
|
|
153
|
-
)}
|
|
154
|
-
{!pr.worktreePath && (
|
|
155
|
-
<button
|
|
156
|
-
type="button"
|
|
157
|
-
class="pr-btn"
|
|
158
|
-
disabled={busy || worktreeBusy}
|
|
159
|
-
title={`Criar um worktree dedicado para ${pr.headRefName} (revisão paralela)`}
|
|
160
|
-
onClick={() => onCreateWorktree(pr)}
|
|
161
|
-
>
|
|
162
|
-
{worktreeBusy ? (
|
|
163
|
-
<Spinner size={12} class="icon spin" />
|
|
164
|
-
) : (
|
|
165
|
-
"Worktree"
|
|
166
|
-
)}
|
|
167
|
-
</button>
|
|
168
|
-
)}
|
|
169
|
-
{pr.url && (
|
|
170
|
-
<a
|
|
171
|
-
class="pr-btn"
|
|
172
|
-
href={pr.url}
|
|
173
|
-
target="_blank"
|
|
174
|
-
rel="noreferrer"
|
|
175
|
-
title="Abrir a PR no GitHub"
|
|
176
|
-
>
|
|
177
|
-
<GitHub size={12} /> GitHub
|
|
178
|
-
</a>
|
|
179
|
-
)}
|
|
180
|
-
{confirming ? (
|
|
181
|
-
<>
|
|
182
|
-
<button
|
|
183
|
-
type="button"
|
|
184
|
-
class="pr-btn danger"
|
|
185
|
-
disabled={busy}
|
|
186
|
-
title="Confirmar checkout da PR"
|
|
187
|
-
onClick={() => onCheckout(pr)}
|
|
188
|
-
>
|
|
189
|
-
{busy ? <Spinner size={12} class="icon spin" /> : "Confirmar"}
|
|
190
|
-
</button>
|
|
191
|
-
<button
|
|
192
|
-
type="button"
|
|
193
|
-
class="pr-btn"
|
|
194
|
-
disabled={busy}
|
|
195
|
-
onClick={onCancel}
|
|
196
|
-
>
|
|
197
|
-
Cancelar
|
|
198
|
-
</button>
|
|
199
|
-
</>
|
|
200
|
-
) : (
|
|
201
|
-
<button
|
|
202
|
-
type="button"
|
|
203
|
-
class="pr-btn"
|
|
204
|
-
disabled={!worktree.clean || busy}
|
|
205
|
-
title={
|
|
206
|
-
worktree.clean
|
|
207
|
-
? `Fazer checkout da branch ${pr.headRefName}`
|
|
208
|
-
: "Há alterações não commitadas — faça commit ou stash antes"
|
|
209
|
-
}
|
|
210
|
-
onClick={() => onConfirm(pr.number)}
|
|
211
|
-
>
|
|
212
|
-
Checkout
|
|
213
|
-
</button>
|
|
214
|
-
)}
|
|
215
|
-
</div>
|
|
216
|
-
</article>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
26
|
+
/** Passo do redimensionamento por teclado (Shift amplia). */
|
|
27
|
+
const KEYBOARD_STEP = 16;
|
|
219
28
|
|
|
220
29
|
export function Sidebar({
|
|
221
30
|
runs,
|
|
@@ -228,224 +37,173 @@ export function Sidebar({
|
|
|
228
37
|
onSelect: (slug: string) => void;
|
|
229
38
|
onRefreshRuns: () => void;
|
|
230
39
|
}) {
|
|
231
|
-
const [
|
|
232
|
-
const [
|
|
233
|
-
const [
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const loadPrs = useCallback(async () => {
|
|
242
|
-
setLoading(true);
|
|
243
|
-
setError("");
|
|
244
|
-
try {
|
|
245
|
-
setData(await api<PrListResponse>("/api/prs"));
|
|
246
|
-
} catch (err) {
|
|
247
|
-
setError(String(err));
|
|
248
|
-
} finally {
|
|
249
|
-
setLoading(false);
|
|
250
|
-
}
|
|
251
|
-
}, []);
|
|
40
|
+
const [width, setWidth] = useState(() => clampSidebarWidth(readSidebarPrefs().width));
|
|
41
|
+
const [collapsed, setCollapsed] = useState(() => readSidebarPrefs().collapsed);
|
|
42
|
+
const [active, setActive] = useState(() =>
|
|
43
|
+
resolveSidebarSectionId(readSidebarPrefs().section),
|
|
44
|
+
);
|
|
45
|
+
const drag = useRef<{
|
|
46
|
+
startX: number;
|
|
47
|
+
startWidth: number;
|
|
48
|
+
pointerId: number;
|
|
49
|
+
} | null>(null);
|
|
252
50
|
|
|
51
|
+
// Persistência com debounce: arrastar o divisor não escreve a cada pixel.
|
|
253
52
|
useEffect(() => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
53
|
+
const id = setTimeout(
|
|
54
|
+
() => saveSidebarPrefs({ width, collapsed, section: active }),
|
|
55
|
+
180,
|
|
56
|
+
);
|
|
57
|
+
return () => clearTimeout(id);
|
|
58
|
+
}, [width, collapsed, active]);
|
|
258
59
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
worktree: {
|
|
277
|
-
...current.worktree,
|
|
278
|
-
clean: false,
|
|
279
|
-
changes:
|
|
280
|
-
result.changes?.length ??
|
|
281
|
-
current.worktree.changes,
|
|
282
|
-
sample:
|
|
283
|
-
result.changes ?? current.worktree.sample,
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
: current,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
setError(
|
|
290
|
-
result.reason === "dirty"
|
|
291
|
-
? `Checkout bloqueado: ${result.detail}`
|
|
292
|
-
: result.detail,
|
|
293
|
-
);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
setConfirming(null);
|
|
297
|
-
setNotice(
|
|
298
|
-
`Checkout de #${pr.number} concluído${
|
|
299
|
-
result.branch ? ` — ${result.branch}` : ""
|
|
300
|
-
}.`,
|
|
301
|
-
);
|
|
302
|
-
await loadPrs();
|
|
303
|
-
onRefreshRuns();
|
|
304
|
-
if (pr.runSlug) onSelect(pr.runSlug);
|
|
305
|
-
} catch (err) {
|
|
306
|
-
setError(String(err));
|
|
307
|
-
} finally {
|
|
308
|
-
setBusy(null);
|
|
309
|
-
}
|
|
60
|
+
const selectSection = useCallback((id: string) => {
|
|
61
|
+
setActive(id);
|
|
62
|
+
setCollapsed(false);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const startResize = useCallback(
|
|
66
|
+
(event: PointerEvent) => {
|
|
67
|
+
if (collapsed || event.button !== 0) return;
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
const target = event.currentTarget as HTMLElement;
|
|
70
|
+
target.setPointerCapture?.(event.pointerId);
|
|
71
|
+
drag.current = {
|
|
72
|
+
startX: event.clientX,
|
|
73
|
+
startWidth: width,
|
|
74
|
+
pointerId: event.pointerId,
|
|
75
|
+
};
|
|
76
|
+
document.body.classList.add("resizing-sidebar");
|
|
310
77
|
},
|
|
311
|
-
[
|
|
78
|
+
[collapsed, width],
|
|
312
79
|
);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
80
|
+
|
|
81
|
+
const moveResize = useCallback((event: PointerEvent) => {
|
|
82
|
+
const state = drag.current;
|
|
83
|
+
if (!state || event.pointerId !== state.pointerId) return;
|
|
84
|
+
setWidth(clampSidebarWidth(state.startWidth + event.clientX - state.startX));
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const endResize = useCallback((event: PointerEvent) => {
|
|
88
|
+
if (!drag.current) return;
|
|
89
|
+
drag.current = null;
|
|
90
|
+
(event.currentTarget as HTMLElement).releasePointerCapture?.(event.pointerId);
|
|
91
|
+
document.body.classList.remove("resizing-sidebar");
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const resizeByKeyboard = useCallback(
|
|
95
|
+
(event: KeyboardEvent) => {
|
|
96
|
+
const step = event.shiftKey ? KEYBOARD_STEP * 2 : KEYBOARD_STEP;
|
|
97
|
+
let next: number | null = null;
|
|
98
|
+
if (event.key === "ArrowLeft") next = width - step;
|
|
99
|
+
if (event.key === "ArrowRight") next = width + step;
|
|
100
|
+
if (event.key === "Home") next = SIDEBAR_MIN_WIDTH;
|
|
101
|
+
if (event.key === "End") next = SIDEBAR_MAX_WIDTH;
|
|
102
|
+
if (next === null) return;
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
setWidth(clampSidebarWidth(next));
|
|
338
105
|
},
|
|
339
|
-
[
|
|
106
|
+
[width],
|
|
340
107
|
);
|
|
341
|
-
return (
|
|
342
|
-
<aside class="sidebar">
|
|
343
|
-
<div class="brand">
|
|
344
|
-
<img class="logo" src="/assets/logo.png" alt="" />
|
|
345
|
-
<div>
|
|
346
|
-
<h1>Houndsman</h1>
|
|
347
|
-
<p class="sub">Revisões e triagem</p>
|
|
348
|
-
</div>
|
|
349
|
-
</div>
|
|
350
108
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
<ListIcon size={13} /> Execuções
|
|
360
|
-
<span class="side-count">{runs.length}</span>
|
|
361
|
-
</button>
|
|
362
|
-
<button
|
|
363
|
-
type="button"
|
|
364
|
-
role="tab"
|
|
365
|
-
aria-selected={view === "prs"}
|
|
366
|
-
class={`side-tab${view === "prs" ? " active" : ""}`}
|
|
367
|
-
onClick={() => setView("prs")}
|
|
368
|
-
>
|
|
369
|
-
<GitPullRequest size={13} /> PR's
|
|
370
|
-
</button>
|
|
371
|
-
</div>
|
|
109
|
+
const section = getSidebarSection(active);
|
|
110
|
+
const context: SidebarSectionContext = {
|
|
111
|
+
runs,
|
|
112
|
+
slug,
|
|
113
|
+
onSelect,
|
|
114
|
+
onRefreshRuns,
|
|
115
|
+
active: false,
|
|
116
|
+
};
|
|
372
117
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
</nav>
|
|
385
|
-
) : (
|
|
386
|
-
<>
|
|
387
|
-
<div class="pr-toolbar">
|
|
388
|
-
<p class="section-label">PRs abertas</p>
|
|
118
|
+
return (
|
|
119
|
+
<aside
|
|
120
|
+
class={`sidebar${collapsed ? " collapsed" : ""}`}
|
|
121
|
+
style={`--side-width:${width}px`}
|
|
122
|
+
>
|
|
123
|
+
<nav class="side-rail" aria-label="Seções do Houndsman">
|
|
124
|
+
{SIDEBAR_SECTIONS.map((item) => {
|
|
125
|
+
const SectionIcon = item.icon;
|
|
126
|
+
const count = item.badge?.(context);
|
|
127
|
+
const isActive = item.id === active;
|
|
128
|
+
return (
|
|
389
129
|
<button
|
|
130
|
+
key={item.id}
|
|
390
131
|
type="button"
|
|
391
|
-
class=
|
|
392
|
-
title=
|
|
393
|
-
|
|
394
|
-
|
|
132
|
+
class={`rail-btn${isActive ? " active" : ""}`}
|
|
133
|
+
title={item.label}
|
|
134
|
+
aria-label={item.label}
|
|
135
|
+
aria-current={isActive ? "page" : undefined}
|
|
136
|
+
aria-controls={`side-section-${item.id}`}
|
|
137
|
+
onClick={() => selectSection(item.id)}
|
|
395
138
|
>
|
|
396
|
-
<
|
|
397
|
-
|
|
398
|
-
class=
|
|
399
|
-
|
|
139
|
+
<SectionIcon size={20} />
|
|
140
|
+
{typeof count === "number" && count > 0 && (
|
|
141
|
+
<span class="rail-badge">
|
|
142
|
+
{count > 99 ? "99+" : count}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
400
145
|
</button>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
<span class="rail-spacer" />
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
class="rail-btn rail-toggle"
|
|
152
|
+
title={collapsed ? "Expandir painel" : "Recolher painel"}
|
|
153
|
+
aria-label={collapsed ? "Expandir painel" : "Recolher painel"}
|
|
154
|
+
aria-expanded={!collapsed}
|
|
155
|
+
onClick={() => setCollapsed((current) => !current)}
|
|
156
|
+
>
|
|
157
|
+
<ChevronRight size={18} class={collapsed ? undefined : "icon flip"} />
|
|
158
|
+
</button>
|
|
159
|
+
</nav>
|
|
160
|
+
|
|
161
|
+
<div class="side-panel">
|
|
162
|
+
<header class="side-head">
|
|
163
|
+
<div class="brand">
|
|
164
|
+
<img class="logo" src="/assets/logo.png" alt="" />
|
|
165
|
+
<div>
|
|
166
|
+
<h1>Houndsman</h1>
|
|
167
|
+
<p class="sub">Revisões e triagem</p>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="side-title">
|
|
171
|
+
<h2>{section.title}</h2>
|
|
172
|
+
{section.hint && <p class="side-hint">{section.hint}</p>}
|
|
401
173
|
</div>
|
|
174
|
+
</header>
|
|
402
175
|
|
|
403
|
-
|
|
404
|
-
{
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
)}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
{data.worktree.changes} alteração(ões) não commitada(s) —
|
|
417
|
-
checkout bloqueado até limpar a árvore.
|
|
418
|
-
</p>
|
|
419
|
-
)}
|
|
420
|
-
{data?.currentBranch && (
|
|
421
|
-
<p class="pr-current-branch">
|
|
422
|
-
Branch atual: <code>{data.currentBranch}</code>
|
|
423
|
-
</p>
|
|
424
|
-
)}
|
|
176
|
+
<div class="side-body">
|
|
177
|
+
{SIDEBAR_SECTIONS.map((item) => (
|
|
178
|
+
<section
|
|
179
|
+
key={item.id}
|
|
180
|
+
id={`side-section-${item.id}`}
|
|
181
|
+
class="side-section"
|
|
182
|
+
hidden={item.id !== active}
|
|
183
|
+
>
|
|
184
|
+
<item.Content {...context} active={item.id === active} />
|
|
185
|
+
</section>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
425
189
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
onCreateWorktree={createWorktree}
|
|
444
|
-
onSelectRun={onSelect}
|
|
445
|
-
/>
|
|
446
|
-
))}
|
|
447
|
-
</nav>
|
|
448
|
-
</>
|
|
190
|
+
{!collapsed && (
|
|
191
|
+
// biome-ignore lint/a11y/useSemanticElements: divisor arrastável, não conteúdo.
|
|
192
|
+
<div
|
|
193
|
+
class="side-resizer"
|
|
194
|
+
role="separator"
|
|
195
|
+
aria-orientation="vertical"
|
|
196
|
+
aria-label="Redimensionar painel lateral"
|
|
197
|
+
aria-valuemin={SIDEBAR_MIN_WIDTH}
|
|
198
|
+
aria-valuemax={SIDEBAR_MAX_WIDTH}
|
|
199
|
+
aria-valuenow={width}
|
|
200
|
+
tabIndex={0}
|
|
201
|
+
onPointerDown={startResize}
|
|
202
|
+
onPointerMove={moveResize}
|
|
203
|
+
onPointerUp={endResize}
|
|
204
|
+
onPointerCancel={endResize}
|
|
205
|
+
onKeyDown={resizeByKeyboard}
|
|
206
|
+
/>
|
|
449
207
|
)}
|
|
450
208
|
</aside>
|
|
451
209
|
);
|