a stopped job stays stopped, and Stop is a real button when expanded

Two things from stopping one by hand.

── It restarted itself ──

Pressing stop produced the exit notification, and the agent read that as a crash:
it brought the server back up, then wrote three paragraphs theorising that
"something outside this conversation sent that SIGTERM" and counting it as the
third external touch on the repo today.

It was not wrong about the signal. It could not have been right about the cause,
because the stop button did not exist when the preamble was written, and the
preamble said only "report" — leaving what to do with an exit entirely open.

So it now says: on exit, one line, and STOP. Do not restart. Do not diagnose. Do
not theorise about what else on the machine might have touched it. A job I stopped
that comes back on its own is the opposite of a stop button — and an exit is at
least as likely to be me as a failure now that pressing a button is how you end
one.

Deliberately not written as "ignore exit code 143": the phrasing is about intent,
not signal numbers, so it holds whether the job dies by SIGTERM, SIGKILL or its
own hand.

── Stop, labelled, in the expanded panel ──

The chip keeps its icon; the panel gets a bordered button that says "Stop". The
panel is where you go to decide whether something should keep running, and the
control that ends it should not be a glyph you have to hover to identify. Both
appear only while the task is running.

tsgo clean, frontend builds, 851 pass / 7 fail unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 02:06:57 +00:00
co-authored by Claude Opus 5
parent 048e718b21
commit 1a9b162615
2 changed files with 33 additions and 2 deletions
@@ -120,5 +120,14 @@ export function buildBackgroundJobPrompt(spec: BackgroundJobSpec): string {
`Do not read the output back to check it started. If it fails, the exit reaches me on its own —`,
`that is the entire point of using the harness's mechanism, and checking by hand just adds noise`,
`to the one case where nothing is wrong.`,
``,
`IF IT EXITS:`,
``,
`Say so in one line and STOP. Do not restart it. Do not diagnose why. Do not theorise about what`,
`else on the machine might have touched it.`,
``,
`I can stop a job myself, from a button on its chip — so an exit is at least as likely to be me`,
`as it is to be a failure, and a job I stopped that comes back on its own is the opposite of a`,
`stop button. If I want it running again I will start it again.`,
].join('\n');
}
@@ -71,7 +71,13 @@ export const BackgroundTaskTray = ({ messages }: BackgroundTaskTrayProps) => {
return (
<div className="mb-2 rounded-lg border border-border bg-muted/40">
{open && <TaskPanel task={open} onClose={() => setOpenId(null)} />}
{open && (
<TaskPanel
task={open}
onClose={() => setOpenId(null)}
onStop={open.status ? undefined : () => void stopTask(open.taskId)}
/>
)}
{pinned.length > 0 && (
<TaskStrip label="Pinned" divided={unpinned.length > 0}>
@@ -222,11 +228,13 @@ const TaskChip = ({ task, active, pinned, onClick, onTogglePin, onDismiss, onSto
// ── Panel ──
type TaskPanelProps = {
/** Present only while the task is running — the panel shows a real button rather than a bare glyph. */
onStop?: () => void;
task: BackgroundTask;
onClose: () => void;
};
const TaskPanel = ({ task, onClose }: TaskPanelProps) => {
const TaskPanel = ({ task, onClose, onStop }: TaskPanelProps) => {
const running = !task.status;
const { data, isLoading } = useTaskDetail(task.taskId, running);
const bodyRef = useRef<HTMLDivElement | null>(null);
@@ -249,6 +257,20 @@ const TaskPanel = ({ task, onClose }: TaskPanelProps) => {
</div>
</div>
{running && <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-warning" />}
{/* Labelled here, unlike the chip's icon-only version. The panel is where you come to decide
whether to keep something running, so the control that ends it should not be a glyph you
have to hover to identify. */}
{running && onStop && (
<button
type="button"
onClick={onStop}
title="Stop this job"
className="flex shrink-0 cursor-pointer items-center gap-1 rounded border border-border px-2 py-1 text-xs text-muted-foreground transition-colors hover:border-destructive/40 hover:text-destructive"
>
<Square className="h-3 w-3" />
Stop
</button>
)}
<button
type="button"
onClick={onClose}