refactor: enhance team provisioning process and UI updates

- Updated provisioning states to include 'configuring', 'assembling', and 'finalizing' for better tracking of team setup progress.
- Refactored the provisioning progress block to utilize a new display step system, improving clarity in the UI.
- Adjusted the README to include a comprehensive table of contents and updated comparison metrics for multi-agent orchestration tools.
- Enhanced team management UI to reflect new provisioning states and improve user experience during team setup.
This commit is contained in:
iliya 2026-03-21 16:47:20 +02:00
parent 7bca2e73a6
commit 6d441efa97
10 changed files with 137 additions and 67 deletions

View file

@ -26,6 +26,17 @@ https://github.com/user-attachments/assets/9cae73cd-7f42-46e5-a8fb-ad6d41737ff8
<br />
## Table of contents
- [What is this](#what-is-this)
- [Installation](#installation)
- [Quick start](#quick-start)
- [FAQ](#faq)
- [Comparison](#comparison)
- [Roadmap](#roadmap)
- [Development](#development)
- [Contributing](#contributing)
## What is this
A new approach to task management with AI agent teams.
@ -78,24 +89,6 @@ A new approach to task management with AI agent teams.
</details>
## Comparison
How we compare to other multi-agent orchestration tools:
| Feature | Claude Agent Teams UI | Vibe Kanban | Dorothy | Cursor | Claude Code CLI |
|---|---|---|---|---|---|
| **Agent-to-agent messaging** | ✅ Native real-time mailbox | ❌ Agents are independent | ⚠️ Only via Super Agent | ❌ | ✅ Built-in (no UI) |
| **Cross-team communication** | ✅ | ❌ | ❌ | ❌ | ✅ (no UI) |
| **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ Auto-assignment | ❌ | ❌ |
| **Per-task code review** | ✅ Accept / reject / comment | ⚠️ PR-level only | ❌ | ✅ BugBot on PRs | ❌ |
| **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ |
| **Review workflow** | ✅ Agents review each other | ❌ | ❌ | ❌ | ✅ (no UI) |
| **Session analysis** | ✅ 6-category token tracking | ❌ | ⚠️ Basic stats | ❌ | ❌ |
| **Execution log viewer** | ✅ Tool calls, reasoning, timeline | ❌ | ❌ | ✅ | ❌ |
| **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ✅ |
| **Multi-agent backend** | 🗓️ Planned | ✅ 6+ agents | ✅ 3 agents | ✅ Own models | — |
| **Git worktree isolation** | ✅ Optional | ✅ Built-in | ❌ | ✅ | ✅ |
| **Price** | **Free** | Free / $30 user/mo | Free | $0$200/mo | Claude subscription |
## Installation
@ -206,6 +199,33 @@ Yes. Run multiple teams in one project or across different projects, even simult
---
## Comparison
How we compare to other multi-agent orchestration tools:
| Feature | Claude Agent Teams UI | Vibe Kanban | Aperant | Cursor | Claude Code CLI |
|---|---|---|---|---|---|
| **Agent-to-agent messaging** | ✅ Native real-time mailbox | ❌ Agents are independent | ❌ Fixed pipeline | ❌ | ✅⚠️ Built-in (no UI) |
| **Cross-team communication** | ✅ | ❌ | ❌ | ❌ | ✅⚠️ (no UI) |
| **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ 6 columns (pipeline) | ❌ | ❌ |
| **Per-task code review** | ✅ Accept / reject / comment | ⚠️ PR-level only | ⚠️ File-level only | ✅ BugBot on PRs | ❌ |
| **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ |
| **Review workflow** | ✅ Agents review each other | ❌ | ⚠️ Auto QA pipeline | ❌ | ✅⚠️ (no UI) |
| **Session analysis** | ✅ 6-category token tracking | ❌ | ⚠️ Execution logs | ❌ | ❌ |
| **Execution log viewer** | ✅ Tool calls, reasoning, timeline | ❌ | ✅ Phase-based logs | ✅ | ❌ |
| **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ✅ |
| **Live processes** | ✅ View, stop, open URLs in browser | ❌ | ✅ 12 agent terminals | ✅ | ❌ |
| **Flexible autonomy** | ✅ Granular settings, per-action approval, notifications | ❌ | ⚠️ Plan approval only | ✅ | ✅ |
| **Full autonomy** | ✅ Agents create, assign, review tasks end-to-end | ❌ Human manages tasks | ❌ Fixed pipeline | ⚠️ Isolated tasks only | ✅⚠️ (no UI) |
| **Task dependencies (blocked by)** | ✅ Guaranteed ordering | ❌ | ⚠️ Within plan only | ❌ | ❌ |
| **Linked tasks** | ✅ Cross-references in messages | ⚠️ Subtasks only | ❌ | ❌ | ❌ |
| **Task attachments** | ✅ Auto-attach, agents read & attach files | ❌ | ✅ Images + files | ❌ | ❌ |
| **Multi-agent backend** | 🗓️ In development | ✅ 6+ agents | ✅ 11 providers | ✅ Own models | — |
| **Git worktree isolation** | ✅ Optional | ⚠️ Mandatory | ⚠️ Mandatory | ✅ | ✅ |
| **Price** | **Free** | Free / $30 user/mo | Free | $0$200/mo | Claude subscription |
---
## Development
<details>

View file

@ -2657,9 +2657,11 @@ export class TeamProvisioningService {
// Restart filesystem monitor for createTeam (launch skips it)
if (!run.isLaunch) {
updateProgress(run, 'configuring', 'Waiting for team configuration...');
run.onProgress(run.progress);
this.startFilesystemMonitor(run, run.request);
} else {
updateProgress(run, 'monitoring', 'CLI running — reconnecting with teammates');
updateProgress(run, 'configuring', 'CLI running — reconnecting with teammates');
run.onProgress(run.progress);
}
@ -3018,6 +3020,8 @@ export class TeamProvisioningService {
// of relying on stdout (which only arrives at the end in text mode).
// When config + members + tasks are all present, kill the process early
// rather than waiting for it to deadlock on system-reminder shutdown.
updateProgress(run, 'configuring', 'Waiting for team configuration...');
run.onProgress(run.progress);
this.startFilesystemMonitor(run, request);
run.timeoutHandle = setTimeout(() => {
@ -3442,7 +3446,7 @@ export class TeamProvisioningService {
// For launch, skip the filesystem monitor — files (config, inboxes, tasks)
// already exist from the previous run and would trigger immediate false
// completion on the first poll. Rely on stream-json result.success instead.
updateProgress(run, 'monitoring', 'CLI running — reconnecting with teammates');
updateProgress(run, 'configuring', 'CLI running — reconnecting with teammates');
run.onProgress(run.progress);
run.timeoutHandle = setTimeout(() => {
@ -3502,7 +3506,11 @@ export class TeamProvisioningService {
if (!run) {
throw new Error('Unknown runId');
}
if (!['spawning', 'monitoring', 'verifying'].includes(run.progress.state)) {
if (
!['spawning', 'configuring', 'assembling', 'finalizing', 'verifying'].includes(
run.progress.state
)
) {
throw new Error('Provisioning cannot be cancelled in current state');
}
@ -6325,7 +6333,7 @@ export class TeamProvisioningService {
run.fsPhase = 'waiting_members';
const progress = updateProgress(
run,
'monitoring',
'assembling',
'Team config created, waiting for members',
{ configReady: true }
);
@ -6336,11 +6344,7 @@ export class TeamProvisioningService {
if (run.fsPhase === 'waiting_members') {
if (request.members.length === 0) {
run.fsPhase = 'waiting_tasks';
const progress = updateProgress(
run,
'monitoring',
'Solo team, skipping member inbox wait'
);
const progress = updateProgress(run, 'finalizing', 'Solo team, preparing workspace');
run.onProgress(progress);
} else {
const teamDir = (await resolveTeamDir()) ?? configuredTeamDir;
@ -6350,14 +6354,14 @@ export class TeamProvisioningService {
run.fsPhase = 'waiting_tasks';
const progress = updateProgress(
run,
'monitoring',
`All ${inboxCount} member inboxes created, waiting for tasks`
'finalizing',
`All ${inboxCount} member inboxes created, preparing workspace`
);
run.onProgress(progress);
} else if (inboxCount > 0) {
const progress = updateProgress(
run,
'monitoring',
'assembling',
`${inboxCount}/${request.members.length} member inboxes created`
);
run.onProgress(progress);

View file

@ -7,15 +7,16 @@ import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
import { MarkdownViewer } from '../chat/viewers/MarkdownViewer';
import { CliLogsRichView } from './CliLogsRichView';
import { STEP_LABELS, STEP_ORDER } from './provisioningSteps';
import { DISPLAY_STEPS } from './provisioningSteps';
import { StepProgressBar } from './StepProgressBar';
import type { StepProgressBarStep } from './StepProgressBar';
/** Pre-built step definitions for the provisioning stepper (excludes 'ready') */
const PROVISIONING_STEPS: StepProgressBarStep[] = STEP_ORDER.filter((s) => s !== 'ready').map(
(s) => ({ key: s, label: STEP_LABELS[s] })
);
/** Pre-built step definitions for the provisioning stepper. */
const PROVISIONING_STEPS: StepProgressBarStep[] = DISPLAY_STEPS.map((s) => ({
key: s.key,
label: s.label,
}));
export interface ProvisioningProgressBlockProps {
/** Title above the steps, e.g. "Launching team" */
@ -26,7 +27,7 @@ export interface ProvisioningProgressBlockProps {
tone?: 'default' | 'error';
/** Whether Live output is expanded by default */
defaultLiveOutputOpen?: boolean;
/** Index of the current step in STEP_ORDER (0-based), or -1 if unknown */
/** Display step index (0-3 for active steps, 4 for ready/all done, -1 for terminal) */
currentStepIndex: number;
/** Show spinner next to title */
loading?: boolean;
@ -155,15 +156,23 @@ export const ProvisioningProgressBlock = ({
// Open CLI logs while loading, collapse when done (unless error).
const prevLoadingRef = useRef(loading);
const hadLogsRef = useRef(Boolean(cliLogsTail));
useEffect(() => {
if (!isError && cliLogsTail) {
if (loading && !prevLoadingRef.current) {
// Started loading → open
if (!isError) {
const hasLogs = Boolean(cliLogsTail);
if (loading && hasLogs && !hadLogsRef.current) {
// Logs just appeared while loading → open
setLogsOpen(true);
} else if (loading && !prevLoadingRef.current && hasLogs) {
// Started loading with logs already present → open
setLogsOpen(true);
} else if (!loading && prevLoadingRef.current) {
// Finished loading → collapse
setLogsOpen(false);
}
hadLogsRef.current = hasLogs;
}
prevLoadingRef.current = loading;
}, [loading, cliLogsTail, isError]);

View file

@ -144,7 +144,9 @@ function resolveTeamStatus(
}
if (
currentProgress &&
['validating', 'spawning', 'monitoring', 'verifying'].includes(currentProgress.state)
['validating', 'spawning', 'configuring', 'assembling', 'finalizing', 'verifying'].includes(
currentProgress.state
)
) {
return 'provisioning';
}

View file

@ -7,9 +7,8 @@ import { CheckCircle2, X } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { ProvisioningProgressBlock } from './ProvisioningProgressBlock';
import { STEP_ORDER } from './provisioningSteps';
import { getDisplayStepIndex } from './provisioningSteps';
import type { ProvisioningStep } from './provisioningSteps';
interface TeamProvisioningBannerProps {
teamName: string;
}
@ -51,15 +50,19 @@ export const TeamProvisioningBanner = ({
const isActive =
progress.state === 'validating' ||
progress.state === 'spawning' ||
progress.state === 'monitoring' ||
progress.state === 'configuring' ||
progress.state === 'assembling' ||
progress.state === 'finalizing' ||
progress.state === 'verifying';
const canCancel =
progress.state === 'spawning' ||
progress.state === 'monitoring' ||
progress.state === 'configuring' ||
progress.state === 'assembling' ||
progress.state === 'finalizing' ||
progress.state === 'verifying';
const progressStepIndex = STEP_ORDER.indexOf(progress.state as ProvisioningStep);
const progressStepIndex = getDisplayStepIndex(progress.state);
if (isFailed) {
return (

View file

@ -1,9 +1,32 @@
export const STEP_ORDER = ['validating', 'spawning', 'monitoring', 'verifying', 'ready'] as const;
export type ProvisioningStep = (typeof STEP_ORDER)[number];
export const STEP_LABELS: Record<ProvisioningStep, string> = {
validating: 'Validate',
spawning: 'Start CLI',
monitoring: 'Provisioning',
verifying: 'Verify',
ready: 'Ready',
};
import type { TeamProvisioningState } from '@shared/types/team';
/** Display steps for the provisioning stepper (0-indexed). */
export const DISPLAY_STEPS = [
{ key: 'starting', label: 'Starting' },
{ key: 'configuring', label: 'Team setup' },
{ key: 'assembling', label: 'Members joining' },
{ key: 'finalizing', label: 'Finalizing' },
] as const;
/**
* Maps a backend provisioning state to a 0-based display step index.
* Returns DISPLAY_STEPS.length for 'ready' (all steps complete), -1 for terminal/unknown.
*/
export function getDisplayStepIndex(state: Exclude<TeamProvisioningState, 'idle'>): number {
switch (state) {
case 'validating':
case 'spawning':
return 0;
case 'configuring':
return 1;
case 'assembling':
return 2;
case 'finalizing':
case 'verifying':
return 3;
case 'ready':
return DISPLAY_STEPS.length;
default:
return -1;
}
}

View file

@ -26,7 +26,14 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const ACTIVE_PROVISIONING_STATES = new Set(['validating', 'spawning', 'monitoring', 'verifying']);
const ACTIVE_PROVISIONING_STATES = new Set([
'validating',
'spawning',
'configuring',
'assembling',
'finalizing',
'verifying',
]);
const TERMINAL_PROVISIONING_STATES = new Set(['ready', 'failed', 'disconnected', 'cancelled']);
function isPendingProvisioningRunId(runId: string): boolean {
@ -1689,7 +1696,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
if (pendingRun) {
delete nextRuns[pendingRunId];
// Only use pending data as fallback if real progress events haven't arrived yet.
// This prevents overwriting real progress (e.g. 'monitoring') with stale pending data ('spawning')
// This prevents overwriting real progress (e.g. 'assembling') with stale pending data ('spawning')
// when the invoke response arrives before IPC progress events.
if (!realProgressAlreadyExists) {
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };
@ -1826,7 +1833,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
if (pendingRun) {
delete nextRuns[pendingRunId];
// Only use pending data as fallback if real progress events haven't arrived yet.
// This prevents overwriting real progress (e.g. 'monitoring') with stale pending data ('spawning')
// This prevents overwriting real progress (e.g. 'assembling') with stale pending data ('spawning')
// when the invoke response arrives before IPC progress events.
if (!realProgressAlreadyExists) {
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };

View file

@ -544,7 +544,9 @@ export type TeamProvisioningState =
| 'idle'
| 'validating'
| 'spawning'
| 'monitoring'
| 'configuring'
| 'assembling'
| 'finalizing'
| 'verifying'
| 'ready'
| 'disconnected'

View file

@ -125,8 +125,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
progress: {
runId: 'run-1',
teamName: 'team-alpha',
state: 'monitoring',
message: 'Monitoring',
state: 'assembling',
message: 'Assembling',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
},
@ -183,8 +183,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
progress: {
runId: 'run-2',
teamName: 'team-alpha',
state: 'monitoring',
message: 'Monitoring',
state: 'assembling',
message: 'Assembling',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
},

View file

@ -442,7 +442,7 @@ describe('teamSlice actions', () => {
store.getState().onProvisioningProgress({
runId: 'run-real',
teamName: 'my-team',
state: 'monitoring',
state: 'assembling',
message: 'Real run',
startedAt: '2026-03-12T10:00:01.000Z',
updatedAt: '2026-03-12T10:00:01.000Z',
@ -454,7 +454,7 @@ describe('teamSlice actions', () => {
expect(store.getState().provisioningRuns['run-real']).toEqual(
expect.objectContaining({
runId: 'run-real',
state: 'monitoring',
state: 'assembling',
})
);
});
@ -513,7 +513,7 @@ describe('teamSlice actions', () => {
store.getState().onProvisioningProgress({
runId: 'pending:my-team:1',
teamName: 'my-team',
state: 'monitoring',
state: 'assembling',
message: 'Late zombie progress',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:02.000Z',
@ -576,7 +576,7 @@ describe('teamSlice actions', () => {
'run-current': {
runId: 'run-current',
teamName: 'my-team',
state: 'monitoring',
state: 'assembling',
message: 'Current run',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
@ -615,7 +615,7 @@ describe('teamSlice actions', () => {
store.getState().onProvisioningProgress({
runId: 'run-current',
teamName: 'my-team',
state: 'monitoring',
state: 'assembling',
message: 'Current run',
startedAt,
updatedAt: startedAt,