# All files / src/components/tools TodoCard.tsx

95.55% Statements 43/45
98.03% Branches 50/51
100% Functions 12/12
95.34% Lines 41/43

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
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194

    49x

    55x

    55x

    55x
    49x

    207x

    207x

    6x
    3x

    12x

    12x

    3x
    3x

    219x

    74x

    29x

    116x

    3x
    3x
    3x
    17x
    17x
    17x
    17x
    17x
    17x
    9x
    8x
    2x

    17x

    3x

    236x

    83x

    31x

    122x

    55x
    236x
    164x
    55x

    55x

    236x

     import { type JSX, type ReactNode } from "react";
    import { Check, Circle, ListChecks } from "lucide-react";
    import type { ToolCardProps } from "./ToolCardProps.js";
    import { ICON_SM, ICON_MD } from "../../utils/iconSize.js";
    import styles from "./toolCards.module.scss";
    import todoStyles from "./TodoCard.module.scss";

    /** A normalized todo item used for rendering. */
    interface TodoItem {
      /** Display text (e.g. "Get bread"). */
      content: string;
      /** Present-tense description shown when in-progress (e.g. "Getting bread"). */
      activeForm?: string;
      /** Lifecycle status. */
      status: "pending" | "in_progress" | "completed";
    }

    /** Checkbox pattern for Goose's markdown checklist: `- [x]`, `- [ ]`, `- [~]`, `- [/]` */
    const CHECKBOX_PATTERN: RegExp = /^[-*]\s*\[([ xX~!/])\]\s*(.+)$/;

    /**
     * Parses todo items from args across all supported runtimes.
     *
     * Handles three formats:
     * - Claude Code TodoWrite: `{ todos: [{ content, activeForm, status }] }`
     * - Codex update_plan:     `{ plan: [{ step, status }] }`
     * - Goose todo_write:      `{ content: "markdown checklist" }`
     */
    function parseTodos(args: unknown): TodoItem[] {
      Iif (args === undefined || typeof args !== "object" || args === null) {
        return [];
      }
      const a = args as Record<string, unknown>;

      // Claude Code: { todos: [...] }
      if (Array.isArray(a.todos)) {
        return (a.todos as unknown[])
          .filter(
            (item): item is Record<string, unknown> =>
              typeof item === "object" &&
              item !== null &&
              typeof (item as Record<string, unknown>).content === "string" &&
              typeof (item as Record<string, unknown>).status === "string",
          )
          .map((item) => ({
            content: item.content as string,
            activeForm: typeof item.activeForm === "string" ? item.activeForm : undefined,
            status: normalizeStatus(item.status as string),
          }));
      }

      // Codex: { plan: [{ step, status }] }
      if (Array.isArray(a.plan)) {
        return (a.plan as unknown[])
          .filter(
            (item): item is Record<string, unknown> =>
              typeof item === "object" &&
              item !== null &&
              typeof (item as Record<string, unknown>).step === "string" &&
              typeof (item as Record<string, unknown>).status === "string",
          )
          .map((item) => ({
            content: item.step as string,
            status: normalizeStatus(item.status as string),
          }));
      }

      // Goose: { content: "markdown checklist" }
      if (typeof a.content === "string") {
        return parseMarkdownChecklist(a.content);
      }

      return [];
    }

    /** Normalizes status strings across runtimes to our three canonical values. */
    function normalizeStatus(status: string): "pending" | "in_progress" | "completed" {
      switch (status.toLowerCase()) {
        case "completed":
        case "done":
        case "complete":
          return "completed";
        case "in_progress":
        case "in-progress":
        case "working":
        case "active":
          return "in_progress";
        default:
          return "pending";
      }
    }

    /** Parses a markdown checklist string (Goose format) into TodoItems. */
    function parseMarkdownChecklist(content: string): TodoItem[] {
      const lines: string[] = content.split("\n");
      const items: TodoItem[] = [];
      for (const line of lines) {
        const match: RegExpExecArray | null = CHECKBOX_PATTERN.exec(line.trim());
        if (match) {
          const marker: string = match[1];
          const text: string = match[2].trim();
          let status: "pending" | "in_progress" | "completed" = "pending";
          if (marker === "x" || marker === "X") {
            status = "completed";
          } else if (marker === "~" || marker === "/" || marker === "!") {
            status = "in_progress";
          }
          items.push({ content: text, status });
        }
      }
      return items;
    }

    /** Status icon for a todo item. */
    function statusIcon(status: string): ReactNode {
      switch (status) {
        case "completed":
          return <Check size={ICON_SM} />;
        case "in_progress":
          return <Circle size={ICON_SM} fill="currentColor" />;
        default:
          return <Circle size={ICON_SM} />;
      }
    }

    /** Renders a TodoWrite tool call as a compact checklist. */
    export function TodoCard({ args }: ToolCardProps): JSX.Element {
      const todos = parseTodos(args);
      const completed: number = todos.filter((t) => t.status === "completed").length;
      const inProgress: TodoItem | undefined = todos.find((t) => t.status === "in_progress");
      const isEmpty: boolean = todos.length === 0;

      return (
        <div className={`${styles.card} ${styles.cardBlue}`} data-testid="tool-card-todo">
          <div className={styles.header}>
            <span className={styles.icon}>
              <ListChecks size={ICON_MD} />
            </span>
            <span className={styles.toolName} style={{ color: "var(--accent-blue)" }}>
              {isEmpty ? "Todos cleared" : "Todos"}
            </span>
            {!isEmpty && (
              <>
                <span className={styles.spacer} />
                <span className={styles.badge} data-testid="tool-card-todo-progress">
                  {completed}/{todos.length}
                </span>
              </>
            )}
          </div>

          {/* Progress bar */}
          {!isEmpty && (
            <div className={todoStyles.progressBar} data-testid="tool-card-todo-bar">
              <div
                className={todoStyles.progressFill}
                style={{ width: `${(completed / todos.length) * 100}%` }}
              />
            </div>
          )}

          {/* Active task callout */}
          {inProgress && (
            <div className={todoStyles.activeTask} data-testid="tool-card-todo-active">
              <span className={todoStyles.activeIcon}>
                <Circle size={ICON_SM} fill="currentColor" />
              </span>
              <span className={todoStyles.activeText}>
                {inProgress.activeForm || inProgress.content}
              </span>
            </div>
          )}

          {/* Checklist */}
          {!isEmpty && (
            <div className={todoStyles.checklist} data-testid="tool-card-todo-list">
              {todos.map((todo, i) => (
                <div
                  key={i}
                  className={`${todoStyles.item} ${todoStyles[todo.status]}`}
                  data-testid="tool-card-todo-item"
                >
                  <span className={todoStyles.itemIcon}>{statusIcon(todo.status)}</span>
                  <span className={todoStyles.itemText}>{todo.content}</span>
                </div>
              ))}
            </div>
          )}

          {isEmpty && <div className={todoStyles.emptyMessage}>All items completed and cleared.</div>}
        </div>
      );
    }