6cd50ab841
Backend: - service_task_environments: fail closed when checkAnySessionRunning errors so a transient DB hiccup can't bypass the running-session guard and tear down a live container/sandbox; teardown is now truly best-effort with errors.Join across container/sandbox/worktree, so a failed Docker call doesn't permanently orphan the worktree on retry. Adds tests for the running-check error path and the multi-resource best-effort path. - executor_execute: propagate GetTaskEnvironmentByTaskID errors instead of silently launching a fresh environment that orphans existing containers/worktrees; gate environment reuse on matching ExecutorType so a profile switch doesn't pass stale PreviousExecutionID/container IDs to the wrong backend. - task_environment_handlers: reject malformed JSON on POST /tasks/:id/environment/reset (still allow empty body) so a destructive endpoint doesn't silently proceed with a bad payload. - worktree (PushEnvironmentBranch): bound git push with a 30s context timeout and disable interactive prompts via GIT_TERMINAL_PROMPT=0 so a stalled remote can't hang the request thread. - manager_environment: explicitly call ensureClient() before ContainerMgr() in DestroyContainer/GetContainerLiveStatus so a missing Docker daemon surfaces as "docker unavailable" rather than the generic "container manager not initialized". - container: WorkingDir always uses the in-container /workspace path rather than the host workspace path (Docker would happily start in an unrelated directory in host bind-mount mode); bootstrap now logs the prepare-script exit code so failures show up in container logs. - docker/client: CreateContainerInteractive honors ContainerConfig.Entrypoint so the same config struct produces consistent behavior on both creation paths. Frontend: - executor-settings-button: extract useTaskEnvironment hook (component shrinks below the 100-line limit); only treat 404 as empty env (other errors no longer wipe a valid environment); track "loaded once" via a ref so loadEnv doesn't depend on env state — that dep was creating a new function identity on every successful poll, cancelling the 3-second interval and turning the popover into a tight loop hammering /environment/live; copy-shell command uses sh instead of bash so it works on user-built minimal images. - task-reset-env-confirm-dialog: AlertDialogAction onClick calls e.preventDefault() so Radix's auto-close doesn't dismiss the dialog before the async reset completes (loading spinner now visible; close is parent-controlled via onOpenChange). - remote-credentials-card: track env method in selectedIds the same way files/gh_cli_token are tracked, and let initialChoice match either selectedIds OR envSecretId. First-time env-credential setup for an agent that exposes both files+env was broken: clicking the env radio never updated state, choice re-derived to "none", and the secret picker disappeared before the user could pick anything. - e2e/executor-reuse: select the acknowledge checkbox by label, not by positional last(), so adding another checkbox to the dialog can't silently target the wrong control. Format pass on prettier-flagged files; gofmt fixed two whitespace nits in lifecycle files.
114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { IconLoader } from "@tabler/icons-react";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@kandev/ui/alert-dialog";
|
|
import { Checkbox } from "@kandev/ui/checkbox";
|
|
|
|
export type TaskResetEnvConfirmDialogProps = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
hasWorktreePath: boolean;
|
|
isResetting?: boolean;
|
|
onConfirm: (opts: { pushBranch: boolean }) => void;
|
|
};
|
|
|
|
export function TaskResetEnvConfirmDialog({
|
|
open,
|
|
onOpenChange,
|
|
hasWorktreePath,
|
|
isResetting,
|
|
onConfirm,
|
|
}: TaskResetEnvConfirmDialogProps) {
|
|
const [acknowledged, setAcknowledged] = useState(false);
|
|
const [pushBranch, setPushBranch] = useState(false);
|
|
|
|
const handleOpenChange = (next: boolean) => {
|
|
if (!next) {
|
|
// reset local state when closing
|
|
setAcknowledged(false);
|
|
setPushBranch(false);
|
|
}
|
|
onOpenChange(next);
|
|
};
|
|
|
|
return (
|
|
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
|
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Reset environment?</AlertDialogTitle>
|
|
<AlertDialogDescription asChild>
|
|
<div className="space-y-2">
|
|
<p>
|
|
This tears down the current container, sandbox, and/or worktree for this task so the
|
|
next session starts with a fresh environment.
|
|
</p>
|
|
<p className="text-destructive">
|
|
Any uncommitted or unpushed changes in the workspace will be lost.
|
|
</p>
|
|
</div>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
|
|
<div className="space-y-3 py-1">
|
|
{hasWorktreePath && (
|
|
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
|
<Checkbox
|
|
checked={pushBranch}
|
|
onCheckedChange={(v) => setPushBranch(v === true)}
|
|
disabled={isResetting}
|
|
/>
|
|
<span>
|
|
Push the current branch to its remote before resetting.
|
|
<span className="block text-xs text-muted-foreground">
|
|
Helps preserve committed work. Uncommitted changes cannot be pushed.
|
|
</span>
|
|
</span>
|
|
</label>
|
|
)}
|
|
|
|
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
|
<Checkbox
|
|
checked={acknowledged}
|
|
onCheckedChange={(v) => setAcknowledged(v === true)}
|
|
disabled={isResetting}
|
|
/>
|
|
<span>I understand any uncommitted changes will be lost.</span>
|
|
</label>
|
|
</div>
|
|
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
disabled={isResetting || !acknowledged}
|
|
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
data-testid="reset-env-confirm"
|
|
onClick={(e) => {
|
|
// Radix's AlertDialogAction auto-closes the dialog on click,
|
|
// which would dismiss it before the async reset completes — the
|
|
// user never sees the loading spinner, and the dialog closes
|
|
// even on failure. Block the auto-close so the parent owns the
|
|
// close decision via `onOpenChange` after the promise settles.
|
|
e.preventDefault();
|
|
if (isResetting || !acknowledged) return;
|
|
onConfirm({ pushBranch });
|
|
}}
|
|
>
|
|
{isResetting ? <IconLoader className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
Reset environment
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
}
|