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:
parent
fbd87da6d6
commit
4b3cda97e4
4 changed files with 91 additions and 20 deletions
|
|
@ -300,16 +300,16 @@ function DocumentViewer({
|
||||||
<div className="border-t border-border px-4 py-3 bg-background shrink-0">
|
<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>
|
<p className="text-[11px] text-muted-foreground mb-2">This document needs your review.</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button size="sm" className="h-8 text-xs flex-1" onClick={onApprove}>
|
<Button size="sm" variant="outline" className="h-8 text-xs flex-1 text-destructive hover:text-destructive" onClick={() => {
|
||||||
<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={() => {
|
|
||||||
onReject?.();
|
onReject?.();
|
||||||
onBack();
|
onBack();
|
||||||
}}>
|
}}>
|
||||||
<RotateCcw className="h-3.5 w-3.5 mr-1" />
|
<XCircle className="h-3.5 w-3.5 mr-1" />
|
||||||
Request Changes
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,60 @@ interface CEOChatPanelProps {
|
||||||
onOpenArtifact?: (key: string, title: string) => void;
|
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 */
|
/** Animated paperclip SVG thinking indicator */
|
||||||
function PaperclipThinking({ className }: { className?: string }) {
|
function PaperclipThinking({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -297,6 +351,23 @@ export function CEOChatPanel({
|
||||||
setInput("");
|
setInput("");
|
||||||
setOptimisticTyping(true);
|
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;
|
const latestId = comments?.[comments.length - 1]?.id ?? null;
|
||||||
setIgnoreBeforeCommentId(latestId);
|
setIgnoreBeforeCommentId(latestId);
|
||||||
setDetectedPlanCommentId(null);
|
setDetectedPlanCommentId(null);
|
||||||
|
|
@ -340,7 +411,7 @@ export function CEOChatPanel({
|
||||||
if (!line.startsWith("data: ")) continue;
|
if (!line.startsWith("data: ")) continue;
|
||||||
try {
|
try {
|
||||||
const event = JSON.parse(line.slice(6));
|
const event = JSON.parse(line.slice(6));
|
||||||
if (event.type === "chunk") {
|
if (event.type === "chunk" && !isSystemChunk(event.text)) {
|
||||||
setStreamingText((prev) => prev + event.text);
|
setStreamingText((prev) => prev + event.text);
|
||||||
} else if (event.type === "done") {
|
} else if (event.type === "done") {
|
||||||
setStreamingText("");
|
setStreamingText("");
|
||||||
|
|
@ -576,6 +647,9 @@ export function CEOChatPanel({
|
||||||
{comments?.map((comment) => {
|
{comments?.map((comment) => {
|
||||||
const isAgent = Boolean(comment.authorAgentId);
|
const isAgent = Boolean(comment.authorAgentId);
|
||||||
const isPlan = detectedPlanCommentId === comment.id;
|
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 (
|
return (
|
||||||
<div key={comment.id}>
|
<div key={comment.id}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -603,11 +677,7 @@ export function CEOChatPanel({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="prose prose-xs dark:prose-invert max-w-none text-[13px] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
<div className="prose prose-xs dark:prose-invert max-w-none text-[13px] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||||
<MarkdownBody>
|
<MarkdownBody>{displayBody}</MarkdownBody>
|
||||||
{isAgent
|
|
||||||
? comment.body.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
||||||
: comment.body}
|
|
||||||
</MarkdownBody>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1110,10 +1110,11 @@ Follow this structure for every role in the plan.`,
|
||||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<p className="text-xs text-muted-foreground">
|
||||||
Your mission drives everything — your CEO, your hires,
|
{!missionPath
|
||||||
and the work your company will do.
|
? "What will your company be called?"
|
||||||
|
: "Your mission drives everything — your CEO, your hires, and the work your company will do."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1133,12 +1134,12 @@ Follow this structure for every role in the plan.`,
|
||||||
placeholder="Acme Corp"
|
placeholder="Acme Corp"
|
||||||
value={companyName}
|
value={companyName}
|
||||||
onChange={(e) => setCompanyName(e.target.value)}
|
onChange={(e) => setCompanyName(e.target.value)}
|
||||||
autoFocus
|
autoFocus={!missionPath}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mission path selector */}
|
{/* Mission path selector — only shows after company name is entered */}
|
||||||
{!missionPath && (
|
{!missionPath && companyName.trim() && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="text-xs text-foreground block">
|
<label className="text-xs text-foreground block">
|
||||||
How would you like to define your mission?
|
How would you like to define your mission?
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ export function Chat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-3rem)] -m-6 -mt-2">
|
<div className="flex h-[calc(100%+3rem)] -m-6">
|
||||||
{/* Left: Chat */}
|
{/* Left: Chat */}
|
||||||
<div className="shrink-0 border-r border-border" style={{ width: chatWidth }}>
|
<div className="shrink-0 border-r border-border" style={{ width: chatWidth }}>
|
||||||
<CEOChatPanel
|
<CEOChatPanel
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue