experiment: filter system output from chat, approval UX, wizard sub-steps

- Strip JSON/system init output from agent messages in chat (no more
  raw tool dumps or session IDs visible to users)
- Filter streaming relay chunks that look like system output
- Create "in progress" artifact when user asks for a hiring plan
- Swap approval button order: Reject (left), Approve (right, green)
- Fix chat/artifact pane height to fill viewport without clipping
- Progressive disclosure in wizard step 1: name first, then mission
- Hide agent comments that are entirely system output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
scotttong 2026-03-19 17:20:32 -07:00
parent fbd87da6d6
commit 4b3cda97e4
4 changed files with 91 additions and 20 deletions

View file

@ -300,16 +300,16 @@ function DocumentViewer({
<div className="border-t border-border px-4 py-3 bg-background shrink-0">
<p className="text-[11px] text-muted-foreground mb-2">This document needs your review.</p>
<div className="flex items-center gap-2">
<Button size="sm" className="h-8 text-xs flex-1" onClick={onApprove}>
<CheckCircle2 className="h-3.5 w-3.5 mr-1" />
Approve
</Button>
<Button size="sm" variant="outline" className="h-8 text-xs flex-1" onClick={() => {
<Button size="sm" variant="outline" className="h-8 text-xs flex-1 text-destructive hover:text-destructive" onClick={() => {
onReject?.();
onBack();
}}>
<RotateCcw className="h-3.5 w-3.5 mr-1" />
Request Changes
<XCircle className="h-3.5 w-3.5 mr-1" />
Reject
</Button>
<Button size="sm" className="h-8 text-xs flex-1 bg-green-600 hover:bg-green-700 text-white" onClick={onApprove}>
<CheckCircle2 className="h-3.5 w-3.5 mr-1" />
Approve
</Button>
</div>
</div>

View file

@ -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 (
<div key={comment.id}>
<div
@ -603,11 +677,7 @@ export function CEOChatPanel({
)}
</div>
<div className="prose prose-xs dark:prose-invert max-w-none text-[13px] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
<MarkdownBody>
{isAgent
? comment.body.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
: comment.body}
</MarkdownBody>
<MarkdownBody>{displayBody}</MarkdownBody>
</div>
</div>

View file

@ -1110,10 +1110,11 @@ Follow this structure for every role in the plan.`,
<Building2 className="h-5 w-5 text-muted-foreground" />
</div>
<div>
<h3 className="font-medium">Define your mission</h3>
<h3 className="font-medium">{!missionPath ? "Name your company" : "Define your mission"}</h3>
<p className="text-xs text-muted-foreground">
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."}
</p>
</div>
</div>
@ -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}
/>
</div>
{/* Mission path selector */}
{!missionPath && (
{/* Mission path selector — only shows after company name is entered */}
{!missionPath && companyName.trim() && (
<div className="space-y-3">
<label className="text-xs text-foreground block">
How would you like to define your mission?

View file

@ -246,7 +246,7 @@ export function Chat() {
}
return (
<div className="flex h-[calc(100vh-3rem)] -m-6 -mt-2">
<div className="flex h-[calc(100%+3rem)] -m-6">
{/* Left: Chat */}
<div className="shrink-0 border-r border-border" style={{ width: chatWidth }}>
<CEOChatPanel