All files / src/components/agents AgentHeader.tsx

0% Statements 0/59
100% Branches 1/1
100% Functions 1/1
0% Lines 0/59

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                                                                                                                                                                                           
/**
 * AgentHeader — presentational header above the agent tab bar (#1419).
 * Shows avatar, name, and heartbeat status indicators.
 *
 * @module
 */
 
import type { JSX } from "react";
import { ArrowLeft } from "lucide-react";
import type { ScheduleData } from "../../hooks/types.js";
import { formatCountdown, formatRelativeTime } from "../../utils/time.js";
import { isImageAvatar } from "./AgentManager.js";
import styles from "./AgentHeader.module.scss";
 
/** Props for {@link AgentHeader}. */
export interface AgentHeaderProps {
  /** Agent display name. */
  name: string;
  /** Emoji, image URL, or data URI. Empty = monogram fallback. */
  avatar: string;
  /** Derived heartbeat schedule (populated by server on read). */
  heartbeat?: ScheduleData;
  /** Navigate back to the home page. */
  onNavigateBack: () => void;
}
 
/** Render an avatar: image, emoji glyph, or name-derived monogram. */
function AvatarDisplay({ name, avatar }: { name: string; avatar: string }): JSX.Element {
  if (avatar && isImageAvatar(avatar)) {
    return (
      <img
        className={styles.avatar}
        src={avatar}
        alt=""
        referrerPolicy="no-referrer"
        loading="lazy"
        data-testid="agent-header-avatar-image"
      />
    );
  }
  const glyph = avatar || (name.trim().charAt(0) || "?").toUpperCase();
  return (
    <span className={styles.avatar} aria-hidden="true" data-testid="agent-header-avatar-glyph">
      {glyph}
    </span>
  );
}
 
/** Agent detail page header with avatar, name, and heartbeat indicators. */
export function AgentHeader({
  name,
  avatar,
  heartbeat,
  onNavigateBack,
}: AgentHeaderProps): JSX.Element {
  return (
    <header className={styles.header} data-testid="agent-header">
      <button
        className={styles.backButton}
        onClick={onNavigateBack}
        aria-label="Back"
        data-testid="agent-header-back"
      >
        <ArrowLeft size={18} />
      </button>
      <AvatarDisplay name={name} avatar={avatar} />
      <div className={styles.info}>
        <h1 className={styles.name} data-testid="agent-header-name">
          {name}
        </h1>
        {heartbeat && (
          <div className={styles.status} data-testid="agent-header-status">
            {heartbeat.enabled && heartbeat.nextRunAt && (
              <span className={styles.statusItem} data-testid="agent-header-next-wake">
                Next wake {formatCountdown(heartbeat.nextRunAt)}
              </span>
            )}
            {!heartbeat.enabled && (
              <span className={styles.statusItem} data-testid="agent-header-paused">
                Paused
              </span>
            )}
            {heartbeat.lastRunAt && (
              <span className={styles.statusItem} data-testid="agent-header-last-activity">
                Last activity {formatRelativeTime(heartbeat.lastRunAt)}
              </span>
            )}
          </div>
        )}
      </div>
    </header>
  );
}