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:
parent
7bca2e73a6
commit
6d441efa97
10 changed files with 137 additions and 67 deletions
56
README.md
56
README.md
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -544,7 +544,9 @@ export type TeamProvisioningState =
|
|||
| 'idle'
|
||||
| 'validating'
|
||||
| 'spawning'
|
||||
| 'monitoring'
|
||||
| 'configuring'
|
||||
| 'assembling'
|
||||
| 'finalizing'
|
||||
| 'verifying'
|
||||
| 'ready'
|
||||
| 'disconnected'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue