diff --git a/ui/src/components/ArtifactsPanel.tsx b/ui/src/components/ArtifactsPanel.tsx index ef634f0c..69c1e2bd 100644 --- a/ui/src/components/ArtifactsPanel.tsx +++ b/ui/src/components/ArtifactsPanel.tsx @@ -300,16 +300,16 @@ function DocumentViewer({

This document needs your review.

- - +
diff --git a/ui/src/components/CEOChatPanel.tsx b/ui/src/components/CEOChatPanel.tsx index 3a9ad9fe..133a2c48 100644 --- a/ui/src/components/CEOChatPanel.tsx +++ b/ui/src/components/CEOChatPanel.tsx @@ -42,6 +42,60 @@ interface CEOChatPanelProps { onOpenArtifact?: (key: string, title: string) => void; } +/** + * Clean agent message content — strip system init JSON, code blocks with + * raw config/tool dumps, and other non-conversational output. + */ +function cleanAgentMessage(body: string): string { + let cleaned = body; + + // Remove markdown links + cleaned = cleaned.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); + + // Remove lines that look like raw JSON objects (system init, config dumps) + cleaned = cleaned.replace(/^\s*\{["\w].*["\w]\}\s*$/gm, ""); + + // Remove code blocks containing JSON or system data + cleaned = cleaned.replace(/```(?:json|plaintext|text)?\s*\n?\{[\s\S]*?\}\s*\n?```/g, ""); + + // Remove lines that are clearly system output (tool lists, session IDs, etc.) + cleaned = cleaned.replace(/^.*"(?:type|subtype|session_id|tools|mcp_servers|model|permissionMode|slash_commands|agents)".*$/gm, ""); + + // Remove excessive blank lines + cleaned = cleaned.replace(/\n{3,}/g, "\n\n"); + + return cleaned.trim(); +} + +/** + * Check if a streaming chunk looks like system/init output rather than + * conversational text. Used to filter relay streaming. + */ +function isSystemChunk(text: string): boolean { + // JSON-like content + if (/^\s*\{/.test(text) && /"type"\s*:/.test(text)) return true; + // Tool/permission dumps + if (/"tools"\s*:\s*\[/.test(text)) return true; + if (/"mcp_servers"\s*:\s*\[/.test(text)) return true; + if (/"session_id"\s*:/.test(text)) return true; + return false; +} + +/** + * Detect if a user message is asking the CEO to create a plan/hire. + */ +function isAskingForPlan(message: string): boolean { + const planPatterns = [ + /\b(hiring|team|org)\s*(plan|strategy)\b/i, + /\b(build|create|draft|start|write)\s*(a\s+)?(hiring|team|the)\s*(plan)?\b/i, + /\bget started\b/i, + /\bhire\b.*\b(team|agents?|roles?)\b/i, + /\blet'?s\s+(build|start|go|do it)\b/i, + /\bready to\s+(hire|build|plan)\b/i, + ]; + return planPatterns.some((p) => p.test(message)); +} + /** Animated paperclip SVG thinking indicator */ function PaperclipThinking({ className }: { className?: string }) { return ( @@ -297,6 +351,23 @@ export function CEOChatPanel({ setInput(""); setOptimisticTyping(true); + // If user is asking for a plan, create a draft artifact immediately + if (isAskingForPlan(trimmed)) { + issuesApi.createWorkProduct(taskId, { + type: "document", + title: "Hiring Plan", + provider: "paperclip", + status: "draft", + reviewState: "none", + isPrimary: true, + summary: "Your CEO is drafting a hiring plan...", + }).then(() => { + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.workProducts(taskId), + }); + }).catch(() => { /* may already exist */ }); + } + const latestId = comments?.[comments.length - 1]?.id ?? null; setIgnoreBeforeCommentId(latestId); setDetectedPlanCommentId(null); @@ -340,7 +411,7 @@ export function CEOChatPanel({ if (!line.startsWith("data: ")) continue; try { const event = JSON.parse(line.slice(6)); - if (event.type === "chunk") { + if (event.type === "chunk" && !isSystemChunk(event.text)) { setStreamingText((prev) => prev + event.text); } else if (event.type === "done") { setStreamingText(""); @@ -576,6 +647,9 @@ export function CEOChatPanel({ {comments?.map((comment) => { const isAgent = Boolean(comment.authorAgentId); const isPlan = detectedPlanCommentId === comment.id; + // Hide comments that are entirely system output + const displayBody = isAgent ? cleanAgentMessage(comment.body) : comment.body; + if (isAgent && !displayBody) return null; return (
- - {isAgent - ? comment.body.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") - : comment.body} - + {displayBody}
diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 27421ba1..91492fd5 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -1110,10 +1110,11 @@ Follow this structure for every role in the plan.`,
-

Define your mission

+

{!missionPath ? "Name your company" : "Define your mission"}

- Your mission drives everything — your CEO, your hires, - and the work your company will do. + {!missionPath + ? "What will your company be called?" + : "Your mission drives everything — your CEO, your hires, and the work your company will do."}

@@ -1133,12 +1134,12 @@ Follow this structure for every role in the plan.`, placeholder="Acme Corp" value={companyName} onChange={(e) => setCompanyName(e.target.value)} - autoFocus + autoFocus={!missionPath} /> - {/* Mission path selector */} - {!missionPath && ( + {/* Mission path selector — only shows after company name is entered */} + {!missionPath && companyName.trim() && (