feat(kanban): merge-all branches + floating running-agents dock

Two board-level additions:

Merge all reviewable agent branches into the main branch sequentially:
- worktrees.mergeBranches() merges candidates in order, advancing HEAD each
  time; refuses if the main tree is dirty, and on the first conflict aborts
  that merge (main left clean) and stops — prior merges stay, untried
  branches reported as unattempted.
- runManager.mergeAll() picks candidates (settled runs that own their
  worktree, branch present, not yet in HEAD; deduped, oldest-first) — so
  refinement runs (which inherit a worktree) are correctly excluded.
- POST /runs/merge-all; ⬇ Merge all button in the board header opens a
  results modal (per-branch ✓/⇢/⚠, conflict banner, inline git output).

Floating dock to view/open running agents (front-end only):
- RunningAgentsBar pins bottom-right (below the card modal), lists every
  running run with live elapsed times, and opens that card's modal on click;
  auto-hides when nothing runs, collapsible otherwise.
EOF && echo "" && git log --oneline -4
This commit is contained in:
2026-06-17 21:04:51 -04:00
parent 408bdb6dd7
commit d538ccdd4e
8 changed files with 636 additions and 1 deletions

View File

@@ -113,6 +113,25 @@ export interface MergeResult {
output: string;
}
/** One branch's outcome in a batch (merge-all) merge. */
export interface BatchMergeItem extends MergeResult {
/** The run this branch belonged to. */
runId: string;
}
/** Result of merging several branches sequentially into the main branch. */
export interface BatchMergeResult {
target: string;
/** One entry per candidate branch, in the order processed. */
items: BatchMergeItem[];
/** The branch that conflicted and stopped the batch, if any. */
haltedOn: string | null;
/** True if the main worktree was dirty and nothing was attempted. */
aborted: boolean;
/** Human-readable reason when aborted or halted. */
reason: string | null;
}
export const orchestratorApi = {
listRuns: (cardId?: string) =>
req<{ runs: AgentRun[] }>(`/runs${cardId ? `?cardId=${encodeURIComponent(cardId)}` : ''}`),
@@ -141,6 +160,9 @@ export const orchestratorApi = {
mergeRun: (id: string) =>
req<MergeResult>(`/runs/${id}/merge`, { method: 'POST' }),
/** Merge all reviewable agent branches into the main branch sequentially. */
mergeAll: () => req<BatchMergeResult>('/runs/merge-all', { method: 'POST' }),
bevyStatus: (id: string) => req<{ running: boolean }>(`/runs/${id}/bevy`),
startBevy: (id: string) => req<{ ok: boolean }>(`/runs/${id}/bevy`, { method: 'POST' }),