Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generate Ink (React for CLI) components for terminal UIs with hooks, state management, and layout components.
.claude/skills/a5c-ai-ink-component-generator/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 42% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 14% | 0% |
| case-12 | ✗→✓ | ▲ Improved | -16% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 55% | 0% |
Generate Ink (React) components for terminal UIs.
Invoke this skill when you need to:
| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | projectName | string | Yes | Project name | | components | array | Yes | Component definitions | | includeHooks | boolean | No | Generate custom hooks |
json{ "components": [ { "name": "SelectList", "type": "interactive", "props": ["items", "onSelect"], "state": ["selectedIndex"] } ] }
tsximport React, { useState, useCallback } from 'react'; import { Box, Text, useInput, useApp } from 'ink'; interface SelectListProps { items: string[]; onSelect: (item: string, index: number) => void; } export const SelectList: React.FC<SelectListProps> = ({ items, onSelect }) => { const [selectedIndex, setSelectedIndex] = useState(0); const { exit } = useApp(); useInput((input, key) => { if (key.upArrow) { setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1)); } else if (key.downArrow) { setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0)); } else if (key.return) { onSelect(items[selectedIndex], selectedIndex); } else if (input === 'q' || key.escape) { exit(); } }); return ( <Box flexDirection="column"> {items.map((item, index) => ( <Box key={item}> <Text color={index === selectedIndex ? 'green' : undefined}> {index === selectedIndex ? '> ' : ' '} {item} </Text> </Box> ))} <Box marginTop={1}> <Text dimColor>Use arrow keys to navigate, Enter to select, q to quit</Text> </Box> </Box> ); };
tsximport React, { useState } from 'react'; import { Box, Text, useInput } from 'ink'; interface TextInputProps { placeholder?: string; onSubmit: (value: string) => void; mask?: string; } export const TextInput: React.FC<TextInputProps> = ({ placeholder = '', onSubmit, mask, }) => { const [value, setValue] = useState(''); const [cursor, setCursor] = useState(0); useInput((input, key) => { if (key.return) { onSubmit(value); return; } if (key.backspace || key.delete) { setValue((prev) => prev.slice(0, -1)); setCursor((prev) => Math.max(0, prev - 1)); return; } if (!key.ctrl && !key.meta && input) { setValue((prev) => prev + input); setCursor((prev) => prev + 1); } }); const displayValue = mask ? mask.repeat(value.length) : value; return ( <Box> <Text> {displayValue || <Text dimColor>{placeholder}</Text>} <Text backgroundColor="white"> </Text> </Text> </Box> ); };
tsximport React, { useState, useEffect } from 'react'; import { Text } from 'ink'; const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; interface SpinnerProps { label?: string; } export const Spinner: React.FC<SpinnerProps> = ({ label }) => { const [frame, setFrame] = useState(0); useEffect(() => { const timer = setInterval(() => { setFrame((prev) => (prev + 1) % frames.length); }, 80); return () => clearInterval(timer); }, []); return ( <Text> <Text color="green">{frames[frame]}</Text> {label && <Text> {label}</Text>} </Text> ); };
tsximport { useState, useEffect, useCallback } from 'react'; interface AsyncState<T> { data: T | null; loading: boolean; error: Error | null; } export function useAsync<T>( asyncFn: () => Promise<T>, deps: any[] = [] ): AsyncState<T> & { refetch: () => void } { const [state, setState] = useState<AsyncState<T>>({ data: null, loading: true, error: null, }); const execute = useCallback(async () => { setState({ data: null, loading: true, error: null }); try { const data = await asyncFn(); setState({ data, loading: false, error: null }); } catch (error) { setState({ data: null, loading: false, error: error as Error }); } }, deps); useEffect(() => { execute(); }, [execute]); return { ...state, refetch: execute }; }
json{ "dependencies": { "ink": "^4.0.0", "react": "^18.0.0" }, "devDependencies": { "@types/react": "^18.0.0", "ink-testing-library": "^3.0.0" } }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 17,916 | 19,528 | +9% | 1 | 1 | 0% | 3,881 | 5,939 | +53% | 0 | 0 | — |
case-02 | fail→pass | 14,344 | 17,646 | +23% | 1 | 1 | 0% | 3,187 | 5,839 | +83% | 0 | 0 | — |
case-03 | fail→fail | 13,283 | 16,706 | +26% | 1 | 1 | 0% | 2,778 | 4,946 | +78% | 0 | 0 | — |
case-04 | fail→fail | 4,662 | 4,734 | +2% | 1 | 1 | 0% | 893 | 2,289 | +156% | 0 | 0 | — |
case-05 | fail→pass | 12,157 | 9,055 | -26% | 1 | 1 | 0% | 2,435 | 3,455 | +42% | 0 | 0 | — |
case-06 | fail→fail | 13,998 | 13,365 | -5% | 1 | 1 | 0% | 2,467 | 4,032 | +63% | 0 | 0 | — |
case-07 | fail→fail | 11,759 | 5,429 | -54% | 1 | 1 | 0% | 2,220 | 2,626 | +18% | 0 | 0 | — |
case-08 | fail→fail | 9,227 | 5,585 | -39% | 1 | 1 | 0% | 1,953 | 2,888 | +48% | 0 | 0 | — |
case-09 | fail→fail | 10,456 | 9,253 | -12% | 1 | 1 | 0% | 2,000 | 3,557 | +78% | 0 | 0 | — |
case-10 | fail→pass | 10,234 | 3,030 | -70% | 1 | 1 | 0% | 1,875 | 2,133 | +14% | 0 | 0 | — |
case-11 | pass→pass | 5,345 | 1,907 | -64% | 1 | 1 | 0% | 1,012 | 1,858 | +84% | 0 | 0 | — |
case-12 | fail→pass | 15,058 | 5,919 | -61% | 1 | 1 | 0% | 3,200 | 2,675 | -16% | 0 | 0 | — |
case-13 | fail→fail | 13,811 | 12,758 | -8% | 1 | 1 | 0% | 2,615 | 4,200 | +61% | 0 | 0 | — |
case-14 | fail→pass | 14,549 | 12,862 | -12% | 1 | 1 | 0% | 2,573 | 3,979 | +55% | 0 | 0 | — |
case-15 | fail→pass | 10,923 | 11,059 | +1% | 1 | 1 | 0% | 1,897 | 3,736 | +97% | 0 | 0 | — |
case-16 | fail→fail | 15,527 | 13,323 | -14% | 1 | 1 | 0% | 2,976 | 4,248 | +43% | 0 | 0 | — |
case-17 | pass→pass | 10,126 | 7,243 | -28% | 1 | 1 | 0% | 1,967 | 3,079 | +57% | 0 | 0 | — |
case-18 | fail→fail | 10,324 | 12,314 | +19% | 1 | 1 | 0% | 1,925 | 3,554 | +85% | 0 | 0 | — |
case-19 | fail→fail | 15,829 | 6,871 | -57% | 1 | 1 | 0% | 2,801 | 2,921 | +4% | 0 | 0 | — |
case-20 | fail→fail | 7,436 | 6,754 | -9% | 1 | 1 | 0% | 1,602 | 3,058 | +91% | 0 | 0 | — |
case-21 | fail→fail | 30,926 | 20,714 | -33% | 1 | 1 | 0% | 6,169 | 7,013 | +14% | 0 | 0 | — |
case-22 | fail→fail | 12,418 | 13,804 | +11% | 1 | 1 | 0% | 2,387 | 3,841 | +61% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.