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:
parent
3fcab46882
commit
8616db00a0
10 changed files with 194 additions and 75 deletions
24
README.md
24
README.md
|
|
@ -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 |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue