feat(fanout): worktree fan-out board — race N agent lanes of one repo (W5)
Fan ONE prompt across N branch/agent lanes: N worktrees, N Claude sessions on the
same prompt, watched side-by-side in the split-grid board, approve/kill per lane,
🏆 keep the winner (losers' worktrees removed). ~90% composition of shipped parts.
- src/http/session-groups.ts (new, pure, no git exec): deriveRepoRoot /
groupSessionsByRepo (cluster sessions by their <repo>-worktrees parent).
- GET /live-sessions/grouped (read-only; registered BEFORE /live-sessions/:id so
"grouped" isn't captured as an id). MAX_FANOUT_LANES env (default 6, = grid-6 cap).
- public/fanout.ts (new, pure): buildFanoutCmd shell-quotes the prompt (single-quote
wrap with '\''-escaping so $(...)/backticks/;/&& can't execute) + collapses newlines
+ caps 4000 chars; laneBranch/slugify. Effective N = min(lanes, maxFanoutLanes, 6),
≥2; the maxSessions cap is enforced server-side ("Started K of N" banner).
- public/tabs.ts launchFanout (N× createWorktree → openProject w/ prompt pre-injected
via the existing initialInput) + keepFanoutWinner; public/projects.ts renderFanoutForm
+ extracted shared createWorktreeReq (DRY). Byte-shuttle preserved (lane = own PTY).
One-click merge deferred (winner session stays open for a manual merge).
Reused unchanged: createWorktree/removeWorktree, addEntry/initialInput, split-grid +
per-quadrant approve/maximize/monitor + gauges. Verified: typecheck + build:web clean,
2063 pass at --test-timeout=30000 (only the known tmux/PTY flake red). Fixed 3
/config/ui exact-shape tests to include the new maxFanoutLanes field.
This commit is contained in:
@@ -456,6 +456,20 @@ body {
|
||||
color: var(--text);
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.cell-keep {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.cell-keep:hover {
|
||||
color: var(--text);
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.cell-monitor-btn {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
@@ -1944,6 +1958,60 @@ body {
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
/* W5 fan-out form — sibling of the New Worktree form. */
|
||||
.proj-fanout-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.proj-fanout-prompt {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
min-height: 44px;
|
||||
font: inherit;
|
||||
}
|
||||
.proj-fanout-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.proj-fanout-lanes {
|
||||
width: 72px;
|
||||
}
|
||||
.proj-fanout-branch {
|
||||
flex: 1 1 160px;
|
||||
min-width: 120px;
|
||||
}
|
||||
.proj-fanout-error {
|
||||
color: var(--danger, #e5534b);
|
||||
font-size: 12px;
|
||||
}
|
||||
.proj-fanout-submit {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.proj-fanout-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* W5 fan-out status/error banner — fixed, dismissible, textContent only. */
|
||||
.fanout-banner {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 16px;
|
||||
transform: translateX(-50%);
|
||||
max-width: min(680px, 92vw);
|
||||
z-index: 60;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-3, #2a2c33);
|
||||
color: var(--text, #e7e8ec);
|
||||
border: 1px solid var(--border, #3a3d46);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Section header with an inline action (CLAUDE.md generate/update). */
|
||||
.proj-section-row {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user