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:
parent
e0eaa27a13
commit
16bb2c0b63
5 changed files with 103 additions and 31 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? () => {
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue