From 34ef846a5b1f0fed745b6b11efdc24fb3d87b195 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 24 Feb 2026 14:32:51 +0200 Subject: [PATCH] feat: accumulate and display assistant output during provisioning - Added `provisioningOutputParts` to `ProvisioningRun` for accumulating assistant text during the provisioning phase. - Updated `updateProgress` and `emitLogsProgress` functions to include the new `assistantOutput` in the progress state. - Enhanced `ProvisioningProgressBlock` to display live assistant output using `MarkdownViewer`. - Modified `TeamProvisioningBanner` to pass the new `assistantOutput` prop for rendering in the UI. --- .../services/team/TeamProvisioningService.ts | 23 +++++- .../team/ProvisioningProgressBlock.tsx | 71 ++++++++----------- .../team/TeamProvisioningBanner.tsx | 1 + src/shared/types/team.ts | 2 + 4 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 0f81e093..8ef60ed3 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -128,6 +128,8 @@ interface ProvisioningRun { * Flushed to liveLeadProcessMessages on result.success. */ directReplyParts: string[]; + /** Accumulates assistant text during provisioning phase for live UI preview. */ + provisioningOutputParts: string[]; } type ProvisioningAuthSource = @@ -446,6 +448,10 @@ function updateProgress( message: string, extras?: Pick ): TeamProvisioningProgress { + const assistantOutput = + run.provisioningOutputParts.length > 0 + ? run.provisioningOutputParts.join('') + : run.progress.assistantOutput; run.progress = { ...run.progress, state, @@ -455,6 +461,7 @@ function updateProgress( error: extras?.error, warnings: extras?.warnings, cliLogsTail: extras?.cliLogsTail ?? run.progress.cliLogsTail, + assistantOutput, }; return run.progress; } @@ -485,13 +492,17 @@ function extractLogsTail(stdoutBuffer: string, stderrBuffer: string): string | u function emitLogsProgress(run: ProvisioningRun): void { const logsTail = extractLogsTail(run.stdoutBuffer, run.stderrBuffer); - if (!logsTail) { + const assistantOutput = + run.provisioningOutputParts.length > 0 ? run.provisioningOutputParts.join('') : undefined; + + if (!logsTail && !assistantOutput) { return; } run.progress = { ...run.progress, updatedAt: nowIso(), - cliLogsTail: logsTail, + ...(logsTail !== undefined && { cliLogsTail: logsTail }), + ...(assistantOutput !== undefined && { assistantOutput }), }; run.onProgress(run.progress); } @@ -702,6 +713,7 @@ export class TeamProvisioningService { fsPhase: 'waiting_config', leadRelayCapture: null, directReplyParts: [], + provisioningOutputParts: [], progress: { runId, teamName: request.teamName, @@ -972,6 +984,7 @@ export class TeamProvisioningService { fsPhase: 'waiting_members', leadRelayCapture: null, directReplyParts: [], + provisioningOutputParts: [], progress: { runId, teamName: request.teamName, @@ -1560,6 +1573,12 @@ export class TeamProvisioningService { if (textParts.length > 0) { const text = textParts.join(''); logger.debug(`[${run.teamName}] assistant: ${text.slice(0, 200)}`); + // During provisioning (before provisioningComplete), accumulate for live UI preview. + // Emission is handled by the throttled emitLogsProgress() in the stdout data handler. + if (!run.provisioningComplete) { + run.provisioningOutputParts.push(text); + } + if (run.leadRelayCapture) { const capture = run.leadRelayCapture; if (!capture.settled) { diff --git a/src/renderer/components/team/ProvisioningProgressBlock.tsx b/src/renderer/components/team/ProvisioningProgressBlock.tsx index 6ddf94b0..e4439e5a 100644 --- a/src/renderer/components/team/ProvisioningProgressBlock.tsx +++ b/src/renderer/components/team/ProvisioningProgressBlock.tsx @@ -1,11 +1,12 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { cn } from '@renderer/lib/utils'; -import hljs from 'highlight.js'; import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react'; +import { MarkdownViewer } from '../chat/viewers/MarkdownViewer'; + import { STEP_LABELS, STEP_ORDER } from './provisioningSteps'; import type { ProvisioningStep } from './provisioningSteps'; @@ -27,6 +28,8 @@ export interface ProvisioningProgressBlockProps { pid?: number; /** Tail of CLI logs */ cliLogsTail?: string; + /** Accumulated assistant text output for live preview */ + assistantOutput?: string; className?: string; } @@ -59,32 +62,6 @@ function useElapsedTimer(startedAt?: string): string | null { return elapsed; } -function highlightLogsHtml(text: string): string { - return text - .split('\n') - .map((line) => { - const trimmed = line.trimStart(); - if (trimmed.startsWith('{') || trimmed.startsWith('[')) { - try { - const parsed: unknown = JSON.parse(trimmed); - const pretty = JSON.stringify(parsed, null, 2); - return hljs.highlight(pretty, { language: 'json' }).value; - } catch { - return escapeHtml(line); - } - } - if (line === '[stdout]' || line === '[stderr]') { - return `${escapeHtml(line)}`; - } - return escapeHtml(line); - }) - .join('\n'); -} - -function escapeHtml(text: string): string { - return text.replace(/&/g, '&').replace(//g, '>'); -} - export const ProvisioningProgressBlock = ({ title, message, @@ -94,22 +71,28 @@ export const ProvisioningProgressBlock = ({ startedAt, pid, cliLogsTail, + assistantOutput, className, }: ProvisioningProgressBlockProps): React.JSX.Element => { const elapsed = useElapsedTimer(startedAt); const [logsOpen, setLogsOpen] = useState(false); const logsRef = useRef(null); - const highlightedHtml = useMemo( - () => (cliLogsTail ? highlightLogsHtml(cliLogsTail) : ''), - [cliLogsTail] - ); + const outputScrollRef = useRef(null); + // Auto-scroll CLI logs useEffect(() => { if (logsOpen && logsRef.current) { logsRef.current.scrollTop = logsRef.current.scrollHeight; } }, [logsOpen, cliLogsTail]); + // Auto-scroll assistant output + useEffect(() => { + if (outputScrollRef.current) { + outputScrollRef.current.scrollTop = outputScrollRef.current.scrollHeight; + } + }, [assistantOutput]); + return (
+ {assistantOutput ? ( +
+

Live output

+
+ +
+
+ ) : null} {cliLogsTail ? (
{logsOpen ? (
 tags, combined with escapeHtml() for non-JSON lines.
-              // Input is CLI stdout/stderr from a local process, not user-supplied web content.
-
-              dangerouslySetInnerHTML={{ __html: highlightedHtml }}
-            />
+              className="mt-1 max-h-[400px] overflow-y-auto rounded border border-[var(--color-border)] bg-[var(--color-surface)] p-2 font-mono text-[11px] leading-relaxed text-[var(--color-text-secondary)]"
+            >
+              {cliLogsTail}
+            
) : null}
) : null} diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx index 52f1a896..c4b9b74a 100644 --- a/src/renderer/components/team/TeamProvisioningBanner.tsx +++ b/src/renderer/components/team/TeamProvisioningBanner.tsx @@ -145,6 +145,7 @@ export const TeamProvisioningBanner = ({ startedAt={progress.startedAt} pid={progress.pid} cliLogsTail={progress.cliLogsTail} + assistantOutput={progress.assistantOutput} onCancel={ canCancel ? () => { diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts index c2932329..e0905c82 100644 --- a/src/shared/types/team.ts +++ b/src/shared/types/team.ts @@ -262,6 +262,8 @@ export interface TeamProvisioningProgress { error?: string; warnings?: string[]; cliLogsTail?: string; + /** Accumulated assistant text output during provisioning (for live preview). */ + assistantOutput?: string; } export interface GlobalTask extends TeamTaskWithKanban {