refactor: update UI and functionality for team provisioning and progress indicators

- Revised the TeamProvisioningService to change event handling from 'Task' to 'Agent' for improved accuracy.
- Enhanced the StepProgressBar component to include error indicators and animations for better user feedback during provisioning.
- Updated the TeamProvisioningBanner to track and display the last active step in case of errors.
- Improved CSS styles for step indicators, adding new animations and error states.
- Refined the sidebar task item styling based on light/dark themes for better visual consistency.
- Adjusted the README to reflect changes in installation instructions and feature comparisons.
This commit is contained in:
iliya 2026-03-21 17:43:29 +02:00
parent 3fcab46882
commit 8616db00a0
10 changed files with 194 additions and 75 deletions

View file

@ -26,17 +26,6 @@ https://github.com/user-attachments/assets/9cae73cd-7f42-46e5-a8fb-ad6d41737ff8
<br />
## Table of contents
- [Installation](#installation)
- [What is this](#what-is-this)
- [Comparison](#comparison)
- [Quick start](#quick-start)
- [FAQ](#faq)
- [Roadmap](#roadmap)
- [Development](#development)
- [Contributing](#contributing)
## Installation
No prerequisites — Claude Code can be installed and configured directly from the app.
@ -77,6 +66,16 @@ No prerequisites — Claude Code can be installed and configured directly from t
</tr>
</table>
## Table of contents
- [What is this](#what-is-this)
- [Comparison](#comparison)
- [Quick start](#quick-start)
- [FAQ](#faq)
- [Roadmap](#roadmap)
- [Development](#development)
- [Contributing](#contributing)
## What is this
A new approach to task management with AI agent teams.
@ -150,8 +149,9 @@ How we compare to other multi-agent orchestration tools:
| **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 | — |
| **Multi-agent backend** | 🗓️ [In development](https://github.com/Alishahryar1/free-claude-code) | ✅ 6+ agents | ✅ 11 providers | ✅ Own models | — |
| **Git worktree isolation** | ✅ Optional | ⚠️ Mandatory | ⚠️ Mandatory | ✅ | ✅ |
| **Built-in code editor** | ✅ With Git support | ❌ | ❌ | ✅ Full IDE | ❌ |
| **Price** | **Free** | Free / $30 user/mo | Free | $0$200/mo | Claude subscription |
---

View file

@ -4281,7 +4281,7 @@ export class TeamProvisioningService {
*/
private captureTeamSpawnEvents(run: ProvisioningRun, content: Record<string, unknown>[]): void {
for (const part of content) {
if (part.type !== 'tool_use' || part.name !== 'Task') continue;
if (part.type !== 'tool_use' || part.name !== 'Agent') continue;
const input = part.input;
if (!input || typeof input !== 'object') continue;
const inp = input as Record<string, unknown>;

View file

@ -147,7 +147,7 @@ export const SidebarTaskItem = ({
return (
<button
type="button"
className={`flex w-full cursor-pointer flex-col justify-center border-b px-2 py-1.5 text-left transition-colors hover:bg-surface-raised ${unreadCount > 0 ? 'bg-blue-500/[0.03]' : ''} ${task.teamDeleted ? 'opacity-50' : ''}`}
className={`flex w-full cursor-pointer flex-col justify-center border-b px-2 py-1.5 text-left transition-colors hover:bg-surface-raised ${unreadCount > 0 ? (isLight ? 'bg-blue-500/[0.03]' : 'bg-blue-500/[0.08]') : ''} ${task.teamDeleted ? 'opacity-50' : ''}`}
style={{ borderColor: 'var(--color-border)' }}
onClick={() => {
if (!isRenaming) {

View file

@ -29,6 +29,8 @@ export interface ProvisioningProgressBlockProps {
defaultLiveOutputOpen?: boolean;
/** Display step index (0-3 for active steps, 4 for ready/all done, -1 for terminal) */
currentStepIndex: number;
/** If set, this step index shows a red error indicator */
errorStepIndex?: number;
/** Show spinner next to title */
loading?: boolean;
/** Cancel button label and handler */
@ -119,6 +121,7 @@ export const ProvisioningProgressBlock = ({
tone = 'default',
defaultLiveOutputOpen = true,
currentStepIndex,
errorStepIndex,
loading = false,
onCancel,
startedAt,
@ -222,7 +225,11 @@ export const ProvisioningProgressBlock = ({
</p>
) : null}
<div className="mt-2 px-2">
<StepProgressBar steps={PROVISIONING_STEPS} currentIndex={currentStepIndex} />
<StepProgressBar
steps={PROVISIONING_STEPS}
currentIndex={currentStepIndex}
errorIndex={errorStepIndex}
/>
</div>
<div className="mt-2">
<button

View file

@ -1,5 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { cn } from '@renderer/lib/utils';
import { Check } from 'lucide-react';
import { Check, X } from 'lucide-react';
export interface StepProgressBarStep {
key: string;
@ -10,28 +12,50 @@ export interface StepProgressBarProps {
steps: StepProgressBarStep[];
/** 0-based index of the current step, -1 if not started */
currentIndex: number;
/** If set, this step shows a red error indicator instead of active/pending */
errorIndex?: number;
className?: string;
}
/**
* Circular step progress indicator with animated connecting lines.
*
* - Completed steps: green circle with checkmark
* - Completed steps: green circle with checkmark + jelly bounce on completion
* - Current step: green outlined circle with pulsing ring + number
* - Error step: red circle with X icon
* - Pending steps: gray circle with number
* - Lines between steps animate with a green fill for completed transitions
*/
export const StepProgressBar = ({
steps,
currentIndex,
errorIndex,
className,
}: StepProgressBarProps): React.JSX.Element => {
// Track which step just completed for jelly + flash animation
const prevIndexRef = useRef(currentIndex);
const [justCompletedIndex, setJustCompletedIndex] = useState<number | null>(null);
useEffect(() => {
const prev = prevIndexRef.current;
prevIndexRef.current = currentIndex;
// Animate the highest step that just became "done"
if (currentIndex > prev && prev >= 0 && errorIndex === undefined) {
const lastDoneIndex = Math.min(currentIndex - 1, steps.length - 1);
setJustCompletedIndex(lastDoneIndex);
const timer = setTimeout(() => setJustCompletedIndex(null), 500);
return () => clearTimeout(timer);
}
}, [currentIndex, errorIndex, steps.length]);
return (
<div className={cn('flex items-start justify-center', className)}>
{steps.map((step, index) => {
const isDone = currentIndex >= 0 && index < currentIndex;
const isCurrent = currentIndex >= 0 && index === currentIndex;
const isError = errorIndex !== undefined && index === errorIndex;
const isDone = !isError && currentIndex >= 0 && index < currentIndex;
const isCurrent = !isError && currentIndex >= 0 && index === currentIndex;
const isLast = index === steps.length - 1;
const isJustCompleted = justCompletedIndex === index;
// The connecting line between this step and the next
const lineState: 'done' | 'active' | 'pending' =
@ -45,50 +69,66 @@ export const StepProgressBar = ({
>
{/* Step circle + label column */}
<div className="flex flex-col items-center" style={{ width: 56 }}>
{/* Circle */}
<div
className={cn(
'relative flex items-center justify-center rounded-full transition-all duration-300',
// Sizing
'size-7',
// Done state
isDone && 'bg-[var(--stepper-done)] shadow-[0_0_8px_var(--stepper-done-glow)]',
// Current state
isCurrent && 'border-2 border-[var(--stepper-current)] bg-transparent',
// Pending state
!isDone &&
!isCurrent &&
'border border-[var(--stepper-pending-border)] bg-[var(--stepper-pending)]'
)}
style={
isCurrent
? { animation: 'stepper-pulse-ring 2s ease-in-out infinite' }
: undefined
}
>
{isDone ? (
<Check className="size-3.5 text-white" strokeWidth={3} />
) : (
<span
className={cn(
'text-[11px] font-semibold leading-none',
isCurrent
? 'text-[var(--stepper-current)]'
: 'text-[var(--stepper-pending-text)]'
)}
>
{index + 1}
</span>
{/* Circle wrapper — holds flash overlay */}
<div className="relative flex items-center justify-center">
{/* Green flash burst on completion */}
{isJustCompleted && isDone && (
<div
className="absolute size-7 rounded-full bg-[var(--stepper-done)]"
style={{ animation: 'stepper-flash 0.4s ease-out forwards' }}
/>
)}
{/* Circle */}
<div
className={cn(
'relative flex items-center justify-center rounded-full transition-all duration-300',
'size-7',
isError &&
'bg-[var(--stepper-error)] shadow-[0_0_8px_var(--stepper-error-glow)]',
isDone && 'bg-[var(--stepper-done)] shadow-[0_0_8px_var(--stepper-done-glow)]',
isCurrent && 'border-2 border-[var(--stepper-current)] bg-transparent',
!isDone &&
!isCurrent &&
!isError &&
'border border-[var(--stepper-pending-border)] bg-[var(--stepper-pending)]'
)}
style={
isJustCompleted && isDone
? { animation: 'stepper-jelly 0.45s ease-out' }
: isCurrent
? { animation: 'stepper-pulse-ring 2s ease-in-out infinite' }
: undefined
}
>
{isError ? (
<X className="size-3.5 text-white" strokeWidth={3} />
) : isDone ? (
<Check className="size-3.5 text-white" strokeWidth={3} />
) : (
<span
className={cn(
'text-[11px] font-semibold leading-none',
isCurrent
? 'text-[var(--stepper-current)]'
: 'text-[var(--stepper-pending-text)]'
)}
>
{index + 1}
</span>
)}
</div>
</div>
{/* Label */}
<span
className={cn(
'mt-1.5 text-center text-[10px] leading-tight transition-colors duration-300',
isDone || isCurrent
? 'font-medium text-[var(--stepper-label-active)]'
: 'text-[var(--stepper-label)]'
isError
? 'font-medium text-[var(--stepper-label-error)]'
: isDone || isCurrent
? 'font-medium text-[var(--stepper-label-active)]'
: 'text-[var(--stepper-label)]'
)}
>
{step.label}
@ -105,10 +145,8 @@ export const StepProgressBar = ({
<div className="absolute inset-0 rounded-full bg-[var(--stepper-line)]" />
{lineState === 'done' ? (
/* Fully filled green line */
<div className="absolute inset-0 rounded-full bg-[var(--stepper-line-done)]" />
) : lineState === 'active' ? (
/* Cyclic sweep — green highlight sliding left-to-right in a loop */
<div
className="absolute top-0 h-full rounded-full bg-[var(--stepper-line-done)]"
style={{

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Button } from '@renderer/components/ui/button';
import { useStore } from '@renderer/store';
@ -24,6 +24,7 @@ export const TeamProvisioningBanner = ({
}))
);
const [dismissed, setDismissed] = useState(false);
const lastActiveStepRef = useRef(-1);
const bannerInstanceKey = useMemo(() => {
if (!progress) return null;
return `${teamName}:${progress.runId}:${progress.startedAt}`;
@ -64,6 +65,11 @@ export const TeamProvisioningBanner = ({
const progressStepIndex = getDisplayStepIndex(progress.state);
// Remember last active step so we can show it as the error location when failed
if (progressStepIndex >= 0 && !isFailed) {
lastActiveStepRef.current = progressStepIndex;
}
if (isFailed) {
return (
<div className="mb-3">
@ -83,7 +89,8 @@ export const TeamProvisioningBanner = ({
title="Launch failed"
message={progress.error ?? null}
tone="error"
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
currentStepIndex={lastActiveStepRef.current}
errorStepIndex={lastActiveStepRef.current >= 0 ? lastActiveStepRef.current : 0}
startedAt={progress.startedAt}
pid={progress.pid}
cliLogsTail={progress.cliLogsTail}

View file

@ -1127,12 +1127,17 @@ export const CreateTeamDialog = ({
{canCreate && launchTeam && (prepareState === 'idle' || prepareState === 'loading') ? (
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
<span className="inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
<span>
{prepareMessage ??
(prepareState === 'idle'
? 'Warming up CLI environment...'
: 'Preparing environment...')}
</span>
<div>
<span>
{prepareMessage ??
(prepareState === 'idle'
? 'Warming up CLI environment...'
: 'Preparing environment...')}
</span>
<p className="mt-0.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
Pre-flight check to catch errors before launch
</p>
</div>
</div>
) : null}

View file

@ -1127,12 +1127,17 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
{prepareState === 'idle' || prepareState === 'loading' ? (
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
<span className="inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
<span>
{prepareMessage ??
(prepareState === 'idle'
? 'Warming up CLI environment...'
: 'Preparing environment...')}
</span>
<div>
<span>
{prepareMessage ??
(prepareState === 'idle'
? 'Warming up CLI environment...'
: 'Preparing environment...')}
</span>
<p className="mt-0.5 text-[10px] text-[var(--color-text-muted)] opacity-70">
Pre-flight check to catch errors before launch
</p>
</div>
</div>
) : null}

View file

@ -245,6 +245,9 @@
--stepper-line-done: #22c55e;
--stepper-label: #a1a1aa;
--stepper-label-active: #fafafa;
--stepper-error: #ef4444;
--stepper-error-glow: rgba(239, 68, 68, 0.3);
--stepper-label-error: #fca5a5;
/* Collapsible section backgrounds (sidebar) */
--color-section-bg: rgba(255, 255, 255, 0.04);
--color-section-bg-open: rgba(255, 255, 255, 0.07);
@ -661,6 +664,9 @@
--stepper-line-done: #16a34a;
--stepper-label: #71717a;
--stepper-label-active: #18181b;
--stepper-error: #dc2626;
--stepper-error-glow: rgba(220, 38, 38, 0.2);
--stepper-label-error: #dc2626;
/* Collapsible section backgrounds (sidebar) */
--color-section-bg: rgba(0, 0, 0, 0.04);
--color-section-bg-open: rgba(0, 0, 0, 0.07);
@ -1203,3 +1209,37 @@ body {
box-shadow: 0 0 0 5px transparent;
}
}
/* Step progress bar — jelly bounce on step completion */
@keyframes stepper-jelly {
0% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
45% {
transform: scale(0.88);
}
65% {
transform: scale(1.08);
}
80% {
transform: scale(0.97);
}
100% {
transform: scale(1);
}
}
/* Step progress bar — green flash burst on step completion */
@keyframes stepper-flash {
0% {
transform: scale(0.8);
opacity: 0.6;
}
100% {
transform: scale(2.2);
opacity: 0;
}
}

View file

@ -34,7 +34,18 @@
position: absolute; inset: 0; width: 100%; height: 100%;
opacity: 0.03; pointer-events: none;
}
#splash-logo { margin-bottom: 18px; }
#splash-logo {
margin-bottom: 18px;
animation: splash-breathe 3s ease-in-out infinite, splash-glow 3s ease-in-out infinite;
}
@keyframes splash-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
@keyframes splash-glow {
0%, 100% { filter: drop-shadow(0 0 12px rgba(129,140,248,0.4)) drop-shadow(0 0 28px rgba(167,139,250,0.18)); }
50% { filter: drop-shadow(0 0 20px rgba(129,140,248,0.6)) drop-shadow(0 0 42px rgba(167,139,250,0.3)); }
}
#splash-text {
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
font-size: 15px; font-weight: 500; letter-spacing: 0.05em;
@ -68,7 +79,13 @@
}
:root.light #splash-text { color: #52525b; }
:root.light #splash-noise { opacity: 0.02; }
:root.light #splash-logo { filter: drop-shadow(0 2px 8px rgba(0,0,0,0.15)); }
:root.light #splash-logo {
animation: splash-breathe 3s ease-in-out infinite, splash-glow-light 3s ease-in-out infinite;
}
@keyframes splash-glow-light {
0%, 100% { filter: drop-shadow(0 0 10px rgba(79,70,229,0.3)) drop-shadow(0 0 24px rgba(139,92,246,0.15)); }
50% { filter: drop-shadow(0 0 16px rgba(79,70,229,0.45)) drop-shadow(0 0 36px rgba(139,92,246,0.25)); }
}
</style>
<script>
// Flash prevention: Apply cached theme before React loads