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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue