feat(web): remove Watch/Steer card buttons and Steer modal

The Steer modal was replaced by the steer bar on the run detail page,
so the per-card Watch and Steer actions are no longer needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-08 08:01:40 -07:00
parent 9b9ebdf50b
commit b75ebbf4da
No known key found for this signature in database
2 changed files with 1 additions and 170 deletions

View file

@ -1,143 +0,0 @@
import { useEffect, useRef, useState } from "react";
import { ApiError } from "../lib/api-client";
import { useSteerRun } from "../lib/mutations";
import { ErrorMessage } from "./ui";
interface SteerComposerProps {
runId: string;
open: boolean;
onClose: () => void;
}
export function SteerComposer({ runId, open, onClose }: SteerComposerProps) {
const [text, setText] = useState("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
const { trigger, isMutating } = useSteerRun(runId);
useEffect(() => {
if (open) {
requestAnimationFrame(() => textareaRef.current?.focus());
} else {
setText("");
setErrorMessage(null);
}
}, [open]);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onCloseRef.current();
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
if (!open) return null;
const trimmed = text.trim();
const canSubmit = trimmed.length > 0 && !isMutating;
async function send(interrupt: boolean) {
if (!canSubmit) return;
setErrorMessage(null);
try {
await trigger({ text: trimmed, interrupt });
onClose();
} catch (err) {
if (err instanceof ApiError) {
// Try to surface the well-known 409 codes inline.
const body = err.body as { code?: string; detail?: string } | null;
if (body?.code === "cli_agent_not_steerable") {
setErrorMessage(
"All running agent stages are CLI-mode and can't be steered.",
);
} else if (body?.code === "use_answer_endpoint") {
setErrorMessage(
"Run is blocked on a question; answer the question first.",
);
} else {
setErrorMessage(body?.detail ?? err.message ?? "Steer failed.");
}
} else {
setErrorMessage("Steer failed; try again.");
}
}
}
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
void send(false);
}
}
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
role="dialog"
aria-modal="true"
aria-label="Steer running agent"
className="w-full max-w-md rounded-lg border border-line bg-bg-elevated p-4 shadow-lg"
>
<div className="mb-2 text-sm font-semibold text-fg">Steer agent</div>
<textarea
ref={textareaRef}
rows={4}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a steering message…"
className="w-full resize-none rounded-md border border-line bg-bg p-2 text-sm text-fg outline-none focus:border-teal-500/50"
maxLength={8192}
/>
{errorMessage && (
<div className="mt-2">
<ErrorMessage message={errorMessage} />
</div>
)}
<div className="mt-3 flex items-center justify-between gap-2">
<span className="text-[11px] text-fg-muted">
Enter to send · Shift+Enter for newline
</span>
<div className="flex gap-2">
<button
type="button"
onClick={onClose}
className="rounded-md border border-line px-3 py-1 text-xs text-fg-2 hover:border-line-strong"
>
Cancel
</button>
<button
type="button"
onClick={() => void send(true)}
disabled={!canSubmit}
className="rounded-md border border-amber/30 px-3 py-1 text-xs text-amber hover:border-amber/60 disabled:cursor-not-allowed disabled:opacity-50"
>
Interrupt
</button>
<button
type="button"
onClick={() => void send(false)}
disabled={!canSubmit}
className="rounded-md bg-teal-500 px-3 py-1 text-xs font-medium text-white hover:bg-teal-600 disabled:cursor-not-allowed disabled:opacity-50"
>
Send
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -25,7 +25,6 @@ import { ciConfig, columnStatusDisplay, columnStatuses, deriveCiStatus, mapRunLi
import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus, ColumnStatus } from "../data/runs";
import { formatRelativeTime } from "../lib/format";
import { EmptyState } from "../components/state";
import { SteerComposer } from "../components/steer-composer";
import { useToast } from "../components/toast";
import { shouldRefreshBoardForEvent, useBoardEvents } from "../lib/board-events";
import { useAuthConfig, useBoardsRuns, useSystemInfo } from "../lib/queries";
@ -47,7 +46,7 @@ interface ColumnStyle {
const columnStyles: Record<ColumnStatus, ColumnStyle> = {
queued: { iconType: "branch", actions: [] },
initializing: { iconType: "branch", actions: [] },
running: { iconType: "branch", actions: ["Watch", "Steer"] },
running: { iconType: "branch", actions: [] },
blocked: { iconType: "branch", actions: ["Answer Question"] },
succeeded: { iconType: "pr", actions: [] },
failed: { iconType: "branch", actions: [] },
@ -308,15 +307,8 @@ function PrCard({
actions?: string[];
}) {
const lifecycleLabel = boardLifecycleStatusLabel(pr);
const [steerOpen, setSteerOpen] = useState(false);
return (
<>
<SteerComposer
runId={pr.id}
open={steerOpen}
onClose={() => setSteerOpen(false)}
/>
<Link to={`/runs/${pr.id}`} className="group block rounded-md border border-line bg-panel p-4 transition-all duration-200 hover:border-line-strong hover:shadow-lg hover:shadow-black/20">
<div className="mb-2 flex items-center gap-1.5">
<Icon className={`size-3.5 shrink-0 ${iconColor}`} />
@ -379,13 +371,6 @@ function PrCard({
key={label}
type="button"
disabled={pr.actionDisabled}
onClick={(e) => {
if (label === "Steer") {
e.preventDefault();
e.stopPropagation();
setSteerOpen(true);
}
}}
className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:text-fg-muted disabled:border-line ${
label === "Merge"
? "border-mint/20 text-mint hover:border-mint/50 hover:text-fg"
@ -396,16 +381,6 @@ function PrCard({
: "border-line-strong text-fg-3 hover:border-teal-500/40 hover:text-fg"
}`}
>
{label === "Watch" && (
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.831.88 9.577.43 8.899a1.62 1.62 0 0 1 0-1.798c.45-.678 1.367-1.932 2.637-3.023C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.824.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z" />
</svg>
)}
{label === "Steer" && (
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7.25-4.5a.75.75 0 0 0-1.5 0v.582a2.75 2.75 0 0 0-2.168 2.168H4.5a.75.75 0 0 0 0 1.5h.582a2.75 2.75 0 0 0 2.168 2.168v.582a.75.75 0 0 0 1.5 0v-.582a2.75 2.75 0 0 0 2.168-2.168h.582a.75.75 0 0 0 0-1.5h-.582A2.75 2.75 0 0 0 8.75 4.082ZM8 6.75a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z" />
</svg>
)}
{label === "Answer Question" && (
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
@ -427,7 +402,6 @@ function PrCard({
</div>
)}
</Link>
</>
);
}