a paused job shows a pause icon, not a spinner
The details panel kept spinning after Pause, so a frozen dev server looked like a working one. The spinner is the whole claim being made, and it was false — the state most likely to be watched, since the panel is where you sit while a `bun dev` runs. Amber either way: it is still a live job holding its port and its memory, which is what separates paused from stopped. What changes is the motion, and the line beside it now reads `paused` rather than `running…`. ── And the state moved up a level ── `paused` lived in the panel, which unmounts when you close it. Closing and reopening would have put a spinning icon back over a stopped process — the exact failure this commit is fixing, arriving by a different route. It is the tray's now, so it survives the panel being closed. Still per-session: a reload forgets, and the button then offers Pause on a job that is already frozen. SIGSTOP makes that harmless, and the honest fix is asking the process rather than remembering — `ps -o state=` on the pids holding the output file would make it survive anything, at the cost of a spawn per poll. Worth doing if this ever misleads. The chip is unchanged: a paused job still shows the running dot there, because live is what that dot means and the distinction belongs where the controls are. tsgo clean, frontend builds, 914 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,15 @@ export const BackgroundTaskTray = ({ messages, sessionId }: BackgroundTaskTrayPr
|
||||
const { tasks, pinned, unpinned, running, clearable, togglePin, dismiss, dismissFinished } =
|
||||
useBackgroundTasks(messages);
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
/**
|
||||
* Which jobs are frozen.
|
||||
*
|
||||
* Held here rather than in the panel because the panel unmounts when you close it, and a `paused` that
|
||||
* resets would put a spinning icon over a stopped process — the exact thing this display is for. Still
|
||||
* per-session: a reload forgets, and the button then says Pause over an already-frozen job, which
|
||||
* SIGSTOP makes harmless.
|
||||
*/
|
||||
const [pausedIds, setPausedIds] = useState<string[]>([]);
|
||||
const client = useClient();
|
||||
|
||||
/**
|
||||
@@ -91,6 +100,7 @@ export const BackgroundTaskTray = ({ messages, sessionId }: BackgroundTaskTrayPr
|
||||
try {
|
||||
const result = await client.post<{ ok: boolean; reason?: string }>(`/chat/tasks/${taskId}/pause`, { paused });
|
||||
if (!result.ok) toast.error(result.reason ?? 'Could not pause that job');
|
||||
else setPausedIds((prev) => (paused ? [...new Set([...prev, taskId])] : prev.filter((id) => id !== taskId)));
|
||||
return result.ok;
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Could not pause that job');
|
||||
@@ -123,6 +133,7 @@ export const BackgroundTaskTray = ({ messages, sessionId }: BackgroundTaskTrayPr
|
||||
onClose={() => setOpenId(null)}
|
||||
onStop={open.status ? undefined : () => void stopTask(open.taskId)}
|
||||
onPause={(next) => pauseTask(open.taskId, next)}
|
||||
paused={pausedIds.includes(open.taskId)}
|
||||
onRestart={() => void restartTask(open.taskId)}
|
||||
/>
|
||||
)}
|
||||
@@ -280,18 +291,16 @@ type TaskPanelProps = {
|
||||
onStop?: () => void;
|
||||
/** Freeze and thaw the process. Present while it is running; the label follows the state. */
|
||||
onPause?: (paused: boolean) => Promise<boolean>;
|
||||
/** Frozen right now — owned by the tray, so closing this panel does not forget it. */
|
||||
paused?: boolean;
|
||||
/** Run the command again, whether it is running or long finished. */
|
||||
onRestart?: () => void;
|
||||
task: BackgroundTask;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const TaskPanel = ({ task, onClose, onStop, onPause, onRestart }: TaskPanelProps) => {
|
||||
const TaskPanel = ({ task, onClose, onStop, onPause, onRestart, paused = false }: TaskPanelProps) => {
|
||||
const running = !task.status;
|
||||
// Local, because a paused process is a short-lived state somebody is watching. It resets on reload,
|
||||
// which is honest enough: SIGCONT is idempotent, and the button says what it will do rather than what
|
||||
// it believes has happened.
|
||||
const [paused, setPaused] = useState(false);
|
||||
const { data, isLoading } = useTaskDetail(task.taskId, running);
|
||||
const bodyRef = useRef<HTMLDivElement | null>(null);
|
||||
const pinnedRef = useRef(true);
|
||||
@@ -309,10 +318,18 @@ const TaskPanel = ({ task, onClose, onStop, onPause, onRestart }: TaskPanelProps
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs text-foreground/80">{task.description || 'Background task'}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{task.taskType ?? 'task'} · {task.status ? (task.summary ?? task.status) : 'running…'}
|
||||
{task.taskType ?? 'task'} · {task.status ? (task.summary ?? task.status) : paused ? 'paused' : 'running…'}
|
||||
</div>
|
||||
</div>
|
||||
{running && <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-warning" />}
|
||||
{/* A frozen process is not a working one, and a spinner says it is. Same amber either way — it is
|
||||
still a live job holding its port and its memory — but STILL, because the animation is the
|
||||
whole claim being made. */}
|
||||
{running &&
|
||||
(paused ? (
|
||||
<Pause className="h-3.5 w-3.5 shrink-0 text-warning" />
|
||||
) : (
|
||||
<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. */}
|
||||
@@ -321,10 +338,7 @@ const TaskPanel = ({ task, onClose, onStop, onPause, onRestart }: TaskPanelProps
|
||||
{running && onPause && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const next = !paused;
|
||||
if (await onPause(next)) setPaused(next);
|
||||
}}
|
||||
onClick={() => void onPause(!paused)}
|
||||
title={paused ? 'Let it continue where it left off' : 'Freeze the process, keeping its state'}
|
||||
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-warning/40 hover:text-warning"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user