import React, { useState } from "react"; import { apiUrl } from "../utils/api.js"; import PlanView from "./PlanView.jsx"; import RunnableCodeBlock, { splitFences } from "./RunnableCodeBlock.jsx"; import ExecutionPlanCard from "./ExecutionPlanCard.jsx"; export default function AssistantMessage({ answer, plan, executionLog, planStatus, owner, repo, onApproveExecution, nextActions, relatedPlan, diff, branch, }) { // Approval-first sandbox: when the planner returns an execution_plan, // render the green ExecutionPlanCard instead of the orange Action Plan. const executionPlan = plan?.execution_plan || null; const [runResult, setRunResult] = useState(null); const [runError, setRunError] = useState(null); const [runBusy, setRunBusy] = useState(false); const approveExecution = async (ep) => { if (onApproveExecution) { onApproveExecution(ep, plan); return; } setRunBusy(true); setRunError(null); setRunResult(null); try { const body = ep.file ? { language: ep.language, code: null } : { language: ep.language, code: ep.inline_code, timeout_sec: ep.timeout_sec }; const res = await fetch(apiUrl("/api/sandbox/run"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) setRunError(data.detail || `HTTP ${res.status}`); else setRunResult(data); } catch (e) { setRunError(e.message); } finally { setRunBusy(false); } }; const hasFileActions = plan?.steps?.some((s) => s.files?.length > 0); const answerText = typeof answer === "string" ? answer.trim() : ""; // Suppress the answer prose when it would just duplicate what the // plan card or success receipt already says. const showAnswerProse = answerText && !executionLog && !(plan && hasFileActions && !executionPlan && answerText === plan.summary); return (
{/* Free-form answer prose. Rendered without a section header so short answers feel like a chat reply, not a debug log. */} {showAnswerProse && (
{splitFences(answerText).map((seg, i) => seg.type === "code" ? ( ) : (

{seg.value}

), )}
)} {/* Sandbox approval card. */} {executionPlan && (
{ /* no-op */ }} /> {runError && (
Run error: {runError}
)} {runResult && (
              {runResult.stdout || runResult.stderr || "(no output)"}
            
)}
)} {/* Proposed execution plan — only when there are file changes and no execution log is present yet. */} {plan && hasFileActions && !executionPlan && !executionLog && (
)} {/* Completed execution receipt. */} {executionLog && ( )} {/* Next actions when there is no execution log (e.g. simple answers). */} {!executionLog && Array.isArray(nextActions) && nextActions.length > 0 && (
{nextActions.map((a, i) => ( ))}
)}
); } function NextActionButton({ action }) { const onClick = () => { if (action.kind === "run_file" && action.payload?.file) { window.dispatchEvent( new CustomEvent("gitpilot:run-file", { detail: { path: action.payload.file }, }), ); } else if (action.kind === "open_workspace" && action.payload?.file) { window.dispatchEvent( new CustomEvent("gitpilot:open-workspace", { detail: { path: action.payload.file }, }), ); } else if (action.kind === "open_in_canvas" && action.payload?.file) { window.dispatchEvent( new CustomEvent("gitpilot:open-in-canvas", { detail: { path: action.payload.file }, }), ); } }; const isPrimary = action.kind === "run_file"; return ( ); } function SuccessReceipt({ executionLog, relatedPlan, owner, repo, branch, diff, nextActions, }) { const steps = executionLog?.steps || []; const totalSteps = steps.length; // Aggregate every file action across steps so "Files changed (N)" can // be a single top-level section instead of per-step duplication. const allFiles = []; if (relatedPlan?.steps?.length) { for (const s of relatedPlan.steps) { for (const f of s.files || []) { if (f.action !== "INDEX") allFiles.push(f); } } } const totalDuration = steps.reduce((acc, s) => { return acc + (s.executions || []).reduce( (a, ex) => a + (typeof ex.duration_ms === "number" ? ex.duration_ms : 0), 0, ); }, 0); // Short headline outcome lines (max 2): "Created X", "Modified Y". const headlines = []; for (const action of ["CREATE", "MODIFY", "DELETE"]) { for (const f of allFiles) { if (f.action === action && headlines.length < 2) { headlines.push({ verb: verbFor(action), path: f.path }); } } } // The bottom-bar already owns Create PR. Only show a pointer text here // when there is a meaningful PR path (branch + file changes). const hasPRPath = Boolean(branch && allFiles.length > 0); // Filter out create-PR-style next-actions (handled by bottom bar); // keep ▶ run / 📂 open as inline buttons. const inlineNextActions = (nextActions || []).filter( (a) => a && a.kind && a.kind !== "create_pr", ); const hasTechLog = totalDuration > 0 || steps.some((s) => s.summary || (s.executions || []).length > 0); return (
Execution completed
Successfully executed {totalSteps} step{totalSteps === 1 ? "" : "s"} · Just now
Executed
{headlines.length > 0 && ( )} {(owner || branch) && (
{owner && repo && (
Repository {owner}/{repo}
)} {branch && (
Branch {branch}
)}
)} {allFiles.length > 0 && (
Files changed ({allFiles.length})
)} {inlineNextActions.length > 0 && (
{inlineNextActions.map((a, i) => ( ))}
)} {hasTechLog && (
View execution log {totalDuration > 0 && ( · {(totalDuration / 1000).toFixed(1)}s )} {steps.map((s) => (
Step {s.step_number} {totalSteps > 1 ? ` of ${totalSteps}` : ""}
{s.summary && (
{s.summary}
)} {Array.isArray(s.executions) && s.executions.map((ex, i) => ( ))}
))}
)} {hasPRPath && (
Next: Create a pull request when ready.
)}
); } function actionMeta(action) { switch (action) { case "READ": return "Read-only"; case "CREATE": return "Created"; case "MODIFY": return "Modified"; case "DELETE": return "Deleted"; case "INDEX": return "Indexed"; default: return ""; } } function verbFor(action) { switch (action) { case "CREATE": return "Created"; case "MODIFY": return "Modified"; case "DELETE": return "Deleted"; default: return action; } } function ExecutionCard({ ex }) { const status = ex.status || "pending"; const statusClass = status === "completed" ? "exec-card-inner--ok" : status === "failed" ? "exec-card-inner--bad" : status === "skipped" ? "exec-card-inner--warn" : "exec-card-inner--info"; return (
{ex.path} {ex.sandbox && ( · {ex.sandbox} )}
{status === "completed" && `Exit ${ex.exit_code} · ${ex.duration_ms} ms`} {status === "failed" && (typeof ex.exit_code === "number" ? `Failed · exit ${ex.exit_code}` : "Failed")} {status === "skipped" && "Skipped"} {status === "pending" && "Running…"}
{ex.command && (
$ {ex.command}
)} {ex.stdout && (
stdout
{ex.stdout}
)} {ex.stderr && (
stderr
            {ex.stderr}
          
)} {ex.error && !ex.stderr && (
{ex.error}
)} {ex.reason && (
{ex.reason}
)}
); }