Skip to content
Brok UI

Loading…

No results

AI Task Progress

Open source

A live agent-plan card: "4/7 tasks complete" over a compact list of steps, each marked pending, active, done, failed or skipped.

Version
v1.0.1
Stability
stable
License
MIT
Related
Todo Item
Stepper
Check List
Bulk Job

Preview

4/7 milestones complete
  • Done Establish the product goals and core user requirements
  • Done Define the primary user flows and information architecture
  • Done Explore visual direction through moodboards and references
  • Done Build the final interface with a consistent design system
  • In progress Translate validated flows into responsive wireframes
  • Pending Prepare an interactive prototype for validation
  • Pending Package final assets and specifications for development
previews.components.ai-task-progress.default.blade.php Blade
<x-ui.ai.task-progress
    class="mx-auto w-full max-w-md"
    noun="milestone|milestones"
    progress
    collapsible
    dismissible
    :items="[
        ['label' => __('Establish the product goals and core user requirements'), 'status' => 'done'],
        ['label' => __('Define the primary user flows and information architecture'), 'status' => 'done'],
        ['label' => __('Explore visual direction through moodboards and references'), 'status' => 'done'],
        ['label' => __('Build the final interface with a consistent design system'), 'status' => 'done'],
        ['label' => __('Translate validated flows into responsive wireframes'), 'status' => 'active'],
        ['label' => __('Prepare an interactive prototype for validation'), 'status' => 'pending'],
        ['label' => __('Package final assets and specifications for development'), 'status' => 'pending'],
    ]"
/>

Installation

terminal
php artisan ui:add ai-task-progress

Note

This component ships an Alpine behavior module at resources/js/ui/ai-task-progress.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './ai-task-progress.js';

Registry contract

php artisan ui:add ai-task-progress writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/ai/task-progress.blade.php
Registry dependencies
spinner progress collapsible
Packages
composer: jml/brok:^0.2
npm: alpinejs

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

ai-task-progress.md
# Brok UI: AI Task Progress (`ai-task-progress`)

A live agent-plan card: "4/7 tasks complete" over a compact list of steps, each marked pending, active, done, failed or skipped.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add ai-task-progress
```

## Usage

```blade
<x-ui.ai.task-progress
    class="mx-auto w-full max-w-md"
    noun="milestone|milestones"
    progress
    collapsible
    dismissible
    :items="[
        ['label' => __('Establish the product goals and core user requirements'), 'status' => 'done'],
        ['label' => __('Define the primary user flows and information architecture'), 'status' => 'done'],
        ['label' => __('Explore visual direction through moodboards and references'), 'status' => 'done'],
        ['label' => __('Build the final interface with a consistent design system'), 'status' => 'done'],
        ['label' => __('Translate validated flows into responsive wireframes'), 'status' => 'active'],
        ['label' => __('Prepare an interactive prototype for validation'), 'status' => 'pending'],
        ['label' => __('Package final assets and specifications for development'), 'status' => 'pending'],
    ]"
/>
```

## Props

- `items` (array, default `[]`) — [['label' => string, 'status' => 'pending'|'active'|'done'|'failed'|'skipped']]. Ignored when `bind` is set.
- `noun` (string, default `tasks`) — What is being counted, plural form ("tasks", "milestones"). Pass `singular|plural` for an irregular word; otherwise the singular is derived automatically for the count-of-one case.
- `progress` (bool, default `false`) — Shows a thin progress bar (the progress component) under the header, filled to done/total.
- `collapsible` (bool, default `false`) — Adds a chevron trigger in the header that folds the item list away, starting open.
- `dismissible` (bool, default `false`) — Adds a close action that hides the card and dispatches `ai-task-progress:dismiss`, for a host that wants to react (e.g. clear the underlying job) rather than just hide it.
- `bind` (string|null, default `null`) — An Alpine expression evaluating to the items array in the surrounding scope, e.g. `plan.steps`. The header count and every marker then follow it live, so a running agent can push status updates with no round trip.

## Use when

- Use to expose AI-specific prompts, states, or reasoning metadata when those details help users supervise or understand an AI workflow.
- Showing the plan a running agent is working through, with a live done-of-total count and a per-step status.
- A step can be pending, in progress, done, failed or skipped — not just checked or unchecked.

## Avoid when

- Do not surface internal AI mechanics when they add confusion or implementation detail without user value.
- The steps are operated by a person with real checkboxes; use todo-item or the todo-list block instead.
- The steps are a numbered wizard rail with no failed/skipped state; use stepper instead.
- The list is a static feature/plan comparison; use check-list instead.

## Anti-patterns

- Decorative AI chrome
- Exposing internal mechanics without user value

## Rules

- Use the `<brok:ai-task-progress>` tag (or `<x-ui.ai-task-progress>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/ai-task-progress
- Registry JSON (files, props, contract): https://brokui.dev/r/open/ai-task-progress.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

