Generate a complete Foldkit program based on this description:
$ARGUMENTS
Phase 1: Analyze the Description
Before writing any code, analyze the description to identify:
- Domain entities: nouns that become Model fields (e.g., "todos", "user", "score")
- User interactions: verbs that become Messages (e.g., "add", "delete", "filter", "submit")
- Async operations: external data that becomes Commands (e.g., "fetch weather", "save to localStorage")
- Real-time needs: streaming data that becomes Subscriptions (e.g., "live updates", "countdown", "WebSocket")
- Pages/navigation: URL structure that becomes routes (e.g., "home page", "detail page")
- UI component needs: interactive widgets that map to Foldkit UI components (e.g., "dropdown" → Menu, "modal" → Dialog, "tabs" → Tabs, "autocomplete" → Combobox, "date picker" → DatePicker, "file upload" → FileDrop, "reorderable list" → DragAndDrop, "toast/notification" → Toast, "hover tooltip" → Tooltip, "range/price filter" → Slider, "long scrolling list" → VirtualList)
- Form validation needs: required fields, format checks, async uniqueness →
foldkit/fieldValidation module (see Phase 4)
- Date handling: birthdays, deadlines, scheduling →
Calendar module + DatePicker or Calendar from @foldkit/ui
- File handling: uploads, attachments, images →
File module + FileDrop from @foldkit/ui
- Remote data: anything fetched, cached, refreshed, or revalidated → the
AsyncData module (see Phase 4). Don't hand-roll a loading/error union
- Multi-state flows: a described process that moves through several named steps with rules about which step follows which (checkout, onboarding, multi-step approval, a connection lifecycle) → consider the
Machine module (foldkit/experimental). Writing the transitions as a table makes the edge set enumerable, so unreachableStates() and deadTransitions() catch a missing or unreachable step by computation instead of by review. Raise it as an option in the analysis you present, noting it is under experimental/, and let the user choose. Plain ts() unions with one M.tagsExhaustive are still right for a flow of two or three states. @foldkit/examples/state-machine/ is the reference
- Host embedding: the program runs inside another app ("a widget in our React app", "embed this in an existing page", "the host needs to control it") →
Runtime.makeElement plus the Runtime.embed lifecycle handle, with Flags for initial data and Ports for ongoing communication in both directions. @foldkit/examples/embedding/ is the canonical reference: a plain TypeScript host driving a Foldkit widget end to end
Present this analysis to the user before proceeding.
If the description is detailed and unambiguous, summarize the analysis and confirm before moving on. But if there are gaps (unclear state transitions, vague UI requirements, unspecified error handling, missing edge cases, ambiguous domain boundaries, unclear counter/reset semantics), ask targeted clarifying questions before proceeding. Don't ask open-ended questions like "anything else?". Ask specific questions about the gaps you found.
UX/behavior gaps:
- "Should the todo list persist across page reloads (localStorage), or start fresh each session?"
- "When the API call fails, should the app show an inline error or a dialog?"
- "You mentioned 'users can edit items'. Is that inline editing or a separate edit page?"
Domain-logic gaps (easy to miss, expensive to fix):
- "When the user skips an interval, does that count as 'completed' for purposes of the streak?"
- "Does a counter that tracks 'completed' increments on successful actions only, or on skipped actions too?"
- "If the user triggers a reset mid-flow, does the counter reset with it, or persist across resets?"
- "You mentioned 'after N events, trigger X'. Is that N events total, or N events since the last X?"
- "On the Nth action in a cycle, which action does it trigger, the cycle's first or last?"
Domain-logic questions often surface off-by-one bugs before they hit the code. If the description has any counter, cycle, streak, or "after N" phrase, ask about edge cases at 0 and 1 and N specifically.
The goal is to resolve ambiguity early so the generated code matches what the user actually wants, not what you assumed.
Phase 2: Study Reference Examples
Read the architecture and conventions guides to internalize the rules:
- Architecture guide: TEA structure, file organization, type patterns
- Conventions guide: naming, Effect-TS patterns, anti-patterns
- Verification checklist: not just for Phase 5, also the generation bar. Skim the Quality Bar section now so you generate code that already meets it rather than code that will fail review.
- Blind spots: what the Phase 6 reviewer will grade you against. Reading it now is cheaper than fixing it later.
These four files are a snapshot of a moving codebase. When they disagree with the live source under @foldkit or the .d.ts in node_modules, the live source is right. Treat the disagreement as a bug in this skill and say so in your final report.
If you have access to a context7 MCP tool, use it to look up Effect-TS documentation when you're unsure about an API. Effect is a large library. Verify function signatures rather than guessing.
Quality exemplars
Two codebases are the quality bar for generated apps. Not just "patterns to copy" but "the level of craft to match":
@foldkit/packages/typing-game/client/src/: production multi-page app: Submodels, OutMessage, update/view decomposition, curried handler extraction, subscription patterns, domain modules.
@foldkit/packages/website/src/: production Foldkit website: page organization, shared view primitives, route-driven rendering, idiomatic domain separation.
Before generating, spot-check at least ONE file from each (the shape of update.ts / how handlers get extracted / how domain files are structured) and match that level of craft in your output. The generated code should be indistinguishable from hand-written exemplar code.
Then read the tier-specific example files that match the app's complexity. Always read at least one tier-specific example. Never generate from memory alone.
Complexity tiers
Tier 1: Single page, no async, minimal state:
Read @foldkit/examples/counter/src/main.ts
Tier 2: Timers, subscriptions, simple stateful apps:
Read @foldkit/examples/stopwatch/src/main.ts (timer via subscription, Duration field pattern) and @foldkit/examples/todo/src/main.ts (CRUD with localStorage via Flags)
Tier 3: Async operations, loading/error states, API calls, form validation:
Read @foldkit/examples/weather/src/main.ts (HTTP with HttpClient) and @foldkit/examples/form/src/main.ts (uses foldkit/fieldValidation; see the Form Validation section in Phase 4)
Tier 4: URL routing, multiple pages, query parameters:
Read @foldkit/examples/routing/src/main.ts and @foldkit/examples/query-sync/src/main.ts
Tier 5: Complex state, nested domain models, CRUD, drag-and-drop:
Read @foldkit/examples/shopping-cart/src/main.ts (nested domain schemas, cart state) and @foldkit/examples/kanban/src/main.ts (CRUD with DragAndDrop, Flags restoring from localStorage, subscriptions)
Tier 6: Submodels, OutMessage, multi-step forms, auth flows, multi-module apps:
Read @foldkit/examples/auth/src/main.ts (login/signup with Submodels, OutMessage, protected routes) and @foldkit/examples/job-application/src/main.ts (multi-step form with deeply nested Submodels in step/, DatePicker, FileDrop, Listbox, Calendar module for date handling)
Tier 7: Real-time, WebSocket, Managed Resources, production-grade:
Read @foldkit/packages/typing-game/client/src/update.ts, then explore its page/home/ and page/room/ directories for the full Submodel/OutMessage pattern.
Read examples from the target tier AND all lower tiers. A Tier 4 app should reflect patterns from Tiers 1-3 as well.
Phase 2.5: Identify Foldkit UI Component Opportunities
Foldkit ships accessible UI components that handle keyboard navigation, ARIA attributes, and focus management automatically. They live in a separate package, @foldkit/ui, and are imported by name:
import { Button, Dialog, Input } from "@foldkit/ui"
There is no Ui namespace on the foldkit package. Reach for Dialog.view, not Ui.Dialog.view. Deep imports (@foldkit/ui/dialog) work too when you want to keep the barrel out of the bundle.
Before generating, check if any part of the app maps to a built-in component:
| User Need |
Component |
What you get for free |
| Modal/dialog/confirmation |
Dialog |
Focus trapping, Escape to close, scroll locking, backdrop |
| Tabbed content |
Tabs |
Arrow key navigation, aria-selected, roving tabindex |
| Dropdown menu |
Menu |
Arrow keys, typeahead search, aria-expanded, click-outside |
| Autocomplete/tag input |
Combobox |
Filtering, arrow key selection, aria-activedescendant |
| Select dropdown |
Select |
Keyboard selection, aria-selected, positioning |
| Single selection from options |
RadioGroup |
Arrow key cycling, aria-checked, read-only navigation |
| On/off toggle |
Switch |
Spacebar toggle, aria-checked |
| Boolean option |
Checkbox |
Spacebar toggle, aria-checked, indeterminate |
| Expandable section |
Disclosure |
Enter/Space toggle, aria-expanded |
| Floating content on hover/click |
Popover |
Positioning, click-outside, focus management |
| Hover tooltip |
Tooltip |
Show-delay, keyboard dismiss, positioning, aria-describedby |
| Single-select list |
Listbox |
Arrow keys, typeahead, aria-selected |
| Text input |
Input |
Consistent styling/behavior wrapper |
| Multi-line text |
Textarea |
Auto-resize, consistent styling |
| Form group |
Fieldset |
Disabled state propagation, grouping |
| Styled button |
Button |
Consistent click/keyboard handling |
| Inline calendar grid |
Calendar |
Month navigation, keyboard nav, aria-selected, date constraints |
| Date input + popover |
DatePicker |
Calendar popover, input masking, keyboard nav, constraints |
| File upload zone |
FileDrop |
Drag-and-drop, click-to-browse, accept filters, validation |
| Reorderable list |
DragAndDrop |
Pointer + keyboard drag, drop zones, announcement region |
| Transient notifications |
Toast |
Auto-dismiss, pause-on-hover, stacking, role=status/alert |
| Numeric range / price filter |
Slider |
Arrow/Home/End keys, aria-valuenow, multi-thumb ranges |
| Long scrolling list |
VirtualList |
Windowed rendering, scroll anchoring, measured item heights |
| Site/section navigation |
Nav |
Current-page marking, keyboard traversal, landmark semantics |
The package is not one shape.
Stateful Submodels carry their own Model, Message, update, and (mostly) OutMessage, and are embedded via h.submodel: Menu, Listbox, Combobox, Calendar, DatePicker, Dialog, Popover, RadioGroup, Tabs, Tooltip, FileDrop, DragAndDrop, Slider, VirtualList, plus Toast once built through Toast.make(PayloadSchema).
Stateless render helpers have no Model at all. You call view directly with a ViewConfig and your own h, and store the value in your own Model: Button, Input, Textarea, Select, Fieldset, Checkbox, Switch, Disclosure, Nav.
Don't take that split on faith, because components have moved across it (Checkbox, Switch, and Disclosure became controlled render helpers; RadioGroup became a Submodel; Tabs and Slider moved their selection to the parent Model). Read the component's public.d.ts: exporting Model and update means Submodel, exporting only view and a ViewConfig / ViewInputs type means render helper. A render helper does not want a Got* Message.
To use a stateful Submodel:
- Add its Model to your Model:
confirmDialog: Dialog.Model
- Add a
Got* Message: GotConfirmDialogMessage with { message: Dialog.Message }
- Initialize in init:
confirmDialog: Dialog.init({ id: 'confirm-dialog' })
- Delegate in update:
GotConfirmDialogMessage: ({ message }) => ...
- Embed in view via
h.submodel: h.submodel({ slotId: 'confirm-dialog', view: Dialog.view, model: model.confirmDialog, toParentMessage: message => Message.GotConfirmDialogMessage({ message }) }) (add viewInputs for components whose view takes them)
Always prefer Foldkit UI components over hand-rolling interactive widgets. They make accessibility the default, not an afterthought.
For form inputs specifically: every text input, textarea, and button in a form MUST go through Input, Textarea, and Button. This is not optional, even though raw input/textarea HTML elements are available on the view's builder h. The form example (@foldkit/examples/form/src/main.ts) defines inputFieldView and textareaFieldView helpers that wrap Input.view and Textarea.view with label + validation feedback. Copy that helper pattern. Raw input/textarea are for non-form cases (search fields, inline editors) where you're intentionally working below the component layer, and even then, reach for the component first.
If the app uses UI components, always read the ui-showcase example first to understand how components are wired. This is the canonical reference for Foldkit UI integration patterns:
@foldkit/examples/ui-showcase/src/main.ts: root wiring, Got* delegation, toParentMessage helpers
@foldkit/examples/ui-showcase/src/ui/message.ts: how component Messages are structured
@foldkit/examples/ui-showcase/src/ui/model.ts: how component Models are composed
@foldkit/examples/ui-showcase/src/ui/update.ts: how component updates are delegated
@foldkit/examples/ui-showcase/src/ui/subscriptions.ts: which components need Subscriptions lifted into the parent (DragAndDrop, Slider, VirtualList)
@foldkit/examples/ui-showcase/src/ui/toast.ts: read when using Toast. It's unique in that it's parameterized on a payload schema via Toast.make(PayloadSchema), returning a typed module you import from
Directory names under @foldkit/examples/ui-showcase/src/ have moved before. List the directory rather than trusting these paths blind.
For apps using DatePicker, FileDrop, or other recently-added components, also read the job-application example (see Tier 6 below). It's the most complete real-world integration of these components together.
Phase 3: Determine File Organization
Match the file structure to the app's complexity. The architecture stays the same at every scale; only the file organization changes.
What lives in which file
Beyond the tier-based layouts below, follow these "schema placement" rules to avoid model.ts bloat:
model.ts holds the Model schema + any schemas that are fields of Model (or composed into fields, like form state / submit state unions). Nothing else.
command.ts holds schemas for the payloads commands send to / receive from external systems, in particular the persistence schema that saveState serializes and flags deserializes. The persistence schema is a command-layer concern, not a model concern; it often looks like a subset of Model but it isn't part of Model.
domain/*.ts holds domain entity schemas and pure operations on them.
message.ts holds messages (only).
route.ts holds route variants + router pipelines.
A common mistake (because kanban colocates SavedBoard in model.ts): putting the persistence schema in model.ts because "it's schema." It's schema for the persistence layer, not for the Model. Move it to where it's used.
Single file (Tier 1-2, under ~300 lines):
src/main.ts ← Model, Message, init, update, view
src/entry.ts ← Runtime.makeApplication + Runtime.run
Split commands + messages (Tier 3, has async operations):
src/main.ts ← Model, init, update, view
src/entry.ts ← Runtime.makeApplication + Runtime.run
src/message.ts ← Message definitions
src/command.ts ← Command functions
Important rule: if you extract command.ts, you MUST also extract message.ts. Commands reference Message constructors (for example, Message.SucceededFetchWeather({...})) as their Effect return values. If Messages live in main.ts and Commands live in command.ts, command.ts imports from main.ts and main.ts uses Commands from command.ts, a circular import. Pull Messages out first, then both main.ts and command.ts import the Message namespace from message.ts.
Full split (Tier 4-5, multiple concerns):
src/main.ts ← init, update, view
src/entry.ts ← Runtime.makeApplication + Runtime.run
src/model.ts ← Model schema
src/message.ts ← Message definitions
src/command.ts ← Command functions
src/route.ts ← Route parser (if routing)
src/view.ts ← View functions (if view is large)
src/domain/ ← Shared domain schemas (if multiple entities)
Submodel directories (Tier 6-7, independent modules):
src/main.ts ← Root init, update, view
src/entry.ts ← Runtime.makeApplication + Runtime.run
src/model.ts ← Root model (contains submodels)
src/message.ts ← Root messages + Got* bridging
src/command.ts ← Shared commands
src/route.ts ← Route parser
src/domain/ ← Shared domain schemas
src/page/
featureA/
main.ts ← Submodel init, update, view
message.ts ← Submodel messages + OutMessage
command.ts ← Submodel commands
featureB/
...
Phase 3.3: Architecture sketch (Tier 4+ only)
For Tier 4+ apps (routing, domain modules, multiple entities, submodels) produce a compact sketch BEFORE generating implementations. Tier 1-3 apps are small enough to generate in one pass; Tier 4+ apps burn a lot of effort if the structure is wrong.
The sketch has five parts. Emit them inline in the conversation, get confirmation, THEN scaffold:
- File tree: the exact paths you will create. Match Phase 3's organization.
- Model shape: the top-level
S.Struct fields and their types. Not the full schema, just the shape.
- Message list: every Message you plan to define, grouped by category (clicks, inputs, commands, out-messages).
- Route list: if routing, every
r('...', {...}) with params and the path each maps to.
- Domain operations: for each file in
domain/, the operations it will expose (Link.byNewest, Link.filterByTag, etc.).
Example for a Tier 4 link saver:
### Sketch
Files:
src/main.ts, entry.ts, model.ts, message.ts, command.ts, route.ts
src/domain/link.ts, index.ts
src/story.test.ts, scene.test.ts
Model:
route: AppRoute
links: ReadonlyArray<Link>
newLinkForm: NewLinkForm (url: Field<string>, title/description/tagsInput: string, submitState)
Messages:
Clicks: ClickedSaveLink, ClickedDeleteLink
Inputs: UpdatedLinkUrl, UpdatedLinkTitle, UpdatedLinkDescription, UpdatedLinkTagsInput, BlurredLinkUrl
Commands: SubmittedNewLinkForm, SucceededSaveLinks, FailedSaveLinks
Routing: ClickedLink, ChangedUrl, CompletedNavigateInternal, CompletedLoadExternal
Toggles: ToggledFavorite
Routes:
HomeRoute → /
NewLinkRoute → /new
TagFilterRoute → /tag/:tag
NotFoundRoute → /* fallback
Domain:
Link: schema + byNewest, filterByTag, toggleFavorite, remove, updateById
After emitting the sketch, ask the user to confirm or adjust. Don't start scaffolding or generation until they do. If the user confirms silently (e.g. "looks good, continue"), proceed. If they adjust, iterate on the sketch. Don't write code against a version they haven't approved.
This step is frequently tempting to skip because the agent "knows what it's doing." Skip it and you ship a fully-generated app that turns out to need structural changes. That's the expensive form of iteration. The sketch is the cheap form.
Phase 3.5: Scaffold the Project
Before generating code, scaffold a runnable project using create-foldkit-app:
npx create-foldkit-app@latest
Run with no flags to drop into the interactive prompts; pick the counter example as the base (simplest starting point) and the user's preferred package manager. The generated project includes:
package.json with all Foldkit and Effect dependencies
vite.config.ts with Tailwind and the Foldkit Vite plugin
tsconfig.json with strict TypeScript settings
index.html with the root container
src/styles.css with Tailwind import
AGENTS.md with Foldkit conventions
Replace the scaffold
Then replace the counter example code in src/main.ts (and add additional source files as needed) with the generated app code.
Phase 3.7: Ground the Foldkit APIs
Before writing any code, READ the type signatures of every Foldkit module you will use. Guessing signatures wastes cycles: each wrong guess is a tsc error, a re-read, an edit, another typecheck. Five minutes of reading prevents thirty minutes of iteration.
The exact files to read
For each Foldkit module you plan to use, read the .d.ts at the paths below. Read the public surface; you don't need the internals. Write a short signature crib in your working notes so you don't have to re-check while generating.
# Every app
<project>/node_modules/foldkit/dist/index.d.ts # top-level re-exports: the authoritative list of what `foldkit` exposes
<project>/node_modules/foldkit/dist/html/index.d.ts # HtmlBuilder<Message>, element signatures, Attribute<Message>, inertHtml
<project>/node_modules/foldkit/dist/message/index.d.ts # defineMessageUnion()
<project>/node_modules/foldkit/dist/schema/index.d.ts # ts(), r()
<project>/node_modules/foldkit/dist/struct/index.d.ts # evo(): check nested-update signature
<project>/node_modules/foldkit/dist/update/public.d.ts # Update.Return, Update.ReturnWithOutMessage, Update.combine, Update.refresh
<project>/node_modules/foldkit/dist/runtime/runtime.d.ts # ApplicationInit, RoutingApplicationInit, makeApplication, makeElement
# If using routing
<project>/node_modules/foldkit/dist/route/parser.d.ts # literal, slash, string, int, Route.root, Route.mapTo, Route.oneOf, Route.parseUrlWithFallback
<project>/node_modules/foldkit/dist/url/index.d.ts # toString
<project>/node_modules/foldkit/dist/navigation/index.d.ts # pushUrl, load: all return Effect<void> (no Effect.ignore needed)
# If using async / side effects
<project>/node_modules/foldkit/dist/command/index.d.ts # Command.define: config object with args/messages/interrupt/execute. Command.mapMessages for parent<-child mapping
<project>/node_modules/foldkit/dist/asyncData/public.d.ts # AsyncData: Idle/Loading/Refreshing/Failure/Stale/Success + Schema, match, isPending, hasData, revalidate
<project>/node_modules/foldkit/dist/http/public.d.ts # Http.layer: provide it to Commands that use HttpClient
<project>/node_modules/foldkit/dist/dom/index.d.ts # focus, advanceFocus, scrollIntoView, showDialog, closeDialog, clickElement, lockScroll, unlockScroll, inertOthers, restoreInert, detectElementMovement, waitForAnimationSettled. For time/random/uuid/delay use Effect's Clock, Random, Effect.uuid, Effect.sleep + Duration directly.
# If using subscriptions
<project>/node_modules/foldkit/dist/subscription/index.d.ts # Subscription.make<Model, Message>, Subscription.lift, Subscription.aggregate
# If using mount / managed-resource / custom-element
<project>/node_modules/foldkit/dist/mount/public.d.ts # Mount.define (one-shot) / Mount.defineStream (continuous): per-instance VNode lifecycle
<project>/node_modules/foldkit/dist/managedResource/public.d.ts # ManagedResource.make / lift / aggregate + tag: for stateful runtime objects keyed on Model condition
<project>/node_modules/foldkit/dist/customElement/index.d.ts # CustomElement.define: for typed bindings to native web components
# If the host application drives the program
<project>/node_modules/foldkit/dist/port/public.d.ts # Port.inbound / outbound / emit / stream / subscription
# If using forms
<project>/node_modules/foldkit/dist/fieldValidation/public.d.ts # Field (tagged union), makeRules({required?, rules}), validate, allValid; rule constructors on the Rule namespace (Rule.url(options), Rule.email, Rule.minLength, Rule.pattern, Rule.fromSchema, ...)
# Rule.Rule is [Predicate, Rule.RuleMessage], NOT {test, message}. Field.Invalid has `errors: NonEmptyArray<string>`, not `error: string`.
# If using any UI component (SEPARATE PACKAGE)
<project>/node_modules/@foldkit/ui/dist/<component>/public.d.ts # Model, Message, init, update, view (Submodel-shaped) or ViewConfig (render-helper-shaped), OutMessage when applicable
# Check: is it a Submodel (Menu/Listbox/Combobox/Calendar/DatePicker/Dialog/Popover/RadioGroup/Tabs/Tooltip/FileDrop/DragAndDrop/Slider/VirtualList/Toast) embedded via h.submodel, or a stateless render helper (Button/Input/Textarea/Select/Fieldset/Checkbox/Switch/Disclosure/Nav) called directly? Submodels carry their own Model/Message/update/OutMessage; render helpers don't. Check ViewInputs (for Submodels) or ViewConfig (for helpers) for the slot callbacks (toView, itemToConfig, etc.).
# If using dates
<project>/node_modules/foldkit/dist/calendar/index.d.ts # CalendarDate, today.local (returns Effect<CalendarDate>); for raw millis use Clock.currentTimeMillis
If a path above doesn't resolve, list the package's dist/ and find the module. The .d.ts is authoritative and this file is not. Where the two disagree, the .d.ts is right and the disagreement is a bug in this skill worth reporting.
What to record in the crib
For each symbol you'll call, write one line:
h: HtmlBuilder<Message> (view parameter, supplied by the runtime): { div, input (VOID), textarea, button, Class, Href, For, Id, Role, OnClick(Message), OnInput(value=>Message), OnBlur(Message), OnSubmit(Message), keyed, empty, submodel, ... }
Route.mapTo(schema)(parser): curried
pushUrl(path): Effect<void> // NOT fallible, no Effect.ignore needed
urlToString(url: Url): string
Update.Return<Model, Message>: readonly [Model, ReadonlyArray<Command<Message>>]
Command.mapMessages(commands, toParentMessage): re-tag a child's Commands
AsyncData.Schema(DataSchema, ErrorSchema): { schema, Idle(), Loading(), Success({data}), Failure({error}), ... }
AsyncData.match(value, { onIdle, onLoading, onRefreshing, onFailure, onStale, onSuccess })
// handlers take BARE values, except onStale:
// onIdle: () => B onLoading: () => B
// onRefreshing: (data) => B onSuccess: (data) => B
// onFailure: (error) => B onStale: ({ error, data }) => B
Command.define(name, { args, messages, execute }): every input is a named field.
`execute` binds at DEFINITION and receives the decoded args object directly, so
you destructure the fields themselves; the call site passes args:
const Fetch = Command.define('Fetch', {
args: { id: S.String },
messages: [Ok, Err],
execute: ({ id }) => ...,
})
update: [Fetch({ id })] // NOT Fetch({ id })(effect)
Document: NOT generic, and `body` is a single Html, not an array
Input.view({ id, value, onInput, isInvalid?, type?, placeholder?, toView: (attrs) => Html }, h)
// from '@foldkit/ui', NOT Ui.Input
// attrs: { label: ReadonlyArray<Attribute<M>>, input: ..., description: ... }
Field (schema): NotValidated | Validating | Valid | Invalid(errors: NonEmpty<Rule Message>)
Specific API pitfalls the generator hits repeatedly
Record these in the crib and keep them visible while generating:
input and br and other void elements take ONLY attributes: input([...]), never input([...], []). textarea and button DO take children.
- The children argument is optional on every other element. Omit it when there are none:
div([Class('divider')]), not div([Class('divider')], []). Attributes stay required, so div([]) is how an element with neither is written. keyed is the same: h.keyed('li')(key, [attrs]), not h.keyed('li')(key, [attrs], []).
UrlRequest tags are Internal and External, not InternalUrl / ExternalUrl.
OnClick and OnSubmit take a Message directly, not a () => Message. Only OnInput takes (value) => Message because it needs the input value.
keyed, empty are properties on the builder h the view receives as its last parameter. They are not top-level exports of foldkit/html.
- Attribute helpers are specific:
Value(...), Type(...), Placeholder(...), Href(...), Target(...), Rel(...), Rows(n), Id(...), For(...), Role(...), AriaLabel(...). There is no generic Attr('...', '...').
ApplicationInit<Model, Message, Flags> has no URL parameter. For routed apps, use RoutingApplicationInit<Model, Message, Flags>: the second arg is url: Url.
Route.mapTo takes the route schema, not a factory function. pipe(literal('new'), Route.mapTo(NewLinkRoute)). NOT Route.mapTo(() => NewLinkRoute()).
Effect.ignore is ONLY for fallible Effects. pushUrl(path).pipe(Effect.as(Message())). No Effect.ignore because pushUrl returns Effect<void>.
Command.define takes a config object with a messages array: Command.define('Fetch', { messages: [Message.SucceededFetch, Message.FailedFetch], execute }). messages is required and is always an array, even for one Message: Command.define('ReadClock', { messages: [Message.RecordedTime], execute }).
makeRules takes { required?: Rule.RuleMessage, rules: Array<Rule.Rule> } where Rule.Rule = [Predicate, Rule.RuleMessage]: a tuple, NOT { test, message }. Rule constructors live on the Rule namespace (Rule.url({ message }), Rule.email(message?), Rule.minLength(n, message?), Rule.pattern(regex, message?), Rule.fromSchema(schema, message)).
Field.Invalid has errors: NonEmptyArray<string>, not error: string. Use Array.headNonEmpty(errors) to get the first message; use Rule.resolveMessage(message, value) to resolve a rule message to its final string.
- Route variants are
HomeRoute, NewLinkRoute, etc., with the Route suffix. Every exemplar uses this convention.
- Routers are callable for printing:
homeRouter() returns '/', tagFilterRouter({ tag: 'foo' }) returns '/tag/foo'. Never hand-construct URLs.
- UI components come from
@foldkit/ui, not from a Ui namespace on foldkit. import { Dialog, Input } from '@foldkit/ui', then Dialog.view(...). There is no Ui export on the foldkit package.
HttpClient and HttpClientRequest come from effect/unstable/http, not @effect/platform. Provide the client to the Command's Effect with Effect.provide(effect, Http.layer), where Http is imported from foldkit. @effect/platform-browser is a different thing, used for BrowserKeyValueStore and BrowserCrypto.
- Map a child Submodel's Commands with
Command.mapMessages(childCommands, message => Message.GotChildMessage({ message })). Not Command.mapEffect.
- Name the update return type once per file, and prefer
Update.Return<Model, Message> from foldkit/update for the alias. Spelling the tuple out by hand is fine. Pass the alias to Message.match<UpdateReturn> and omit a redundant : UpdateReturn annotation from update. Use M.withReturnType<UpdateReturn>() only for an Effect Match over a different tagged union inside a handler.
- Branch on a Model array with
Array.match, not the predicates. Array.isArrayEmpty and Array.isArrayNonEmpty (note the names: not isEmptyArray / isNonEmptyArray) take a mutable Array<A>, so neither compiles against the ReadonlyArray an S.Array(...) field decodes to. Array.match takes ReadonlyArray and is what the exemplars use.
empty and keyed are properties on h, so they are never in the foldkit/html import list. Import the types (import type { Document, Html, HtmlBuilder } from 'foldkit/html') and reach for h.empty / h.keyed off the view's builder.
Phase 4: Generate the App
Generate files following the architecture and conventions guides exactly. Write all source files into the scaffolded project's src/ directory. For each file, follow these rules:
Model
- Define as
S.Struct with Effect Schema types
- Use discriminated unions for state:
Idle | Loading | Error | Ok, never booleans for multi-valued state
- Use
Option for fields that may be absent. Never empty strings or null
- Prefix Option-typed fields with
maybe: maybeCurrentUser, maybeError
- For remote data, use the
AsyncData module rather than hand-rolling a union. AsyncData.Schema(WeatherData, S.String) returns { schema, Idle, Loading, Refreshing, Failure, Stale, Success }; put schema in the Model and build values with the constructors. The Refreshing and Stale states are the point: they let a reload keep the current data on screen, and a failed reload keep it rather than discarding it. @foldkit/examples/weather/src/main.ts is the canonical use. Read AsyncData.match's signature before calling it: the handlers take bare values (onSuccess: data => ..., onFailure: error => ...), except onStale, which takes { error, data }
- For non-remote multi-valued state (form steps, editor modes, connection phases), define variants with
ts() and compose into an S.Union. See Discriminated Unions for State in conventions.md
- For apps with multiple domain entities referenced across modules, extract shared schemas into
src/domain/ (e.g., domain/product.ts, domain/session.ts). See the shopping-cart and auth examples for this pattern, and read @foldkit/packages/website/src/page/projectOrganization.ts for guidance on when and how to structure domain modules
Messages
Declare the Message union and its type together:
export const Message = defineMessageUnion({
ClickedSubmit: {},
UpdatedEmail: { value: S.String },
SucceededLogin: { user: User },
FailedLogin: { error: S.String },
CompletedFocusInput: {},
})
export type Message = typeof Message.Type
Keep the defineMessageUnion() declaration and type Message alias adjacent. Construct variants through the namespace, such as Message.ClickedSubmit() and Message.UpdatedEmail({ value }). Never destructure constructors from Message or OutMessage; the owning namespace stays visible at every call site.
Keep each case's payload object on one line when it fits. Let Prettier wrap payloads that need more space, so the declaration remains easy to scan as one variant per line.
Name messages by category:
Clicked*: button/link clicks
Updated*: input value changes (with { value: S.String }) and external state updates from subscriptions (UpdatedRoom, UpdatedPlayerProgress)
Submitted*: form submissions
Succeeded* / Failed*: paired, for commands that can meaningfully fail
Completed*: every other Command result, named from the Command (verb+object: CompletedFocusInput, CompletedGenerateCardId)
Got*: child module results via OutMessage pattern
Pressed*: keyboard input
Blurred*: focus loss
Selected*: choice made from a list
Toggled*: binary state flip
Every message must carry meaning. No NoOp.
Flags (if the initial Model needs side effects)
- Define a
Flags Schema for data the initial Model needs from side effects
- Define
flags as an Effect<Flags> that computes the values (localStorage reads, current time, etc.)
- Pass
flags to Runtime.run(application, { flags }) for a fresh browser boot. Hydrated applications call Runtime.hydrate(application, { buildId: import.meta.env.FOLDKIT_BUILD_ID }) and use only the server-encoded Flags payload
- Pass the result into init. Never perform side effects at module level or inside init directly
- See the Flags section in architecture.md for the full pattern
Init
- Return
[Model, ReadonlyArray<Command<Message>>]
- If Flags are used, accept them as the first parameter:
(flags: Flags) => [Model, Commands] or (flags: Flags, url: Url) => [Model, Commands]
- Include startup Commands (initial fetch, focus first input, etc.)
- Use callable Schema constructors for the initial Model:
Model({ field: value })
Update
Name the return type once per file from the framework's alias:
type UpdateReturn = Update.Return<Model, Message>
Update.ReturnWithOutMessage<Model, Message, OutMessage> is the Submodel counterpart. Update.Return is the preferred spelling; a hand-written tuple alias is what most examples still use and is fine.
Use Message.match<UpdateReturn>(message, {...}). Never switch. Keep Effect Match for other tagged unions, partial matches with fallbacks, and handlers shared across several tags
Use evo(model, { field: () => newValue }) for immutable updates
When a Succeeded* handler has to write several caches and kick off refetches, sequence them with Update.combine(model, [step, step, ...]) and build the refetch steps with Update.refresh({ read, revalidate, write, load }), which reloads a cache only when it actually holds data. @foldkit/examples/route-transitions/src/main.ts shows both
In evo, use point-free field transformers when the update only depends on that field's current value: items: Array.map(updateItem), count: Number.increment, priceSlider: Slider.reflectRange({ min: minPrice, max: maxPrice }). Use () => value for replacement values from Messages, child updates, Commands, or other Model fields.
Extract complex handlers to separate functions when a case exceeds ~15 lines
For Submodels: return [Model, ReadonlyArray<Command<Message>>, Option.Option<OutMessage>]
See the OutMessage pattern in architecture.md. Child modules signal to parents via Option.some(OutMessage), parents handle with Got* Messages and Message.match
Commands
- Define Command identities with
Command.define, whose second argument is a config object: args (optional) declares the args Schema, messages lists every Message the Command can produce, and execute holds the Effect. With args the shape is Command.define('Fetch', { args: { id: S.String }, messages: [Message.SucceededFetch, Message.FailedFetch], execute: ({ id }) => Effect }): execute binds at definition and receives the args, and the update returns Fetch({ id })
- To make a Command interruptible, add
interrupt. interrupt: true keys every invocation by the Command name; interrupt: { keyFields, toKey } selects the args that identify an invocation and derives its key so concurrent invocations can be cancelled independently. The selected fields become the args required by the Definition's Interrupt constructor
- Always assign defini
…(truncated)
1---2name: generate-program3description: Generate a complete, idiomatic Foldkit program from a natural language description. Use when the user wants to create a new Foldkit program, scaffold a project, or says things like "build me a..." or "I want a program that..."4---56Generate a complete Foldkit program based on this description:78**$ARGUMENTS**910## Phase 1: Analyze the Description1112Before writing any code, analyze the description to identify:13141. **Domain entities**: nouns that become Model fields (e.g., "todos", "user", "score")152. **User interactions**: verbs that become Messages (e.g., "add", "delete", "filter", "submit")163. **Async operations**: external data that becomes Commands (e.g., "fetch weather", "save to localStorage")174. **Real-time needs**: streaming data that becomes Subscriptions (e.g., "live updates", "countdown", "WebSocket")185. **Pages/navigation**: URL structure that becomes routes (e.g., "home page", "detail page")196. **UI component needs**: interactive widgets that map to Foldkit UI components (e.g., "dropdown" → Menu, "modal" → Dialog, "tabs" → Tabs, "autocomplete" → Combobox, "date picker" → DatePicker, "file upload" → FileDrop, "reorderable list" → DragAndDrop, "toast/notification" → Toast, "hover tooltip" → Tooltip, "range/price filter" → Slider, "long scrolling list" → VirtualList)207. **Form validation needs**: required fields, format checks, async uniqueness → `foldkit/fieldValidation` module (see Phase 4)218. **Date handling**: birthdays, deadlines, scheduling → `Calendar` module + `DatePicker` or `Calendar` from `@foldkit/ui`229. **File handling**: uploads, attachments, images → `File` module + `FileDrop` from `@foldkit/ui`2310. **Remote data**: anything fetched, cached, refreshed, or revalidated → the `AsyncData` module (see Phase 4). Don't hand-roll a loading/error union2411. **Multi-state flows**: a described process that moves through several named steps with rules about which step follows which (checkout, onboarding, multi-step approval, a connection lifecycle) → consider the `Machine` module (`foldkit/experimental`). Writing the transitions as a table makes the edge set enumerable, so `unreachableStates()` and `deadTransitions()` catch a missing or unreachable step by computation instead of by review. Raise it as an option in the analysis you present, noting it is under `experimental/`, and let the user choose. Plain `ts()` unions with one `M.tagsExhaustive` are still right for a flow of two or three states. `@foldkit/examples/state-machine/` is the reference2512. **Host embedding**: the program runs inside another app ("a widget in our React app", "embed this in an existing page", "the host needs to control it") → `Runtime.makeElement` plus the `Runtime.embed` lifecycle handle, with Flags for initial data and Ports for ongoing communication in both directions. `@foldkit/examples/embedding/` is the canonical reference: a plain TypeScript host driving a Foldkit widget end to end2627Present this analysis to the user before proceeding.2829If the description is detailed and unambiguous, summarize the analysis and confirm before moving on. But if there are gaps (unclear state transitions, vague UI requirements, unspecified error handling, missing edge cases, ambiguous domain boundaries, unclear counter/reset semantics), ask targeted clarifying questions before proceeding. Don't ask open-ended questions like "anything else?". Ask specific questions about the gaps you found.3031**UX/behavior gaps:**3233- "Should the todo list persist across page reloads (localStorage), or start fresh each session?"34- "When the API call fails, should the app show an inline error or a dialog?"35- "You mentioned 'users can edit items'. Is that inline editing or a separate edit page?"3637**Domain-logic gaps (easy to miss, expensive to fix):**3839- "When the user skips an interval, does that count as 'completed' for purposes of the streak?"40- "Does a counter that tracks 'completed' increments on successful actions only, or on skipped actions too?"41- "If the user triggers a reset mid-flow, does the counter reset with it, or persist across resets?"42- "You mentioned 'after N events, trigger X'. Is that N events total, or N events since the last X?"43- "On the Nth action in a cycle, which action does it trigger, the cycle's first or last?"4445Domain-logic questions often surface off-by-one bugs before they hit the code. If the description has any counter, cycle, streak, or "after N" phrase, ask about edge cases at 0 and 1 and N specifically.4647The goal is to resolve ambiguity early so the generated code matches what the user actually wants, not what you assumed.4849## Phase 2: Study Reference Examples5051Read the architecture and conventions guides to internalize the rules:5253- [Architecture guide](architecture.md): TEA structure, file organization, type patterns54- [Conventions guide](conventions.md): naming, Effect-TS patterns, anti-patterns55- [Verification checklist](checklist.md): not just for Phase 5, also the generation bar. Skim the **Quality Bar** section now so you generate code that already meets it rather than code that will fail review.56- [Blind spots](blindSpots.md): what the Phase 6 reviewer will grade you against. Reading it now is cheaper than fixing it later.5758These four files are a snapshot of a moving codebase. When they disagree with the live source under `@foldkit` or the `.d.ts` in `node_modules`, the live source is right. Treat the disagreement as a bug in this skill and say so in your final report.5960If you have access to a context7 MCP tool, use it to look up Effect-TS documentation when you're unsure about an API. Effect is a large library. Verify function signatures rather than guessing.6162### Quality exemplars6364Two codebases are the _quality bar_ for generated apps. Not just "patterns to copy" but "the level of craft to match":6566- `@foldkit/packages/typing-game/client/src/`: production multi-page app: Submodels, OutMessage, update/view decomposition, curried handler extraction, subscription patterns, domain modules.67- `@foldkit/packages/website/src/`: production Foldkit website: page organization, shared view primitives, route-driven rendering, idiomatic domain separation.6869Before generating, spot-check at least ONE file from each (the shape of `update.ts` / how handlers get extracted / how domain files are structured) and match that level of craft in your output. The generated code should be indistinguishable from hand-written exemplar code.7071Then read the tier-specific example files that match the app's complexity. **Always read at least one tier-specific example.** Never generate from memory alone.7273### Complexity tiers7475**Tier 1: Single page, no async, minimal state:**76Read `@foldkit/examples/counter/src/main.ts`7778**Tier 2: Timers, subscriptions, simple stateful apps:**79Read `@foldkit/examples/stopwatch/src/main.ts` (timer via subscription, `Duration` field pattern) and `@foldkit/examples/todo/src/main.ts` (CRUD with localStorage via Flags)8081**Tier 3: Async operations, loading/error states, API calls, form validation:**82Read `@foldkit/examples/weather/src/main.ts` (HTTP with `HttpClient`) and `@foldkit/examples/form/src/main.ts` (uses `foldkit/fieldValidation`; see the Form Validation section in Phase 4)8384**Tier 4: URL routing, multiple pages, query parameters:**85Read `@foldkit/examples/routing/src/main.ts` and `@foldkit/examples/query-sync/src/main.ts`8687**Tier 5: Complex state, nested domain models, CRUD, drag-and-drop:**88Read `@foldkit/examples/shopping-cart/src/main.ts` (nested domain schemas, cart state) and `@foldkit/examples/kanban/src/main.ts` (CRUD with `DragAndDrop`, Flags restoring from localStorage, subscriptions)8990**Tier 6: Submodels, OutMessage, multi-step forms, auth flows, multi-module apps:**91Read `@foldkit/examples/auth/src/main.ts` (login/signup with Submodels, OutMessage, protected routes) and `@foldkit/examples/job-application/src/main.ts` (multi-step form with deeply nested Submodels in `step/`, `DatePicker`, `FileDrop`, `Listbox`, `Calendar` module for date handling)9293**Tier 7: Real-time, WebSocket, Managed Resources, production-grade:**94Read `@foldkit/packages/typing-game/client/src/update.ts`, then explore its `page/home/` and `page/room/` directories for the full Submodel/OutMessage pattern.9596Read examples from the target tier AND all lower tiers. A Tier 4 app should reflect patterns from Tiers 1-3 as well.9798## Phase 2.5: Identify Foldkit UI Component Opportunities99100Foldkit ships accessible UI components that handle keyboard navigation, ARIA attributes, and focus management automatically. They live in a **separate package**, `@foldkit/ui`, and are imported by name:101102```ts103import { Button, Dialog, Input } from "@foldkit/ui"104```105106There is no `Ui` namespace on the `foldkit` package. Reach for `Dialog.view`, not `Ui.Dialog.view`. Deep imports (`@foldkit/ui/dialog`) work too when you want to keep the barrel out of the bundle.107108Before generating, check if any part of the app maps to a built-in component:109110| User Need | Component | What you get for free |111| ------------------------------- | ------------- | --------------------------------------------------------------- |112| Modal/dialog/confirmation | `Dialog` | Focus trapping, Escape to close, scroll locking, backdrop |113| Tabbed content | `Tabs` | Arrow key navigation, aria-selected, roving tabindex |114| Dropdown menu | `Menu` | Arrow keys, typeahead search, aria-expanded, click-outside |115| Autocomplete/tag input | `Combobox` | Filtering, arrow key selection, aria-activedescendant |116| Select dropdown | `Select` | Keyboard selection, aria-selected, positioning |117| Single selection from options | `RadioGroup` | Arrow key cycling, aria-checked, read-only navigation |118| On/off toggle | `Switch` | Spacebar toggle, aria-checked |119| Boolean option | `Checkbox` | Spacebar toggle, aria-checked, indeterminate |120| Expandable section | `Disclosure` | Enter/Space toggle, aria-expanded |121| Floating content on hover/click | `Popover` | Positioning, click-outside, focus management |122| Hover tooltip | `Tooltip` | Show-delay, keyboard dismiss, positioning, aria-describedby |123| Single-select list | `Listbox` | Arrow keys, typeahead, aria-selected |124| Text input | `Input` | Consistent styling/behavior wrapper |125| Multi-line text | `Textarea` | Auto-resize, consistent styling |126| Form group | `Fieldset` | Disabled state propagation, grouping |127| Styled button | `Button` | Consistent click/keyboard handling |128| Inline calendar grid | `Calendar` | Month navigation, keyboard nav, aria-selected, date constraints |129| Date input + popover | `DatePicker` | Calendar popover, input masking, keyboard nav, constraints |130| File upload zone | `FileDrop` | Drag-and-drop, click-to-browse, accept filters, validation |131| Reorderable list | `DragAndDrop` | Pointer + keyboard drag, drop zones, announcement region |132| Transient notifications | `Toast` | Auto-dismiss, pause-on-hover, stacking, role=status/alert |133| Numeric range / price filter | `Slider` | Arrow/Home/End keys, aria-valuenow, multi-thumb ranges |134| Long scrolling list | `VirtualList` | Windowed rendering, scroll anchoring, measured item heights |135| Site/section navigation | `Nav` | Current-page marking, keyboard traversal, landmark semantics |136137The package is not one shape.138139**Stateful Submodels** carry their own Model, Message, update, and (mostly) OutMessage, and are embedded via `h.submodel`: `Menu`, `Listbox`, `Combobox`, `Calendar`, `DatePicker`, `Dialog`, `Popover`, `RadioGroup`, `Tabs`, `Tooltip`, `FileDrop`, `DragAndDrop`, `Slider`, `VirtualList`, plus `Toast` once built through `Toast.make(PayloadSchema)`.140141**Stateless render helpers** have no Model at all. You call `view` directly with a ViewConfig and your own `h`, and store the value in your own Model: `Button`, `Input`, `Textarea`, `Select`, `Fieldset`, `Checkbox`, `Switch`, `Disclosure`, `Nav`.142143Don't take that split on faith, because components have moved across it (`Checkbox`, `Switch`, and `Disclosure` became controlled render helpers; `RadioGroup` became a Submodel; `Tabs` and `Slider` moved their selection to the parent Model). Read the component's `public.d.ts`: exporting `Model` and `update` means Submodel, exporting only `view` and a `ViewConfig` / `ViewInputs` type means render helper. A render helper does not want a `Got*` Message.144145To use a stateful Submodel:1461471. Add its Model to your Model: `confirmDialog: Dialog.Model`1482. Add a `Got*` Message: `GotConfirmDialogMessage` with `{ message: Dialog.Message }`1493. Initialize in init: `confirmDialog: Dialog.init({ id: 'confirm-dialog' })`1504. Delegate in update: `GotConfirmDialogMessage: ({ message }) => ...`1515. Embed in view via `h.submodel`: `h.submodel({ slotId: 'confirm-dialog', view: Dialog.view, model: model.confirmDialog, toParentMessage: message => Message.GotConfirmDialogMessage({ message }) })` (add `viewInputs` for components whose view takes them)152153**Always prefer Foldkit UI components over hand-rolling interactive widgets.** They make accessibility the default, not an afterthought.154155**For form inputs specifically:** every text input, textarea, and button in a form MUST go through `Input`, `Textarea`, and `Button`. This is not optional, even though raw `input`/`textarea` HTML elements are available on the view's builder `h`. The form example (`@foldkit/examples/form/src/main.ts`) defines `inputFieldView` and `textareaFieldView` helpers that wrap `Input.view` and `Textarea.view` with label + validation feedback. Copy that helper pattern. Raw `input`/`textarea` are for non-form cases (search fields, inline editors) where you're intentionally working below the component layer, and even then, reach for the component first.156157If the app uses UI components, **always read the ui-showcase example first** to understand how components are wired. This is the canonical reference for Foldkit UI integration patterns:158159- `@foldkit/examples/ui-showcase/src/main.ts`: root wiring, `Got*` delegation, `toParentMessage` helpers160- `@foldkit/examples/ui-showcase/src/ui/message.ts`: how component Messages are structured161- `@foldkit/examples/ui-showcase/src/ui/model.ts`: how component Models are composed162- `@foldkit/examples/ui-showcase/src/ui/update.ts`: how component updates are delegated163- `@foldkit/examples/ui-showcase/src/ui/subscriptions.ts`: which components need Subscriptions lifted into the parent (`DragAndDrop`, `Slider`, `VirtualList`)164- `@foldkit/examples/ui-showcase/src/ui/toast.ts`: read when using `Toast`. It's unique in that it's parameterized on a payload schema via `Toast.make(PayloadSchema)`, returning a typed module you import from165166Directory names under `@foldkit/examples/ui-showcase/src/` have moved before. List the directory rather than trusting these paths blind.167168For apps using `DatePicker`, `FileDrop`, or other recently-added components, also read the `job-application` example (see Tier 6 below). It's the most complete real-world integration of these components together.169170## Phase 3: Determine File Organization171172Match the file structure to the app's complexity. The architecture stays the same at every scale; only the file organization changes.173174### What lives in which file175176Beyond the tier-based layouts below, follow these "schema placement" rules to avoid model.ts bloat:177178- **`model.ts`** holds the `Model` schema + any schemas that are fields of Model (or composed into fields, like form state / submit state unions). Nothing else.179- **`command.ts`** holds schemas for the payloads commands send to / receive from external systems, in particular the persistence schema that `saveState` serializes and `flags` deserializes. The persistence schema is a command-layer concern, not a model concern; it often looks like a subset of Model but it isn't part of Model.180- **`domain/*.ts`** holds domain entity schemas and pure operations on them.181- **`message.ts`** holds messages (only).182- **`route.ts`** holds route variants + router pipelines.183184A common mistake (because kanban colocates `SavedBoard` in `model.ts`): putting the persistence schema in `model.ts` because "it's schema." It's schema for the persistence layer, not for the Model. Move it to where it's used.185186**Single file** (Tier 1-2, under ~300 lines):187188```189src/main.ts ← Model, Message, init, update, view190src/entry.ts ← Runtime.makeApplication + Runtime.run191```192193**Split commands + messages** (Tier 3, has async operations):194195```196src/main.ts ← Model, init, update, view197src/entry.ts ← Runtime.makeApplication + Runtime.run198src/message.ts ← Message definitions199src/command.ts ← Command functions200```201202**Important rule:** if you extract `command.ts`, you MUST also extract `message.ts`. Commands reference Message constructors (for example, `Message.SucceededFetchWeather({...})`) as their Effect return values. If Messages live in `main.ts` and Commands live in `command.ts`, `command.ts` imports from `main.ts` _and_ `main.ts` uses Commands from `command.ts`, a circular import. Pull Messages out first, then both `main.ts` and `command.ts` import the `Message` namespace from `message.ts`.203204**Full split** (Tier 4-5, multiple concerns):205206```207src/main.ts ← init, update, view208src/entry.ts ← Runtime.makeApplication + Runtime.run209src/model.ts ← Model schema210src/message.ts ← Message definitions211src/command.ts ← Command functions212src/route.ts ← Route parser (if routing)213src/view.ts ← View functions (if view is large)214src/domain/ ← Shared domain schemas (if multiple entities)215```216217**Submodel directories** (Tier 6-7, independent modules):218219```220src/main.ts ← Root init, update, view221src/entry.ts ← Runtime.makeApplication + Runtime.run222src/model.ts ← Root model (contains submodels)223src/message.ts ← Root messages + Got* bridging224src/command.ts ← Shared commands225src/route.ts ← Route parser226src/domain/ ← Shared domain schemas227src/page/228 featureA/229 main.ts ← Submodel init, update, view230 message.ts ← Submodel messages + OutMessage231 command.ts ← Submodel commands232 featureB/233 ...234```235236## Phase 3.3: Architecture sketch (Tier 4+ only)237238For Tier 4+ apps (routing, domain modules, multiple entities, submodels) produce a compact sketch BEFORE generating implementations. Tier 1-3 apps are small enough to generate in one pass; Tier 4+ apps burn a lot of effort if the structure is wrong.239240The sketch has five parts. Emit them inline in the conversation, get confirmation, THEN scaffold:2412421. **File tree**: the exact paths you will create. Match Phase 3's organization.2432. **Model shape**: the top-level `S.Struct` fields and their types. Not the full schema, just the shape.2443. **Message list**: every Message you plan to define, grouped by category (clicks, inputs, commands, out-messages).2454. **Route list**: if routing, every `r('...', {...})` with params and the path each maps to.2465. **Domain operations**: for each file in `domain/`, the operations it will expose (`Link.byNewest`, `Link.filterByTag`, etc.).247248Example for a Tier 4 link saver:249250```251### Sketch252253Files:254 src/main.ts, entry.ts, model.ts, message.ts, command.ts, route.ts255 src/domain/link.ts, index.ts256 src/story.test.ts, scene.test.ts257258Model:259 route: AppRoute260 links: ReadonlyArray<Link>261 newLinkForm: NewLinkForm (url: Field<string>, title/description/tagsInput: string, submitState)262263Messages:264 Clicks: ClickedSaveLink, ClickedDeleteLink265 Inputs: UpdatedLinkUrl, UpdatedLinkTitle, UpdatedLinkDescription, UpdatedLinkTagsInput, BlurredLinkUrl266 Commands: SubmittedNewLinkForm, SucceededSaveLinks, FailedSaveLinks267 Routing: ClickedLink, ChangedUrl, CompletedNavigateInternal, CompletedLoadExternal268 Toggles: ToggledFavorite269270Routes:271 HomeRoute → /272 NewLinkRoute → /new273 TagFilterRoute → /tag/:tag274 NotFoundRoute → /* fallback275276Domain:277 Link: schema + byNewest, filterByTag, toggleFavorite, remove, updateById278```279280After emitting the sketch, ask the user to confirm or adjust. Don't start scaffolding or generation until they do. If the user confirms silently (e.g. "looks good, continue"), proceed. If they adjust, iterate on the sketch. Don't write code against a version they haven't approved.281282This step is frequently tempting to skip because the agent "knows what it's doing." Skip it and you ship a fully-generated app that turns out to need structural changes. That's the expensive form of iteration. The sketch is the cheap form.283284## Phase 3.5: Scaffold the Project285286Before generating code, scaffold a runnable project using `create-foldkit-app`:287288```bash289npx create-foldkit-app@latest290```291292Run with no flags to drop into the interactive prompts; pick the counter example as the base (simplest starting point) and the user's preferred package manager. The generated project includes:293294- `package.json` with all Foldkit and Effect dependencies295- `vite.config.ts` with Tailwind and the Foldkit Vite plugin296- `tsconfig.json` with strict TypeScript settings297- `index.html` with the root container298- `src/styles.css` with Tailwind import299- `AGENTS.md` with Foldkit conventions300301### Replace the scaffold302303Then replace the counter example code in `src/main.ts` (and add additional source files as needed) with the generated app code.304305## Phase 3.7: Ground the Foldkit APIs306307Before writing any code, READ the type signatures of every Foldkit module you will use. Guessing signatures wastes cycles: each wrong guess is a tsc error, a re-read, an edit, another typecheck. Five minutes of reading prevents thirty minutes of iteration.308309### The exact files to read310311For each Foldkit module you plan to use, read the `.d.ts` at the paths below. Read the public surface; you don't need the internals. Write a short signature crib in your working notes so you don't have to re-check while generating.312313```text314# Every app315<project>/node_modules/foldkit/dist/index.d.ts # top-level re-exports: the authoritative list of what `foldkit` exposes316<project>/node_modules/foldkit/dist/html/index.d.ts # HtmlBuilder<Message>, element signatures, Attribute<Message>, inertHtml317<project>/node_modules/foldkit/dist/message/index.d.ts # defineMessageUnion()318<project>/node_modules/foldkit/dist/schema/index.d.ts # ts(), r()319<project>/node_modules/foldkit/dist/struct/index.d.ts # evo(): check nested-update signature320<project>/node_modules/foldkit/dist/update/public.d.ts # Update.Return, Update.ReturnWithOutMessage, Update.combine, Update.refresh321<project>/node_modules/foldkit/dist/runtime/runtime.d.ts # ApplicationInit, RoutingApplicationInit, makeApplication, makeElement322323# If using routing324<project>/node_modules/foldkit/dist/route/parser.d.ts # literal, slash, string, int, Route.root, Route.mapTo, Route.oneOf, Route.parseUrlWithFallback325<project>/node_modules/foldkit/dist/url/index.d.ts # toString326<project>/node_modules/foldkit/dist/navigation/index.d.ts # pushUrl, load: all return Effect<void> (no Effect.ignore needed)327328# If using async / side effects329<project>/node_modules/foldkit/dist/command/index.d.ts # Command.define: config object with args/messages/interrupt/execute. Command.mapMessages for parent<-child mapping330<project>/node_modules/foldkit/dist/asyncData/public.d.ts # AsyncData: Idle/Loading/Refreshing/Failure/Stale/Success + Schema, match, isPending, hasData, revalidate331<project>/node_modules/foldkit/dist/http/public.d.ts # Http.layer: provide it to Commands that use HttpClient332<project>/node_modules/foldkit/dist/dom/index.d.ts # focus, advanceFocus, scrollIntoView, showDialog, closeDialog, clickElement, lockScroll, unlockScroll, inertOthers, restoreInert, detectElementMovement, waitForAnimationSettled. For time/random/uuid/delay use Effect's Clock, Random, Effect.uuid, Effect.sleep + Duration directly.333334# If using subscriptions335<project>/node_modules/foldkit/dist/subscription/index.d.ts # Subscription.make<Model, Message>, Subscription.lift, Subscription.aggregate336337# If using mount / managed-resource / custom-element338<project>/node_modules/foldkit/dist/mount/public.d.ts # Mount.define (one-shot) / Mount.defineStream (continuous): per-instance VNode lifecycle339<project>/node_modules/foldkit/dist/managedResource/public.d.ts # ManagedResource.make / lift / aggregate + tag: for stateful runtime objects keyed on Model condition340<project>/node_modules/foldkit/dist/customElement/index.d.ts # CustomElement.define: for typed bindings to native web components341342# If the host application drives the program343<project>/node_modules/foldkit/dist/port/public.d.ts # Port.inbound / outbound / emit / stream / subscription344345# If using forms346<project>/node_modules/foldkit/dist/fieldValidation/public.d.ts # Field (tagged union), makeRules({required?, rules}), validate, allValid; rule constructors on the Rule namespace (Rule.url(options), Rule.email, Rule.minLength, Rule.pattern, Rule.fromSchema, ...)347# Rule.Rule is [Predicate, Rule.RuleMessage], NOT {test, message}. Field.Invalid has `errors: NonEmptyArray<string>`, not `error: string`.348349# If using any UI component (SEPARATE PACKAGE)350<project>/node_modules/@foldkit/ui/dist/<component>/public.d.ts # Model, Message, init, update, view (Submodel-shaped) or ViewConfig (render-helper-shaped), OutMessage when applicable351# Check: is it a Submodel (Menu/Listbox/Combobox/Calendar/DatePicker/Dialog/Popover/RadioGroup/Tabs/Tooltip/FileDrop/DragAndDrop/Slider/VirtualList/Toast) embedded via h.submodel, or a stateless render helper (Button/Input/Textarea/Select/Fieldset/Checkbox/Switch/Disclosure/Nav) called directly? Submodels carry their own Model/Message/update/OutMessage; render helpers don't. Check ViewInputs (for Submodels) or ViewConfig (for helpers) for the slot callbacks (toView, itemToConfig, etc.).352353# If using dates354<project>/node_modules/foldkit/dist/calendar/index.d.ts # CalendarDate, today.local (returns Effect<CalendarDate>); for raw millis use Clock.currentTimeMillis355```356357If a path above doesn't resolve, list the package's `dist/` and find the module. **The `.d.ts` is authoritative and this file is not.** Where the two disagree, the `.d.ts` is right and the disagreement is a bug in this skill worth reporting.358359### What to record in the crib360361For each symbol you'll call, write one line:362363```text364h: HtmlBuilder<Message> (view parameter, supplied by the runtime): { div, input (VOID), textarea, button, Class, Href, For, Id, Role, OnClick(Message), OnInput(value=>Message), OnBlur(Message), OnSubmit(Message), keyed, empty, submodel, ... }365Route.mapTo(schema)(parser): curried366pushUrl(path): Effect<void> // NOT fallible, no Effect.ignore needed367urlToString(url: Url): string368Update.Return<Model, Message>: readonly [Model, ReadonlyArray<Command<Message>>]369Command.mapMessages(commands, toParentMessage): re-tag a child's Commands370AsyncData.Schema(DataSchema, ErrorSchema): { schema, Idle(), Loading(), Success({data}), Failure({error}), ... }371AsyncData.match(value, { onIdle, onLoading, onRefreshing, onFailure, onStale, onSuccess })372 // handlers take BARE values, except onStale:373 // onIdle: () => B onLoading: () => B374 // onRefreshing: (data) => B onSuccess: (data) => B375 // onFailure: (error) => B onStale: ({ error, data }) => B376Command.define(name, { args, messages, execute }): every input is a named field.377 `execute` binds at DEFINITION and receives the decoded args object directly, so378 you destructure the fields themselves; the call site passes args:379 const Fetch = Command.define('Fetch', {380 args: { id: S.String },381 messages: [Ok, Err],382 execute: ({ id }) => ...,383 })384 update: [Fetch({ id })] // NOT Fetch({ id })(effect)385Document: NOT generic, and `body` is a single Html, not an array386Input.view({ id, value, onInput, isInvalid?, type?, placeholder?, toView: (attrs) => Html }, h)387 // from '@foldkit/ui', NOT Ui.Input388 // attrs: { label: ReadonlyArray<Attribute<M>>, input: ..., description: ... }389Field (schema): NotValidated | Validating | Valid | Invalid(errors: NonEmpty<Rule Message>)390```391392### Specific API pitfalls the generator hits repeatedly393394Record these in the crib and keep them visible while generating:395396- **`input` and `br` and other void elements take ONLY attributes**: `input([...])`, never `input([...], [])`. `textarea` and `button` DO take children.397- **The children argument is optional on every other element.** Omit it when there are none: `div([Class('divider')])`, not `div([Class('divider')], [])`. Attributes stay required, so `div([])` is how an element with neither is written. `keyed` is the same: `h.keyed('li')(key, [attrs])`, not `h.keyed('li')(key, [attrs], [])`.398- **`UrlRequest` tags are `Internal` and `External`**, not `InternalUrl` / `ExternalUrl`.399- **`OnClick` and `OnSubmit` take a Message directly**, not a `() => Message`. Only `OnInput` takes `(value) => Message` because it needs the input value.400- **`keyed`, `empty` are properties on the builder `h`** the view receives as its last parameter. They are not top-level exports of `foldkit/html`.401- **Attribute helpers are specific**: `Value(...)`, `Type(...)`, `Placeholder(...)`, `Href(...)`, `Target(...)`, `Rel(...)`, `Rows(n)`, `Id(...)`, `For(...)`, `Role(...)`, `AriaLabel(...)`. There is no generic `Attr('...', '...')`.402- **`ApplicationInit<Model, Message, Flags>` has no URL parameter.** For routed apps, use `RoutingApplicationInit<Model, Message, Flags>`: the second arg is `url: Url`.403- **`Route.mapTo` takes the route schema, not a factory function.** `pipe(literal('new'), Route.mapTo(NewLinkRoute))`. NOT `Route.mapTo(() => NewLinkRoute())`.404- **`Effect.ignore` is ONLY for fallible Effects.** `pushUrl(path).pipe(Effect.as(Message()))`. No `Effect.ignore` because `pushUrl` returns `Effect<void>`.405- **`Command.define` takes a config object with a `messages` array**: `Command.define('Fetch', { messages: [Message.SucceededFetch, Message.FailedFetch], execute })`. `messages` is required and is always an array, even for one Message: `Command.define('ReadClock', { messages: [Message.RecordedTime], execute })`.406- **`makeRules` takes `{ required?: Rule.RuleMessage, rules: Array<Rule.Rule> }` where `Rule.Rule = [Predicate, Rule.RuleMessage]`**: a tuple, NOT `{ test, message }`. Rule constructors live on the `Rule` namespace (`Rule.url({ message })`, `Rule.email(message?)`, `Rule.minLength(n, message?)`, `Rule.pattern(regex, message?)`, `Rule.fromSchema(schema, message)`).407- **`Field.Invalid` has `errors: NonEmptyArray<string>`, not `error: string`.** Use `Array.headNonEmpty(errors)` to get the first message; use `Rule.resolveMessage(message, value)` to resolve a rule message to its final string.408- **Route variants are `HomeRoute`, `NewLinkRoute`, etc., with the `Route` suffix.** Every exemplar uses this convention.409- **Routers are callable for printing**: `homeRouter()` returns `'/'`, `tagFilterRouter({ tag: 'foo' })` returns `'/tag/foo'`. Never hand-construct URLs.410- **UI components come from `@foldkit/ui`, not from a `Ui` namespace on `foldkit`.** `import { Dialog, Input } from '@foldkit/ui'`, then `Dialog.view(...)`. There is no `Ui` export on the `foldkit` package.411- **`HttpClient` and `HttpClientRequest` come from `effect/unstable/http`**, not `@effect/platform`. Provide the client to the Command's Effect with `Effect.provide(effect, Http.layer)`, where `Http` is imported from `foldkit`. `@effect/platform-browser` is a different thing, used for `BrowserKeyValueStore` and `BrowserCrypto`.412- **Map a child Submodel's Commands with `Command.mapMessages(childCommands, message => Message.GotChildMessage({ message }))`.** Not `Command.mapEffect`.413- **Name the update return type once per file**, and prefer `Update.Return<Model, Message>` from `foldkit/update` for the alias. Spelling the tuple out by hand is fine. Pass the alias to `Message.match<UpdateReturn>` and omit a redundant `: UpdateReturn` annotation from update. Use `M.withReturnType<UpdateReturn>()` only for an Effect `Match` over a different tagged union inside a handler.414- **Branch on a Model array with `Array.match`, not the predicates.** `Array.isArrayEmpty` and `Array.isArrayNonEmpty` (note the names: not `isEmptyArray` / `isNonEmptyArray`) take a mutable `Array<A>`, so neither compiles against the `ReadonlyArray` an `S.Array(...)` field decodes to. `Array.match` takes `ReadonlyArray` and is what the exemplars use.415- **`empty` and `keyed` are properties on `h`**, so they are never in the `foldkit/html` import list. Import the types (`import type { Document, Html, HtmlBuilder } from 'foldkit/html'`) and reach for `h.empty` / `h.keyed` off the view's builder.416417## Phase 4: Generate the App418419Generate files following the architecture and conventions guides exactly. Write all source files into the scaffolded project's `src/` directory. For each file, follow these rules:420421### Model422423- Define as `S.Struct` with Effect Schema types424- Use discriminated unions for state: `Idle | Loading | Error | Ok`, never booleans for multi-valued state425- Use `Option` for fields that may be absent. Never empty strings or null426- Prefix Option-typed fields with `maybe`: `maybeCurrentUser`, `maybeError`427- For remote data, use the `AsyncData` module rather than hand-rolling a union. `AsyncData.Schema(WeatherData, S.String)` returns `{ schema, Idle, Loading, Refreshing, Failure, Stale, Success }`; put `schema` in the Model and build values with the constructors. The `Refreshing` and `Stale` states are the point: they let a reload keep the current data on screen, and a failed reload keep it rather than discarding it. `@foldkit/examples/weather/src/main.ts` is the canonical use. Read `AsyncData.match`'s signature before calling it: the handlers take bare values (`onSuccess: data => ...`, `onFailure: error => ...`), except `onStale`, which takes `{ error, data }`428- For non-remote multi-valued state (form steps, editor modes, connection phases), define variants with `ts()` and compose into an `S.Union`. See Discriminated Unions for State in [conventions.md](conventions.md)429- For apps with multiple domain entities referenced across modules, extract shared schemas into `src/domain/` (e.g., `domain/product.ts`, `domain/session.ts`). See the shopping-cart and auth examples for this pattern, and read `@foldkit/packages/website/src/page/projectOrganization.ts` for guidance on when and how to structure domain modules430431### Messages432433Declare the Message union and its type together:434435```ts436export const Message = defineMessageUnion({437 ClickedSubmit: {},438 UpdatedEmail: { value: S.String },439 SucceededLogin: { user: User },440 FailedLogin: { error: S.String },441 CompletedFocusInput: {},442})443export type Message = typeof Message.Type444```445446Keep the `defineMessageUnion()` declaration and `type Message` alias adjacent. Construct variants through the namespace, such as `Message.ClickedSubmit()` and `Message.UpdatedEmail({ value })`. Never destructure constructors from `Message` or `OutMessage`; the owning namespace stays visible at every call site.447448Keep each case's payload object on one line when it fits. Let Prettier wrap payloads that need more space, so the declaration remains easy to scan as one variant per line.449450Name messages by category:451452- `Clicked*`: button/link clicks453- `Updated*`: input value changes (with `{ value: S.String }`) and external state updates from subscriptions (`UpdatedRoom`, `UpdatedPlayerProgress`)454- `Submitted*`: form submissions455- `Succeeded*` / `Failed*`: paired, for commands that can meaningfully fail456- `Completed*`: every other Command result, named from the Command (verb+object: `CompletedFocusInput`, `CompletedGenerateCardId`)457- `Got*`: child module results via OutMessage pattern458- `Pressed*`: keyboard input459- `Blurred*`: focus loss460- `Selected*`: choice made from a list461- `Toggled*`: binary state flip462463Every message must carry meaning. No `NoOp`.464465### Flags (if the initial Model needs side effects)466467- Define a `Flags` Schema for data the initial Model needs from side effects468- Define `flags` as an `Effect<Flags>` that computes the values (localStorage reads, current time, etc.)469- Pass `flags` to `Runtime.run(application, { flags })` for a fresh browser boot. Hydrated applications call `Runtime.hydrate(application, { buildId: import.meta.env.FOLDKIT_BUILD_ID })` and use only the server-encoded Flags payload470- Pass the result into init. Never perform side effects at module level or inside init directly471- See the Flags section in [architecture.md](architecture.md) for the full pattern472473### Init474475- Return `[Model, ReadonlyArray<Command<Message>>]`476- If Flags are used, accept them as the first parameter: `(flags: Flags) => [Model, Commands]` or `(flags: Flags, url: Url) => [Model, Commands]`477- Include startup Commands (initial fetch, focus first input, etc.)478- Use callable Schema constructors for the initial Model: `Model({ field: value })`479480### Update481482- Name the return type once per file from the framework's alias:483484 ```ts485 type UpdateReturn = Update.Return<Model, Message>486 ```487488 `Update.ReturnWithOutMessage<Model, Message, OutMessage>` is the Submodel counterpart. `Update.Return` is the preferred spelling; a hand-written tuple alias is what most examples still use and is fine.489490- Use `Message.match<UpdateReturn>(message, {...})`. Never switch. Keep Effect `Match` for other tagged unions, partial matches with fallbacks, and handlers shared across several tags491- Use `evo(model, { field: () => newValue })` for immutable updates492- When a `Succeeded*` handler has to write several caches and kick off refetches, sequence them with `Update.combine(model, [step, step, ...])` and build the refetch steps with `Update.refresh({ read, revalidate, write, load })`, which reloads a cache only when it actually holds data. `@foldkit/examples/route-transitions/src/main.ts` shows both493- In `evo`, use point-free field transformers when the update only depends on that field's current value: `items: Array.map(updateItem)`, `count: Number.increment`, `priceSlider: Slider.reflectRange({ min: minPrice, max: maxPrice })`. Use `() => value` for replacement values from Messages, child updates, Commands, or other Model fields.494- Extract complex handlers to separate functions when a case exceeds ~15 lines495- For Submodels: return `[Model, ReadonlyArray<Command<Message>>, Option.Option<OutMessage>]`496- See the OutMessage pattern in [architecture.md](architecture.md). Child modules signal to parents via `Option.some(OutMessage)`, parents handle with `Got*` Messages and `Message.match`497498### Commands499500- Define Command identities with `Command.define`, whose second argument is a config object: `args` (optional) declares the args Schema, `messages` lists every Message the Command can produce, and `execute` holds the Effect. With args the shape is `Command.define('Fetch', { args: { id: S.String }, messages: [Message.SucceededFetch, Message.FailedFetch], execute: ({ id }) => Effect })`: `execute` binds at definition and receives the args, and the update returns `Fetch({ id })`501- To make a Command interruptible, add `interrupt`. `interrupt: true` keys every invocation by the Command name; `interrupt: { keyFields, toKey }` selects the args that identify an invocation and derives its key so concurrent invocations can be cancelled independently. The selected fields become the args required by the Definition's `Interrupt` constructor502- Always assign defini503504…(truncated)