@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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/cli/index.ts +9 -1
  3. package/src/core/ai/ai-client.ts +21 -0
  4. package/src/core/ai/ai-sdk-model-provider.ts +43 -18
  5. package/src/core/ai/features/technical-summarizer.ts +43 -0
  6. package/src/core/ai/index.ts +4 -1
  7. package/src/core/ai/prompts/base.ts +7 -0
  8. package/src/core/ai/prompts/context.ts +5 -4
  9. package/src/core/ai/prompts/technical.ts +31 -0
  10. package/src/core/ai/provider-capabilities.ts +45 -0
  11. package/src/core/ai/schemas.ts +19 -3
  12. package/src/core/ai/types.ts +3 -1
  13. package/src/core/brief.ts +25 -4
  14. package/src/core/commands/scaffold.ts +5 -0
  15. package/src/core/config-loader.ts +18 -4
  16. package/src/core/config.ts +17 -1
  17. package/src/core/engine/review-workspace.ts +6 -2
  18. package/src/core/git-runner.ts +8 -1
  19. package/src/core/orchestrator.ts +114 -2
  20. package/src/core/paths.ts +3 -0
  21. package/src/core/pr-context.ts +11 -6
  22. package/src/core/project-config.ts +9 -1
  23. package/src/core/renderers/resume-renderer.ts +17 -34
  24. package/src/core/report-builder.ts +6 -6
  25. package/src/core/services/worktree-manager.ts +90 -7
  26. package/src/core/storage/types.ts +1 -0
  27. package/src/core/technical-summary.ts +398 -0
  28. package/src/harness/agent/agent-factory.ts +1 -0
  29. package/src/harness/agent/review-agent.ts +11 -1
  30. package/src/harness/config/harness-config.ts +6 -1
  31. package/src/index.ts +2 -0
  32. package/src/server/client/App.tsx +2 -1
  33. package/src/server/client/PrsSection.tsx +296 -0
  34. package/src/server/client/ResumePanel.tsx +10 -13
  35. package/src/server/client/RunsSection.tsx +73 -0
  36. package/src/server/client/Sidebar.tsx +171 -413
  37. package/src/server/client/sidebar-sections.tsx +73 -0
  38. package/src/server/client/utils/constants.ts +7 -2
  39. package/src/server/client/utils/sidebar-prefs.ts +64 -0
  40. package/src/server/client/utils/types.ts +1 -0
  41. package/src/server/index.ts +23 -409
  42. package/src/server/styles.css +454 -0
  43. package/src/tests/__snapshots__/baseline.test.ts.snap +5 -0
  44. package/src/tests/ai-core.test.ts +71 -0
  45. package/src/tests/ai-features.test.ts +47 -0
  46. package/src/tests/baseline.test.ts +4 -1
  47. package/src/tests/harness-agent.test.ts +19 -0
  48. package/src/tests/review-resume.test.ts +36 -8
  49. package/src/tests/review-scope.test.ts +67 -0
  50. package/src/tests/review-scripts.test.ts +2 -2
  51. package/src/tests/server.test.ts +16 -1
  52. package/src/tests/sidebar-sections.test.ts +94 -0
  53. package/src/tests/technical-summary.test.ts +176 -0
  54. package/src/tests/worktree.test.ts +110 -5
  55. package/assets/logo-legacy.png +0 -0