long-content.blade.php Blade
{{-- Long milestone labels wrap under their markers, and a long noun keeps
     the header count on one line beside the actions. --}}
<div class="w-72">
    <x-ui.ai.task-progress
        noun="deliverables for the client handoff"
        collapsible
        dismissible
        :items="[
            ['label' => __('Establish the product goals, the core user requirements and the success metrics for launch'), 'status' => 'done'],
            ['label' => __('Define the primary user flows and the information architecture for every signed-in surface'), 'status' => 'done'],
            ['label' => __('Translate the validated flows into responsive wireframes for phone, tablet and desktop'), 'status' => 'active'],
            ['label' => __('Prepare an interactive prototype for moderated usability validation with five participants'), 'status' => 'failed'],
            ['label' => __('Package the final assets and the specifications for development'), 'status' => 'skipped'],
            ['label' => __('Hand off'), 'status' => 'pending'],
        ]"
    />
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
items array [] [['label' => string, 'status' => 'pending'|'active'|'done'|'failed'|'skipped']]. Ignored when `bind` is set.
noun string tasks What is being counted, plural form ("tasks", "milestones"). Pass `singular|plural` for an irregular word; otherwise the singular is derived automatically for the count-of-one case.
progress bool false Shows a thin progress bar (the progress component) under the header, filled to done/total.
collapsible bool false Adds a chevron trigger in the header that folds the item list away, starting open.
dismissible bool false Adds a close action that hides the card and dispatches `ai-task-progress:dismiss`, for a host that wants to react (e.g. clear the underlying job) rather than just hide it.
bind string | null null An Alpine expression evaluating to the items array in the surrounding scope, e.g. `plan.steps`. The header count and every marker then follow it live, so a running agent can push status updates with no round trip.

Slots

Default Blade slot only.

Data slots

Stable hooks for CSS overrides and browser tests.

ai-task-progress ai-task-progress-count ai-task-progress-dismiss ai-task-progress-header ai-task-progress-item ai-task-progress-label ai-task-progress-list ai-task-progress-marker

Behavior

  • The header count is aria-live="polite", so a status change is announced without moving focus.
  • Each marker carries its own sr-only status text; the glyph and colour alone never carry the only meaning.
  • A done step's label is muted, never struck through — this is a live plan, not a completed checklist.
  • The active marker reuses the spinner primitive; the pulse is gated behind prefers-reduced-motion via spinner's own motion handling.
  • Server rows and bound rows resolve status to the same marker/text classes, so the two sources cannot colour a status differently.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, interactive, authoredStateFixtures, responsive, rtl, darkMode, localized, reducedMotion.

Guidance

AI-assisted interaction

Help users prompt, inspect, or supervise an AI-assisted operation.

Use when

  • Use to expose AI-specific prompts, states, or reasoning metadata when those details help users supervise or understand an AI workflow.
  • Showing the plan a running agent is working through, with a live done-of-total count and a per-step status.
  • A step can be pending, in progress, done, failed or skipped — not just checked or unchecked.

Avoid when

  • Do not surface internal AI mechanics when they add confusion or implementation detail without user value.
  • The steps are operated by a person with real checkboxes; use todo-item or the todo-list block instead.
  • The steps are a numbered wizard rail with no failed/skipped state; use stepper instead.
  • The list is a static feature/plan comparison; use check-list instead.

