All files / src/components/panels TaskActionButtons.tsx

0% Statements 0/15
0% Branches 0/16
0% Functions 0/1
0% Lines 0/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146                                                                                                                                                                                                                                                                                                   
import type { JSX } from "react";
import type { TaskData } from "../../hooks/types.js";
import styles from "./TaskActionButtons.module.scss";
 
/**
 * Normalized session statuses for which Resume is meaningful. A task can be
 * `paused` either because its session is alive but idle (awaiting input) or
 * because its session is genuinely suspended/stopped; only the latter is
 * resumable, so Resume is gated on these statuses.
 */
const RESUMABLE_SESSION_STATUSES: ReadonlySet<string> = new Set(["stopped", "suspended"]);
 
/** Props for {@link TaskActionButtons}. */
export interface TaskActionButtonsProps {
  /** The task whose status drives which buttons are shown. */
  task: TaskData;
  /** Active session id — required to enable the Pause button. */
  sessionId: string | undefined;
  /**
   * Status of the task's latest session (already normalized by
   * `mapSessionStatus`: `idle` / `running` / `stopped` / `suspended` / ...).
   * Gates the Resume button — Resume only makes sense when the session is
   * actually suspended/stopped, not while it is alive and awaiting input.
   */
  latestSessionStatus: string | undefined;
  /** Whether the task is blocked by incomplete dependencies. */
  isBlocked: boolean;
  /** Start (or retry) the task. */
  onStart: () => void;
  /** Resume a paused task. */
  onResume: () => void;
  /** Stop the task. */
  onStop: () => void;
  /** Pause the task by killing its session. */
  onPause: () => void;
  /** Request task deletion (typically opens a confirm dialog). */
  onDelete: () => void;
  /** Open the task editor. */
  onEdit: () => void;
}
 
/**
 * Renders status-appropriate action buttons for a task.
 *
 * Which buttons appear depends on `task.status` and `isBlocked`.
 * Returns `undefined` for unrecognized statuses.
 */
export function TaskActionButtons({
  task,
  sessionId,
  latestSessionStatus,
  isBlocked,
  onStart,
  onResume,
  onStop,
  onPause,
  onDelete,
  onEdit,
}: TaskActionButtonsProps): JSX.Element | undefined {
  if (task.status === "not_started") {
    if (isBlocked) {
      return (
        <div className={styles.actionButtons} data-testid="task-action-buttons">
          <button onClick={onEdit} className={styles.btnGhost} data-testid="task-action-edit">
            Edit
          </button>
          <button onClick={onDelete} className={styles.btnDanger} data-testid="task-action-delete">
            Delete
          </button>
        </div>
      );
    }
    return (
      <div className={styles.actionButtons} data-testid="task-action-buttons">
        <button data-testid="task-header-start" onClick={onStart} className={styles.btnPrimary}>
          Start
        </button>
        <button onClick={onEdit} className={styles.btnGhost} data-testid="task-action-edit">
          Edit
        </button>
        <button onClick={onDelete} className={styles.btnDanger} data-testid="task-action-delete">
          Delete
        </button>
      </div>
    );
  }
  if (task.status === "working") {
    return (
      <div className={styles.actionButtons} data-testid="task-action-buttons">
        <button onClick={onStop} className={styles.btnDanger} data-testid="task-action-stop">
          Stop
        </button>
        <button
          onClick={onPause}
          disabled={!sessionId}
          className={styles.btnGhost}
          data-testid="task-action-pause"
        >
          Pause
        </button>
      </div>
    );
  }
  if (task.status === "paused") {
    const resumable =
      latestSessionStatus !== undefined && RESUMABLE_SESSION_STATUSES.has(latestSessionStatus);
    return (
      <div className={styles.actionButtons} data-testid="task-action-buttons">
        <button onClick={onStop} className={styles.btnPrimary} data-testid="task-action-stop">
          Stop
        </button>
        {resumable && (
          <button onClick={onResume} className={styles.btnGhost} data-testid="task-action-resume">
            Resume
          </button>
        )}
        <button onClick={onDelete} className={styles.btnDanger} data-testid="task-action-delete">
          Delete
        </button>
      </div>
    );
  }
  if (task.status === "complete") {
    return (
      <div className={styles.actionButtons} data-testid="task-action-buttons">
        <button onClick={onDelete} className={styles.btnDanger} data-testid="task-action-delete">
          Delete
        </button>
      </div>
    );
  }
  if (task.status === "failed") {
    return (
      <div className={styles.actionButtons} data-testid="task-action-buttons">
        <button onClick={onStart} className={styles.btnPrimary} data-testid="task-header-start">
          Retry
        </button>
        <button onClick={onDelete} className={styles.btnDanger} data-testid="task-action-delete">
          Delete
        </button>
      </div>
    );
  }
  return undefined;
}