SLOPSHOPPER

tui-kit-example

Example mod that copies in the tui-kit and its adapters, and draws every kit component in a pane and the status badge in a band.

newpaneband
v0.1.0no licenseupdated 2026-10-04pavel-rp/wf-plugin/plugins/tui-kit/example
A shopper browsing a rack in a slop shop
README

wf-marketplace

Claude Code marketplace hosting the wf core plugin and its capability packs:

  • wf — the domain-free Spec-Driven Development spine: a wf:* skill chain for spec → plan → implement → verify → QA → commit/PR, carrying zero stack, domain, or project knowledge.
  • wf-browser-qa — the standalone browser-QA feature pack: a stack-agnostic browser-automation QA engine (qa-execution engine provider) that core's /wf:qa-auto dispatches to.
  • wf-node-ts — the standalone Node/TypeScript stack pack: a dependency-free unit-test harness for pure TS helpers (implement-phase test-authoring guidance).
  • tui-kit — a host-free terminal UI kit for Claude Code mods, independent of wf: semantic colour tokens mapped to the host's theme keys, an accessibility profile, NO_COLOR/FORCE_COLOR handling, control-sequence sanitising, and a status badge rendered to plain text, ANSI and markdown, shipped as a copy-in template.
  • wf-audit — the standalone audit + self-review pack: five domain-free adversarial verify lenses plus an optional composite retrospective, co-located with the pre-commit self-review lens that reuses the audit correctness rubric.

Install

In Claude Code:

/plugin marketplace add pavel-rp/wf-plugin
/plugin install wf