Use instead

  • Deterministic conventional workflow

Anti-patterns

  • Decorative AI chrome
  • Exposing internal mechanics without user value
Anatomy
ai-task-progress ai-task-progress-header ai-task-progress-count ai-task-progress-dismiss ai-task-progress-list ai-task-progress-item ai-task-progress-marker ai-task-progress-label
Theming hooks
Composes semantic muted, border, input, success, destructive, spinner and progress tokens.

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • The count region is a live region, so a screen reader hears "4/7 tasks complete" as it changes.
  • Decorative markers are aria-hidden; the status they carry is exposed as adjacent sr-only text instead.
  • The collapse trigger is a real button with aria-expanded/aria-controls; the dismiss action has an accessible name.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="ai-task-progress-{{ $record->id }}">
    <x-ui.ai.task-progress
        class="mx-auto w-full max-w-md"
        noun="milestone|milestones"
        progress
        collapsible
        dismissible
        :items="[
            ['label' => __('Establish the product goals and core user requirements'), 'status' => 'done'],
            ['label' => __('Define the primary user flows and information architecture'), 'status' => 'done'],
            ['label' => __('Explore visual direction through moodboards and references'), 'status' => 'done'],
            ['label' => __('Build the final interface with a consistent design system'), 'status' => 'done'],
            ['label' => __('Translate validated flows into responsive wireframes'), 'status' => 'active'],
            ['label' => __('Prepare an interactive prototype for validation'), 'status' => 'pending'],
            ['label' => __('Package final assets and specifications for development'), 'status' => 'pending'],
        ]"
    />
</div>

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/ai/task-progress.blade.php Blade
{{--
    AI Task Progress — the live plan an agent is working through: "4/7 tasks
    complete" over a compact list of steps, each with its own status. This is
    the running-plan sibling of todo-item (a real checkbox a person operates)
    and stepper (a numbered wizard rail) — here nothing is clickable, the
    marker only reports what the agent already decided, so the vocabulary is
    five states (pending|active|done|failed|skipped) instead of a boolean.

    Two sources, one markup, the same shape fact-list uses:

      - `items` — a PHP array of `{ label, status }`. The default.
      - `bind`  — an Alpine expression naming an array of the same shape in
        the surrounding scope, so a running agent can push status updates
        into it and the header count and every marker follow live, with no
        round trip.

    `noun` names what is being counted ("tasks", "milestones", …); pass
    `singular|plural` for an irregular word, otherwise the plural form is
    singularised for the count-of-one case.

    `progress` adds a thin bar under the header (reusing `progress` — no
    second bar primitive). `collapsible` adds a chevron trigger that folds
    the list away (built on the same `uiCollapsible` behaviour `collapsible`
    ships, applied to this component's own root so the header never has to
    live inside a second wrapper). `dismissible` adds an close action that
    hides the card and dispatches `ai-task-progress:dismiss`, for a host that
    wants to react (e.g. clear the underlying job) rather than just hide it.
--}}
@props([
    'items' => [],
    'noun' => 'tasks',
    'progress' => false,
    'collapsible' => false,
    'dismissible' => false,
    'bind' => null,
])

@php
    $showProgress = filter_var($progress, FILTER_VALIDATE_BOOLEAN);
    $collapsible = filter_var($collapsible, FILTER_VALIDATE_BOOLEAN);
    $dismissible = filter_var($dismissible, FILTER_VALIDATE_BOOLEAN);
    $live = $bind !== null;

    $statuses = ['pending', 'active', 'done', 'failed', 'skipped'];

    $rows = collect((array) $items)
        ->filter(fn ($item) => is_array($item))
        ->map(fn (array $item): array => [
            'label' => (string) ($item['label'] ?? ''),
            'status' => in_array($item['status'] ?? 'pending', $statuses, true) ? $item['status'] : 'pending',
        ])
        ->values();

    $total = $rows->count();
    $done = $rows->where('status', 'done')->count();

    // `noun` is a plain word from the caller, not a translation key. A
    // `singular|plural` pair covers an irregular word; otherwise the plural
    // form the caller gave us is singularised for the count-of-one case, so
    // trans_choice still drives the real pluralisation instead of a
    // hand-rolled ternary.
    $nounInput = trim((string) $noun) !== '' ? (string) $noun : 'tasks';
    if (str_contains($nounInput, '|')) {
        [$nounSingular, $nounPlural] = array_pad(explode('|', $nounInput, 2), 2, $nounInput);
    } else {
        $nounPlural = $nounInput;
        $nounSingular = \Illuminate\Support\Str::singular($nounInput);
    }
    $nounWord = trans_choice($nounSingular.'|'.$nounPlural, $live ? 2 : max($total, 1));
    $headerText = __(':done/:total :noun complete', ['done' => $done, 'total' => $total, 'noun' => $nounWord]);

    $statusLabels = [
        'pending' => __('Pending'),
        'active' => __('In progress'),
        'done' => __('Done'),
        'failed' => __('Failed'),
        'skipped' => __('Skipped'),
    ];

    $markerClasses = [
        'pending' => 'border-2 border-input bg-transparent',
        'active' => 'text-primary',
        'done' => 'bg-success text-success-foreground',
        'failed' => 'bg-destructive text-destructive-foreground',
        'skipped' => 'border border-border bg-muted text-muted-foreground',
    ];
    $markerGlyphs = [
        'done' => '<path d="M20 6 9 17l-5-5" />',
        'failed' => '<path d="M18 6 6 18M6 6l12 12" />',
        'skipped' => '<path d="M5 12h14" />',
    ];
    $textClasses = [
        'pending' => 'text-muted-foreground',
        'active' => 'text-foreground font-medium',
        'done' => 'text-muted-foreground',
        'failed' => 'text-destructive',
        'skipped' => 'text-muted-foreground',
    ];

    // Live mode reads the same maps client-side, so a bound row and a server
    // row cannot resolve the same status to two different classes.
    $markerClassesJson = json_encode($markerClasses, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
    $textClassesJson = json_encode($textClasses, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
    $statusLabelsJson = json_encode($statusLabels, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);

    // Built from single-quoted PHP strings throughout (never double-quoted)
    // so the JS variable names below are never mistaken for PHP interpolation.
    $headerLiveExpr = '((rows) => {'
        .' var total = rows.length;'
        .' var done = rows.filter(function (item) { return item.status === "done"; }).length;'
        .' var noun = total === 1 ? '.\Illuminate\Support\Js::from($nounSingular).' : '.\Illuminate\Support\Js::from($nounPlural).';'
        .' return done + "/" + total + " " + noun + " " + '.\Illuminate\Support\Js::from(__('complete')).';'
        .' })('.$bind.')';

    // The progress bar's live value, when live: same single-quoted-PHP rule
    // as $headerLiveExpr. Passed through `:bind` (a normal PHP-bound prop, not
    // a raw @if inside the tag — Blade's component-tag compiler does not
    // parse a directive embedded in an opening tag correctly).
    $progressBindExpr = $live
        ? '(('.$bind.').filter(function (item) { return item.status === "done"; }).length)'
        : null;

    // x-data assembly: `uiCollapsible` (from the collapsible dependency) is
    // spread in only when collapsible is on, `dismissed` only when
    // dismissible is on, so an instance using neither ships no Alpine at all.
    $xData = match (true) {
        $collapsible && $dismissible => '{ ...uiCollapsible({ open: true }), dismissed: false }',
        $collapsible => 'uiCollapsible({ open: true })',
        $dismissible => '{ dismissed: false }',
        default => null,
    };
@endphp

<div
    data-slot="ai-task-progress"
    @if ($xData) x-data="{{ $xData }}" @endif
    @if ($collapsible) x-id="['collapsible-content']" @endif
    @if ($dismissible) x-show="!dismissed" x-cloak @endif
    {{ $attributes->merge(['class' => 'overflow-hidden rounded-lg border border-border bg-muted/40 text-foreground']) }}
>
    <div data-slot="ai-task-progress-header" class="flex items-center justify-between gap-2 px-3 py-2">
        <div class="flex min-w-0 items-center gap-2">
            <svg class="size-4 shrink-0 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                <path d="m3 17 2 2 4-4" />
                <path d="m3 7 2 2 4-4" />
                <path d="M13 6h8" />
                <path d="M13 12h8" />
                <path d="M13 18h8" />
            </svg>
            <span
                data-slot="ai-task-progress-count"
                aria-live="polite"
                class="min-w-0 truncate text-sm font-medium text-foreground"
                @if ($live) x-text="{{ $headerLiveExpr }}" @endif
            >{{ $headerText }}</span>
        </div>

        <div class="flex shrink-0 items-center gap-1">
            @if ($collapsible)
                <x-ui.collapsible.trigger indicator="chevron" class="size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground motion-reduce:transition-none">
                    <span class="sr-only">{{ __('Toggle task list') }}</span>
                </x-ui.collapsible.trigger>
            @endif
            @if ($dismissible)
                <button
                    type="button"
                    data-slot="ai-task-progress-dismiss"
                    x-on:click="dismissed = true; $dispatch('ai-task-progress:dismiss')"
                    aria-label="{{ __('Dismiss') }}"
                    class="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none"
                >
                    <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
                </button>
            @endif
        </div>
    </div>

    @if ($showProgress)
        <div class="px-3 pb-3">
            <x-ui.progress
                :value="$done"
                :max="max($total, 1)"
                :label="$headerText"
                :bind="$progressBindExpr"
            />
        </div>
    @endif

@if ($collapsible)
    <x-ui.collapsible.content>
@endif
        <ul data-slot="ai-task-progress-list" role="list" class="divide-y divide-border border-t border-border">
            @if ($live)
                <template x-for="(item, index) in ({{ $bind }})" :key="index">
                    <li data-slot="ai-task-progress-item" x-bind:data-status="item.status" class="flex items-start gap-3 px-3 py-2 text-sm">
                        <span
                            data-slot="ai-task-progress-marker"
                            class="relative flex size-5 shrink-0 items-center justify-center rounded-full"
                            x-bind:class="({{ $markerClassesJson }})[item.status] ?? ''"
                            aria-hidden="true"
                        >
                            <template x-if="item.status === 'active'"><x-ui.spinner size="sm" /></template>
                            <template x-if="item.status === 'done'">
                                <svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
                            </template>
                            <template x-if="item.status === 'failed'">
                                <svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12" /></svg>
                            </template>
                            <template x-if="item.status === 'skipped'">
                                <svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /></svg>
                            </template>
                        </span>
                        <span class="sr-only" x-text="(({{ $statusLabelsJson }})[item.status] ?? '')"></span>
                        <span data-slot="ai-task-progress-label" class="min-w-0 flex-1 break-words" x-text="item.label" x-bind:class="({{ $textClassesJson }})[item.status] ?? ''"></span>
                    </li>
                </template>
            @else
                @foreach ($rows as $row)
                    @php $status = $row['status']; @endphp
                    <li data-slot="ai-task-progress-item" data-status="{{ $status }}" class="flex items-start gap-3 px-3 py-2 text-sm">
                        <span data-slot="ai-task-progress-marker" class="relative flex size-5 shrink-0 items-center justify-center rounded-full {{ $markerClasses[$status] }}" aria-hidden="true">
                            @if ($status === 'active')
                                <x-ui.spinner size="sm" />
                            @elseif (isset($markerGlyphs[$status]))
                                <svg class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">{!! $markerGlyphs[$status] !!}</svg>
                            @endif
                        </span>
                        <span class="sr-only">{{ $statusLabels[$status] }}</span>
                        <span data-slot="ai-task-progress-label" class="min-w-0 flex-1 break-words {{ $textClasses[$status] }}">{{ $row['label'] }}</span>
                    </li>
                @endforeach
            @endif
        </ul>
@if ($collapsible)
    </x-ui.collapsible.content>
@endif
</div>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.1
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0