The onConfirm handler in Sidebar resolves (not rejects) when the daemon refuses an unforced kill — it mutates state to switch to force mode and returns. That left `pending` stuck at true in the KillDialog, so the "Force kill" button rendered disabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
// React port of ../../killDialog.ts: the two kill prompts (#81, #160). The
|
|
// daemon refuses an unforced kill over a worktree holding unsaved work; the
|
|
// same component in `force` mode is the override that follows that refusal.
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
import { formatInTheWay, type InTheWay } from "../../types.js";
|
|
|
|
export interface KillDialogProps {
|
|
/** Session name, as the sidebar shows it. */
|
|
name: string;
|
|
/** What the card last saw in the way, when anything was. */
|
|
blockers: InTheWay | null;
|
|
/** Set once the daemon has refused an unforced kill: this pass forces it,
|
|
* and `reason` carries the daemon's refusal message. */
|
|
force?: boolean;
|
|
/** Message from the daemon's refusal, shown when there are no blockers to
|
|
* spell out. */
|
|
reason?: string;
|
|
/** Run the kill (or the forced kill). A rejection is rendered inline, and
|
|
* the dialog stays open so the press can be retried. */
|
|
onConfirm(): Promise<void>;
|
|
onCancel(): void;
|
|
}
|
|
|
|
export function KillDialog(props: KillDialogProps) {
|
|
const { name, blockers, force, reason, onConfirm, onCancel } = props;
|
|
const confirmRef = useRef<HTMLButtonElement>(null);
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// The acting button takes the focus in both modes: the operator already
|
|
// asked for the kill, so Enter confirms it.
|
|
useEffect(() => confirmRef.current?.focus(), []);
|
|
|
|
// When the dialog transitions from normal → force mode, the previous
|
|
// onConfirm resolved without error (it mutated parent state and returned),
|
|
// so `pending` is still true. Reset it so the force button is clickable.
|
|
useEffect(() => setPending(false), [force]);
|
|
|
|
const handleConfirm = () => {
|
|
setPending(true);
|
|
setError(null);
|
|
void onConfirm().catch((e: unknown) => {
|
|
setPending(false);
|
|
setError(`Failed to kill: ${(e as Error).message}`);
|
|
});
|
|
};
|
|
|
|
const body = blockers
|
|
? force
|
|
? `Its worktree still has ${formatInTheWay(blockers)}. Forcing the kill removes the session and leaves the worktree on disk.`
|
|
: `Its worktree still has ${formatInTheWay(blockers)}. The kill will be refused unless it is forced.`
|
|
: force
|
|
? `${reason ?? ""} Forcing the kill removes the session and leaves the worktree on disk.`
|
|
: "The session and everything running in it go away. Its worktree stays on disk.";
|
|
|
|
return createPortal(
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={`Kill ${name}`}
|
|
className="kill-dialog fixed inset-0 z-modal flex items-center justify-center bg-bg/70"
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) onCancel();
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Escape") onCancel();
|
|
}}
|
|
>
|
|
<div className="kill-dialog-panel min-w-[280px] max-w-[420px] rounded-r-2 border border-border-strong bg-surface p-sp-4 shadow-2">
|
|
<h2 className="kill-dialog-title m-0 text-fs-15 font-semibold text-ink">Kill "{name}"?</h2>
|
|
<p className="kill-dialog-body mt-sp-2 text-fs-13 text-ink">{body}</p>
|
|
{error && <p className="kill-dialog-error mt-sp-2 text-fs-12 text-danger">{error}</p>}
|
|
<div className="kill-dialog-actions mt-sp-3 flex justify-end gap-sp-2">
|
|
<button
|
|
type="button"
|
|
className="kill-dialog-cancel rounded-r-1 border border-border px-sp-3 py-sp-1 text-fs-12 text-ink"
|
|
onClick={onCancel}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
ref={confirmRef}
|
|
type="button"
|
|
disabled={pending}
|
|
className={`rounded-r-1 bg-danger px-sp-3 py-sp-1 text-fs-12 text-accent-ink disabled:opacity-60 ${
|
|
force ? "kill-dialog-force" : "kill-dialog-kill"
|
|
}`}
|
|
onClick={handleConfirm}
|
|
>
|
|
{force ? "Force kill" : "Kill"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|