@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
|
@@ -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
|
+
}
|
|
@@ -1,196 +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
|
-
RunSummary,
|
|
28
|
-
WorktreeState,
|
|
29
|
-
} from "./utils/types";
|
|
30
|
-
|
|
31
|
-
type SidebarView = "runs" | "prs";
|
|
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";
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
CHANGES_REQUESTED: "mudanças",
|
|
36
|
-
REVIEW_REQUIRED: "revisão pendente",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function RunItem({
|
|
40
|
-
run,
|
|
41
|
-
active,
|
|
42
|
-
onSelect,
|
|
43
|
-
}: {
|
|
44
|
-
run: RunSummary;
|
|
45
|
-
active: boolean;
|
|
46
|
-
onSelect: (slug: string) => void;
|
|
47
|
-
}) {
|
|
48
|
-
const status = describeRunStatus(run);
|
|
49
|
-
const fixReady = status?.kind === "fix_done";
|
|
50
|
-
return (
|
|
51
|
-
<button
|
|
52
|
-
type="button"
|
|
53
|
-
title={`Abrir a run da branch ${run.branch}`}
|
|
54
|
-
class={`run${active ? " active" : ""}`}
|
|
55
|
-
onClick={() => onSelect(run.slug)}
|
|
56
|
-
>
|
|
57
|
-
<span class="run-top">
|
|
58
|
-
<BranchIcon size={14} class="run-icon" />
|
|
59
|
-
<span class="run-branch">{run.branch}</span>
|
|
60
|
-
</span>
|
|
61
|
-
<span class="run-meta">
|
|
62
|
-
<span class="run-when">
|
|
63
|
-
<Clock size={12} />
|
|
64
|
-
{formatRunTime(run.updatedAt) || "sem data"}
|
|
65
|
-
</span>
|
|
66
|
-
<span
|
|
67
|
-
class="run-count"
|
|
68
|
-
title={`${run.summary.present} de ${run.summary.total} findings presentes`}
|
|
69
|
-
>
|
|
70
|
-
{run.summary.present}/{run.summary.total}
|
|
71
|
-
</span>
|
|
72
|
-
</span>
|
|
73
|
-
{fixReady ? (
|
|
74
|
-
<span class="run-badge">
|
|
75
|
-
<Sparkles size={12} /> Correção pronta
|
|
76
|
-
</span>
|
|
77
|
-
) : (
|
|
78
|
-
status && (
|
|
79
|
-
<span class={`run-status status-${status.kind}`}>{status.label}</span>
|
|
80
|
-
)
|
|
81
|
-
)}
|
|
82
|
-
</button>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function PrItem({
|
|
87
|
-
pr,
|
|
88
|
-
currentBranch,
|
|
89
|
-
worktree,
|
|
90
|
-
busy,
|
|
91
|
-
confirming,
|
|
92
|
-
onConfirm,
|
|
93
|
-
onCancel,
|
|
94
|
-
onCheckout,
|
|
95
|
-
onSelectRun,
|
|
96
|
-
}: {
|
|
97
|
-
pr: PrListItem;
|
|
98
|
-
currentBranch: string | null;
|
|
99
|
-
worktree: WorktreeState;
|
|
100
|
-
busy: boolean;
|
|
101
|
-
confirming: boolean;
|
|
102
|
-
onConfirm: (number: number) => void;
|
|
103
|
-
onCancel: () => void;
|
|
104
|
-
onCheckout: (pr: PrListItem) => void;
|
|
105
|
-
onSelectRun: (slug: string) => void;
|
|
106
|
-
}) {
|
|
107
|
-
const isCurrent = currentBranch === pr.headRefName;
|
|
108
|
-
const runSlug = pr.runSlug;
|
|
109
|
-
const review = pr.reviewDecision ? REVIEW_LABELS[pr.reviewDecision] : undefined;
|
|
110
|
-
return (
|
|
111
|
-
<article class={`pr${isCurrent ? " current" : ""}`}>
|
|
112
|
-
<div class="pr-head">
|
|
113
|
-
<GitPullRequest size={14} class="pr-icon" />
|
|
114
|
-
<span class="pr-number">#{pr.number}</span>
|
|
115
|
-
{pr.isDraft && <span class="tag">draft</span>}
|
|
116
|
-
{isCurrent && <span class="pr-current">atual</span>}
|
|
117
|
-
<span class="spacer" />
|
|
118
|
-
{pr.updatedAt && (
|
|
119
|
-
<span class="run-when">
|
|
120
|
-
<Clock size={12} />
|
|
121
|
-
{formatRunTime(pr.updatedAt)}
|
|
122
|
-
</span>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
<p class="pr-title">{pr.title}</p>
|
|
126
|
-
<p class="pr-meta">
|
|
127
|
-
<span class="pr-branch">{pr.headRefName}</span>
|
|
128
|
-
<span class="pr-arrow">→</span>
|
|
129
|
-
<span>{pr.baseRefName || "—"}</span>
|
|
130
|
-
<span>· {pr.author}</span>
|
|
131
|
-
</p>
|
|
132
|
-
{review && <p class={`pr-review pr-review-${pr.reviewDecision}`}>{review}</p>}
|
|
133
|
-
<div class="pr-actions">
|
|
134
|
-
{runSlug && (
|
|
135
|
-
<button
|
|
136
|
-
type="button"
|
|
137
|
-
class="pr-btn"
|
|
138
|
-
title="Abrir a run local desta branch"
|
|
139
|
-
onClick={() => onSelectRun(runSlug)}
|
|
140
|
-
>
|
|
141
|
-
Abrir run
|
|
142
|
-
</button>
|
|
143
|
-
)}
|
|
144
|
-
{pr.url && (
|
|
145
|
-
<a
|
|
146
|
-
class="pr-btn"
|
|
147
|
-
href={pr.url}
|
|
148
|
-
target="_blank"
|
|
149
|
-
rel="noreferrer"
|
|
150
|
-
title="Abrir a PR no GitHub"
|
|
151
|
-
>
|
|
152
|
-
<GitHub size={12} /> GitHub
|
|
153
|
-
</a>
|
|
154
|
-
)}
|
|
155
|
-
{confirming ? (
|
|
156
|
-
<>
|
|
157
|
-
<button
|
|
158
|
-
type="button"
|
|
159
|
-
class="pr-btn danger"
|
|
160
|
-
disabled={busy}
|
|
161
|
-
title="Confirmar checkout da PR"
|
|
162
|
-
onClick={() => onCheckout(pr)}
|
|
163
|
-
>
|
|
164
|
-
{busy ? <Spinner size={12} class="icon spin" /> : "Confirmar"}
|
|
165
|
-
</button>
|
|
166
|
-
<button
|
|
167
|
-
type="button"
|
|
168
|
-
class="pr-btn"
|
|
169
|
-
disabled={busy}
|
|
170
|
-
onClick={onCancel}
|
|
171
|
-
>
|
|
172
|
-
Cancelar
|
|
173
|
-
</button>
|
|
174
|
-
</>
|
|
175
|
-
) : (
|
|
176
|
-
<button
|
|
177
|
-
type="button"
|
|
178
|
-
class="pr-btn"
|
|
179
|
-
disabled={!worktree.clean || busy}
|
|
180
|
-
title={
|
|
181
|
-
worktree.clean
|
|
182
|
-
? `Fazer checkout da branch ${pr.headRefName}`
|
|
183
|
-
: "Há alterações não commitadas — faça commit ou stash antes"
|
|
184
|
-
}
|
|
185
|
-
onClick={() => onConfirm(pr.number)}
|
|
186
|
-
>
|
|
187
|
-
Checkout
|
|
188
|
-
</button>
|
|
189
|
-
)}
|
|
190
|
-
</div>
|
|
191
|
-
</article>
|
|
192
|
-
);
|
|
193
|
-
}
|
|
26
|
+
/** Passo do redimensionamento por teclado (Shift amplia). */
|
|
27
|
+
const KEYBOARD_STEP = 16;
|
|
194
28
|
|
|
195
29
|
export function Sidebar({
|
|
196
30
|
runs,
|
|
@@ -203,194 +37,173 @@ export function Sidebar({
|
|
|
203
37
|
onSelect: (slug: string) => void;
|
|
204
38
|
onRefreshRuns: () => void;
|
|
205
39
|
}) {
|
|
206
|
-
const [
|
|
207
|
-
const [
|
|
208
|
-
const [
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
|
50
|
+
|
|
51
|
+
// Persistência com debounce: arrastar o divisor não escreve a cada pixel.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const id = setTimeout(
|
|
54
|
+
() => saveSidebarPrefs({ width, collapsed, section: active }),
|
|
55
|
+
180,
|
|
56
|
+
);
|
|
57
|
+
return () => clearTimeout(id);
|
|
58
|
+
}, [width, collapsed, active]);
|
|
214
59
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
setData(await api<PrListResponse>("/api/prs"));
|
|
220
|
-
} catch (err) {
|
|
221
|
-
setError(String(err));
|
|
222
|
-
} finally {
|
|
223
|
-
setLoading(false);
|
|
224
|
-
}
|
|
60
|
+
const selectSection = useCallback((id: string) => {
|
|
61
|
+
setActive(id);
|
|
62
|
+
setCollapsed(false);
|
|
225
63
|
}, []);
|
|
226
64
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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");
|
|
77
|
+
},
|
|
78
|
+
[collapsed, width],
|
|
79
|
+
);
|
|
232
80
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
result.changes ?? current.worktree.sample,
|
|
258
|
-
},
|
|
259
|
-
}
|
|
260
|
-
: current,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
setError(
|
|
264
|
-
result.reason === "dirty"
|
|
265
|
-
? `Checkout bloqueado: ${result.detail}`
|
|
266
|
-
: result.detail,
|
|
267
|
-
);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
setConfirming(null);
|
|
271
|
-
setNotice(
|
|
272
|
-
`Checkout de #${pr.number} concluído${
|
|
273
|
-
result.branch ? ` — ${result.branch}` : ""
|
|
274
|
-
}.`,
|
|
275
|
-
);
|
|
276
|
-
await loadPrs();
|
|
277
|
-
onRefreshRuns();
|
|
278
|
-
if (pr.runSlug) onSelect(pr.runSlug);
|
|
279
|
-
} catch (err) {
|
|
280
|
-
setError(String(err));
|
|
281
|
-
} finally {
|
|
282
|
-
setBusy(null);
|
|
283
|
-
}
|
|
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));
|
|
284
105
|
},
|
|
285
|
-
[
|
|
106
|
+
[width],
|
|
286
107
|
);
|
|
287
108
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
</div>
|
|
109
|
+
const section = getSidebarSection(active);
|
|
110
|
+
const context: SidebarSectionContext = {
|
|
111
|
+
runs,
|
|
112
|
+
slug,
|
|
113
|
+
onSelect,
|
|
114
|
+
onRefreshRuns,
|
|
115
|
+
active: false,
|
|
116
|
+
};
|
|
297
117
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 (
|
|
129
|
+
<button
|
|
130
|
+
key={item.id}
|
|
131
|
+
type="button"
|
|
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)}
|
|
138
|
+
>
|
|
139
|
+
<SectionIcon size={20} />
|
|
140
|
+
{typeof count === "number" && count > 0 && (
|
|
141
|
+
<span class="rail-badge">
|
|
142
|
+
{count > 99 ? "99+" : count}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
<span class="rail-spacer" />
|
|
309
149
|
<button
|
|
310
150
|
type="button"
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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)}
|
|
315
156
|
>
|
|
316
|
-
<
|
|
157
|
+
<ChevronRight size={18} class={collapsed ? undefined : "icon flip"} />
|
|
317
158
|
</button>
|
|
318
|
-
</
|
|
159
|
+
</nav>
|
|
319
160
|
|
|
320
|
-
|
|
321
|
-
<
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
) : (
|
|
333
|
-
<>
|
|
334
|
-
<div class="pr-toolbar">
|
|
335
|
-
<p class="section-label">PRs abertas</p>
|
|
336
|
-
<button
|
|
337
|
-
type="button"
|
|
338
|
-
class="icon-btn"
|
|
339
|
-
title="Atualizar lista de PRs"
|
|
340
|
-
disabled={loading}
|
|
341
|
-
onClick={() => void loadPrs()}
|
|
342
|
-
>
|
|
343
|
-
<Refresh
|
|
344
|
-
size={13}
|
|
345
|
-
class={loading ? "icon spin" : undefined}
|
|
346
|
-
/>
|
|
347
|
-
</button>
|
|
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>}
|
|
348
173
|
</div>
|
|
174
|
+
</header>
|
|
349
175
|
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
{data.worktree.changes} alteração(ões) não commitada(s) —
|
|
364
|
-
checkout bloqueado até limpar a árvore.
|
|
365
|
-
</p>
|
|
366
|
-
)}
|
|
367
|
-
{data?.currentBranch && (
|
|
368
|
-
<p class="pr-current-branch">
|
|
369
|
-
Branch atual: <code>{data.currentBranch}</code>
|
|
370
|
-
</p>
|
|
371
|
-
)}
|
|
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>
|
|
372
189
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
/>
|
|
391
|
-
))}
|
|
392
|
-
</nav>
|
|
393
|
-
</>
|
|
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
|
+
/>
|
|
394
207
|
)}
|
|
395
208
|
</aside>
|
|
396
209
|
);
|
|
@@ -120,6 +120,16 @@ export const Alert = (p: IconProps) => (
|
|
|
120
120
|
</Svg>
|
|
121
121
|
);
|
|
122
122
|
|
|
123
|
+
/** Lixeira — excluir a run. */
|
|
124
|
+
export const Trash = (p: IconProps) => (
|
|
125
|
+
<Svg {...p}>
|
|
126
|
+
<path d="M4 7h16" />
|
|
127
|
+
<path d="M9 7V4h6v3" />
|
|
128
|
+
<path d="m6.5 7 1 13h9l1-13" />
|
|
129
|
+
<path d="M10 11v6M14 11v6" />
|
|
130
|
+
</Svg>
|
|
131
|
+
);
|
|
132
|
+
|
|
123
133
|
export const ListIcon = (p: IconProps) => (
|
|
124
134
|
<Svg {...p}>
|
|
125
135
|
<path d="M9 6h12M9 12h12M9 18h12" />
|