feat: enhance ProvisioningProgressBlock with live output toggle and default state

- Added a new prop `defaultLiveOutputOpen` to control the initial state of the live output section in the ProvisioningProgressBlock component.
- Implemented state management for the live output visibility, allowing it to be expanded or collapsed based on user interaction.
- Updated the UI to display a message when no output is captured, improving user feedback during provisioning processes.
- Adjusted the TeamProvisioningBanner to utilize the new live output feature, enhancing the user experience during team provisioning.

Made-with: Cursor
This commit is contained in:
iliya 2026-03-03 16:14:05 +02:00
parent e0eaa27a13
commit 16bb2c0b63
5 changed files with 103 additions and 31 deletions

View file

@ -19,6 +19,8 @@ export interface ProvisioningProgressBlockProps {
message?: string | null;
/** Visual tone (e.g. highlight errors) */
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 */
currentStepIndex: number;
/** Show spinner next to title */
@ -69,6 +71,7 @@ export const ProvisioningProgressBlock = ({
title,
message,
tone = 'default',
defaultLiveOutputOpen = true,
currentStepIndex,
loading = false,
onCancel,
@ -80,16 +83,21 @@ export const ProvisioningProgressBlock = ({
}: ProvisioningProgressBlockProps): React.JSX.Element => {
const elapsed = useElapsedTimer(startedAt);
const [logsOpen, setLogsOpen] = useState(false);
const [liveOutputOpen, setLiveOutputOpen] = useState(defaultLiveOutputOpen);
const outputScrollRef = useRef<HTMLDivElement>(null);
const isError = tone === 'error';
const hasAnyOutput = !!assistantOutput || !!cliLogsTail;
// Auto-scroll assistant output
useEffect(() => {
if (outputScrollRef.current) {
if (liveOutputOpen && outputScrollRef.current) {
outputScrollRef.current.scrollTop = outputScrollRef.current.scrollHeight;
}
}, [assistantOutput]);
}, [assistantOutput, liveOutputOpen]);
// If parent changes the default (e.g. transitioning to "ready"), respect it.
useEffect(() => {
setLiveOutputOpen(defaultLiveOutputOpen);
}, [defaultLiveOutputOpen]);
return (
<div
@ -165,20 +173,38 @@ export const ProvisioningProgressBlock = ({
);
})}
</div>
{assistantOutput ? (
<div className="mt-2">
<p className="mb-1 text-[11px] font-medium text-[var(--color-text-muted)]">Live output</p>
<div className="mt-2">
<button
type="button"
className="flex items-center gap-1 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
onClick={() => setLiveOutputOpen((v) => !v)}
>
{liveOutputOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
Live output
</button>
{liveOutputOpen ? (
<div
ref={outputScrollRef}
className={cn(
'max-h-[400px] overflow-y-auto rounded border border-[var(--color-border)] bg-[var(--color-surface)] p-2',
'mt-1 max-h-[400px] overflow-y-auto rounded border border-[var(--color-border)] bg-[var(--color-surface)] p-2',
isError && 'border-red-500/40'
)}
>
<MarkdownViewer content={assistantOutput} bare maxHeight="max-h-none" />
{assistantOutput ? (
<MarkdownViewer content={assistantOutput} bare maxHeight="max-h-none" />
) : (
<p
className={cn(
'text-[11px]',
isError ? 'text-red-200/80' : 'text-[var(--color-text-muted)]'
)}
>
No output captured yet.
</p>
)}
</div>
</div>
) : null}
) : null}
</div>
{cliLogsTail ? (
<div className="mt-2">
<button
@ -192,16 +218,6 @@ export const ProvisioningProgressBlock = ({
{logsOpen ? <CliLogsRichView cliLogsTail={cliLogsTail} className="mt-1" /> : null}
</div>
) : null}
{!hasAnyOutput ? (
<p
className={cn(
'mt-2 text-[11px]',
isError ? 'text-red-200/80' : 'text-[var(--color-text-muted)]'
)}
>
No output captured yet.
</p>
) : null}
</div>
);
};

View file

@ -111,6 +111,7 @@ export const TeamProvisioningBanner = ({
</Button>
</div>
<ProvisioningProgressBlock
key={progress.runId}
title="Launch failed"
message={progress.error ?? null}
tone="error"
@ -119,6 +120,7 @@ export const TeamProvisioningBanner = ({
pid={progress.pid}
cliLogsTail={progress.cliLogsTail}
assistantOutput={progress.assistantOutput}
defaultLiveOutputOpen
onCancel={null}
/>
</div>
@ -157,6 +159,7 @@ export const TeamProvisioningBanner = ({
</Button>
</div>
<ProvisioningProgressBlock
key={progress.runId}
title="Launch details"
message={progress.message}
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
@ -164,6 +167,7 @@ export const TeamProvisioningBanner = ({
pid={progress.pid}
cliLogsTail={progress.cliLogsTail}
assistantOutput={progress.assistantOutput}
defaultLiveOutputOpen={false}
onCancel={null}
/>
</div>
@ -174,6 +178,7 @@ export const TeamProvisioningBanner = ({
return (
<div className="mb-3">
<ProvisioningProgressBlock
key={progress.runId}
title="Launching team"
message={progress.message}
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
@ -182,6 +187,7 @@ export const TeamProvisioningBanner = ({
pid={progress.pid}
cliLogsTail={progress.cliLogsTail}
assistantOutput={progress.assistantOutput}
defaultLiveOutputOpen
onCancel={
canCancel
? () => {

View file

@ -176,6 +176,8 @@ export const ActivityItem = ({
const structured = parseStructuredAgentMessage(message.text);
// Only flag agent messages as rate-limited, not user's own quotes
const rateLimited = message.from !== 'user' && isRateLimitMessage(message.text);
// Highlight messages containing API errors
const isApiError = message.text.includes('API Error');
// Never collapse rate limit messages as noise — they must be visible
const noiseLabel = structured && !rateLimited ? getNoiseLabel(structured) : null;
@ -225,11 +227,15 @@ export const ActivityItem = ({
<article
className="group overflow-hidden rounded-md"
style={{
backgroundColor: rateLimited ? 'var(--tool-result-error-bg)' : CARD_BG,
border: rateLimited ? '1px solid var(--tool-result-error-border)' : CARD_BORDER_STYLE,
borderLeft: rateLimited
? '3px solid var(--tool-result-error-text)'
: `3px solid ${colors.border}`,
backgroundColor: rateLimited || isApiError ? 'var(--tool-result-error-bg)' : CARD_BG,
border:
rateLimited || isApiError
? '1px solid var(--tool-result-error-border)'
: CARD_BORDER_STYLE,
borderLeft:
rateLimited || isApiError
? '3px solid var(--tool-result-error-text)'
: `3px solid ${colors.border}`,
}}
>
{/* Header — div with role=button (cannot use <button> due to nested buttons inside) */}
@ -311,6 +317,14 @@ export const ActivityItem = ({
</span>
) : null}
{/* API Error warning badge */}
{isApiError && !rateLimited ? (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
<AlertTriangle size={10} />
API Error
</span>
) : null}
{/* Recipient — arrow + avatar + badge */}
{message.to && message.to !== message.from ? (
<>

View file

@ -716,9 +716,27 @@ export const CreateTeamDialog = ({
) : null}
{canCreate && prepareState === 'ready' ? (
<div className="flex items-center gap-2 text-xs text-emerald-400">
<CheckCircle2 className="size-3.5 shrink-0" />
<span>CLI environment ready</span>
<div className="space-y-1">
<div className="flex items-center gap-2 text-xs text-emerald-400">
<CheckCircle2 className="size-3.5 shrink-0" />
<span>
{prepareWarnings.length > 0
? 'CLI environment ready (with warnings)'
: 'CLI environment ready'}
</span>
</div>
{prepareMessage ? (
<p className="text-[11px] text-[var(--color-text-muted)]">{prepareMessage}</p>
) : null}
{prepareWarnings.length > 0 ? (
<div className="space-y-0.5">
{prepareWarnings.map((warning) => (
<p key={warning} className="text-[11px] text-amber-300">
{warning}
</p>
))}
</div>
) : null}
</div>
) : null}
</div>

View file

@ -432,9 +432,27 @@ export const LaunchTeamDialog = ({
) : null}
{prepareState === 'ready' ? (
<div className="flex items-center gap-2 text-xs text-emerald-400">
<CheckCircle2 className="size-3.5 shrink-0" />
<span>CLI environment ready</span>
<div className="space-y-1">
<div className="flex items-center gap-2 text-xs text-emerald-400">
<CheckCircle2 className="size-3.5 shrink-0" />
<span>
{prepareWarnings.length > 0
? 'CLI environment ready (with warnings)'
: 'CLI environment ready'}
</span>
</div>
{prepareMessage ? (
<p className="text-[11px] text-[var(--color-text-muted)]">{prepareMessage}</p>
) : null}
{prepareWarnings.length > 0 ? (
<div className="space-y-0.5">
{prepareWarnings.map((warning) => (
<p key={warning} className="text-[11px] text-amber-300">
{warning}
</p>
))}
</div>
) : null}
</div>
) : null}