diff --git a/README.md b/README.md index 62259df8..96a81cb5 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,17 @@ https://github.com/user-attachments/assets/9cae73cd-7f42-46e5-a8fb-ad6d41737ff8
+## 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. -## 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
diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 23df85b8..de0203f9 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -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); diff --git a/src/renderer/components/team/ProvisioningProgressBlock.tsx b/src/renderer/components/team/ProvisioningProgressBlock.tsx index 329a81db..621b1c43 100644 --- a/src/renderer/components/team/ProvisioningProgressBlock.tsx +++ b/src/renderer/components/team/ProvisioningProgressBlock.tsx @@ -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]); diff --git a/src/renderer/components/team/TeamListView.tsx b/src/renderer/components/team/TeamListView.tsx index 71b52441..e096b325 100644 --- a/src/renderer/components/team/TeamListView.tsx +++ b/src/renderer/components/team/TeamListView.tsx @@ -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'; } diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx index 266e0d30..5f4b45c3 100644 --- a/src/renderer/components/team/TeamProvisioningBanner.tsx +++ b/src/renderer/components/team/TeamProvisioningBanner.tsx @@ -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 ( diff --git a/src/renderer/components/team/provisioningSteps.ts b/src/renderer/components/team/provisioningSteps.ts index 6446431e..1127cd38 100644 --- a/src/renderer/components/team/provisioningSteps.ts +++ b/src/renderer/components/team/provisioningSteps.ts @@ -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 = { - 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): 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; + } +} diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 5bf77a87..d33f2a4e 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -26,7 +26,14 @@ function sleep(ms: number): Promise { 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 = (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 = (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 }; diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts index d36c5460..01218e08 100644 --- a/src/shared/types/team.ts +++ b/src/shared/types/team.ts @@ -544,7 +544,9 @@ export type TeamProvisioningState = | 'idle' | 'validating' | 'spawning' - | 'monitoring' + | 'configuring' + | 'assembling' + | 'finalizing' | 'verifying' | 'ready' | 'disconnected' diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index b521d28f..6b30db2d 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -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', }, diff --git a/test/renderer/store/teamSlice.test.ts b/test/renderer/store/teamSlice.test.ts index 72fb7157..98a48107 100644 --- a/test/renderer/store/teamSlice.test.ts +++ b/test/renderer/store/teamSlice.test.ts @@ -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,