@@ -1,221 +1,30 @@
1
1
  /**
2
- * Sidebar: logo do houndsman + abas `Execuções` e `PR's`.
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
- * A aba de execuções mantém a lista de runs (status, data e findings). A aba de
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
- BranchIcon,
12
- Clock,
13
- GitHub,
14
- GitPullRequest,
15
- ListIcon,
16
- Refresh,
17
- Sparkles,
18
- Spinner,
19
- } from "./icons";
20
- import { api } from "./utils/api";
21
- import { describeRunStatus } from "./utils/constants";
22
- import { formatRunTime } from "./utils/format";
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
- function PrItem({
88
- pr,
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 [view, setView] = useState<SidebarView>("runs");
232
- const [data, setData] = useState<PrListResponse | null>(null);
233
- const [loading, setLoading] = useState(false);
234
- const [error, setError] = useState("");
235
- const [notice, setNotice] = useState("");
236
- const [busy, setBusy] = useState<number | null>(null);
237
- const [worktreeBusy, setWorktreeBusy] = useState<number | null>(null);
238
- const [confirming, setConfirming] = useState<number | null>(null);
239
- const requested = useRef(false);
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
- if (view !== "prs" || requested.current) return;
255
- requested.current = true;
256
- void loadPrs();
257
- }, [view, loadPrs]);
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 checkout = useCallback(
260
- async (pr: PrListItem) => {
261
- setBusy(pr.number);
262
- setError("");
263
- setNotice("");
264
- try {
265
- const result = await api<CheckoutResponse>("/api/prs/checkout", {
266
- method: "POST",
267
- headers: { "content-type": "application/json" },
268
- body: JSON.stringify({ number: pr.number }),
269
- });
270
- if (!result.ok) {
271
- if (result.reason === "dirty") {
272
- setData((current) =>
273
- current
274
- ? {
275
- ...current,
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
- [loadPrs, onRefreshRuns, onSelect],
78
+ [collapsed, width],
312
79
  );
313
- const createWorktree = useCallback(
314
- async (pr: PrListItem) => {
315
- setWorktreeBusy(pr.number);
316
- setError("");
317
- setNotice("");
318
- try {
319
- const result = await api<PrWorktreeResponse>("/api/prs/worktree", {
320
- method: "POST",
321
- headers: { "content-type": "application/json" },
322
- body: JSON.stringify({ number: pr.number }),
323
- });
324
- if (!result.ok) {
325
- setError(result.detail);
326
- return;
327
- }
328
- setNotice(
329
- `Worktree de #${pr.number} em ${result.path}. ` +
330
- `Rode \`houndsman --pr ${pr.number}\` para revisar em paralelo.`,
331
- );
332
- await loadPrs();
333
- } catch (err) {
334
- setError(String(err));
335
- } finally {
336
- setWorktreeBusy(null);
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
- [loadPrs],
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
- <div class="side-tabs" role="tablist" aria-label="Navegação lateral">
352
- <button
353
- type="button"
354
- role="tab"
355
- aria-selected={view === "runs"}
356
- class={`side-tab${view === "runs" ? " active" : ""}`}
357
- onClick={() => setView("runs")}
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
- {view === "runs" ? (
374
- <nav class="runs" aria-label="Execuções">
375
- {runs.length === 0 && <p class="empty">Nenhuma run encontrada.</p>}
376
- {runs.map((run) => (
377
- <RunItem
378
- key={run.slug}
379
- run={run}
380
- active={run.slug === slug}
381
- onSelect={onSelect}
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="icon-btn"
392
- title="Atualizar lista de PRs"
393
- disabled={loading}
394
- onClick={() => void loadPrs()}
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
- <Refresh
397
- size={13}
398
- class={loading ? "icon spin" : undefined}
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
- {error && <p class="error">{error}</p>}
404
- {notice && <p class="pr-notice">{notice}</p>}
405
- {data && !data.available && (
406
- <p class="empty">
407
- GitHub indisponível
408
- {data.reason ? `: ${data.reason}` : "."}
409
- </p>
410
- )}
411
- {data && !data.worktree.available && (
412
- <p class="empty">Não foi possível ler o estado do git.</p>
413
- )}
414
- {data?.worktree.available && !data.worktree.clean && (
415
- <p class="pr-warning">
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
- <nav class="prs" aria-label="Pull requests abertas">
427
- {loading && !data && <p class="empty">Carregando PRs…</p>}
428
- {data?.available && data.prs.length === 0 && (
429
- <p class="empty">Nenhuma PR aberta.</p>
430
- )}
431
- {data?.prs.map((pr) => (
432
- <PrItem
433
- key={pr.number}
434
- pr={pr}
435
- currentBranch={data.currentBranch}
436
- worktree={data.worktree}
437
- busy={busy !== null}
438
- confirming={confirming === pr.number}
439
- worktreeBusy={worktreeBusy === pr.number}
440
- onConfirm={setConfirming}
441
- onCancel={() => setConfirming(null)}
442
- onCheckout={checkout}
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
  );