mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
feat(open-swe): copy-plan-as-markdown button + fix premature "ready" banner (#1603)
- Add a "Copy markdown" button to the plan header that copies the whole plan. Cross-browser: async Clipboard API in secure contexts, hidden-textarea + execCommand fallback for older Safari/Firefox and non-HTTPS origins. - Fix the conversation banner: it showed "A plan is ready for your review" for every non-approved/cancelled status, including "planning" — so it claimed the plan was ready the instant plan mode began (the agent shares the link early to follow along), then the plan page correctly said it was still being written. Now: planning → "writing a plan", revising → "revising", ready → "ready for your review".
This commit is contained in:
parent
714914b14c
commit
534f402f48
3 changed files with 66 additions and 4 deletions
|
|
@ -75,7 +75,9 @@ test.describe("Plan review (HTTP comments)", () => {
|
|||
|
||||
// 3. The OWNER opens the conversation, follows the "Review plan" banner, and
|
||||
// sees the rendered plan.
|
||||
const ownerCtx = await browser.newContext();
|
||||
const ownerCtx = await browser.newContext({
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
});
|
||||
await ownerCtx.request.post("/control/login", { data: OWNER });
|
||||
const owner = await ownerCtx.newPage();
|
||||
await owner.goto(`/agents/${threadId}`);
|
||||
|
|
@ -93,6 +95,13 @@ test.describe("Plan review (HTTP comments)", () => {
|
|||
// comment exists.
|
||||
await expect(owner.getByTestId("reject-plan")).toBeDisabled();
|
||||
|
||||
// Copy the whole plan as markdown.
|
||||
await owner.getByTestId("copy-plan").click();
|
||||
await expect(owner.getByTestId("copy-plan")).toContainText("Copied!");
|
||||
const clipboard = await owner.evaluate(() => navigator.clipboard.readText());
|
||||
expect(clipboard).toContain("## Plan: Add greet() helper");
|
||||
expect(clipboard).toContain("### Verification");
|
||||
|
||||
// Owner leaves a comment.
|
||||
await addComment(owner, "Owner: looks solid, ship it.");
|
||||
await expect(owner.getByTestId("plan-comment")).toHaveCount(1);
|
||||
|
|
|
|||
|
|
@ -81,9 +81,11 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
|||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<MapIcon className="size-3.5 text-[var(--ui-accent)]" />
|
||||
{thread.planStatus === "revising"
|
||||
? "The agent is revising the plan."
|
||||
: "A plan is ready for your review."}
|
||||
{thread.planStatus === "ready"
|
||||
? "A plan is ready for your review."
|
||||
: thread.planStatus === "revising"
|
||||
? "The agent is revising the plan."
|
||||
: "The agent is writing a plan."}
|
||||
</span>
|
||||
<span className="font-medium text-[var(--ui-accent)]">
|
||||
Review plan →
|
||||
|
|
|
|||
|
|
@ -14,6 +14,38 @@ import { useResolvedTheme } from "@/lib/theme"
|
|||
|
||||
const POLL_MS = 4000
|
||||
|
||||
// Copy text to the clipboard across browsers: prefer the async Clipboard API
|
||||
// (needs a secure context), and fall back to a hidden-textarea + execCommand
|
||||
// for older Safari/Firefox and non-HTTPS origins. Returns whether it copied.
|
||||
async function copyToClipboard(text: string): Promise<boolean> {
|
||||
// The DOM types mark navigator.clipboard required, but it's absent in older
|
||||
// browsers and non-secure origins — treat it as optional.
|
||||
const nav = navigator as { clipboard?: Clipboard }
|
||||
try {
|
||||
if (window.isSecureContext && nav.clipboard) {
|
||||
await nav.clipboard.writeText(text)
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
/* fall through to the legacy path */
|
||||
}
|
||||
try {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = text
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.top = "-9999px"
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
textarea.setSelectionRange(0, text.length)
|
||||
const ok = document.execCommand("copy")
|
||||
document.body.removeChild(textarea)
|
||||
return ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function PlanReview({ plan }: { plan: PlanData }) {
|
||||
const resolvedTheme = useResolvedTheme()
|
||||
const [comments, setComments] = useState<Array<PlanComment>>([])
|
||||
|
|
@ -22,6 +54,7 @@ export function PlanReview({ plan }: { plan: PlanData }) {
|
|||
const [decision, setDecision] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState<"approve" | "reject" | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// Poll so reviewers see each other's comments without a realtime transport.
|
||||
useEffect(() => {
|
||||
|
|
@ -91,6 +124,16 @@ export function PlanReview({ plan }: { plan: PlanData }) {
|
|||
[plan.threadId]
|
||||
)
|
||||
|
||||
const copyPlan = useCallback(async () => {
|
||||
setError(null)
|
||||
if (await copyToClipboard(plan.markdown)) {
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 1500)
|
||||
} else {
|
||||
setError("Couldn't copy the plan to the clipboard.")
|
||||
}
|
||||
}, [plan.markdown])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="plan-review"
|
||||
|
|
@ -116,6 +159,14 @@ export function PlanReview({ plan }: { plan: PlanData }) {
|
|||
{decision}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
data-testid="copy-plan"
|
||||
variant="secondary"
|
||||
disabled={!plan.markdown.trim()}
|
||||
onClick={() => void copyPlan()}
|
||||
>
|
||||
{copied ? "Copied!" : "Copy markdown"}
|
||||
</Button>
|
||||
{plan.isOwner && (
|
||||
<Button
|
||||
data-testid="approve-plan"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue