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 />
|
<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
|
## What is this
|
||||||
|
|
||||||
A new approach to task management with AI agent teams.
|
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>
|
</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
|
## 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
|
## Development
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|
|
||||||
|
|
@ -2657,9 +2657,11 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
// Restart filesystem monitor for createTeam (launch skips it)
|
// Restart filesystem monitor for createTeam (launch skips it)
|
||||||
if (!run.isLaunch) {
|
if (!run.isLaunch) {
|
||||||
|
updateProgress(run, 'configuring', 'Waiting for team configuration...');
|
||||||
|
run.onProgress(run.progress);
|
||||||
this.startFilesystemMonitor(run, run.request);
|
this.startFilesystemMonitor(run, run.request);
|
||||||
} else {
|
} else {
|
||||||
updateProgress(run, 'monitoring', 'CLI running — reconnecting with teammates');
|
updateProgress(run, 'configuring', 'CLI running — reconnecting with teammates');
|
||||||
run.onProgress(run.progress);
|
run.onProgress(run.progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3018,6 +3020,8 @@ export class TeamProvisioningService {
|
||||||
// of relying on stdout (which only arrives at the end in text mode).
|
// of relying on stdout (which only arrives at the end in text mode).
|
||||||
// When config + members + tasks are all present, kill the process early
|
// When config + members + tasks are all present, kill the process early
|
||||||
// rather than waiting for it to deadlock on system-reminder shutdown.
|
// 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);
|
this.startFilesystemMonitor(run, request);
|
||||||
|
|
||||||
run.timeoutHandle = setTimeout(() => {
|
run.timeoutHandle = setTimeout(() => {
|
||||||
|
|
@ -3442,7 +3446,7 @@ export class TeamProvisioningService {
|
||||||
// For launch, skip the filesystem monitor — files (config, inboxes, tasks)
|
// For launch, skip the filesystem monitor — files (config, inboxes, tasks)
|
||||||
// already exist from the previous run and would trigger immediate false
|
// already exist from the previous run and would trigger immediate false
|
||||||
// completion on the first poll. Rely on stream-json result.success instead.
|
// 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.onProgress(run.progress);
|
||||||
|
|
||||||
run.timeoutHandle = setTimeout(() => {
|
run.timeoutHandle = setTimeout(() => {
|
||||||
|
|
@ -3502,7 +3506,11 @@ export class TeamProvisioningService {
|
||||||
if (!run) {
|
if (!run) {
|
||||||
throw new Error('Unknown runId');
|
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');
|
throw new Error('Provisioning cannot be cancelled in current state');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6325,7 +6333,7 @@ export class TeamProvisioningService {
|
||||||
run.fsPhase = 'waiting_members';
|
run.fsPhase = 'waiting_members';
|
||||||
const progress = updateProgress(
|
const progress = updateProgress(
|
||||||
run,
|
run,
|
||||||
'monitoring',
|
'assembling',
|
||||||
'Team config created, waiting for members',
|
'Team config created, waiting for members',
|
||||||
{ configReady: true }
|
{ configReady: true }
|
||||||
);
|
);
|
||||||
|
|
@ -6336,11 +6344,7 @@ export class TeamProvisioningService {
|
||||||
if (run.fsPhase === 'waiting_members') {
|
if (run.fsPhase === 'waiting_members') {
|
||||||
if (request.members.length === 0) {
|
if (request.members.length === 0) {
|
||||||
run.fsPhase = 'waiting_tasks';
|
run.fsPhase = 'waiting_tasks';
|
||||||
const progress = updateProgress(
|
const progress = updateProgress(run, 'finalizing', 'Solo team, preparing workspace');
|
||||||
run,
|
|
||||||
'monitoring',
|
|
||||||
'Solo team, skipping member inbox wait'
|
|
||||||
);
|
|
||||||
run.onProgress(progress);
|
run.onProgress(progress);
|
||||||
} else {
|
} else {
|
||||||
const teamDir = (await resolveTeamDir()) ?? configuredTeamDir;
|
const teamDir = (await resolveTeamDir()) ?? configuredTeamDir;
|
||||||
|
|
@ -6350,14 +6354,14 @@ export class TeamProvisioningService {
|
||||||
run.fsPhase = 'waiting_tasks';
|
run.fsPhase = 'waiting_tasks';
|
||||||
const progress = updateProgress(
|
const progress = updateProgress(
|
||||||
run,
|
run,
|
||||||
'monitoring',
|
'finalizing',
|
||||||
`All ${inboxCount} member inboxes created, waiting for tasks`
|
`All ${inboxCount} member inboxes created, preparing workspace`
|
||||||
);
|
);
|
||||||
run.onProgress(progress);
|
run.onProgress(progress);
|
||||||
} else if (inboxCount > 0) {
|
} else if (inboxCount > 0) {
|
||||||
const progress = updateProgress(
|
const progress = updateProgress(
|
||||||
run,
|
run,
|
||||||
'monitoring',
|
'assembling',
|
||||||
`${inboxCount}/${request.members.length} member inboxes created`
|
`${inboxCount}/${request.members.length} member inboxes created`
|
||||||
);
|
);
|
||||||
run.onProgress(progress);
|
run.onProgress(progress);
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@ import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
|
||||||
import { MarkdownViewer } from '../chat/viewers/MarkdownViewer';
|
import { MarkdownViewer } from '../chat/viewers/MarkdownViewer';
|
||||||
|
|
||||||
import { CliLogsRichView } from './CliLogsRichView';
|
import { CliLogsRichView } from './CliLogsRichView';
|
||||||
import { STEP_LABELS, STEP_ORDER } from './provisioningSteps';
|
import { DISPLAY_STEPS } from './provisioningSteps';
|
||||||
import { StepProgressBar } from './StepProgressBar';
|
import { StepProgressBar } from './StepProgressBar';
|
||||||
|
|
||||||
import type { StepProgressBarStep } from './StepProgressBar';
|
import type { StepProgressBarStep } from './StepProgressBar';
|
||||||
|
|
||||||
/** Pre-built step definitions for the provisioning stepper (excludes 'ready') */
|
/** Pre-built step definitions for the provisioning stepper. */
|
||||||
const PROVISIONING_STEPS: StepProgressBarStep[] = STEP_ORDER.filter((s) => s !== 'ready').map(
|
const PROVISIONING_STEPS: StepProgressBarStep[] = DISPLAY_STEPS.map((s) => ({
|
||||||
(s) => ({ key: s, label: STEP_LABELS[s] })
|
key: s.key,
|
||||||
);
|
label: s.label,
|
||||||
|
}));
|
||||||
|
|
||||||
export interface ProvisioningProgressBlockProps {
|
export interface ProvisioningProgressBlockProps {
|
||||||
/** Title above the steps, e.g. "Launching team" */
|
/** Title above the steps, e.g. "Launching team" */
|
||||||
|
|
@ -26,7 +27,7 @@ export interface ProvisioningProgressBlockProps {
|
||||||
tone?: 'default' | 'error';
|
tone?: 'default' | 'error';
|
||||||
/** Whether Live output is expanded by default */
|
/** Whether Live output is expanded by default */
|
||||||
defaultLiveOutputOpen?: boolean;
|
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;
|
currentStepIndex: number;
|
||||||
/** Show spinner next to title */
|
/** Show spinner next to title */
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
@ -155,15 +156,23 @@ export const ProvisioningProgressBlock = ({
|
||||||
|
|
||||||
// Open CLI logs while loading, collapse when done (unless error).
|
// Open CLI logs while loading, collapse when done (unless error).
|
||||||
const prevLoadingRef = useRef(loading);
|
const prevLoadingRef = useRef(loading);
|
||||||
|
const hadLogsRef = useRef(Boolean(cliLogsTail));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isError && cliLogsTail) {
|
if (!isError) {
|
||||||
if (loading && !prevLoadingRef.current) {
|
const hasLogs = Boolean(cliLogsTail);
|
||||||
// Started loading → open
|
|
||||||
|
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);
|
setLogsOpen(true);
|
||||||
} else if (!loading && prevLoadingRef.current) {
|
} else if (!loading && prevLoadingRef.current) {
|
||||||
// Finished loading → collapse
|
// Finished loading → collapse
|
||||||
setLogsOpen(false);
|
setLogsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hadLogsRef.current = hasLogs;
|
||||||
}
|
}
|
||||||
prevLoadingRef.current = loading;
|
prevLoadingRef.current = loading;
|
||||||
}, [loading, cliLogsTail, isError]);
|
}, [loading, cliLogsTail, isError]);
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,9 @@ function resolveTeamStatus(
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
currentProgress &&
|
currentProgress &&
|
||||||
['validating', 'spawning', 'monitoring', 'verifying'].includes(currentProgress.state)
|
['validating', 'spawning', 'configuring', 'assembling', 'finalizing', 'verifying'].includes(
|
||||||
|
currentProgress.state
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return 'provisioning';
|
return 'provisioning';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ import { CheckCircle2, X } from 'lucide-react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { ProvisioningProgressBlock } from './ProvisioningProgressBlock';
|
import { ProvisioningProgressBlock } from './ProvisioningProgressBlock';
|
||||||
import { STEP_ORDER } from './provisioningSteps';
|
import { getDisplayStepIndex } from './provisioningSteps';
|
||||||
|
|
||||||
import type { ProvisioningStep } from './provisioningSteps';
|
|
||||||
interface TeamProvisioningBannerProps {
|
interface TeamProvisioningBannerProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
}
|
}
|
||||||
|
|
@ -51,15 +50,19 @@ export const TeamProvisioningBanner = ({
|
||||||
const isActive =
|
const isActive =
|
||||||
progress.state === 'validating' ||
|
progress.state === 'validating' ||
|
||||||
progress.state === 'spawning' ||
|
progress.state === 'spawning' ||
|
||||||
progress.state === 'monitoring' ||
|
progress.state === 'configuring' ||
|
||||||
|
progress.state === 'assembling' ||
|
||||||
|
progress.state === 'finalizing' ||
|
||||||
progress.state === 'verifying';
|
progress.state === 'verifying';
|
||||||
|
|
||||||
const canCancel =
|
const canCancel =
|
||||||
progress.state === 'spawning' ||
|
progress.state === 'spawning' ||
|
||||||
progress.state === 'monitoring' ||
|
progress.state === 'configuring' ||
|
||||||
|
progress.state === 'assembling' ||
|
||||||
|
progress.state === 'finalizing' ||
|
||||||
progress.state === 'verifying';
|
progress.state === 'verifying';
|
||||||
|
|
||||||
const progressStepIndex = STEP_ORDER.indexOf(progress.state as ProvisioningStep);
|
const progressStepIndex = getDisplayStepIndex(progress.state);
|
||||||
|
|
||||||
if (isFailed) {
|
if (isFailed) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,32 @@
|
||||||
export const STEP_ORDER = ['validating', 'spawning', 'monitoring', 'verifying', 'ready'] as const;
|
import type { TeamProvisioningState } from '@shared/types/team';
|
||||||
export type ProvisioningStep = (typeof STEP_ORDER)[number];
|
|
||||||
export const STEP_LABELS: Record<ProvisioningStep, string> = {
|
/** Display steps for the provisioning stepper (0-indexed). */
|
||||||
validating: 'Validate',
|
export const DISPLAY_STEPS = [
|
||||||
spawning: 'Start CLI',
|
{ key: 'starting', label: 'Starting' },
|
||||||
monitoring: 'Provisioning',
|
{ key: 'configuring', label: 'Team setup' },
|
||||||
verifying: 'Verify',
|
{ key: 'assembling', label: 'Members joining' },
|
||||||
ready: 'Ready',
|
{ 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));
|
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']);
|
const TERMINAL_PROVISIONING_STATES = new Set(['ready', 'failed', 'disconnected', 'cancelled']);
|
||||||
|
|
||||||
function isPendingProvisioningRunId(runId: string): boolean {
|
function isPendingProvisioningRunId(runId: string): boolean {
|
||||||
|
|
@ -1689,7 +1696,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
if (pendingRun) {
|
if (pendingRun) {
|
||||||
delete nextRuns[pendingRunId];
|
delete nextRuns[pendingRunId];
|
||||||
// Only use pending data as fallback if real progress events haven't arrived yet.
|
// 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.
|
// when the invoke response arrives before IPC progress events.
|
||||||
if (!realProgressAlreadyExists) {
|
if (!realProgressAlreadyExists) {
|
||||||
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };
|
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };
|
||||||
|
|
@ -1826,7 +1833,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
if (pendingRun) {
|
if (pendingRun) {
|
||||||
delete nextRuns[pendingRunId];
|
delete nextRuns[pendingRunId];
|
||||||
// Only use pending data as fallback if real progress events haven't arrived yet.
|
// 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.
|
// when the invoke response arrives before IPC progress events.
|
||||||
if (!realProgressAlreadyExists) {
|
if (!realProgressAlreadyExists) {
|
||||||
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };
|
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };
|
||||||
|
|
|
||||||
|
|
@ -544,7 +544,9 @@ export type TeamProvisioningState =
|
||||||
| 'idle'
|
| 'idle'
|
||||||
| 'validating'
|
| 'validating'
|
||||||
| 'spawning'
|
| 'spawning'
|
||||||
| 'monitoring'
|
| 'configuring'
|
||||||
|
| 'assembling'
|
||||||
|
| 'finalizing'
|
||||||
| 'verifying'
|
| 'verifying'
|
||||||
| 'ready'
|
| 'ready'
|
||||||
| 'disconnected'
|
| 'disconnected'
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
progress: {
|
progress: {
|
||||||
runId: 'run-1',
|
runId: 'run-1',
|
||||||
teamName: 'team-alpha',
|
teamName: 'team-alpha',
|
||||||
state: 'monitoring',
|
state: 'assembling',
|
||||||
message: 'Monitoring',
|
message: 'Assembling',
|
||||||
startedAt: '2026-03-12T10:00:00.000Z',
|
startedAt: '2026-03-12T10:00:00.000Z',
|
||||||
updatedAt: '2026-03-12T10:00:00.000Z',
|
updatedAt: '2026-03-12T10:00:00.000Z',
|
||||||
},
|
},
|
||||||
|
|
@ -183,8 +183,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
progress: {
|
progress: {
|
||||||
runId: 'run-2',
|
runId: 'run-2',
|
||||||
teamName: 'team-alpha',
|
teamName: 'team-alpha',
|
||||||
state: 'monitoring',
|
state: 'assembling',
|
||||||
message: 'Monitoring',
|
message: 'Assembling',
|
||||||
startedAt: '2026-03-12T10:00:00.000Z',
|
startedAt: '2026-03-12T10:00:00.000Z',
|
||||||
updatedAt: '2026-03-12T10:00:00.000Z',
|
updatedAt: '2026-03-12T10:00:00.000Z',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ describe('teamSlice actions', () => {
|
||||||
store.getState().onProvisioningProgress({
|
store.getState().onProvisioningProgress({
|
||||||
runId: 'run-real',
|
runId: 'run-real',
|
||||||
teamName: 'my-team',
|
teamName: 'my-team',
|
||||||
state: 'monitoring',
|
state: 'assembling',
|
||||||
message: 'Real run',
|
message: 'Real run',
|
||||||
startedAt: '2026-03-12T10:00:01.000Z',
|
startedAt: '2026-03-12T10:00:01.000Z',
|
||||||
updatedAt: '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(store.getState().provisioningRuns['run-real']).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
runId: 'run-real',
|
runId: 'run-real',
|
||||||
state: 'monitoring',
|
state: 'assembling',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -513,7 +513,7 @@ describe('teamSlice actions', () => {
|
||||||
store.getState().onProvisioningProgress({
|
store.getState().onProvisioningProgress({
|
||||||
runId: 'pending:my-team:1',
|
runId: 'pending:my-team:1',
|
||||||
teamName: 'my-team',
|
teamName: 'my-team',
|
||||||
state: 'monitoring',
|
state: 'assembling',
|
||||||
message: 'Late zombie progress',
|
message: 'Late zombie progress',
|
||||||
startedAt: '2026-03-12T10:00:00.000Z',
|
startedAt: '2026-03-12T10:00:00.000Z',
|
||||||
updatedAt: '2026-03-12T10:00:02.000Z',
|
updatedAt: '2026-03-12T10:00:02.000Z',
|
||||||
|
|
@ -576,7 +576,7 @@ describe('teamSlice actions', () => {
|
||||||
'run-current': {
|
'run-current': {
|
||||||
runId: 'run-current',
|
runId: 'run-current',
|
||||||
teamName: 'my-team',
|
teamName: 'my-team',
|
||||||
state: 'monitoring',
|
state: 'assembling',
|
||||||
message: 'Current run',
|
message: 'Current run',
|
||||||
startedAt: '2026-03-12T10:00:00.000Z',
|
startedAt: '2026-03-12T10:00:00.000Z',
|
||||||
updatedAt: '2026-03-12T10:00:00.000Z',
|
updatedAt: '2026-03-12T10:00:00.000Z',
|
||||||
|
|
@ -615,7 +615,7 @@ describe('teamSlice actions', () => {
|
||||||
store.getState().onProvisioningProgress({
|
store.getState().onProvisioningProgress({
|
||||||
runId: 'run-current',
|
runId: 'run-current',
|
||||||
teamName: 'my-team',
|
teamName: 'my-team',
|
||||||
state: 'monitoring',
|
state: 'assembling',
|
||||||
message: 'Current run',
|
message: 'Current run',
|
||||||
startedAt,
|
startedAt,
|
||||||
updatedAt: startedAt,
|
updatedAt: startedAt,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue