Files
kandev/apps/web/components/task/task-reset-env-confirm-dialog.tsx
T
José Almeida 6cd50ab841 fix: address PR review feedback (CI, CodeRabbit, Greptile, Claude)
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.
2026-05-04 21:09:39 +01:00

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>
);
}