Source-backed engineering practices and reviewed skill repositories, in a Claude Code pane.

Engineering practices drawn from mature open-source repositories, each tied to the source it came from, plus a shelf of reviewed third-party skill repositories. It runs as a mod inside Claude Code, a live pane installed as a plugin, and as a command-line tool.

Type /grounded and the pane reads the repository you have open. It shows the practices that fit what it finds (tests with no CI gate, agent instructions only Claude reads, no OS sandbox) and why each one applies. Adapt asks Claude to propose the smallest change that brings the practice into your repository; nothing is written until you approve it. Every card links to the sources it was drawn from, pinned to the revision that was read.
Needs Claude Code 2.1.288 or later, in the terminal or the desktop app's Code tab. Mods install as plugins:
/plugin marketplace add madjagstudios/grounded-engineering
/plugin install grounded-engineering@grounded-engineering
Then type /grounded. In the terminal the pane also has keyboard shortcuts: 1 and 2 switch screens, d shows what it detected, and a runs the open card's main action.
Needs Node.js 20 or later. It writes the same practices into AGENTS.md, CLAUDE.md or a neutral Markdown file; see Adopt a profile.

The second screen lists sixteen third-party skill repositories, each read at a pinned commit and approved by a maintainer before it is listed. The shelf holds links and our own short notes only; credit and stars go to the authors. Explain install has Claude read the repository at that commit and list what it would add (hooks, scripts, network use, settings it changes) before quoting the author's own install steps. Claude installs nothing unless you agree, and you type any slash commands yourself. Authors who ask to be removed are delisted. The records live in research/skill-repos/.
research/ Source observations, pinned references, and category audits
practices/ Short, reusable engineering-practice cards
integrations/ Consumer-specific translation guidance for agent instruction files
plugin/ The Claude Code mod: pane, skills, and generated catalog
src/, bin/ The command-line tool
scripts/ Validation, catalog build, and source-drift tools
Adoption does not rewrite existing policy. preview only reads, and create stores a proposal under .grounded-engineering/proposals/<proposal-id>/ for you to review; nothing is written until apply --confirm.
# one-off, nothing installed
npx grounded-engineering adopt preview --profile ai-assisted --adapter claude
# or install the command
npm install -g grounded-engineering
grounded-engineering adopt create --profile ai-assisted --adapter claude
# Review proposal.yaml, plan.md, and diff.patch; complete local_decisions.
grounded-engineering adopt apply <proposal-id> --confirm
grounded-engineering check
Profiles:
baseline: the original eight cards on repository context, code quality, testing and verification. Its pack metadata stays at v0.2.0 so existing adopters stay green.ai-assisted: all seventeen cards.Adapters:
neutral (default) writes Markdown to docs/grounded-engineering.md if the repository has a docs/ folder, otherwise to GROUNDED_ENGINEERING.md.codex writes to AGENTS.md. If AGENTS.override.md exists, Codex reads it instead, so the tool reports it and writes nothing.claude writes to the repository-root CLAUDE.md. It reports .claude/CLAUDE.md, nested CLAUDE.md and CLAUDE.local.md files but does not edit them.Every adapter writes only inside managed blocks keyed by card ID, and leaves everything outside them byte for byte. Apply writes the target and .grounded-engineering/manifest.yaml only after re-checking the proposal's preconditions. A repository can have one adapter target. Adding a second target, and choosing cards with --cards outside preview, are not supported yet.
grounded-engineering check reads the manifest and the target files and compares them with the pack bundled in the installed CLI. It exits 0 when clean, 1 on drift or a repository-state mismatch, and 2 on an invocation error. The write path is specified in the adopt apply policy; compatibility notes for each release are in CHANGELOG.md.
Cards paraphrase their sources and link to the revision and the file or heading they draw on. We quote only where a source's license allows it, and never copy vendor prompt files. Each card says whether its practice is observed, recommended, or validated in use.
A card does not change when its source does. npm run check:sources reports drift, and a card marked validated records which revision of each source it was checked against.
Start with CONTRIBUTING.md and research/README.md. Keep source observations in research/, the practice in practices/, and tool-specific wording in integrations/; a change that mixes them is harder to review.
Grounded Engineering is built and maintained with AI agents.
Original repository content is available under the MIT License. Third-party sources remain under their own licenses and terms; links and attribution are recorded with the evidence.
hooks/register.tsx 196 lines1// Engine rules this file follows:
2// - State atoms are declared here, as top-level consts with a literal plugin and key.
3// - The engine does not follow `$` across an import, so every function that takes `$` is in this file.
4// - Each hook passed to on() is a function literal.
5// - A render never writes state.
6
7import type { Register } from 'claude-code'
8import { atom, read, update } from 'claude-code'
9import { loadCatalog, type Catalog, type Host } from './catalog'
10import { readAdoption, readSignals } from './signals'
11import type { GroundedScreen, GroundedSignals } from '../types'
12import { TERMINAL_PALETTE } from './theme'
13import { slimCatalog, type Ack, type PaneModel } from './model'
14import { paneScreen, linkLabel } from './screens'
15import { pressOf, validAct } from './client/apply'
16
17const PANE = 'grounded'
18// Prints one of the pane's links in the transcript, where the desktop opens it; hidden from the menu.
19const LINK_COMMAND = 'grounded-link'
20
21const screenState = atom({ plugin: 'grounded-engineering', key: 'screen' } as const, 'practices' as GroundedScreen)
22const selectedState = atom({ plugin: 'grounded-engineering', key: 'selected' } as const, null as string | null)
23const queryState = atom({ plugin: 'grounded-engineering', key: 'query' } as const, '')
24const categoryState = atom({ plugin: 'grounded-engineering', key: 'category' } as const, 'All')
25const tagState = atom({ plugin: 'grounded-engineering', key: 'tag' } as const, 'All')
26const sortState = atom({ plugin: 'grounded-engineering', key: 'sort' } as const, 'fit' as 'fit' | 'name')
27const showSignalsState = atom({ plugin: 'grounded-engineering', key: 'showSignals' } as const, false)
28const collapsedState = atom({ plugin: 'grounded-engineering', key: 'collapsed' } as const, { fits: false, all: false } as { fits: boolean; all: boolean })
29const signalsState = atom({ plugin: 'grounded-engineering', key: 'signals' } as const, null as GroundedSignals | null)
30const adoptionState = atom({ plugin: 'grounded-engineering', key: 'adoption' } as const, null as { profile: string | null; cards: string[] } | null)
31
32// The parsed catalog and the plugin root it was read from. A hot reload starts this module fresh, which empties it.
33let catalogCache: { root: string; catalog: Catalog } | null = null
34
35function hostOf($: any): Host {
36 return { fs: { read: (p) => $.fs.read(p).then(String), exists: (p) => $.fs.exists(p), list: (p) => $.fs.list(p) }, plugin: { root: $.plugin.root } }
37}
38
39async function refreshRepo($: any) {
40 const host = hostOf($)
41 const [signals, adoption] = await Promise.all([readSignals(host), readAdoption(host)])
42 await update($, signalsState, () => signals)
43 await update($, adoptionState, () => adoption)
44}
45
46async function catalogFor($: any): Promise<{ catalog: Catalog | null; error: string | null }> {
47 const host = hostOf($)
48 if (catalogCache?.root === host.plugin.root) return { catalog: catalogCache.catalog, error: null }
49 try {
50 const catalog = await loadCatalog(host)
51 catalogCache = { root: host.plugin.root, catalog }
52 return { catalog, error: null }
53 } catch (err) { return { catalog: null, error: (err as Error).message } }
54}
55
56// Acks for the 16 most recently posting clients. Evicting a live client would rerun its
57// unacknowledged presses; a redrawn Client gets a new cid, so 16 is far more than a session uses.
58const CLIENTS = 16
59const acks = new Map<string, Ack>()
60function touchAck(cid: string): Ack {
61 const ack = acks.get(cid) ?? { received: 0, done: 0 }
62 acks.delete(cid)
63 acks.set(cid, ack)
64 if (acks.size > CLIENTS) acks.delete(acks.keys().next().value!)
65 return ack
66}
67// Client presses run one after another, in the order they were made; see ui.message.
68let clientWork: Promise<unknown> = Promise.resolve()
69
70// Everything the pane draws, as plain data: the terminal draws it, the desktop Client receives it.
71async function paneModel($: any): Promise<PaneModel> {
72 const { catalog, error } = await catalogFor($)
73 const [screen, selected, query, category, tag, sort, showSignals, collapsed, storedSignals, storedAdoption] = await Promise.all([
74 read($, screenState), read($, selectedState), read($, queryState), read($, categoryState), read($, tagState), read($, sortState),
75 read($, showSignalsState), read($, collapsedState), read($, signalsState), read($, adoptionState),
76 ])
77 const host = hostOf($)
78 const [signals, adoption] = storedSignals !== null ? [storedSignals, storedAdoption] : await Promise.all([readSignals(host), readAdoption(host)])
79 return {
80 catalog: catalog ? slimCatalog(catalog) : null, error, screen, selected, query, category, tag, sort, showSignals, collapsed,
81 signals, adoption, acks: Object.fromEntries([...acks].map(([cid, a]) => [cid, { ...a }])),
82 }
83}
84
85// The plugin's skills run as commands, only from a press.
86async function runSkill($: any, skill: 'adapt' | 'explain', id: string) {
87 void $.command.run({ command: `grounded-engineering:${skill}`, args: id })
88 .catch(() => $.ui.toast(`Could not start /grounded-engineering:${skill} ${id}`))
89}
90
91// The transcript line for a link the pane draws, or null.
92async function linkLine($: any, url: unknown): Promise<string | null> {
93 const { catalog } = await catalogFor($)
94 if (!catalog || typeof url !== 'string') return null
95 const label = linkLabel(catalog, url)
96 return label ? `${label}: ${url}` : null
97}
98
99// The command is registered immediate, so it runs at once even mid-turn.
100async function showLink($: any, url: unknown) {
101 if (!(await linkLine($, url))) return
102 void $.command.run({ command: LINK_COMMAND, args: url })
103 .catch(() => $.ui.toast('Could not show the link'))
104}
105
106// Every action the pane can take, each returning a promise that resolves once its state is
107// written. The ui.message hook awaits these for a Client's presses.
108function actions($: any) {
109 return {
110 // Search belongs to the screen it was typed on, so a tab change clears it.
111 tab: (s: 'practices' | 'skills') => Promise.all([update($, screenState, () => s), update($, selectedState, () => null), update($, queryState, () => '')]),
112 select: (key: string) => update($, selectedState, (cur) => (cur === key ? null : key)),
113 search: (q: string) => update($, queryState, () => q),
114 category: (c: string) => update($, categoryState, () => c),
115 tag: (t: string) => update($, tagState, () => t),
116 sort: (v: 'fit' | 'name') => update($, sortState, () => v),
117 toggleSignals: () => update($, showSignalsState, (v) => !v),
118 toggleLane: (lane: 'fits' | 'all') => update($, collapsedState, (cur) => ({ ...cur, [lane]: !cur[lane] })),
119 adapt: (id: string) => runSkill($, 'adapt', id),
120 explain: (id: string) => runSkill($, 'explain', id),
121 link: (url: string) => showLink($, url),
122 }
123}
124
125function handlers($: any) {
126 const a = actions($) as Record<string, (...args: any[]) => Promise<unknown>>
127 const out: Record<string, (...args: any[]) => void> = {}
128 for (const name of Object.keys(a)) out[name] = (...args: any[]) => { void a[name]!(...args).catch(() => undefined) }
129 return out
130}
131
132async function openOn($: any, screen: 'practices' | 'skills', text: string) {
133 await update($, screenState, () => screen)
134 await update($, selectedState, () => null)
135 await update($, queryState, () => '')
136 await update($, collapsedState, () => ({ fits: false, all: false }))
137 // A request for a wider dock; a width the person dragged wins.
138 await $.ui.open({ id: PANE, title: 'Grounded', columns: 100 })
139 await refreshRepo($)
140 return { text }
141}
142
143export const register: Register = on => {
144 on('session.start', async ($, e, next) => {
145 await $.command.register({ name: 'grounded-skills', description: 'Open Grounded Engineering: reviewed skill repos' })
146 await $.command.register({ name: LINK_COMMAND, description: 'Print a Grounded Engineering link', argumentHint: '[url]', immediate: true })
147 // A pane restored with the session can render before any command has run. A render that
148 // runs before this finishes reads the repository itself.
149 void refreshRepo($).catch(() => undefined)
150 return next(e)
151 })
152
153 // /grounded is skills/grounded, answered here: the desktop slash menu lists a plugin's skills,
154 // not the commands session.start registers.
155 on('command.run', { command: 'grounded-engineering:grounded' }, async ($) => openOn($, 'practices', 'Grounded Engineering opened on Practices.'))
156 on('command.run', { command: 'grounded-skills' }, async ($) => openOn($, 'skills', 'Grounded Engineering opened on Skill repos.'))
157 on('command.run', { command: LINK_COMMAND }, async ($, e) => ({ text: (await linkLine($, e.args.trim())) ?? 'Not a link the Grounded pane shows.' }))
158 on('command.describe', { command: LINK_COMMAND }, async ($, e, next) => next({ ...e, isHidden: true }))
159 // The pane runs these two.
160 on('command.describe', { command: 'grounded-engineering:adapt' }, async ($, e, next) => next({ ...e, isHidden: true }))
161 on('command.describe', { command: 'grounded-engineering:explain' }, async ($, e, next) => next({ ...e, isHidden: true }))
162
163 // Runs each press the Client posts once, in order; one that is not a valid action is acknowledged, not run.
164 on('ui.message', async ($, e, next) => {
165 if (e.element !== 'grounded-app') return next(e)
166 const d = e.data as { kind?: string; acts?: unknown[] } | null
167 const posted = d?.kind === 'acts' && Array.isArray(d.acts) ? d.acts : []
168 const { catalog } = posted.length ? await catalogFor($) : { catalog: null }
169 const table = actions($) as Record<string, (...args: any[]) => Promise<unknown>>
170 let ran = false
171 for (const a of posted) {
172 const press = pressOf(a)
173 if (!press) continue
174 const ack = touchAck(press.cid)
175 if (press.seq <= ack.received) continue
176 ack.received = press.seq
177 const act = validAct(catalog, a) ? a : null
178 ran = true
179 clientWork = clientWork.then(() => (act ? table[act.name]!(...act.args) : undefined)).catch(() => undefined)
180 .then(() => { ack.done = press.seq })
181 }
182 if (ran) await clientWork
183 return { props: await paneModel($) }
184 })
185
186 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
187 const ui = $.ui.resolve(e)
188 // On desktop the app paints a Pane's own redraws only on the next input; a Client paints itself.
189 if (e.surface === 'desktop' && 'Client' in ui) {
190 const { Client } = ui
191 return <Client key="grounded-app" module="./client/app.tsx" props={await paneModel($)} width="100%" flexGrow={1} />
192 }
193 return paneScreen(ui, await paneModel($), handlers($) as any, TERMINAL_PALETTE, e.props.bodyColumns ?? 0, 1)
194 })
195}
196hooks/catalog.ts 50 lines1import type { FsEntry } from 'claude-code'
2
3// What the core reads through. Built from a hook's `$` by `hostOf` in register.tsx.
4export type Host = {
5 fs: { read(path: string): Promise<string>; exists(path: string): Promise<boolean>; list(path?: string): Promise<FsEntry[]> }
6 plugin: { root: string }
7}
8
9export type Source = { id: string; url: string }
10export type FitRule = { card: string; when: { all: string[]; any: string[]; none: string[] }; why: string }
11export type Practice = {
12 id: string; title: string; category: string; subcategory: string; pattern: string; rationale: string
13 agent_snippet: string | null; applicability: string[]; control_types: string[]; confidence: string
14 validation_status: string; source_ids: string[]; path: string; body: string
15}
16export type SkillRepo = {
17 id: string; name: string; repo: string; license: string; pinned_commit: string; reviewed_on: string
18 best_for: string[]; tags: string[]; summary: string; watch_out_for: string
19 install: string[]; install_note: string | null; featured: boolean
20}
21export type Catalog = {
22 catalog_version: number; package_version: string; repository: string
23 signals: { name: string; type: string; description: string }[]
24 categories: string[]; practices: Practice[]; skill_repos: SkillRepo[]; fit_rules: FitRule[]; sources: Source[]
25}
26
27export async function loadCatalog(host: Host): Promise<Catalog> {
28 try {
29 const catalog = JSON.parse(String(await host.fs.read(`${host.plugin.root}/catalog.json`))) as Catalog | null
30 if (catalog === null || typeof catalog !== 'object' || catalog.catalog_version !== 1) {
31 throw new Error('unexpected catalog_version or shape')
32 }
33 for (const field of ['practices', 'fit_rules', 'skill_repos', 'categories', 'signals', 'sources'] as const) {
34 if (!Array.isArray(catalog[field])) throw new Error(`${field} is not a list`)
35 }
36 // Card links are built on this URL, and the engine refuses the whole pane over one bad link.
37 if (typeof catalog.repository !== 'string' || !/^https:\/\/[^\s@]+$/.test(catalog.repository)) {
38 throw new Error('repository is not an https URL')
39 }
40 if (new URL(catalog.repository).href !== catalog.repository) throw new Error('repository is not in normal URL form')
41 return catalog
42 } catch (error) {
43 throw new Error(`catalog missing or invalid: ${String(error)}`)
44 }
45}
46
47export const cardUrl = (catalog: { repository: string; package_version: string }, practice: { path: string }) =>
48 `${catalog.repository}/blob/v${catalog.package_version}/${practice.path}`
49export const repoUrl = (repo: { repo: string }) => `https://github.com/${repo.repo}`
50hooks/signals.ts 94 lines1import type { Host } from './catalog'
2
3export const SIGNAL_NAMES = ['has_claude_md', 'has_agents_md', 'has_skills', 'has_large_skill_md', 'has_subagents', 'subagents_without_tool_limits', 'hooks_configured', 'sandbox_enabled', 'has_tests', 'has_ci', 'grounded_adopted', 'languages', 'test_framework'] as const
4
5export type Signals = {
6 has_claude_md: boolean; has_agents_md: boolean; has_skills: boolean; has_large_skill_md: boolean
7 has_subagents: boolean; subagents_without_tool_limits: boolean; hooks_configured: boolean
8 sandbox_enabled: boolean; has_tests: boolean; has_ci: boolean; grounded_adopted: boolean
9 languages: string[]; test_framework: string | null
10}
11
12export type Adoption = { profile: string | null; cards: string[] }
13
14type Fs = Host['fs']
15
16const exists = (fs: Fs, path: string) => fs.exists(path).catch(() => false)
17const readText = (fs: Fs, path: string) => fs.read(path).then(String, () => null)
18const list = (fs: Fs, path: string) => fs.list(path).catch(() => [])
19const readJson = async (fs: Fs, path: string) => {
20 const text = await readText(fs, path)
21 if (text === null) return null
22 try { return JSON.parse(text) } catch { return null }
23}
24
25export async function readSignals(host: Host): Promise<Signals> {
26 const fs = host.fs
27 const skillDirs = (await list(fs, '.claude/skills')).filter((e) => e.kind === 'dir').map((e) => e.name)
28 const skillTexts = (await Promise.all(skillDirs.map((d) => readText(fs, `.claude/skills/${d}/SKILL.md`)))).filter((t): t is string => t !== null)
29 const agentFiles = (await list(fs, '.claude/agents')).filter((e) => e.kind === 'file' && e.name.endsWith('.md'))
30 const agentTexts = (await Promise.all(agentFiles.map((e) => readText(fs, `.claude/agents/${e.name}`)))).filter((t): t is string => t !== null)
31 const settings = await readJson(fs, '.claude/settings.json')
32 const pkg = await readJson(fs, 'package.json')
33 const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) }
34 const workflows = (await list(fs, '.github/workflows')).filter((e) => /\.ya?ml$/.test(e.name))
35 const hasPython = (await exists(fs, 'pyproject.toml')) || (await exists(fs, 'requirements.txt'))
36 const hasGo = await exists(fs, 'go.mod')
37 const hasRust = await exists(fs, 'Cargo.toml')
38
39 let test_framework: string | null = null
40 if (deps.vitest) test_framework = 'vitest'
41 else if (deps.jest) test_framework = 'jest'
42 else if (/node --test/.test(String(pkg?.scripts?.test ?? ''))) test_framework = 'node-test'
43 else if (hasPython && ((await exists(fs, 'pytest.ini')) || (await exists(fs, 'conftest.py')) || /pytest/.test((await readText(fs, 'pyproject.toml')) ?? ''))) test_framework = 'pytest'
44 else if (hasGo) test_framework = 'go-test'
45 else if (hasRust) test_framework = 'cargo-test'
46
47 const languages: string[] = []
48 if (pkg) languages.push((await exists(fs, 'tsconfig.json')) || deps.typescript ? 'typescript' : 'javascript')
49 if (hasPython) languages.push('python')
50 if (hasGo) languages.push('go')
51 if (hasRust) languages.push('rust')
52
53 const hasTestDir = (await exists(fs, 'test')) || (await exists(fs, 'tests')) || (await exists(fs, '__tests__'))
54
55 return {
56 has_claude_md: (await exists(fs, 'CLAUDE.md')) || (await exists(fs, '.claude/CLAUDE.md')),
57 has_agents_md: await exists(fs, 'AGENTS.md'),
58 has_skills: skillTexts.length > 0,
59 has_large_skill_md: skillTexts.some((t) => t.replace(/\r?\n$/, '').split('\n').length > 500),
60 has_subagents: agentTexts.length > 0,
61 subagents_without_tool_limits: agentTexts.some((t) => !/^tools\s*:/m.test(frontmatter(t))),
62 hooks_configured: !!settings?.hooks && Object.keys(settings.hooks).length > 0,
63 sandbox_enabled: settings?.sandbox?.enabled === true,
64 has_tests: test_framework !== null || hasTestDir,
65 has_ci: workflows.length > 0 || (await exists(fs, '.gitlab-ci.yml')) || (await exists(fs, '.circleci/config.yml')),
66 grounded_adopted: await exists(fs, '.grounded-engineering/manifest.yaml'),
67 languages,
68 test_framework,
69 }
70}
71
72function frontmatter(text: string): string {
73 const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)
74 return m?.[1] ?? ''
75}
76
77export async function readAdoption(host: Host): Promise<Adoption | null> {
78 const text = await readText(host.fs, '.grounded-engineering/manifest.yaml')
79 if (text === null) return null
80 const profile = /^pack_id:\s*([\w-]+)/m.exec(text)?.[1] ?? null
81 // Card ids are the `id:` of each entry under the top-level `cards:`; an id named in a
82 // card's free text (a decision, a revisit trigger) is not an adopted card.
83 const cards = new Set<string>()
84 let inCards = false
85 for (const line of text.split(/\r?\n/)) {
86 if (/^\S/.test(line)) inCards = /^cards:/.test(line)
87 else if (inCards) {
88 const id = /^\s*(?:-\s+)?id:\s*["']?(GE-[A-Z]{2}-\d{3})["']?\s*$/.exec(line)?.[1]
89 if (id) cards.add(id)
90 }
91 }
92 return { profile, cards: [...cards].sort() }
93}
94hooks/theme.ts 19 lines1// Colours for the pane. Every value is a theme key, not a hex colour: the drawing surface
2// resolves keys for the app's light or dark mode, which a plugin cannot read.
3export type Tone = 'ok' | 'warn' | 'neutral' | 'accent' | 'failed'
4type Swatch = { bg?: string; edge: string; text: string }
5export type Palette = Record<Tone, Swatch> & { dim: string }
6
7export const PALETTE: Palette = {
8 ok: { bg: 'diffAddedDimmed', edge: 'success', text: 'success' },
9 warn: { bg: 'userMessageBackground', edge: 'warning', text: 'warning' },
10 neutral: { bg: 'userMessageBackground', edge: 'promptBorder', text: 'text' },
11 accent: { bg: 'background', edge: 'claude', text: 'claude' },
12 failed: { bg: 'diffRemovedDimmed', edge: 'error', text: 'error' },
13 dim: 'inactive',
14}
15
16// The terminal theme has no 'background' key for a surface fill; it draws one in solid cyan,
17// so an open card there keeps only its accent edge.
18export const TERMINAL_PALETTE: Palette = { ...PALETTE, accent: { edge: 'claude', text: 'claude' } }
19hooks/model.ts 34 lines1import type { Catalog, FitRule, Practice, SkillRepo } from './catalog'
2import type { Adoption, Signals } from './signals'
3
4export type SlimPractice = Omit<Practice, 'body'>
5export type SlimSkillRepo = Omit<SkillRepo, 'install' | 'install_note'>
6export type SlimCatalog = {
7 package_version: string; repository: string; categories: string[]
8 signals: { name: string; description: string }[]
9 practices: SlimPractice[]; skill_repos: SlimSkillRepo[]; fit_rules: FitRule[]
10}
11export type Screen = 'practices' | 'skills'
12// Per client: the highest `seq` received, and the highest whose action has finished. Posts carry
13// a client's presses in `seq` order, so these two numbers are enough.
14export type Ack = { received: number; done: number }
15export type Acks = Record<string, Ack>
16export type PaneModel = {
17 catalog: SlimCatalog | null; error: string | null
18 screen: Screen; selected: string | null; query: string; category: string; tag: string; sort: 'fit' | 'name'
19 showSignals: boolean; collapsed: { fits: boolean; all: boolean }
20 signals: Signals | null; adoption: Adoption | null
21 acks: Acks
22}
23
24// What the pane draws. Card bodies, the source list and install steps stay behind: the pane never
25// shows them, the skills read them from catalog.json, and Client props are size-bounded.
26export function slimCatalog(c: Catalog): SlimCatalog {
27 return {
28 package_version: c.package_version, repository: c.repository, categories: c.categories,
29 signals: c.signals.map(({ name, description }) => ({ name, description })),
30 practices: c.practices.map(({ body, ...rest }) => rest),
31 skill_repos: c.skill_repos.map(({ install, install_note, ...rest }) => rest), fit_rules: c.fit_rules,
32 }
33}
34hooks/screens.tsx 256 lines1// The pane's screens: pure functions of a plain-data model, the element table and handlers.
2// The terminal draws them directly; on desktop the Client module draws the same screens.
3import type { Palette } from './theme'
4import { tile, laneHeader, badge, chip, option, field, shorten, room } from './tiles'
5import { cardUrl, repoUrl } from './catalog'
6import { rankPractices, sortSkillRepos, matchesQuery } from './fit'
7import type { PaneModel, Screen, SlimCatalog, SlimPractice, SlimSkillRepo } from './model'
8
9export const HANDLER_NAMES = ['tab', 'select', 'search', 'category', 'tag', 'sort', 'toggleSignals', 'toggleLane', 'adapt', 'explain', 'link'] as const
10
11type Go = {
12 tab: (s: Screen) => void; select: (key: string) => void; search: (q: string) => void
13 category: (c: string) => void; tag: (t: string) => void; sort: (s: 'fit' | 'name') => void
14 toggleSignals: () => void; toggleLane: (lane: 'fits' | 'all') => void
15 adapt: (id: string) => void; explain: (id: string) => void
16 link: (url: string) => void
17}
18
19// The other lists the skills screen points to; the terminal draws each label with a ›.
20const MORE = [
21 { label: 'awesome-claude-skills', href: 'https://github.com/ComposioHQ/awesome-claude-skills' },
22 { label: 'awesome-claude-code-mods', href: 'https://github.com/karanb192/awesome-claude-code-mods' },
23]
24
25// Below this many columns the tabs take the whole header row and the title is left out.
26const NARROW = 60
27
28const PANE = 2 // pane padding
29const SYMBOL = 2 // a row's status symbol and its gap
30const OUTLINE = 4 // the outline and the label padding
31const TILE = 4 // a tile's border and padding
32const TERMINAL = 9 // the terminal's fixed overhead per row
33const TERMINAL_TAGS = 8 // the terminal's room for a repo row's tags
34// On desktop, titles are cut to one line; below WIDE columns the category, tags and badge give way.
35const WIDE = 70
36const outlined = (ui: any) => ui.outlinesTitles === true
37const tight = (ui: any, columns: number) => outlined(ui) && columns > 0 && columns < WIDE
38
39export function rowTitleRoom(ui: any, columns: number, scale: number, category: string): number {
40 if (!outlined(ui)) return room(columns, TERMINAL + category.length, scale)
41 return room(columns, PANE + SYMBOL + OUTLINE + (tight(ui, columns) ? 0 : category.length + 1), scale)
42}
43
44export function repoTitleRoom(ui: any, columns: number, scale: number, tags: string): number {
45 if (!outlined(ui)) return room(columns, TERMINAL + TERMINAL_TAGS, scale)
46 return room(columns, PANE + OUTLINE + (tight(ui, columns) ? 0 : tags.length + 1), scale)
47}
48
49export function tileTitleRoom(ui: any, columns: number, scale: number, badgeText: string): number {
50 return room(columns, PANE + TILE + OUTLINE + (tight(ui, columns) ? 0 : badgeText.length + 1), scale)
51}
52
53// The links the pane draws, each with what the transcript says before it.
54type LinkCatalog = { repository: string; package_version: string; practices: { id: string; path: string }[]; skill_repos: SlimSkillRepo[] }
55export function linkLabel(c: LinkCatalog, url: string): string | null {
56 const practice = c.practices.find((x) => cardUrl(c, x) === url)
57 if (practice) return `Evidence for ${practice.id}`
58 const repo = c.skill_repos.find((r) => repoUrl(r) === url)
59 if (repo) return `${repo.name} on GitHub`
60 if (url === c.repository) return 'Grounded Engineering on GitHub'
61 const more = MORE.find((l) => l.href === url)
62 return more ? more.label : null
63}
64
65const isValidated = (x: { validation_status: string }) => x.validation_status === 'validated'
66const byline = (r: SlimSkillRepo) => `${r.featured ? 'Featured · ' : ''}${r.repo.split('/')[0]} · ${r.license}`
67
68export function paneScreen(ui: any, m: PaneModel, go: Go, p: Palette, columns: number, scale: number) {
69 const { Box, Text, Button } = ui
70 if (!m.catalog) {
71 return (
72 <Box key="catalog-error" flexDirection="column">
73 <Text color={p.failed.text}>{`${m.error ?? 'catalog missing or invalid'}. Reinstall the plugin: /plugin install grounded-engineering@grounded-engineering`}</Text>
74 </Box>
75 )
76 }
77 const c = m.catalog
78 return (
79 <Box flexDirection="column" gap={1}>
80 <Box flexDirection="row" justifyContent="space-between" gap={1}>
81 {columns > 0 && columns < NARROW ? null : <Box flexGrow={1} flexShrink={1} minWidth={0}><Text bold wrap="truncate-end">Grounded Engineering</Text></Box>}
82 <Box flexDirection="row" gap={1} flexShrink={0}>
83 <Button key="tab-practices" hotkey="1" variant={m.screen === 'practices' ? 'primary' : 'secondary'} label={`Practices ${c.practices.length}`} onPress={() => go.tab('practices')} />
84 <Button key="tab-skills" hotkey="2" variant={m.screen === 'skills' ? 'primary' : 'secondary'} label={`Skill repos ${c.skill_repos.length}`} onPress={() => go.tab('skills')} />
85 </Box>
86 </Box>
87 {m.signals === null
88 ? <Text color={p.dim}>Reading this repository…</Text>
89 : m.screen === 'practices' ? practicesScreen(ui, m, c, go, p, columns, scale) : skillsScreen(ui, m, c, go, p, columns, scale)}
90 </Box>
91 )
92}
93
94function practicesScreen(ui: any, m: PaneModel, c: SlimCatalog, go: Go, p: Palette, columns: number, scale: number) {
95 const { Box, Text, Button, Link } = ui
96 const s = m.signals!
97 const all = rankPractices(c, s, m.adoption, Infinity)
98 const fits = all.slice(0, 3)
99 const adopted = new Set(m.adoption?.cards ?? [])
100 const visible = c.practices.filter((x) => (m.category === 'All' || x.category === m.category) && matchesQuery([x.title, x.pattern, x.id], m.query))
101 const gapText = all.length === 0 ? 'no gaps' : `${all.length} gap${all.length === 1 ? '' : 's'}`
102 const detected = [...s.languages, s.test_framework ?? (s.has_tests ? 'tests (framework unknown)' : 'no tests')].join(' · ')
103 return (
104 <Box flexDirection="column" gap={1}>
105 <Box flexDirection="row" justifyContent="space-between" gap={1}>
106 <Box flexDirection="row" gap={1} flexShrink={1} minWidth={0} flexWrap="wrap">
107 {chip(ui, p, all.length ? 'warn' : 'ok', gapText)}
108 {chip(ui, p, 'neutral', `Detected: ${detected}`)}
109 {m.adoption ? chip(ui, p, 'neutral', `Adopted: ${m.adoption.profile ?? 'custom'} · ${m.adoption.cards.length}`) : null}
110 </Box>
111 <Box flexShrink={0}><Button key="details" plain hotkey="d" label={m.showSignals ? 'Hide details' : 'Details ›'} onPress={() => go.toggleSignals()} /></Box>
112 </Box>
113 {m.showSignals ? (
114 <Box key="signals" flexDirection="column">
115 {c.signals.map((sig) => <Text color={p.dim} wrap="truncate-end">{`${sig.name}: ${JSON.stringify((s as Record<string, unknown>)[sig.name])}`}</Text>)}
116 <Link href={c.repository}>☆ Star Grounded Engineering</Link>
117 </Box>
118 ) : null}
119 {field(ui, p, 'search', 'Search practices', m.query, (v) => go.search(v))}
120 {laneHeader(ui, p, 'warn', 'fits', '✋ Fits this repo', fits.length, !m.collapsed.fits, () => go.toggleLane('fits'))}
121 {m.collapsed.fits ? null : fits.length === 0
122 ? <Text color={p.dim}>Your repo already covers the basics.</Text>
123 : <Box flexDirection="column" gap={1}>{fits.map((f) => practiceTile(ui, m, c, go, p, f.practice, `fit-${f.practice.id}`, 'Why here', f.why, adopted.has(f.practice.id), columns, scale, f.practice.agent_snippet ?? f.practice.pattern))}</Box>}
124 {laneHeader(ui, p, 'neutral', 'all', 'All practices', visible.length, !m.collapsed.all, () => go.toggleLane('all'))}
125 {m.collapsed.all ? null : (
126 <Box flexDirection="column" gap={1}>
127 <Box flexDirection="row" columnGap={2} rowGap={0} flexWrap="wrap">
128 {option(ui, p, 'opt-cat-All', 'All', m.category === 'All', () => go.category('All'))}
129 {c.categories.map((cat) => option(ui, p, `opt-cat-${cat}`, cat, m.category === cat, () => go.category(cat)))}
130 </Box>
131 {visible.length === 0 ? <Text color={p.dim}>No matches.</Text> : (
132 <Box flexDirection="column">
133 {visible.map((x) => m.selected === `all-${x.id}`
134 ? practiceTile(ui, m, c, go, p, x, `all-${x.id}`, 'In short', x.agent_snippet ?? x.pattern, adopted.has(x.id), columns, scale)
135 : practiceRow(ui, go, p, x, columns, scale))}
136 </Box>
137 )}
138 </Box>
139 )}
140 </Box>
141 )
142}
143
144function practiceRow(ui: any, go: Go, p: Palette, x: SlimPractice, columns: number, scale: number) {
145 const { Box, Text, Button } = ui
146 const tone = isValidated(x) ? 'ok' : 'warn'
147 const beside = !tight(ui, columns)
148 const budget = rowTitleRoom(ui, columns, scale, x.category)
149 return (
150 <Box key={`row-${x.id}`} flexDirection="row" gap={1}>
151 <Box flexShrink={0}><Text color={p[tone].text}>{isValidated(x) ? '✓' : '●'}</Text></Box>
152 <Box flexGrow={1} flexShrink={1} minWidth={0}>
153 <Button key={`open-all-${x.id}`} plain label={shorten(x.title, budget)} onPress={() => go.select(`all-${x.id}`)} />
154 </Box>
155 {beside ? <Box flexShrink={0}><Text color={p.dim} wrap="truncate-end">{x.category}</Text></Box> : null}
156 </Box>
157 )
158}
159
160// A tile's title and its badge: side by side, or on desktop below WIDE columns one under the other.
161function tileTitle(ui: any, key: string, label: string, onPress: () => void, badgeText: string, badgeNode: unknown, columns: number, scale: number) {
162 const { Box, Button } = ui
163 const under = tight(ui, columns)
164 const text = outlined(ui) ? shorten(label, tileTitleRoom(ui, columns, scale, badgeText)) : label
165 const title = (
166 <Box flexGrow={1} flexShrink={1} minWidth={0}>
167 <Button key={key} plain label={text} onPress={onPress} />
168 </Box>
169 )
170 return under
171 ? <Box flexDirection="column">{title}{badgeNode}</Box>
172 : <Box flexDirection="row" gap={1}>{title}{badgeNode}</Box>
173}
174
175function practiceTile(ui: any, m: PaneModel, c: SlimCatalog, go: Go, p: Palette, x: SlimPractice, key: string, calloutLabel: string, callout: string, isAdopted: boolean, columns: number, scale: number, summary: string | null = null) {
176 const { Box, Text, Button, Link } = ui
177 const isOpen = m.selected === key
178 const tone = isValidated(x) ? 'ok' : 'warn'
179 const badgeText = isValidated(x) ? '✓ Validated' : '● Needs review'
180 return tile(ui, p, isOpen ? 'accent' : 'neutral', `tile-${key}`, [
181 tileTitle(ui, `open-${key}`, x.title, () => go.select(key), badgeText, badge(ui, p, tone, badgeText), columns, scale),
182 isAdopted ? <Text color={p.dim}>Adopted through the CLI</Text> : null,
183 // What the practice is, before it opens; the opened tile shows its pattern and trade-off instead.
184 summary && !isOpen ? <Text color={p.dim}>{`In short: ${summary}`}</Text> : null,
185 <Text><Text bold>{`${calloutLabel}: `}</Text>{callout}</Text>,
186 isOpen ? <Text color={p.dim}>{x.pattern}</Text> : null,
187 isOpen ? <Text color={p.dim}><Text color={p.warn.text}>! </Text>{`Trade-off: ${x.rationale}`}</Text> : null,
188 isOpen ? <Text color={p.dim} wrap="truncate-end">{`${x.category} · ${x.source_ids.join(', ')} · ${x.id}`}</Text> : null,
189 isOpen ? (
190 <Box flexDirection="row" columnGap={2} rowGap={0} flexWrap="wrap">
191 {isAdopted ? null : <Button key={`primary-${key}`} variant="primary" hotkey="a" label="Adapt to this repo" onPress={() => go.adapt(x.id)} />}
192 <Link href={cardUrl(c, x)}>Evidence ›</Link>
193 </Box>
194 ) : null,
195 ])
196}
197
198function repoRow(ui: any, go: Go, p: Palette, r: SlimSkillRepo, columns: number, scale: number) {
199 const { Box, Text, Button } = ui
200 const tags = `${r.featured ? 'Featured · ' : ''}${r.tags.join(', ')}`
201 const beside = !tight(ui, columns)
202 const budget = repoTitleRoom(ui, columns, scale, tags)
203 return (
204 <Box key={`row-repo-${r.id}`} flexDirection="row" gap={1}>
205 <Box flexGrow={1} flexShrink={1} minWidth={0}>
206 <Button key={`open-repo-${r.id}`} plain label={shorten(r.name, budget)} onPress={() => go.select(`repo-${r.id}`)} />
207 </Box>
208 {beside ? <Box flexShrink={0}><Text color={p.dim} wrap="truncate-end">{tags}</Text></Box> : null}
209 </Box>
210 )
211}
212
213function skillsScreen(ui: any, m: PaneModel, c: SlimCatalog, go: Go, p: Palette, columns: number, scale: number) {
214 const { Box, Text, Button, Link } = ui
215 const tags = [...new Set(c.skill_repos.flatMap((r) => r.tags))].sort()
216 const repos = sortSkillRepos(c.skill_repos, m.signals!, m.sort)
217 .filter((r) => (m.tag === 'All' || r.tags.includes(m.tag)) && matchesQuery([r.name, r.repo, r.summary], m.query))
218 return (
219 <Box flexDirection="column" gap={1}>
220 {field(ui, p, 'search', 'Search skill repos', m.query, (v) => go.search(v))}
221 <Box flexDirection="row" justifyContent="space-between" gap={1}>
222 <Box flexShrink={1} minWidth={0}><Text bold wrap="truncate-end">{`Reviewed skill repos ${c.skill_repos.length}`}</Text></Box>
223 <Box flexDirection="row" gap={2} flexShrink={0}>
224 {option(ui, p, 'opt-sort-fit', 'Fit', m.sort === 'fit', () => go.sort('fit'))}
225 {option(ui, p, 'opt-sort-name', 'Name', m.sort === 'name', () => go.sort('name'))}
226 </Box>
227 </Box>
228 {tags.length ? (
229 <Box flexDirection="row" columnGap={2} rowGap={0} flexWrap="wrap">
230 {option(ui, p, 'opt-tag-All', 'All', m.tag === 'All', () => go.tag('All'))}
231 {tags.map((t) => option(ui, p, `opt-tag-${t}`, t, m.tag === t, () => go.tag(t)))}
232 </Box>
233 ) : null}
234 {c.skill_repos.length === 0 ? <Text color={p.dim}>No skill repos are listed yet.</Text>
235 : repos.length === 0 ? <Text color={p.dim}>No matches.</Text>
236 : (
237 <Box flexDirection="column" gap={1}>
238 {repos.map((r) => m.selected === `repo-${r.id}` ? tile(ui, p, 'accent', `tile-repo-${r.id}`, [
239 tileTitle(ui, `open-repo-${r.id}`, r.name, () => go.select(`repo-${r.id}`), byline(r), <Text color={p.dim} wrap="truncate-end">{byline(r)}</Text>, columns, scale),
240 <Text>{r.summary}</Text>,
241 <Text color={p.dim}><Text color={p.warn.text}>! </Text>{`Watch out: ${r.watch_out_for}`}</Text>,
242 <Text color={p.dim} wrap="truncate-end">{`Reviewed ${r.reviewed_on} · pinned at ${r.pinned_commit.slice(0, 7)}`}</Text>,
243 <Box flexDirection="row" columnGap={2} rowGap={0} flexWrap="wrap">
244 <Button key={`primary-repo-${r.id}`} variant="primary" hotkey="a" label="Explain install" onPress={() => go.explain(r.id)} />
245 <Link href={repoUrl(r)}>☆ Star on GitHub</Link>
246 </Box>,
247 ]) : repoRow(ui, go, p, r, columns, scale))}
248 </Box>
249 )}
250 <Text bold>Want more?</Text>
251 {MORE.map((l) => <Link href={l.href}>{`${l.label} ›`}</Link>)}
252 <Text color={p.dim}>Authors can ask to be removed.</Text>
253 </Box>
254 )
255}
256hooks/client/apply.ts 59 lines1import type { PaneModel } from '../model'
2
3// A press: the client id (`cid`), its sequence number within that client (`seq`, from 1), and
4// the action it asks for.
5export type Msg = { cid: string; seq: number; name: string; args: unknown[] }
6// Presses the Client shows at once, before the plugin answers.
7export const LOCAL_NAMES = new Set(['tab', 'select', 'search', 'category', 'tag', 'sort', 'toggleSignals', 'toggleLane'])
8
9// The model as it will be once this press lands; used only for presses not yet acknowledged.
10export function applyLocal(m: PaneModel, msg: Msg): PaneModel {
11 const [a] = msg.args as [any]
12 switch (msg.name) {
13 case 'tab': return { ...m, screen: a, selected: null, query: '' }
14 case 'select': return { ...m, selected: m.selected === a ? null : a }
15 case 'search': return { ...m, query: a }
16 case 'category': return { ...m, category: a }
17 case 'tag': return { ...m, tag: a }
18 case 'sort': return { ...m, sort: a }
19 case 'toggleSignals': return { ...m, showSignals: !m.showSignals }
20 case 'toggleLane': return { ...m, collapsed: { ...m.collapsed, [a]: !m.collapsed[a as 'fits' | 'all'] } }
21 default: return m
22 }
23}
24
25// A posted press is data from code, so it runs only when its name is one of the pane's actions
26// and its arguments are what the pane gives that action: the right count, the right types,
27// and for a skill the id of a card or repo in the catalog.
28type Known = { practices: { id: string }[]; skill_repos: { id: string }[] }
29const isString = (v: unknown): v is string => typeof v === 'string'
30const oneOf = (v: unknown, options: readonly string[]): boolean => isString(v) && options.includes(v)
31const ARGS: Record<string, (c: Known | null, a: unknown[]) => boolean> = {
32 tab: (_, a) => a.length === 1 && oneOf(a[0], ['practices', 'skills']),
33 select: (_, a) => a.length === 1 && isString(a[0]),
34 search: (_, a) => a.length === 1 && isString(a[0]),
35 category: (_, a) => a.length === 1 && isString(a[0]),
36 tag: (_, a) => a.length === 1 && isString(a[0]),
37 sort: (_, a) => a.length === 1 && oneOf(a[0], ['fit', 'name']),
38 toggleSignals: (_, a) => a.length === 0,
39 toggleLane: (_, a) => a.length === 1 && oneOf(a[0], ['fits', 'all']),
40 adapt: (c, a) => a.length === 1 && isString(a[0]) && !!c?.practices.some((x) => x.id === a[0]),
41 explain: (c, a) => a.length === 1 && isString(a[0]) && !!c?.skill_repos.some((x) => x.id === a[0]),
42 link: (_, a) => a.length === 1 && isString(a[0]),
43}
44// The `cid` and `seq` of a posted press, or null when either is malformed; such a press cannot
45// be acknowledged, so the plugin ignores it.
46const CID = /^[\w-]{1,40}$/
47export function pressOf(act: unknown): { cid: string; seq: number } | null {
48 if (!act || typeof act !== 'object') return null
49 const { cid, seq } = act as Record<string, unknown>
50 return isString(cid) && CID.test(cid) && Number.isSafeInteger(seq) && (seq as number) > 0 ? { cid, seq: seq as number } : null
51}
52export function validAct(catalog: Known | null, act: unknown): act is Msg {
53 if (!pressOf(act)) return false
54 const { name, args } = act as Record<string, unknown>
55 if (!isString(name) || !Array.isArray(args)) return false
56 const check = Object.prototype.hasOwnProperty.call(ARGS, name) ? ARGS[name] : undefined
57 return !!check && check(catalog, args)
58}
59hooks/client/app.tsx 65 lines1// The desktop pane: draws the shared screens from the model it is given and shows a press at
2// once; ./outbox says how presses reach the plugin.
3import { paneScreen, HANDLER_NAMES } from '../screens'
4import type { PaneModel } from '../model'
5import { PALETTE } from '../theme'
6import { ackOf, enqueue, nextPost, starting, unfinished, unseen, type PostState } from './outbox'
7import { applyLocal, LOCAL_NAMES, type Msg } from './apply'
8import { clientLook } from './look'
9
10const CHARS_PER_CELL = 1.25
11
12type Outbox = { cid: string; outbox: Msg[]; local: Msg[]; seq: number }
13type Local = { box: Outbox; n: number }
14
15export default function GroundedApp(model: PaneModel, surface: any) {
16 let created: Outbox | null = null
17 if (surface.state === undefined) {
18 // First call: start the timers. State is set from a timer or a press, never while drawing.
19 const box: Outbox = { cid: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, outbox: [], local: [], seq: 0 }
20 created = box
21 // Every draw before the first tick starts its own timers. Only the box the state holds is in
22 // use; a timer holding another box stops itself and its twin.
23 let sent: PostState = { syncDue: false, sentKey: '', sinceSend: 0 }
24 const stopPoster = surface.every(60, () => {
25 if (surface.state !== undefined && (surface.state as Local).box !== box) { stopPoster(); stopSync(); return }
26 if (surface.state === undefined) surface.setState({ box, n: 0 })
27 const r = nextPost(box.outbox, sent)
28 sent = r.s
29 if (r.post) surface.post(r.post)
30 })
31 // Once a second a sync is due, which brings what changed outside the pane.
32 const stopSync = surface.every(1000, () => {
33 sent = { ...sent, syncDue: true }
34 })
35 }
36 const local = surface.state as Local | undefined
37 const box = local?.box ?? created
38 if (box) {
39 const ack = ackOf(model.acks, box.cid)
40 box.outbox = unseen(box.outbox, ack)
41 box.local = unfinished(box.local, ack)
42 }
43 const send = (name: string, args: unknown[]) => {
44 const b = (surface.state as Local | undefined)?.box ?? box
45 if (!b) return
46 b.seq += 1
47 const msg: Msg = { cid: b.cid, seq: b.seq, name, args }
48 b.outbox = enqueue(b.outbox, msg)
49 b.local = [...b.local, msg]
50 surface.setState({ box: b, n: b.seq }) // a distinct state per press, so the Client redraws
51 }
52 const go: Record<string, (...args: unknown[]) => void> = {}
53 for (const name of HANDLER_NAMES) go[name] = (...args: unknown[]) => send(name, args)
54 const waiting = box?.local ?? []
55 const shown = waiting.filter((m) => LOCAL_NAMES.has(m.name)).reduce(applyLocal, model)
56 const { Box, Text } = surface.elements
57 const columns = surface.columns ?? 0
58 return (
59 <Box flexDirection="column" gap={1}>
60 {starting(waiting) ? <Text color={PALETTE.dim}>Starting…</Text> : null}
61 {paneScreen(clientLook(surface.elements, PALETTE, go), shown, go as any, PALETTE, columns, CHARS_PER_CELL)}
62 </Box>
63 )
64}
65hooks/tiles.tsx 71 lines1// Building blocks for the pane: pure functions of the element table, a palette and data.
2import type { Palette, Tone } from './theme'
3
4type Ui = any
5
6// How many characters fit in the columns left after `overhead` cells of other things on the
7// row. scale is characters per cell: 1 on the terminal's monospace grid, about 1.25 on the
8// desktop, whose proportional font fits a little more. Zero columns means the width is unknown.
9export function room(columns: number, overhead: number, scale: number): number {
10 if (!columns) return 0
11 return Math.max(0, Math.floor((columns - overhead) * scale))
12}
13
14// One line, cut with an ellipsis when it would not fit; room 10 or less means do not cut.
15export function shorten(text: string, budget: number): string {
16 const line = text.split('\n').find((l) => l.trim()) ?? ''
17 if (budget <= 10 || line.length <= budget) return line
18 return `${line.slice(0, budget - 1).trimEnd()}…`
19}
20
21// No margin of its own: every column of tiles is spaced with gap={1}.
22export function tile(ui: Ui, p: Palette, tone: Tone, key: string, children: unknown[]) {
23 const { Box } = ui
24 const c = p[tone]
25 return (
26 <Box key={key} flexDirection="column" borderStyle="round" borderColor={c.edge} {...(c.bg ? { backgroundColor: c.bg } : {})} paddingX={1}>
27 {children}
28 </Box>
29 )
30}
31
32export function laneHeader(ui: Ui, p: Palette, tone: Tone, key: string, label: string, count: number, isOpen: boolean, onToggle: () => void) {
33 const { Box, Text, Button } = ui
34 return (
35 <Box key={key} flexDirection="row" justifyContent="space-between">
36 <Button key={`lane-${key}`} plain label={`${isOpen ? '▾' : '▸'} ${label}`} onPress={onToggle} />
37 <Text color={p[tone].text}>{String(count)}</Text>
38 </Box>
39 )
40}
41
42// A badge never shrinks: squeezed, it breaks into fragments.
43export function badge(ui: Ui, p: Palette, tone: Tone, text: string) {
44 const { Box, Text } = ui
45 return <Box flexShrink={0} alignSelf="flex-start"><Text color={p[tone].text} wrap="truncate-end">{text}</Text></Box>
46}
47
48export function chip(ui: Ui, p: Palette, tone: Tone, text: string) {
49 const { Text } = ui
50 return <Text color={p[tone].text} {...(p[tone].bg ? { backgroundColor: p[tone].bg } : {})} wrap="truncate-end">{` ${text} `}</Text>
51}
52
53// A picked-dot choice: a plain Button that reads as a radio option.
54export function option(ui: Ui, p: Palette, key: string, label: string, isOn: boolean, onPress: () => void) {
55 const { Button } = ui
56 return <Button key={key} plain dimColor={!isOn} label={`${isOn ? '●' : '○'} ${label}`} onPress={onPress} />
57}
58
59// A labelled text field drawn inside a box on every surface, so the terminal shows where to type.
60export function field(ui: Ui, p: Palette, key: string, label: string, value: string, onChange: (v: string) => void) {
61 const { Box, Text, Input } = ui
62 return (
63 <Box key={`${key}-box`} flexDirection="column">
64 <Text color={p.dim}>{label}</Text>
65 <Box borderStyle="round" borderColor={p.neutral.edge} paddingX={1}>
66 <Input key={key} value={value} onInput={(v: string) => onChange(v)} onSubmit={(v: string) => onChange(v)} />
67 </Box>
68 </Box>
69 )
70}
71hooks/fit.ts 42 lines1import type { FitRule, SkillRepo } from './catalog'
2import { SIGNAL_NAMES, type Adoption, type Signals } from './signals'
3
4type Bools = { [K in keyof Signals]: Signals[K] extends boolean ? K : never }[keyof Signals]
5
6// The pane lists the first `limit` fits; its gap count asks for them all (`Infinity`).
7export function rankPractices<P extends { id: string; validation_status: string }>(catalog: { practices: P[]; fit_rules: FitRule[] }, signals: Signals, adoption: Adoption | null, limit = 3): { practice: P; why: string }[] {
8 const on = (name: string) => signals[name as Bools] === true
9 const adopted = new Set(adoption?.cards ?? [])
10 const byId = new Map(catalog.practices.map((p) => [p.id, p]))
11 const picked = new Map<string, string>()
12 const known = new Set<string>(SIGNAL_NAMES)
13 for (const rule of catalog.fit_rules) {
14 // A rule naming a signal this plugin does not read cannot be judged, so it never fires.
15 if (![...rule.when.all, ...rule.when.any, ...rule.when.none].every((name) => known.has(name))) continue
16 const fires = rule.when.all.every(on) && (rule.when.any.length === 0 || rule.when.any.some(on)) && !rule.when.none.some(on)
17 if (fires && !picked.has(rule.card) && !adopted.has(rule.card)) picked.set(rule.card, rule.why)
18 }
19 return [...picked]
20 .map(([id, why]) => ({ practice: byId.get(id)!, why }))
21 .sort((a, b) => Number(b.practice.validation_status === 'validated') - Number(a.practice.validation_status === 'validated') || (a.practice.id < b.practice.id ? -1 : 1))
22 .slice(0, limit)
23}
24
25const TAG_SIGNALS = new Map<string, (s: Signals) => boolean>([
26 ['typescript', (s) => s.languages.includes('typescript')],
27 ['python', (s) => s.languages.includes('python')],
28 ['testing', (s) => s.has_tests],
29 ['devops', (s) => s.has_ci],
30])
31
32export function sortSkillRepos<R extends Pick<SkillRepo, 'name' | 'tags' | 'featured'>>(repos: R[], signals: Signals, mode: 'fit' | 'name'): R[] {
33 const score = (r: R) => r.tags.filter((t) => TAG_SIGNALS.get(t)?.(signals)).length
34 const fit = (a: R, b: R) => Number(b.featured) - Number(a.featured) || score(b) - score(a)
35 return [...repos].sort((a, b) => (mode === 'fit' ? fit(a, b) : 0) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
36}
37
38export function matchesQuery(text: string[], query: string): boolean {
39 const q = query.trim().toLowerCase()
40 return q === '' || text.some((t) => t.toLowerCase().includes(q))
41}
42hooks/client/outbox.ts 47 lines1// The desktop Client's press queue: pure helpers with no Node imports, since the Client runs in the app's view.
2import type { Msg } from './apply'
3import type { Ack, Acks } from '../model'
4
5// Edits carry the whole value, so a newer one replaces an unsent older one.
6const EDITS = new Set(['search'])
7
8export function enqueue(outbox: Msg[], msg: Msg): Msg[] {
9 const kept = EDITS.has(msg.name) ? outbox.filter((m) => m.name !== msg.name) : outbox
10 return [...kept, msg]
11}
12
13export type PostState = { syncDue: boolean; sentKey: string; sinceSend: number }
14export type Post = { kind: 'acts'; acts: Msg[] } | { kind: 'sync' }
15
16// The post for this tick. The app keeps only the last post of a frame, and a busy or background
17// window can run several ticks in one frame, so a press posted on its own could be silently
18// replaced (a skill that never starts). Every post therefore carries all presses not yet
19// received, in order; the plugin skips any `seq` at or below the last it received.
20export const RESEND = 8
21export function nextPost(outbox: Msg[], s: PostState): { post: Post | null; s: PostState } {
22 const key = outbox.map((m) => m.seq).join(',')
23 if (outbox.length && (key !== s.sentKey || s.sinceSend >= RESEND)) {
24 return { post: { kind: 'acts', acts: outbox }, s: { syncDue: false, sentKey: key, sinceSend: 0 } }
25 }
26 if (!outbox.length && s.syncDue) return { post: { kind: 'sync' }, s: { syncDue: false, sentKey: '', sinceSend: 0 } }
27 return { post: null, s: { ...s, sinceSend: s.sinceSend + 1 } }
28}
29
30export function ackOf(acks: Acks | undefined, cid: string): Ack {
31 return acks?.[cid] ?? { received: 0, done: 0 }
32}
33
34// Still to post.
35export function unseen(outbox: Msg[], ack: Ack): Msg[] {
36 return outbox.filter((m) => m.seq > ack.received)
37}
38
39// Still to show.
40export function unfinished(local: Msg[], ack: Ack): Msg[] {
41 return local.filter((m) => m.seq > ack.done)
42}
43
44export function starting(local: Msg[]): boolean {
45 return local.some((m) => m.name === 'adapt' || m.name === 'explain')
46}
47hooks/client/look.tsx 69 lines1// How the desktop Client draws the pane's controls. The Client paints a Button with no
2// chrome, stretched across its row with the label centred, so these wrap the table's
3// Button (and Link) and the shared screens read there as they do in the terminal, which
4// keeps its own table untouched. A filled Box reads as highlighted text in the Client (it
5// fills only the text's own line), so a button is an outlined box sized to its label; the
6// label carries the padding, so every cell inside the outline is the Button.
7import type { Palette } from '../theme'
8
9// A short, stable name for an address, so a link's Button keeps its key across draws.
10function hashOf(text: string): string {
11 let h = 0x811c9dc5
12 for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 0x01000193)
13 return (h >>> 0).toString(36)
14}
15
16export function clientLook(el: any, p: Palette, go: { link?: (url: string) => void } = {}) {
17 const { Box, Button } = el
18 const outline = (button: unknown, primary = false) => (
19 <Box flexDirection="row" flexShrink={0}>
20 <Box borderStyle="round" borderColor={primary ? p.accent.edge : p.neutral.edge} {...(primary ? { backgroundColor: p.accent.bg } : {})}>
21 {button}
22 </Box>
23 </Box>
24 )
25 const LookButton = (props: any) => {
26 // A hotkey answers only in the terminal pane, and a dim label is not known to draw in
27 // the Client: the picked dot already marks which option is on.
28 const { children, hotkey: _hotkey, dimColor: _dimColor, ...rest } = props
29 const label = String(rest.label ?? (typeof children === 'string' ? children : ''))
30 const key = String(rest.key ?? '')
31 // A title that opens a tile is outlined like any action, but gives way to what sits
32 // beside it: the screens cut it to one line, and minWidth 0 lets it shrink, not wrap.
33 if (key.startsWith('open-')) {
34 delete rest.plain
35 return (
36 <Box flexDirection="row" flexShrink={1} minWidth={0}>
37 <Box borderStyle="round" borderColor={p.neutral.edge} flexShrink={1} minWidth={0}>
38 <Button {...rest} label={` ${label} `} />
39 </Box>
40 </Box>
41 )
42 }
43 // Text stays a plain control where its place says so: an option with its picked dot, a
44 // lane header with its arrow, the details toggle.
45 if (/^(opt-|lane-|details)/.test(key)) {
46 return <Box flexDirection="row" minWidth={0}><Button {...rest} plain label={label} /></Box>
47 }
48 delete rest.plain
49 return outline(<Button {...rest} label={` ${label} `} />, rest.variant === 'primary')
50 }
51 // The desktop refuses a Link (and a Markdown) inside a Client and unmounts the whole Client
52 // over it, and a Client has no way to open an address. So a link is an outlined button
53 // that asks the plugin to print the address in the transcript, where it opens; the plugin
54 // prints only addresses the pane itself draws. Its key comes from the address, numbered
55 // when the same address is drawn twice in one draw.
56 const drawn = new Map<string, number>()
57 const LookLink = ({ href, children, label }: any) => {
58 const own = [children].flat().filter((c) => typeof c === 'string').join('')
59 const text = String(own || label || href).replace(/ ›$/, '')
60 const base = `link-${hashOf(String(href))}`
61 const n = (drawn.get(base) ?? 0) + 1
62 drawn.set(base, n)
63 const key = n === 1 ? base : `${base}-${n}`
64 return outline(<Button key={key} label={` ${text} ↗ `} onPress={() => go.link?.(String(href))} />)
65 }
66 // outlinesTitles: titles are drawn outlined here, so the screens budget for the outline.
67 return { ...el, Button: LookButton, Link: LookLink, outlinesTitles: true }
68}
69