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;
|
message?: string | null;
|
||||||
/** Visual tone (e.g. highlight errors) */
|
/** Visual tone (e.g. highlight errors) */
|
||||||
tone?: 'default' | 'error';
|
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 */
|
/** Index of the current step in STEP_ORDER (0-based), or -1 if unknown */
|
||||||
currentStepIndex: number;
|
currentStepIndex: number;
|
||||||
/** Show spinner next to title */
|
/** Show spinner next to title */
|
||||||
|
|
@ -69,6 +71,7 @@ export const ProvisioningProgressBlock = ({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
tone = 'default',
|
tone = 'default',
|
||||||
|
defaultLiveOutputOpen = true,
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
loading = false,
|
loading = false,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
|
@ -80,16 +83,21 @@ export const ProvisioningProgressBlock = ({
|
||||||
}: ProvisioningProgressBlockProps): React.JSX.Element => {
|
}: ProvisioningProgressBlockProps): React.JSX.Element => {
|
||||||
const elapsed = useElapsedTimer(startedAt);
|
const elapsed = useElapsedTimer(startedAt);
|
||||||
const [logsOpen, setLogsOpen] = useState(false);
|
const [logsOpen, setLogsOpen] = useState(false);
|
||||||
|
const [liveOutputOpen, setLiveOutputOpen] = useState(defaultLiveOutputOpen);
|
||||||
const outputScrollRef = useRef<HTMLDivElement>(null);
|
const outputScrollRef = useRef<HTMLDivElement>(null);
|
||||||
const isError = tone === 'error';
|
const isError = tone === 'error';
|
||||||
const hasAnyOutput = !!assistantOutput || !!cliLogsTail;
|
|
||||||
|
|
||||||
// Auto-scroll assistant output
|
// Auto-scroll assistant output
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (outputScrollRef.current) {
|
if (liveOutputOpen && outputScrollRef.current) {
|
||||||
outputScrollRef.current.scrollTop = outputScrollRef.current.scrollHeight;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -165,20 +173,38 @@ export const ProvisioningProgressBlock = ({
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{assistantOutput ? (
|
<div className="mt-2">
|
||||||
<div className="mt-2">
|
<button
|
||||||
<p className="mb-1 text-[11px] font-medium text-[var(--color-text-muted)]">Live output</p>
|
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
|
<div
|
||||||
ref={outputScrollRef}
|
ref={outputScrollRef}
|
||||||
className={cn(
|
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'
|
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>
|
||||||
</div>
|
) : null}
|
||||||
) : null}
|
</div>
|
||||||
{cliLogsTail ? (
|
{cliLogsTail ? (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -192,16 +218,6 @@ export const ProvisioningProgressBlock = ({
|
||||||
{logsOpen ? <CliLogsRichView cliLogsTail={cliLogsTail} className="mt-1" /> : null}
|
{logsOpen ? <CliLogsRichView cliLogsTail={cliLogsTail} className="mt-1" /> : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ export const TeamProvisioningBanner = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ProvisioningProgressBlock
|
<ProvisioningProgressBlock
|
||||||
|
key={progress.runId}
|
||||||
title="Launch failed"
|
title="Launch failed"
|
||||||
message={progress.error ?? null}
|
message={progress.error ?? null}
|
||||||
tone="error"
|
tone="error"
|
||||||
|
|
@ -119,6 +120,7 @@ export const TeamProvisioningBanner = ({
|
||||||
pid={progress.pid}
|
pid={progress.pid}
|
||||||
cliLogsTail={progress.cliLogsTail}
|
cliLogsTail={progress.cliLogsTail}
|
||||||
assistantOutput={progress.assistantOutput}
|
assistantOutput={progress.assistantOutput}
|
||||||
|
defaultLiveOutputOpen
|
||||||
onCancel={null}
|
onCancel={null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -157,6 +159,7 @@ export const TeamProvisioningBanner = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ProvisioningProgressBlock
|
<ProvisioningProgressBlock
|
||||||
|
key={progress.runId}
|
||||||
title="Launch details"
|
title="Launch details"
|
||||||
message={progress.message}
|
message={progress.message}
|
||||||
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
||||||
|
|
@ -164,6 +167,7 @@ export const TeamProvisioningBanner = ({
|
||||||
pid={progress.pid}
|
pid={progress.pid}
|
||||||
cliLogsTail={progress.cliLogsTail}
|
cliLogsTail={progress.cliLogsTail}
|
||||||
assistantOutput={progress.assistantOutput}
|
assistantOutput={progress.assistantOutput}
|
||||||
|
defaultLiveOutputOpen={false}
|
||||||
onCancel={null}
|
onCancel={null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,6 +178,7 @@ export const TeamProvisioningBanner = ({
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<ProvisioningProgressBlock
|
<ProvisioningProgressBlock
|
||||||
|
key={progress.runId}
|
||||||
title="Launching team"
|
title="Launching team"
|
||||||
message={progress.message}
|
message={progress.message}
|
||||||
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
||||||
|
|
@ -182,6 +187,7 @@ export const TeamProvisioningBanner = ({
|
||||||
pid={progress.pid}
|
pid={progress.pid}
|
||||||
cliLogsTail={progress.cliLogsTail}
|
cliLogsTail={progress.cliLogsTail}
|
||||||
assistantOutput={progress.assistantOutput}
|
assistantOutput={progress.assistantOutput}
|
||||||
|
defaultLiveOutputOpen
|
||||||
onCancel={
|
onCancel={
|
||||||
canCancel
|
canCancel
|
||||||
? () => {
|
? () => {
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,8 @@ export const ActivityItem = ({
|
||||||
const structured = parseStructuredAgentMessage(message.text);
|
const structured = parseStructuredAgentMessage(message.text);
|
||||||
// Only flag agent messages as rate-limited, not user's own quotes
|
// Only flag agent messages as rate-limited, not user's own quotes
|
||||||
const rateLimited = message.from !== 'user' && isRateLimitMessage(message.text);
|
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
|
// Never collapse rate limit messages as noise — they must be visible
|
||||||
const noiseLabel = structured && !rateLimited ? getNoiseLabel(structured) : null;
|
const noiseLabel = structured && !rateLimited ? getNoiseLabel(structured) : null;
|
||||||
|
|
||||||
|
|
@ -225,11 +227,15 @@ export const ActivityItem = ({
|
||||||
<article
|
<article
|
||||||
className="group overflow-hidden rounded-md"
|
className="group overflow-hidden rounded-md"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: rateLimited ? 'var(--tool-result-error-bg)' : CARD_BG,
|
backgroundColor: rateLimited || isApiError ? 'var(--tool-result-error-bg)' : CARD_BG,
|
||||||
border: rateLimited ? '1px solid var(--tool-result-error-border)' : CARD_BORDER_STYLE,
|
border:
|
||||||
borderLeft: rateLimited
|
rateLimited || isApiError
|
||||||
? '3px solid var(--tool-result-error-text)'
|
? '1px solid var(--tool-result-error-border)'
|
||||||
: `3px solid ${colors.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) */}
|
{/* Header — div with role=button (cannot use <button> due to nested buttons inside) */}
|
||||||
|
|
@ -311,6 +317,14 @@ export const ActivityItem = ({
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : 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 */}
|
{/* Recipient — arrow + avatar + badge */}
|
||||||
{message.to && message.to !== message.from ? (
|
{message.to && message.to !== message.from ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -716,9 +716,27 @@ export const CreateTeamDialog = ({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{canCreate && prepareState === 'ready' ? (
|
{canCreate && prepareState === 'ready' ? (
|
||||||
<div className="flex items-center gap-2 text-xs text-emerald-400">
|
<div className="space-y-1">
|
||||||
<CheckCircle2 className="size-3.5 shrink-0" />
|
<div className="flex items-center gap-2 text-xs text-emerald-400">
|
||||||
<span>CLI environment ready</span>
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -432,9 +432,27 @@ export const LaunchTeamDialog = ({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{prepareState === 'ready' ? (
|
{prepareState === 'ready' ? (
|
||||||
<div className="flex items-center gap-2 text-xs text-emerald-400">
|
<div className="space-y-1">
|
||||||
<CheckCircle2 className="size-3.5 shrink-0" />
|
<div className="flex items-center gap-2 text-xs text-emerald-400">
|
||||||
<span>CLI environment ready</span>
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue