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.
Preview
- 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
<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
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:
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.
-
resources/views/components/ui/ai/task-progress.blade.php
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.
# 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
{{-- 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
Props
| 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.
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
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
- Theming hooks
Accessibility
- 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-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, 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 underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<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.
{{--
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