feat(ui): reimagine Spinner as brutalist grid-based loader

- Replace orbital spinner with 3x3 grid of blocks
- Diagonal wave animation matches dithering aesthetic
- Sharp squares, no rounded corners (brutalist)
- Inline horizontal layout with monospace text
- Consistent styling across all loading states

The new loader evokes "tools being constructed" - fitting
for a tool registry. Uses staggered opacity/scale animation
creating a wave pattern across the grid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-12-11 08:19:44 +10:00
parent 1e58537a65
commit 150d48d0ba
11 changed files with 6003 additions and 76 deletions

View file

@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "npx @react-grab/claude-code@latest && next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",

View file

@ -119,9 +119,11 @@ export default function ToolDetailPage({
<div className="min-h-screen bg-background">
<AppHeader />
<Container size="xl" padding="md" className="py-12">
<div className="flex flex-col items-center justify-center py-24 gap-6">
<Spinner size="xl" />
<span className="text-foreground-secondary text-lg">Loading tool...</span>
<div className="flex items-center justify-center py-24 gap-4">
<Spinner size="lg" />
<span className="text-foreground-secondary font-mono text-sm tracking-wide">
Loading tool...
</span>
</div>
</Container>
</div>

View file

@ -88,9 +88,11 @@ export default function BrokenToolsPage(): React.ReactElement {
{/* Loading state */}
{loading && (
<div className="flex flex-col items-center justify-center py-24 gap-6">
<Spinner size="xl" />
<span className="text-foreground-secondary text-lg">Loading broken tools...</span>
<div className="flex items-center justify-center py-24 gap-4">
<Spinner size="lg" />
<span className="text-foreground-secondary font-mono text-sm tracking-wide">
Loading broken tools...
</span>
</div>
)}

View file

@ -184,9 +184,11 @@ export default function ToolSearchPage(): React.ReactElement {
{/* Loading state */}
{loading && (
<div className="flex flex-col items-center justify-center py-24 gap-6">
<Spinner size="xl" />
<span className="text-foreground-secondary text-lg">Loading tools...</span>
<div className="flex items-center justify-center py-24 gap-4">
<Spinner size="lg" />
<span className="text-foreground-secondary font-mono text-sm tracking-wide">
Loading tools...
</span>
</div>
)}

View file

@ -323,9 +323,11 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
})()}
</div>
) : isExecuting ? (
<div className="flex flex-col items-center justify-center py-12 gap-6">
<Spinner size="xl" />
<p className="text-foreground-secondary">Executing...</p>
<div className="flex items-center justify-center py-12 gap-4">
<Spinner size="lg" />
<p className="text-foreground-secondary font-mono text-sm tracking-wide">
Executing...
</p>
</div>
) : (
<div className="text-center py-12">

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,739 @@
# Hierarchical Cascading Tree: Tool & Context Loading
A pattern for any agent chat interface where tool plans are generated speculatively,
skills/context are inferred backwards from those plans, and execution proceeds with
minimal context at each step.
---
## The Core Insight
Traditional agent flow:
```
User Query → Load ALL tools → Model picks tools → Execute
(huge context)
```
Hierarchical cascading flow:
```
User Query → Generate Y Plans → Infer Skills from Plans → Load minimal context per step
(context derived from likely tools)
```
**Key idea**: The tool plans themselves tell you what context/skills the agent needs.
You don't load everything—you load what the plan reveals is relevant.
---
## Real World Example: Website Audit
**User Query**:
> "Perform a complete audit of my website including SEO, performance, accessibility,
> and security, then generate a prioritized action plan"
### Step 1: Generate Y Plans (speculatively, in parallel)
The system generates 3 alternative plans before any execution:
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PLAN GENERATION │
│ │
│ User Query: "audit my website..." │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PLAN A │ │ PLAN B │ │ PLAN C │ │
│ │ (thorough) │ │ (quick) │ │ (balanced) │ │
│ │ 22 steps │ │ 8 steps │ │ 14 steps │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
### Step 2: Analyze Plans → Infer Required Domains
Look at ALL tools across ALL plans to identify skill domains:
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ DOMAIN INFERENCE FROM PLANS │
│ │
│ Plan A Tools: Plan B Tools: Plan C Tools: │
│ ───────────── ───────────── ───────────── │
│ sitemap-discoverer lighthouse-runner sitemap-discoverer │
│ page-fetcher security-scanner lighthouse-runner │
│ meta-tag-analyzer report-generator wcag-checker │
│ heading-structure-analyzer ssl-checker │
│ internal-link-analyzer action-plan-generator │
│ keyword-density-analyzer report-generator │
│ schema-markup-checker │
│ page-speed-analyzer │
│ asset-analyzer │
│ core-web-vitals-checker │
│ wcag-checker │
│ color-contrast-checker │
│ alt-text-checker │
│ keyboard-nav-checker │
│ ssl-checker │
│ header-security-checker │
│ vulnerability-scanner │
│ ... │
│ │
│ ↓ CLUSTER BY DOMAIN ↓ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ SEO │ │ PERFORMANCE │ │ACCESSIBILITY│ │ SECURITY │ │
│ │ domain │ │ domain │ │ domain │ │ domain │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
### Step 3: Load Domain-Specific Context (Skills)
Each domain has associated contextual knowledge:
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SKILL/CONTEXT LOADING │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ SEO SKILL CONTEXT │ │
│ │ ─────────────────── │ │
│ │ • Meta tag best practices (title 50-60 chars, desc 150-160) │ │
│ │ • Heading hierarchy rules (single H1, logical nesting) │ │
│ │ • Internal linking patterns (hub & spoke, silo structure) │ │
│ │ • Schema.org markup types and validation │ │
│ │ • Core Web Vitals thresholds (LCP < 2.5s, FID < 100ms, CLS < 0.1)
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ ACCESSIBILITY SKILL CONTEXT │ │
│ │ ─────────────────────────── │ │
│ │ • WCAG 2.1 AA requirements checklist │ │
│ │ • Color contrast ratios (4.5:1 normal, 3:1 large text) │ │
│ │ • ARIA roles and proper usage patterns │ │
│ │ • Keyboard navigation requirements │ │
│ │ • Screen reader compatibility guidelines │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ SECURITY SKILL CONTEXT │ │
│ │ ────────────────────── │ │
│ │ • Security header requirements (CSP, HSTS, X-Frame-Options) │ │
│ │ • SSL/TLS configuration best practices │ │
│ │ • OWASP Top 10 vulnerability patterns │ │
│ │ • Cookie security attributes (Secure, HttpOnly, SameSite) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
### Step 4: The Cascading Tree During Execution
Now execution proceeds. At each step, only load:
1. The current tool
2. Context relevant to that tool's domain
3. The likely next 1-2 tools
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ HIERARCHICAL CASCADING EXECUTION │
│ │
│ Time ─────────────────────────────────────────────────────────────────────► │
│ │
│ STEP 1: Discovery │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ LOADED IN CONTEXT: │ │
│ │ ┌─────────────────┐ │ │
│ │ │ sitemap- │ ← Current tool │ │
│ │ │ discoverer │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ │ likely next │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ page-fetcher │ ← Preloaded (90% likely) │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ Context: [web-crawling basics, robots.txt rules] │ │
│ │ Tokens: ~800 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ↓ output: sitemap with 47 pages │
│ │
│ STEP 2: Fetch Pages │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ LOADED IN CONTEXT: │ │
│ │ ┌─────────────────┐ │ │
│ │ │ page-fetcher │ ← Current tool │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ┌──────┴──────┐ likely next (fan out to parallel) │ │
│ │ ▼ ▼ │ │
│ │ ┌─────┐ ┌─────────────┐ │ │
│ │ │ SEO │ │ PERFORMANCE │ ← Domain branches preloaded │ │
│ │ │tools│ │ tools │ │ │
│ │ └─────┘ └─────────────┘ │ │
│ │ │ │
│ │ Context: [HTTP fetching, rate limiting, caching headers] │ │
│ │ Tokens: ~600 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ↓ output: 47 page objects │
│ │
│ STEP 3-7: SEO Analysis (parallel group) │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ LOADED IN CONTEXT: │ │
│ │ │ │
│ │ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │ │
│ │ │ meta-tag │ heading │ internal │ schema │ │ │
│ │ │ analyzer │ analyzer │ link │ checker │ │ │
│ │ │ │ │ analyzer │ │ │ │
│ │ └─────────────┴─────────────┴─────────────┴─────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ └─────────────┴──────┬──────┴─────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ seo-score-calc │ ← Convergence point │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ Context: [SEO SKILL - full domain context loaded] │ │
│ │ Tokens: ~2,400 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ... similar parallel groups for PERFORMANCE, ACCESSIBILITY, SECURITY ... │
│ │
│ FINAL STEP: Report Generation │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ LOADED IN CONTEXT: │ │
│ │ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ action-plan-generator │ ← Current tool │ │
│ │ └───────────┬───────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ audit-report-generator│ ← Final tool │ │
│ │ └───────────────────────┘ │ │
│ │ │ │
│ │ Context: [report writing, prioritization frameworks, all scores] │ │
│ │ Tokens: ~1,800 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
---
## The Full Tree Visualization
Here's the complete hierarchical tree for the website audit:
```
USER QUERY
│ "audit my website..."
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ PLAN A │ │ PLAN B │ │ PLAN C │
│thorough │ │ quick │ │balanced │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
│ │ │
┌─────────┴─────────┐ │ ┌────────┴────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ (...) ┌─────────┐ ┌─────────┐
│ SKILL │ │ SKILL │ │ SKILL │ │ SKILL │
│ SEO │ │SECURITY │ │ PERF │ │ A11Y │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
│ context │ context │ context │ context
│ docs │ docs │ docs │ docs
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│meta-tag │ │ssl-check│ │lighthouse│ │wcag- │
│heading │ │headers │ │webvitals│ │contrast │
│links │ │vulns │ │assets │ │alt-text │
│schema │ │... │ │... │ │keyboard │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
DETAIL: Cascading through SEO branch
═══════════════════════════════════════════════════════════════════════════════
Step 1 Step 2 Step 3 (parallel)
─────────────────────────────────────────────────────────────────────────────
┌──────────────┐
│ sitemap- │
│ discoverer │◄─── ACTIVE
└──────┬───────┘
│ preload likely next
┌──────────────┐ ┌──────────────┐
│ page- │ │ page- │
│ fetcher │───────►│ fetcher │◄─── ACTIVE
└──────────────┘ └──────┬───────┘
(shadowed) │
│ preload domain tools
┌──────────────┐
│ SEO tools │
│ (grouped) │───────────────┐
└──────────────┘ │
(shadowed) │
┌─────────────────────────┐
│ │
│ ┌──────────────┐ │
│ │ meta-tag │◄── │
│ │ analyzer │ │ │
│ └──────────────┘ │ │
│ │ │
│ ┌──────────────┐ │ │
│ │ heading │◄─┤ │
│ │ analyzer │ │ │
│ └──────────────┘ ├───┼── ALL ACTIVE
│ │ │ (parallel)
│ ┌──────────────┐ │ │
│ │ link │◄─┤ │
│ │ analyzer │ │ │
│ └──────────────┘ │ │
│ │ │
│ ┌──────────────┐ │ │
│ │ schema │◄── │
│ │ checker │ │
│ └──────────────┘ │
│ │
│ + SEO SKILL CONTEXT │
│ loaded for all │
│ │
└─────────────────────────┘
CONTEXT SIZE AT EACH STEP
═══════════════════════════════════════════════════════════════════════════════
Traditional approach (load everything):
┌─────────────────────────────────────────────────────────────────────────────┐
│ █████████████████████████████████████████████████████████████████████████ │
│ ALL 22 tools + ALL skill docs = ~45,000 tokens │
└─────────────────────────────────────────────────────────────────────────────┘
Hierarchical cascading approach:
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Step 1: ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ~800 tokens │
│ Step 2: ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ~600 tokens │
│ Step 3: █████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ~2,400 tokens │
│ Step 4: ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ~2,100 tokens │
│ Step 5: ███████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ~1,900 tokens │
│ Step 6: ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ~1,600 tokens │
│ Final: ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ~1,800 tokens │
│ │
│ Average per step: ~1,600 tokens (96% reduction from loading everything) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## The Algorithm
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ 1. PLAN GENERATION PHASE │
│ ───────────────────── │
│ │
│ Input: User query │
│ Output: Y candidate plans │
│ │
│ ┌─────────────┐ │
│ │ Query │ │
│ │ Analyzer │──────► Extract intent, entities, constraints │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Tool │ │
│ │ Searcher │──────► Find candidate tools (semantic + keyword) │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Plan │ │
│ │ Generator │──────► Generate Y distinct plans │
│ └─────────────┘ │
│ │
│ │
│ 2. SKILL INFERENCE PHASE │
│ ────────────────────── │
│ │
│ Input: Y plans │
│ Output: Skill domains + contextual docs │
│ │
│ ┌─────────────┐ │
│ │ Tool │ │
│ │ Clusterer │──────► Group tools by domain/category │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Skill │ │
│ │ Mapper │──────► Map domains → skill documents │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Context │ │
│ │ Loader │──────► Fetch relevant docs, examples, constraints │
│ └─────────────┘ │
│ │
│ │
│ 3. EXECUTION PHASE (cascading) │
│ ─────────────────────────── │
│ │
│ For each step: │
│ │
│ ┌─────────────┐ │
│ │ Probability │ │
│ │ Calculator │──────► Calculate P(next_tool) for all candidates │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Context │ │
│ │ Assembler │──────► current_tool + top_k likely + domain_context │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Executor │──────► Run tool, capture output │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ State │ │
│ │ Updater │──────► Update context, prune unlikely branches │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Probability-Based Preloading
At each step, calculate which tools are most likely needed next:
```
CURRENT STATE
│ completed: [step_1, step_2]
│ current: step_3
┌────────────────────────────────────┐
│ NEXT TOOL PROBABILITIES │
│ │
│ Based on: │
│ • Plan structure (what's next) │
│ • Current output type │
│ • Historical patterns │
│ • User's stated goals │
│ │
│ ┌──────────────────────────────┐ │
│ │ seo-score-calc P=0.92 │──┼──► PRELOAD
│ │ accessibility-check P=0.85 │──┼──► PRELOAD
│ │ perf-score-calc P=0.78 │──┼──► PRELOAD
│ │ security-scanner P=0.45 │ │
│ │ other-tool P=0.12 │ │
│ │ ... │ │
│ └──────────────────────────────┘ │
│ │
│ Threshold: P > 0.70 → preload │
│ │
└────────────────────────────────────┘
```
---
## The User Choice Interface
When Y plans are generated, present them to the user:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Your request: "Audit my website for SEO, performance, and security" │
│ │
│ I've generated 3 approaches: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ◉ PLAN A: Comprehensive Audit [SELECT] │ │
│ │ │ │
│ │ 22 steps • ~15 min • $2.00 estimated │ │
│ │ │ │
│ │ Covers: SEO (7), Performance (3), Accessibility (4), Security (3)│ │
│ │ Plus: Scoring, prioritization, PDF report │ │
│ │ │ │
│ │ Skills loaded: SEO best practices, WCAG 2.1, Security headers, │ │
│ │ Core Web Vitals, Schema.org markup │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ○ PLAN B: Quick Scan [SELECT] │ │
│ │ │ │
│ │ 8 steps • ~3 min • $0.40 estimated │ │
│ │ │ │
│ │ Covers: Lighthouse audit, basic security scan │ │
│ │ Output: Summary scores only │ │
│ │ │ │
│ │ Skills loaded: Lighthouse interpretation, SSL basics │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ○ PLAN C: Balanced Review [SELECT] │ │
│ │ │ │
│ │ 14 steps • ~8 min • $1.20 estimated │ │
│ │ │ │
│ │ Covers: Key checks from each domain │ │
│ │ Output: Prioritized action items │ │
│ │ │ │
│ │ Skills loaded: SEO essentials, A11y critical, Security must-haves│ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Or: [Let AI decide based on your site] [Customize plan] [Cancel] │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Adaptive Execution: Plan as Guide, Not Law
Once skills are loaded from the plan, execution can be:
### Mode 1: Strict Plan Following
```
Plan Step 1 → Execute Tool A → Plan Step 2 → Execute Tool B → ...
The plan is the law. Execute exactly as specified.
```
### Mode 2: Plan-Guided Free Execution
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Plan provides: │
│ • Which tools are available (pre-approved set) │
│ • What context/skills are loaded │
│ • Rough ordering guidance │
│ │
│ Model decides: │
│ • Exact tool call order │
│ • Whether to skip steps │
│ • Whether to repeat steps │
│ • Parameter values │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Agent has access to: │ │
│ │ │ │
│ │ TOOLS (from plan): SKILLS (from plan): │ │
│ │ ├── sitemap-discoverer ├── SEO best practices │ │
│ │ ├── page-fetcher ├── WCAG 2.1 guidelines │ │
│ │ ├── meta-analyzer ├── Security header docs │ │
│ │ ├── wcag-checker └── Core Web Vitals guide │ │
│ │ ├── ssl-checker │ │
│ │ └── report-generator │ │
│ │ │ │
│ │ Agent can call these tools in any order, with the skills │ │
│ │ providing domain expertise for intelligent decisions. │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Mode 3: Hybrid (Checkpoints)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Phase 1: Discovery Phase 2: Analysis Phase 3: Report │
│ ───────────────── ────────────────── ──────────────── │
│ │
│ [sitemap-discoverer] [CHECKPOINT: got pages] [CHECKPOINT: got │
│ │ │ all scores] │
│ ▼ ▼ │ │
│ [page-fetcher] Agent freely uses: ▼ │
│ │ • seo tools [action-plan-gen] │
│ ▼ • perf tools │ │
│ [CHECKPOINT] • a11y tools ▼ │
│ • security tools [report-generator] │
│ in whatever order │
│ makes sense │
│ │
│ Strict Flexible Strict │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Dynamic Tool Installation (Registry Integration)
In a registry-backed system (like TPMJS), if the agent needs a tool that isn't loaded:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ During execution, agent realizes it needs a tool not in the plan: │
│ │
│ Agent: "The SSL certificate uses ECDSA which my ssl-checker doesn't │
│ fully support. I need a more specialized tool." │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🔍 Searching registry for: "ECDSA certificate" │ │
│ │ │ │
│ │ Found: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ ecdsa-cert-analyzer │ │ │
│ │ │ "Analyzes ECDSA and EdDSA certificates" │ │ │
│ │ │ Health: ✓ Healthy │ │ │
│ │ │ Quality: 0.87 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ [Install and use] [Skip this check] [Ask user] │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ On install: │
│ 1. Fetch tool metadata from registry │
│ 2. Load tool's associated skill/context docs │
│ 3. Add to current execution context │
│ 4. Continue execution │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Summary: The Complete Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ USER QUERY ARRIVES │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ 1. GENERATE Y PLANS │ │
│ │ (speculative) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Plan A │ │ Plan B │ │ Plan C │ │
│ └────┬───┘ └────┬───┘ └────┬───┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ 2. CLUSTER TOOLS BY │ │
│ │ DOMAIN/SKILL │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌─────────┬───────┼───────┬─────────┐ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌────────┐ ┌──────┐ ┌─────┐ ┌──────┐ ┌───────┐ │
│ │SEO │ │Perf │ │A11y │ │Sec │ │Report │ │
│ │domain │ │domain│ │domain│ │domain│ │domain │ │
│ └────┬───┘ └──┬───┘ └──┬──┘ └──┬───┘ └───┬───┘ │
│ │ │ │ │ │ │
│ └────────┴────────┼───────┴─────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ 3. LOAD SKILL DOCS │ │
│ │ FOR EACH DOMAIN │ │
│ └───────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 4. PRESENT PLANS TO USER (optional) │ │
│ │ or AUTO-SELECT │ │
│ └───────────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ 5. EXECUTE WITH │ │
│ │ CASCADING CONTEXT │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌─────────────────┴─────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Strict: Follow plan │ │ Flexible: Use plan │ │
│ │ exactly │ │ as context guide │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ At each step: │
│ • Load only current tool + likely next tools │
│ • Load only relevant domain context │
│ • Keep context window small (~1-3k tokens) │
│ • Prune unlikely branches as execution proceeds │
│ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ FINAL OUTPUT │ │
│ └───────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Key Takeaways
1. **Plans reveal skills**: Instead of loading all context upfront, generate plans first
and let them tell you what context is actually needed.
2. **Small context, right context**: At each step, load only what's needed for that step
plus likely next steps. 96% reduction in context size.
3. **User choice as a feature**: Multiple plans aren't a bug—they're a feature. Users
can pick their preferred approach.
4. **Adaptive execution**: Plans can be strict guides or loose frameworks. The skill
context enables intelligent tool use either way.
5. **Registry as escape hatch**: If the agent needs something not in the plan, it can
query the registry, install on-demand, and continue.
The hierarchical cascading tree pattern makes agents both more capable (access to any
tool in the registry) and more efficient (minimal context at each step).

View file

@ -0,0 +1,980 @@
/**
* ============================================================================
* PLANNER TYPE DEFINITIONS
* ============================================================================
*
* Complete TypeScript types for the hierarchical cascading tool planner system.
* Based on research into:
* - Dynamic tool orchestration
* - Y×X plan generation (Y alternatives, X steps each)
* - Skill/context inference from tool plans
* - Pathway learning algorithms
* - Fragility management
*
* ============================================================================
*/
// ============================================================================
// CORE PRIMITIVES
// ============================================================================
/**
* A tool from the registry
*
*
* TOOL
*
* packageName: "tpmjs-web-scraper"
* exportName: "scrapeUrl"
* description: "Fetches webpage..."
* parameters: [...]
* returns: { type: "string" }
* category: "web-scraping"
* qualityScore: 0.87
* healthStatus: "HEALTHY"
*
*/
export interface Tool {
id: string;
packageName: string;
exportName: string;
description: string;
parameters: ToolParameter[];
returns: ToolReturn;
category: ToolCategory;
qualityScore: number;
healthStatus: ToolHealthStatus;
aiAgent?: ToolAIAgent;
tier: 'minimal' | 'rich';
}
export interface ToolParameter {
name: string;
type: ParameterType;
required: boolean;
description: string;
default?: unknown;
enum?: string[];
}
export type ParameterType =
| 'string'
| 'number'
| 'boolean'
| 'object'
| 'array'
| 'string[]'
| 'number[]';
export interface ToolReturn {
type: string;
description: string;
schema?: JSONSchema;
}
export interface ToolAIAgent {
useCase: string;
limitations?: string;
examples?: string[];
}
export type ToolCategory =
| 'web-scraping'
| 'data-analysis'
| 'file-generation'
| 'image-processing'
| 'text-processing'
| 'communication'
| 'ai-ml'
| 'database'
| 'api-integration'
| 'general';
export type ToolHealthStatus = 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
export interface JSONSchema {
type: string;
properties?: Record<string, JSONSchema>;
items?: JSONSchema;
required?: string[];
[key: string]: unknown;
}
// ============================================================================
// QUERY ANALYSIS
// ============================================================================
/**
* Result of analyzing a user query
*
* Query: "Scrape competitor prices from example.com and make a report"
*
*
* QUERY ANALYSIS
*
* intent: "price-comparison-report"
* entities: [{ type: "url", value: "example.com" }]
* capabilities: ["fetch-webpage", "extract-prices",
* "analyze-data", "generate-report"]
* outputFormat: "pdf"
* complexity: 7
*
*/
export interface QueryAnalysis {
intent: string;
entities: QueryEntity[];
requiredCapabilities: string[];
outputFormat?: OutputFormat;
constraints: QueryConstraints;
ambiguities: string[];
complexity: number; // 1-10
}
export interface QueryEntity {
type: EntityType;
value: string;
confidence: number;
}
export type EntityType = 'url' | 'file_path' | 'email' | 'date' | 'number' | 'name' | 'keyword';
export type OutputFormat = 'json' | 'csv' | 'pdf' | 'xlsx' | 'markdown' | 'html' | 'image' | 'text';
export interface QueryConstraints {
format?: string;
style?: string;
maxTime?: number;
maxCost?: number;
quality?: 'draft' | 'standard' | 'high';
}
// ============================================================================
// EXECUTION PLANS
// ============================================================================
/**
* A complete execution plan (one of Y alternatives)
*
*
* EXECUTION PLAN
*
*
* metadata:
* query: "Scrape competitor prices..."
* complexity: 7
* estimatedDuration: 45000ms
* estimatedCost: $0.45
* confidence: 0.87
*
* classification:
* approach: "thorough"
* riskLevel: "low"
*
* steps: [step1, step2, step3, ...]
* skills: [seo, scraping, analysis]
*
*
*/
export interface ExecutionPlan {
id: string;
version: '1.0';
metadata: PlanMetadata;
classification: PlanClassification;
steps: PlanStep[];
skills: Skill[];
expectedOutput: ExpectedOutput;
}
export interface PlanMetadata {
generatedAt: string;
query: string;
complexity: number;
estimatedDuration: number; // milliseconds
estimatedCost: number; // USD
confidence: number; // 0-1
}
export interface PlanClassification {
approach: 'thorough' | 'quick' | 'balanced';
riskLevel: 'low' | 'medium' | 'high';
parallelizable: boolean;
}
export interface ExpectedOutput {
type: string;
schema?: JSONSchema;
description: string;
}
// ============================================================================
// PLAN STEPS
// ============================================================================
/**
* A single step in an execution plan
*
*
* PLAN STEP
*
*
* id: "step_3"
* order: 3
* tool: { packageName: "tpmjs-price-extractor", ... }
* purpose: "Extract and normalize price values"
*
* input:
* fromStep: { stepId: "step_2", path: "$.elements" }
* static: { currency: "USD" }
*
* output:
* type: "array"
* storeAs: "prices"
*
* execution:
* timeout: 5000
* retries: 2
* fallbackTools: ["alt-price-extractor"]
*
* dependsOn: ["step_2"]
* parallelGroup: "extraction"
*
*
*/
export interface PlanStep {
id: string;
order: number;
tool: ToolReference;
purpose: string;
input: StepInput;
output: StepOutput;
execution: StepExecution;
dependsOn: string[];
parallelGroup?: string;
}
export interface ToolReference {
packageName: string;
exportName: string;
version?: string;
}
export interface StepInput {
static?: Record<string, unknown>;
fromStep?: StepReference | StepReference[];
fromQuery?: QueryReference;
computed?: ComputedInput;
}
export interface StepReference {
stepId: string;
path: string; // JSONPath
as?: string; // alias
}
export interface QueryReference {
entityType: EntityType;
index?: number;
}
export interface ComputedInput {
expression: string;
dependencies: string[];
}
export interface StepOutput {
type: string;
storeAs: string;
validate?: ValidationRule[];
}
export interface ValidationRule {
rule: 'minLength' | 'maxLength' | 'regex' | 'type' | 'required';
value: unknown;
message?: string;
}
export interface StepExecution {
timeout: number;
retries: number;
canSkipOnError: boolean;
fallbackTools?: string[];
}
// ============================================================================
// SKILLS (CONTEXTUAL KNOWLEDGE)
// ============================================================================
/**
* A skill is domain knowledge inferred from tools in a plan
*
* Tools in Plan Inferred Skill
*
*
* web-scraper SKILL: web-scraping
* html-parser
* url-validator context:
* Rate limiting rules
* Robots.txt guidelines
* CSS selector best practices
*
*/
export interface Skill {
domain: ToolCategory;
context: string;
tools: Tool[];
priority: number; // For ordering in context
}
export interface SkillContext {
domain: ToolCategory;
content: string;
tokenEstimate: number;
}
// ============================================================================
// EXECUTION CONTEXT
// ============================================================================
/**
* Runtime state during plan execution
*
*
* EXECUTION CONTEXT
*
*
* query: "Scrape competitor prices..."
*
* variables:
* step_1 { html: "<html>..." }
* step_2 { elements: [...] }
* step_3 { prices: [19.99, 24.99] }
*
* completed: { step_1, step_2, step_3 }
* currentStep: "step_4"
*
* errors: []
* retryCount: { step_2: 1 }
*
* timing:
* startTime: 1702234567890
* stepTimings: { step_1: 823, step_2: 234, step_3: 567 }
*
* tokens:
* input: 4521
* output: 1234
*
*
*/
export interface ExecutionContext {
query: string;
entities: QueryEntity[];
variables: Map<string, unknown>;
completed: Set<string>;
currentStep: string | null;
errors: ExecutionError[];
retryCount: Map<string, number>;
timing: ExecutionTiming;
tokens: TokenUsage;
}
export interface ExecutionError {
stepId: string;
tool: string;
error: string;
timestamp: number;
recoverable: boolean;
}
export interface ExecutionTiming {
startTime: number;
stepTimings: Map<string, number>;
}
export interface TokenUsage {
input: number;
output: number;
perStep: Map<string, { input: number; output: number }>;
}
// ============================================================================
// EXECUTION EVENTS (FOR STREAMING)
// ============================================================================
/**
* Events emitted during plan execution
*
* Time
*
*
* plan_startstep_startstep_done step_startplan_end
*
*/
export type ExecutionEvent =
| PlanStartEvent
| StepStartEvent
| ContextLoadedEvent
| StepProgressEvent
| StepCompleteEvent
| StepErrorEvent
| StepSkippedEvent
| PlanCompleteEvent
| PlanErrorEvent;
export interface PlanStartEvent {
type: 'plan_start';
planId: string;
totalSteps: number;
skills: string[];
}
export interface StepStartEvent {
type: 'step_start';
stepId: string;
tool: string;
purpose: string;
}
export interface ContextLoadedEvent {
type: 'context_loaded';
stepId: string;
tokens: number;
skills: string[];
}
export interface StepProgressEvent {
type: 'step_progress';
stepId: string;
progress: number; // 0-100
message?: string;
}
export interface StepCompleteEvent {
type: 'step_complete';
stepId: string;
duration: number;
resultPreview: string;
}
export interface StepErrorEvent {
type: 'step_error';
stepId: string;
error: string;
willRetry: boolean;
fallbackTool?: string;
}
export interface StepSkippedEvent {
type: 'step_skipped';
stepId: string;
reason: string;
}
export interface PlanCompleteEvent {
type: 'plan_complete';
success: boolean;
totalDuration: number;
totalTokens: number;
output: unknown;
}
export interface PlanErrorEvent {
type: 'plan_error';
error: string;
failedStep: string;
completedSteps: string[];
}
// ============================================================================
// PATHWAY LEARNING
// ============================================================================
/**
* Record of a pathway execution for learning
*
*
* PATHWAY RECORD
*
*
* queryPattern: "scrape.*price.*report"
* steps: ["scraper", "parser", "analyzer", "reporter"]
*
* stats:
* successes: 847
* failures: 45
* avgDuration: 34500ms
*
* lastUsed: 2024-01-15T10:30:00Z
* firstUsed: 2024-01-01T08:00:00Z
*
*
*/
export interface PathwayRecord {
id: string;
queryPattern: string;
steps: string[]; // Tool IDs in order
stats: PathwayStats;
lastUsed: Date;
firstUsed: Date;
metadata: PathwayMetadata;
}
export interface PathwayStats {
successes: number;
failures: number;
totalRuns: number;
avgDuration: number;
avgTokens: number;
avgCost: number;
}
export interface PathwayMetadata {
createdBy: 'user' | 'auto';
tags: string[];
notes?: string;
}
/**
* K-factor represents convergence toward determinism
*
* K = 0.00 No pattern, explore freely
* K = 0.50 Weak pattern, prefer but explore
* K = 0.85 Strong pattern, mostly deterministic
* K = 0.99 Near-locked, rare deviation
*/
export interface KFactorResult {
value: number; // 0-1
confidence: number;
dominantPath: string[] | null;
totalObservations: number;
}
// ============================================================================
// LEARNING ALGORITHMS
// ============================================================================
/**
* Configuration for pathway selection algorithms
*/
export interface LearningConfig {
algorithm: LearningAlgorithm;
params: AlgorithmParams;
}
export type LearningAlgorithm =
| 'epsilon_greedy'
| 'ucb'
| 'thompson_sampling'
| 'contextual_bandit'
| 'q_learning'
| 'mcts';
export type AlgorithmParams =
| EpsilonGreedyParams
| UCBParams
| ThompsonParams
| ContextualBanditParams
| QLearningParams
| MCTSParams;
export interface EpsilonGreedyParams {
type: 'epsilon_greedy';
epsilon: number; // Exploration rate (0-1)
decayRate: number; // How fast epsilon decays
minEpsilon: number; // Floor for epsilon
}
export interface UCBParams {
type: 'ucb';
explorationConstant: number; // C in UCB formula
}
export interface ThompsonParams {
type: 'thompson_sampling';
priorAlpha: number; // Beta distribution prior
priorBeta: number;
}
export interface ContextualBanditParams {
type: 'contextual_bandit';
features: ContextFeature[];
learningRate: number;
}
export type ContextFeature =
| 'user_type'
| 'query_complexity'
| 'time_of_day'
| 'previous_success_rate';
export interface QLearningParams {
type: 'q_learning';
learningRate: number; // Alpha
discountFactor: number; // Gamma
explorationRate: number; // Epsilon
}
export interface MCTSParams {
type: 'mcts';
simulations: number;
explorationConstant: number;
maxDepth: number;
}
// ============================================================================
// FRAGILITY MANAGEMENT
// ============================================================================
/**
* Fragility budget for a plan
*
*
* FRAGILITY BUDGET
*
*
* Target success rate: 90%
*
* If each step has P(success) = 0.98:
* Max steps = floor(log(0.90) / log(0.98)) = 5
*
* If each step has P(success) = 0.95:
* Max steps = floor(log(0.90) / log(0.95)) = 2
*
*
*/
export interface FragilityBudget {
targetSuccessRate: number; // e.g., 0.90 for 90%
avgStepSuccessRate: number; // e.g., 0.98
maxSteps: number; // Calculated limit
currentSteps: number;
withinBudget: boolean;
}
export interface FragilityAnalysis {
plan: ExecutionPlan;
budget: FragilityBudget;
sequentialFragility: number; // P(all sequential steps succeed)
parallelBenefit: number; // Improvement from parallel execution
fallbackBenefit: number; // Improvement from fallbacks
effectiveSuccessRate: number; // Final calculated rate
recommendations: FragilityRecommendation[];
}
export interface FragilityRecommendation {
type: 'reduce_steps' | 'add_fallbacks' | 'parallelize' | 'add_checkpoints';
description: string;
impact: number; // Estimated improvement
}
// ============================================================================
// PLAN GENERATION OPTIONS
// ============================================================================
/**
* Options for generating plans
*/
export interface PlanGenerationOptions {
numPlans: number; // Y in Y×X
maxStepsPerPlan: number; // Max X
strategies: PlanStrategy[];
fragilityBudget?: FragilityBudget;
preferredCategories?: ToolCategory[];
excludeTools?: string[];
}
export interface PlanStrategy {
name: string;
maxSteps: number;
preferQuality: boolean;
preferSpeed: boolean;
allowParallel: boolean;
requireFallbacks: boolean;
}
// ============================================================================
// CASCADING CONTEXT
// ============================================================================
/**
* Context assembled for a single step (minimal footprint)
*
* Traditional: Load ALL tools + ALL docs = ~45,000 tokens
*
* Cascading:
*
* STEP CONTEXT (~1,500 tokens)
*
*
*
* CURRENT TOOL (~400 tokens)
* web-scraper: description, parameters, examples
*
*
*
* DOMAIN CONTEXT (~600 tokens)
* Web scraping best practices, rate limits, selectors
*
*
*
* UPCOMING TOOLS (~200 tokens)
* Next: html-parser, price-extractor
*
*
*
* PRIOR RESULTS (~300 tokens)
* Summarized output from dependencies
*
*
*
*/
export interface CascadingContext {
currentTool: ToolContext;
domainContext: SkillContext | null;
upcomingTools: ToolPreview[];
priorResults: PriorResult[];
totalTokens: number;
}
export interface ToolContext {
tool: Tool;
formattedDescription: string;
formattedParameters: string;
examples: string[];
tokenEstimate: number;
}
export interface ToolPreview {
exportName: string;
purpose: string;
tokenEstimate: number;
}
export interface PriorResult {
stepId: string;
summary: string;
tokenEstimate: number;
}
// ============================================================================
// PRELOADING / PROBABILITY
// ============================================================================
/**
* Probability calculation for preloading next tools
*
* Current: step_2 (html-parser)
*
*
* NEXT TOOL PROBABILITIES
*
* price-extractor P = 0.92 LOAD
* text-cleaner P = 0.78 LOAD
* image-extractor P = 0.34
* link-extractor P = 0.21
*
* Threshold: 0.70
*
*/
export interface ToolProbability {
tool: Tool;
probability: number;
reason: ProbabilityReason;
}
export type ProbabilityReason =
| 'plan_next' // Next in plan
| 'plan_soon' // Coming up in plan
| 'learned_pattern' // Historical data
| 'output_type_match' // Output type matches input
| 'same_domain'; // Same category
export interface PreloadDecision {
toLoad: Tool[];
threshold: number;
reasoning: Map<string, ProbabilityReason>;
}
// ============================================================================
// USER INTERFACE TYPES
// ============================================================================
/**
* Plan summary for UI display
*/
export interface PlanSummary {
id: string;
name: string;
approach: 'thorough' | 'quick' | 'balanced';
stepCount: number;
estimatedDuration: string; // "~2 min"
estimatedCost: string; // "$0.45"
confidence: number;
skills: string[];
riskLevel: 'low' | 'medium' | 'high';
}
/**
* Execution progress for UI display
*/
export interface ExecutionProgress {
status: 'idle' | 'planning' | 'running' | 'complete' | 'error';
currentStep: number;
totalSteps: number;
completedSteps: StepProgress[];
currentStepProgress?: {
stepId: string;
tool: string;
progress: number;
message?: string;
};
contextTokens: number;
elapsedTime: number;
}
export interface StepProgress {
stepId: string;
tool: string;
status: 'complete' | 'skipped' | 'error';
duration: number;
resultPreview?: string;
error?: string;
}
// ============================================================================
// API TYPES
// ============================================================================
/**
* Request/Response types for API routes
*/
export interface GeneratePlansRequest {
query: string;
numPlans?: number;
options?: Partial<PlanGenerationOptions>;
}
export interface GeneratePlansResponse {
success: boolean;
data: {
plans: PlanSummary[];
kFactor: KFactorResult;
learnedPathAvailable: boolean;
};
error?: string;
}
export interface ExecutePlanRequest {
planId: string;
query: string;
options?: {
useFallbacks?: boolean;
maxRetries?: number;
};
}
// Response is SSE stream of ExecutionEvent
export interface GetToolsRequest {
capability: string;
category?: ToolCategory;
limit?: number;
healthyOnly?: boolean;
}
export interface GetToolsResponse {
success: boolean;
data: {
tools: Tool[];
total: number;
};
error?: string;
}
// ============================================================================
// DATABASE MODELS (for Prisma schema reference)
// ============================================================================
/**
* Database models for persistence
*
* Add to packages/db/prisma/schema.prisma:
*
* model PathwayRecord {
* id String @id @default(cuid())
* queryPattern String
* steps String[] // Tool IDs
* successes Int @default(0)
* failures Int @default(0)
* avgDuration Float @default(0)
* avgTokens Float @default(0)
* avgCost Float @default(0)
* lastUsed DateTime @default(now())
* firstUsed DateTime @default(now())
* createdBy String @default("auto")
* tags String[]
* notes String?
*
* @@index([queryPattern])
* @@index([lastUsed])
* }
*
* model PlanExecution {
* id String @id @default(cuid())
* planId String
* query String
* steps Json // PlanStep[]
* status String // 'running' | 'complete' | 'error'
* output Json?
* totalTokens Int @default(0)
* totalCost Float @default(0)
* duration Int @default(0)
* createdAt DateTime @default(now())
* completedAt DateTime?
*
* @@index([planId])
* @@index([createdAt])
* }
*/
export interface PathwayRecordDB {
id: string;
queryPattern: string;
steps: string[];
successes: number;
failures: number;
avgDuration: number;
avgTokens: number;
avgCost: number;
lastUsed: Date;
firstUsed: Date;
createdBy: string;
tags: string[];
notes: string | null;
}
export interface PlanExecutionDB {
id: string;
planId: string;
query: string;
steps: PlanStep[];
status: 'running' | 'complete' | 'error';
output: unknown | null;
totalTokens: number;
totalCost: number;
duration: number;
createdAt: Date;
completedAt: Date | null;
}
// ============================================================================
// UTILITY TYPES
// ============================================================================
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
export type AsyncGenerator<T> = {
next(): Promise<{ value: T; done: boolean }>;
[Symbol.asyncIterator](): AsyncGenerator<T>;
};
export type Result<T, E = Error> = { success: true; data: T } | { success: false; error: E };

File diff suppressed because it is too large Load diff

View file

@ -2,25 +2,17 @@
import { cn } from '@tpmjs/utils/cn';
const sizeClasses = {
xs: 'w-5 h-5',
sm: 'w-8 h-8',
md: 'w-12 h-12',
lg: 'w-16 h-16',
xl: 'w-24 h-24',
} as const;
const dotSizeClasses = {
xs: 'w-1 h-1',
sm: 'w-1.5 h-1.5',
md: 'w-2 h-2',
lg: 'w-3 h-3',
xl: 'w-4 h-4',
const sizeConfig = {
xs: { container: 'w-4 h-4', block: 3, gap: 1 },
sm: { container: 'w-6 h-6', block: 4, gap: 2 },
md: { container: 'w-8 h-8', block: 6, gap: 2 },
lg: { container: 'w-12 h-12', block: 8, gap: 3 },
xl: { container: 'w-16 h-16', block: 12, gap: 4 },
} as const;
export interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
/** Size variant */
size?: keyof typeof sizeClasses;
size?: keyof typeof sizeConfig;
/** Optional label for accessibility */
label?: string;
}
@ -28,8 +20,9 @@ export interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Spinner component
*
* An elegant orbital loading spinner with three dots rotating
* in a synchronized dance pattern.
* A brutalist grid-based loader that evokes the feeling of
* tools being constructed, block by block. Matches the TPMJS
* dithering aesthetic with sharp squares and wave animations.
*/
export function Spinner({
className,
@ -37,68 +30,73 @@ export function Spinner({
label = 'Loading...',
...props
}: SpinnerProps): React.ReactElement {
const sizeClass = sizeClasses[size];
const dotSize = dotSizeClasses[size];
const config = sizeConfig[size];
const blockSize = config.block;
const gap = config.gap;
// 3x3 grid positions with staggered delays (diagonal wave)
const blocks = [
{ id: 'b00', row: 0, col: 0, delay: 0 },
{ id: 'b01', row: 0, col: 1, delay: 0.1 },
{ id: 'b02', row: 0, col: 2, delay: 0.2 },
{ id: 'b10', row: 1, col: 0, delay: 0.1 },
{ id: 'b11', row: 1, col: 1, delay: 0.2 },
{ id: 'b12', row: 1, col: 2, delay: 0.3 },
{ id: 'b20', row: 2, col: 0, delay: 0.2 },
{ id: 'b21', row: 2, col: 1, delay: 0.3 },
{ id: 'b22', row: 2, col: 2, delay: 0.4 },
];
return (
// biome-ignore lint/a11y/useSemanticElements: Spinner requires role="status" for screen reader announcements, <output> is not semantically appropriate
// biome-ignore lint/a11y/useSemanticElements: Spinner requires role="status" for screen reader announcements
<div
role="status"
aria-label={label}
className={cn('relative', sizeClass, className)}
className={cn(
'relative inline-flex items-center justify-center',
config.container,
className
)}
{...props}
>
<style>
{`
@keyframes spinnerOrbit {
0% {
transform: rotate(0deg) translateX(140%) rotate(0deg);
@keyframes blockPulse {
0%, 100% {
opacity: 0.15;
transform: scale(0.85);
}
100% {
transform: rotate(360deg) translateX(140%) rotate(-360deg);
50% {
opacity: 1;
transform: scale(1);
}
}
`}
</style>
{/* Orbital ring hint */}
<div className="absolute inset-[15%] rounded-full border border-foreground/10" />
<div
className="relative"
style={{
width: blockSize * 3 + gap * 2,
height: blockSize * 3 + gap * 2,
}}
>
{blocks.map((block) => (
<div
key={block.id}
className="absolute bg-foreground"
style={{
width: blockSize,
height: blockSize,
left: block.col * (blockSize + gap),
top: block.row * (blockSize + gap),
animation: 'blockPulse 1.2s ease-in-out infinite',
animationDelay: `${block.delay}s`,
}}
/>
))}
</div>
{/* Three orbiting dots with staggered animations */}
<div
className={cn('absolute rounded-full bg-foreground', dotSize)}
style={{
top: '50%',
left: '50%',
marginTop: '-0.25rem',
marginLeft: '-0.25rem',
animation: 'spinnerOrbit 1.4s cubic-bezier(0.5, 0, 0.5, 1) infinite',
}}
/>
<div
className={cn('absolute rounded-full bg-foreground/50', dotSize)}
style={{
top: '50%',
left: '50%',
marginTop: '-0.25rem',
marginLeft: '-0.25rem',
animation: 'spinnerOrbit 1.4s cubic-bezier(0.5, 0, 0.5, 1) infinite',
animationDelay: '-0.45s',
}}
/>
<div
className={cn('absolute rounded-full bg-foreground/25', dotSize)}
style={{
top: '50%',
left: '50%',
marginTop: '-0.25rem',
marginLeft: '-0.25rem',
animation: 'spinnerOrbit 1.4s cubic-bezier(0.5, 0, 0.5, 1) infinite',
animationDelay: '-0.9s',
}}
/>
{/* Screen reader text */}
<span className="sr-only">{label}</span>
</div>
);