(Replace pavel-rp/wf-plugin with the repo's actual owner/name once pushed, or use a local path / full git URL.)

Then, in any git repo you want to work in:

/wf:init

/wf:init is per-repo: it scaffolds _local/ for per-task artifacts, adds gitignore entries, and writes the Node test runner. No per-machine setup is needed — Claude Code auto-discovers the plugin's skills and agents on install, and nested subagent delegation (e.g. wf:run→wf:phase-runner) works out of the box.

What's in the plugins

Each plugin's skills are invoked under its own namespace: core wf skills as /wf:<skill> (/wf:spec, /wf:plan, /wf:qa-auto, …), and each capability pack's skills as /<pack>:<skill> (skill names are bare; the namespace prefix comes from the plugin).

See plugins/wf/README.md for the full, authoritative core skill and subagent reference. In brief, the core skills cover triage → spec → plan → implement → verify → QA → commit/PR, plus a /wf:run dispatcher that drives the safe front of the chain hands-off. See each capability pack's README for the stack/domain capabilities that attach to the spine.

Authoring

See CLAUDE.md for conventions when adding or editing skills and agents in this plugin.

Source 2 files
hooks/register.mjs 121 lines
1// Example mod: the tui-kit and its adapters copied into lib/, drawing every
2// kit component. `/tui-kit-demo` opens one pane per component; the band above
3// the prompt shows the status badge. On the terminal the checks list is
4// selectable; elsewhere it is display-only and says so.
5//
6// The host lets `$` reach only functions declared in this file, so every host
7// call is made here and the adapters get plain values: the element table from
8// `$.ui.resolve(e)`, the surface, and the profile built from the host reads.
9
10import { atom, read, update } from 'claude-code';
11
12import { defineBadge, definePanel, defineList, defineTable, defineMeter } from '../lib/kit/index.mjs';
13import { drawView, planPane, profileFromHost, shouldMount } from '../lib/adapters/index.mjs';
14
15const picked = atom({ plugin: 'tui-kit-example', key: 'picked' }, -1);
16
17const CHECKS = ['lint', 'tests', 'docs'];
18
19const PANES = Object.freeze({
20  'tui-kit-badge': { title: 'Badge', view: () => ({ kind: 'badge', def: defineBadge({ status: 'warn' }) }) },
21  'tui-kit-panel': {
22    title: 'Panel',
23    view: () => ({ kind: 'panel', def: definePanel({ title: 'Build', status: 'error', body: ['3 tests failed', 'see the log'] }) }),
24  },
25  'tui-kit-list': {
26    title: 'Checks',
27    view: () => ({
28      kind: 'list',
29      def: defineList({ items: [{ text: CHECKS[0], status: 'ok' }, { text: CHECKS[1], status: 'error' }, CHECKS[2]] }),
30    }),
31  },
32  'tui-kit-table': {
33    title: 'Table',
34    view: () => ({
35      kind: 'table',
36      def: defineTable({
37        columns: [{ key: 'check', header: 'Check' }, { key: 'result', header: 'Result' }, { key: 'ms', header: 'Time', align: 'right' }],
38        rows: [{ check: 'lint', result: { status: 'ok' }, ms: 12 }, { check: 'tests', result: { status: 'error' }, ms: 340 }],
39      }),
40    }),
41  },
42  'tui-kit-meter': {
43    title: 'Meter',
44    view: () => ({ kind: 'meter', def: defineMeter({ label: 'Disk', value: 8, max: 10, status: 'warn' }) }),
45  },
46});
47
48/**
49 * Read the host's accessibility values; a read that fails counts as unreachable.
50 *
51 * @param {any} $
52 * @param {{ screenReaderOff: boolean }} settings
53 */
54async function hostProfile($, settings) {
55  let configRows;
56  let hostSettings;
57  let screenReaderEnv;
58  try {
59    configRows = await $.config.list();
60  } catch {
61    configRows = undefined;
62  }
63  try {
64    hostSettings = await $.settings.read();
65  } catch {
66    hostSettings = undefined;
67  }
68  try {
69    screenReaderEnv = await $.env.get('CLAUDE_AX_SCREEN_READER');
70  } catch {
71    screenReaderEnv = undefined;
72  }
73  return profileFromHost({ configRows, settings: hostSettings, screenReaderEnv }, settings);
74}
75
76/** @type {import('claude-code').Register} */
77export const register = (on, options) => {
78  const settings = {
79    screenReaderOff: options.screenReaderOff === true,
80    panesDrawForScreenReader: options.panesDrawForScreenReader === true,
81  };
82
83  on('command.run', { command: 'tui-kit-demo' }, async ($) => {
84    const profile = await hostProfile($, settings);
85    const skipped = [];
86    for (const id of Object.keys(PANES)) {
87      const pane = PANES[id];
88      const plan = planPane(pane.view(), { ...settings, profile });
89      if (plan.isMounted) {
90        await $.ui.open({ id, title: pane.title });
91      } else {
92        for (const line of plan.lines) $.ui.log(line);
93        skipped.push(pane.title);
94      }
95    }
96    return { text: skipped.length === 0 ? 'Kit panes opened.' : `Kit panes skipped for the screen reader: ${skipped.join(', ')}.` };
97  });
98
99  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
100    if (!Object.hasOwn(PANES, e.requestId)) return next(e);
101    const profile = await hostProfile($, settings);
102    const el = $.ui.resolve(e);
103    const view = PANES[e.requestId].view();
104    if (view.kind !== 'list') return drawView(el, e.surface, view, { profile });
105    const index = await read($, picked);
106    return el.Box({
107      flexDirection: 'column',
108      children: [
109        drawView(el, e.surface, { ...view, onSelect: (i) => update($, picked, () => i) }, { profile }),
110        el.Text({ dimColor: true, children: `Picked: ${index >= 0 && index < CHECKS.length ? CHECKS[index] : 'none'}` }),
111      ],
112    });
113  });
114
115  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
116    const profile = await hostProfile($, settings);
117    if (e.props.hasSurvey || !shouldMount(profile, settings)) return next(e);
118    return drawView($.ui.resolve(e), e.surface, PANES['tui-kit-badge'].view(), { profile });
119  });
120};
121
types/index.d.ts 8 lines
1export type TuiKitExamplePicked = number
2
3declare module 'claude-code' {
4  interface PluginState {
5    'tui-kit-example': { picked: TuiKitExamplePicked }
6  }
7}
8