The execute tool
How an agent reads and changes a design with execute snippets.
execute runs a JavaScript snippet against a design. The snippet reads and changes the design with the helpers on this page. The local server runs it against the design open in the Knighn app. The hosted server runs it against a design in the Knighn account of the user.
How a call works
- The snippet is the body of an async function. Use
awaitandreturn. - Do the work in few, large calls. Each call is a round trip of some seconds. Set the tokens and define the components in one call. Add a whole screen, or two to three sections, in the next call. Take one screenshot at the end, not one after each section.
- Put changes that do not depend on each other in one snippet, such as all the fixes after a screenshot.
- Do not read again what the last response gave you. It lists the ids of new layers with a
data-name, and the helpers return ids. - A call is one transaction. In the app, it is one undo step with the name "Agent edit".
- Before the snippet runs, Knighn checks all of it and gives every problem at once, each with its line: names that are not defined (a misspelt helper, a value that an earlier call did not keep), helper calls with too few or too many arguments,
tsx.Nameblocks that do not exist, and errors in the TSX ofDefine,Insert,ReplaceandAddScreencalls whose TSX is in the snippet. This is a check of names and argument counts against api.d.ts, not a TypeScript type check. - If a call throws, it changes nothing, and Knighn discards its globals. The error gives the line of the snippet that failed, with a caret under it.
Commit()keeps the steps before it: when a later step fails, Knighn applies the steps up to the lastCommit(), rolls back the rest, and gives the error in the result and the warnings. Put aCommit()after each part of a large call that must stay.- A call that deletes screens, or that changes or removes more than 200 layers that were there before it, fails unless it passes
{ force: true }toDeleteorReplaceor callsAllowLargeChange(). New layers do not count. DryRun(), ordryRun: truenext toinput, runs the snippet and applies nothing. The warnings say what the call would change.- When a call fails, send
editswith theeditIdfrom the failure, not the whole snippet again. Each edit replacesfindwithreplacein the failed snippet, then the snippet runs again from the start. If it fails again, patch it again with the sameeditId.findmust match the snippet as you patched it. - The snippet is JavaScript. It can use loops,
.map(), template strings and helper functions to write TSX. The TSX that you pass to a helper must stay in the subset of tsx.md. - Keep a value for later calls: assign it without
constorlet, such ashero = Insert(…), or give a helper that makes something the option{ as: 'hero' }. The response lists the names that the call kept. In the app, globals stay until the MCP server stops. On the hosted server, Knighn keeps them for each file and account.GetAppState()lists their names. - Keep only ids and small values in globals. Do not keep large results.
- Hosted server only: give
fileIdon each call.fileId: "new"makes a new design; the new id is inGetAppState().file.id. - Local server only: give
document_idon each call when you work on more than one open design.GetAppState().designslists them;GetAppState().documentIdis the design of this call. - Hosted server only: the user can archive a design. Archived designs are not in
GetAppState().files, and search marks them "archived". When two designs have almost the same name, edit the one that is not archived. Do not edit an archived design unless the user tells you to.
Ids
- Each layer, frame, page and component root has an id of 8 characters, such as
Xk9f2Lm3. Knighn makes the ids. Never make them yourself. - Ids live in memory only. The file has no ids. After Knighn reloads a file, a layer keeps its id when Knighn matches it.
- A component has a name, such as
Button. Helpers that take a layer id also take a component name where this page says so. - A helper that takes an id also takes a name: the
data-nameof a layer, the name of a screen or component, or the name of a page, such asGet('Hero'). Knighn finds the exact name first, then the name in any case. When more than one layer has the name, the error lists them with their ids. A name that is almost right gets a "Did you mean" hint. - A helper that makes something returns an object
{ id, kind, name, … }:AddScreen,Insert,Replace,Copy,Place,AddPage,MakeComponent,InsertTemplate,InsertIcon,InsertLogo,TraceImage,ImportWebPage,ImportHTML,ImportSimulatorandImportSafari. Each helper that takes an id takes this object too. Where a string is necessary, the object gives its id:`${box}`andbox + ''are the id.Definereturns the component names. GetandTreeshow ids.Getputsdata-kn="<id>"on each element and instance of the TSX. Knighn ignoresdata-knin TSX that you give to a helper, so you can change the TSX fromGetand give it back.
TSX in a snippet
Give TSX as a template string, between backticks. Use ${…} in the snippet to put values into it.
When the TSX itself has a template literal, such as a class list with parts in Define, escape it in the snippet: write \` for each backtick and \${ for each ${ of the TSX.
Define(`
export function Pill({ label = 'New', className = '' }: { label?: string; className?: string }) {
return <span className={\`rounded-full bg-muted px-2 py-0.5 text-xs \${className}\`}>{label}</span>
}
`)
TSX blocks
To send long TSX without escapes, put it in the tsx argument of execute, next to input. tsx is an object of named blocks of plain TSX. The snippet reads a block as tsx.Name. A block is not in a JavaScript string, so it needs no escapes for backticks or ${.
{
"input": "Define(tsx.Card)\nAddScreen('Pricing', tsx.Pricing)",
"tsx": {
"Card": "export function Card({ title = 'Plan' }: { title?: string }) {\n return <div className=\"rounded-xl p-6\">{title}</div>\n}",
"Pricing": "<main className=\"flex size-full flex-col bg-background text-foreground\">\n <Card title=\"Pro\" />\n</main>"
}
}
- Without
input, Knighn applies each block by its name. A block withexport functiongoes toDefine. Each other block is the root of the screen with that name:Replacechanges the screen when it exists, andAddScreenadds it when it does not. - Knighn checks the syntax of each block before the snippet runs. An error gives the block name and the line in the block.
editsafter a failure can change the text of a block, as you wrote it.
The response
The response has these parts, in this order:
- The lines from
Print. Result:and the value that the snippet returned. A string comes back whole, up to 200,000 characters, also inside an object. Knighn shortens only bulk data, such as data URLs.Created: each new layer with adata-name, and its id.Did: one line for each change.- Warnings, such as diagnostics and ignored options. Fix them in the next call. A warning also comes when the call used 80% of a limit: its time (about 110 seconds), the size of the result, or the screenshots of one call.
- Notes: how long the call took, and the names that it kept for later calls.
- The images from
Screenshot, each after a line with its name. - On the hosted server, the files from
Export: each with a download link, and each PNG or JPG also as an image.
Three or more changes in a row in one component show as one Did line, such as "Changed 4 layers in Hero".
The API as TypeScript
api.d.ts declares each helper with its arguments and return type. Read it with ReadSkill('api.d.ts'). Knighn checks each call against it before the snippet runs.
API
Only these names exist in a snippet.
// The id of the page that the call works on.
const page: string
// Read
function Tree(scope?: string, options?: { depth?: number }): string
function Get(id: string, options?: { depth?: number; bounds?: boolean }): Promise<NodeView>
function Find(scope: string, selector: string | ((node: NodeInfo) => boolean)): string[]
// Screens, components and frames
function AddScreen(name: string, tsx: string, options?: FrameOptions & Named): Screen
function Define(source: string, options?: Named): string[]
function Place(component: string, options?: FrameOptions & Named & { props?: Props }): Ref
function FindEmptySpace(options: SpaceOptions): Promise<{ x: number; y: number }>
function ListTemplates(options?: {
kind?: 'website' | 'app' | 'mobile' | 'slides'
query?: string
}): TemplateSummary[]
function InsertTemplate(
id: string,
options?: Named & { page?: string; x?: number; y?: number }
): Ref & { screens: string[]; components: string[]; renamed: Record<string, string> }
function UseKit(kit: 'shadcn' | 'heroui', options?: { only?: string[] }): string[]
// Change layers
function Insert(parent: string, tsx: string, options?: Named & { index?: number }): Ref
function Replace(id: string, tsx: string, options?: Named & { force?: boolean }): Ref
function Update(id: string, patch: Patch): void
function Move(id: string, parent: string, index?: number): void
function Copy(id: string, parent?: string, options?: Named & { index?: number }): Ref
function Delete(ids: string | string[], options?: { force?: boolean }): void
function Archive(screens: string | string[]): { page: Ref; screens: string[] }
function ProposeOptions(screen: string, options: { prompt: string; options: string[]; name?: string }): Round
function Rounds(screen?: string): Round[]
function AcceptOption(round: string | Round, option: number): Ref
// States: tooltips, hover, open menus and confirmation steps
function AddTooltip(
id: string,
text: string,
options?: Named & {
side?: 'top' | 'bottom' | 'left' | 'right'
gap?: number
className?: string
name?: string
}
): Promise<Ref>
function AddState(
screen: string,
state: string,
changes?: Record<string, Patch>,
options?: Named
): Screen
// Tokens
function GetTokens(): Tokens
function SetTokens(
tokens: Record<string, string | null>,
options?: { mode?: string | Record<string, string> }
): void
function ImportTokens(
css: string,
options?: { darkMode?: string }
): { tokens: Record<string, string>; dark: Record<string, string> }
// Check and export
function Screenshot(
ids: string | string[],
options?: { scale?: number; state?: 'hover' | 'focus' | 'active' | 'disabled' }
): Promise<void>
function Lint(ids?: string | string[], options?: { layout?: boolean }): Promise<Problem[]>
function Export(
ids: string | string[],
format: 'png' | 'jpg' | 'pdf',
path?: string,
options?: {
scale?: number
page?: 'A4' | 'A5' | 'Letter' | { width: number | string; height: number | string }
}
): Promise<ExportedFile[]>
function Export(ids: string | string[], format: 'svg', path?: string): Promise<ExportedFile[]>
function Export(ids: string | string[], format: 'tsx', path?: string): Promise<string>
// Images, icons and imports
function UploadImage(urlOrDataUrl: string): Promise<string>
function GenerateImage(
prompt: string,
options?: { kind?: 'photo' | 'logo'; width?: number; height?: number }
): Promise<string>
function TraceImage(
image: string,
options?: Named & {
threshold?: number
detail?: number
smoothness?: number
colour?: string
invert?: boolean
replace?: boolean
parent?: string
}
): Promise<Ref>
function InsertIcon(
parent: string,
name: string,
options?: Named & { className?: string; index?: number }
): Promise<Ref>
function InsertLogo(
parent: string,
logo: string | Logo,
options?: Named & {
theme?: 'light' | 'dark'
wordmark?: boolean
className?: string
name?: string
index?: number
}
): Promise<Ref>
function SearchLogos(query: string, options?: { limit?: number }): Promise<Logo[]>
function ImportWebPage(
url:
| string
| {
url: string
width?: number
colorScheme?: 'dark' | 'light'
hideOverlays?: boolean
headers?: Record<string, string>
cookies?:
| { name: string; value: string; domain?: string; path?: string }[]
| Record<string, string>
| string
page?: string
}
): Promise<ImportResult>
function ImportHTML(
html:
| string
| {
html: string
css?: string
width?: number
name?: string
colorScheme?: 'dark' | 'light'
page?: string
method?: 'browser' | 'headless'
}
): Promise<ImportResult & { method: 'browser' | 'headless' }>
function ImportSimulator(options?: {
device?: string
name?: string
page?: string
}): Promise<ImportResult>
function ImportSafari(options?: {
tab?: number
width?: number
colorScheme?: 'dark' | 'light'
hideOverlays?: boolean
page?: string
}): Promise<ImportResult>
function ImportFile(
file: string,
options?: Named & { format?: 'fig' | 'pen' | 'svg'; name?: string; page?: string }
): Promise<ImportResult>
// Help and state
function GetAppState(): Promise<AppState>
function ReadSkill(path?: string): string
function Search(
query: string,
options?: { kinds?: string[]; limit?: number; cursor?: string; scope?: string; compact?: boolean }
): Promise<SearchResult>
function GetStyle(name?: string): StyleSummary[] | Style
function Print(...values: unknown[]): void
function GetErrors(options?: {
limit?: number
since?: number
runtime?: string
}): Promise<ErrorReport[]>
function ResolveError(id: string): Promise<void>
function LinkTo(place: string): PlaceLink
function OpenProject(path: string): Promise<{ opened: string }>
function Select(ids: string | string[], options?: { zoom?: boolean }): { selected: string[] }
// The user's designs (the hosted server)
function ArchiveDesign(fileId: string): Promise<{ id: string; name: string; archived: boolean }>
function UnarchiveDesign(fileId: string): Promise<{ id: string; name: string; archived: boolean }>
function RenameDesign(fileId: string, name: string): Promise<{ id: string; name: string }>
function DuplicateDesign(fileId: string): Promise<{ id: string; name: string }>
function ShareDesign(
fileId: string,
on?: boolean
): Promise<{ id: string; name: string; shared: boolean; url?: string }>
function ListVersions(fileId: string): Promise<Version[]>
function RestoreVersion(
fileId: string,
versionId: string
): Promise<{ id: string; name: string; version: number }>
// Comments (a design on Knighn)
function ListComments(options?: { resolved?: boolean }): Promise<Comment[]>
function AddComment(
target: string | { page?: string; x: number; y: number },
text: string
): Promise<Comment>
function ReplyToComment(id: string, text: string): Promise<Comment>
function ResolveComment(id: string, resolved?: boolean): Promise<Comment>
// The run
function Commit(name?: string): void
function Undo(count?: number): { undone: string[] }
function Redo(count?: number): { redone: string[] }
function RevertTo(name: string): { reverted: string }
function AllowLargeChange(): void
function DryRun(): void
type Props = Record<string, string | number | boolean>
interface FrameOptions {
page?: string // a page id; the default is `page`
x?: number // canvas pixels; the default is empty space to the right of the frames
y?: number
width?: number // the default for AddScreen is 1440
height?: number // the default is none: the frame hugs its content
paper?: 'A4' | 'A5' | 'Letter' // AddScreen: a print page of that size, with p-[20mm] on the root
modes?: Record<string, string> // such as { theme: 'dark' }
}
// Each id argument also takes a Ref, a value read with Get or a name, such as 'Hero'.
interface Ref {
id: string // also what the Ref gives where a string is necessary
kind: string // 'element', 'instance', 'screen', 'page' or 'component'
name: string
}
interface Named {
as?: string // keep the result for later calls under this name
}
interface Screen extends Ref {
component: string // the component name
item: string // the id of the frame, the same as id
root: string // the id of the root element
}
interface Logo {
title: string // such as 'Stripe'
categories: string[]
route: { light: string; dark: string } // the mark for a light and a dark background
wordmark?: { light: string; dark: string } // the mark with the name
url?: string // the brand's site
brandUrl?: string // its brand guidelines
}
interface TemplateSummary {
id: string // such as 'landing-page'
name: string
summary: string
kind: 'website' | 'app' | 'mobile' | 'slides'
tags: string[]
screens: number // the number of frames
}
interface NodeView {
id: string
component: string // the component that holds the layer
tsx: string // TSX with data-kn="<id>" on each element and instance
item?: {
page: string
x: number
y: number
width?: number
height?: number
props: Props
modes?: Record<string, string>
}
bounds?: Record<string, Bounds> // with { bounds: true }: the layer and each element in tsx, by id
}
interface Bounds {
x: number // canvas pixels
y: number
width: number
height: number
}
interface NodeInfo {
id: string
kind: 'element' | 'instance' | 'text' | 'ref' | 'if' | 'fragment' | 'comment'
tag?: string // elements: 'section', 'h1', 'svg'
of?: string // instances: the component, such as 'Button'
name?: string // the data-name
classes: string[] // the literal classes
attrs: Props // the attributes of an element, or the props of an instance
text?: string // the text of a text layer, or of an element with only text in it
component: string // the component that holds the layer
parentId: string | null
}
interface Patch {
classes?: string // add classes; each one replaces the classes that set the same property
className?: string // replace all literal classes; parts such as ${className} stay
text?: string
attrs?: Record<string, string | number | boolean | null> // null removes the attribute
props?: Record<string, string | number | boolean | null> // null goes back to the default
name?: string | null // the data-name; null removes it
x?: number // frames only
y?: number // frames only
width?: number | null // frames only
height?: number | null // frames only; null makes the frame hug its content
modes?: Record<string, string> | null // frames only
}
interface SpaceOptions {
width: number
height: number
direction?: 'right' | 'bottom' | 'left' | 'top' // the default is 'right'
padding?: number // the smallest distance to other frames
nodeId?: string // a frame id: look beside this frame
page?: string
}
interface Tokens {
tokens: Record<string, string> // the @theme block, such as { '--color-accent': '#007aff' }
modes: Record<string, Record<string, Record<string, string>>> // such as { theme: { dark: { … } } }
}
interface Problem {
severity: 'error' | 'warning' | 'info'
code: string // see tsx.md
message: string
line: number // 0 when Knighn knows the layer but not the line
column: number
fix?: string // a replacement, such as the correct class
nodeId?: string // the layer
component?: string // the component that holds the layer
}
interface ExportedFile {
path: string
mimeType: string
size: number
url?: string // a download URL, when the host gives one
}
interface PlaceLink {
place: string // such as 'settings/agents' or 'design/<file id>'
title: string // such as 'Settings: Agents'
where: string // the menu path, such as 'Settings > Agents'
web: string // https://app.knighn.com/go/settings/agents
mac: string // knighn://go/settings/agents opens the macOS app
ios: string // knighn://go/settings/agents opens the iOS app
}
Read
page
page is the id of the page that the call works on. In the app, it is the page that is open. On the hosted server, it is the first canvas page.
return Tree(page, { depth: 2 })
Tree
Tree(scope?, { depth }) returns an outline as text. It has one line for each page, frame, component and layer, with the id last.
scope: a page id, a frame id, a component name, a layer id, or'document'for all pages and components. The default ispage.depth: the levels of layers to show under each frame or component. The default is 3. Deeper layers show as….
return Tree('Button', { depth: 2 })
The result looks like this:
Page Web Pg4Tz8Qa (open)
Frame Landing 1440 wide at 0, 0 It8Kd2Lm
<main> Xk9f2Lm3
<section> Hero Rt5Hq2Na
<h1> "Invoices that pay themselves" Pq3Lm8Vb
<Button label="Start free" /> Mn7Qa2Xc
<section> Stats Vb2Nm5Kq
…
A line shows the tag or the instance, the data-name, the first words of the text and the id.
Get
await Get(id, { depth, bounds }) returns one layer as TSX.
id: a layer id, a frame id, a component name, or a layer name such as'Hero'. A component name gives the whole component: its props type, its variant maps and its function.depth: the levels of children to print. The default is all. Knighn prints deeper children as a comment with their ids.bounds: true: addsbounds, the box of the layer and of each element in the TSX, in canvas pixels. It needs a renderer: the app, or the daemon of the user for the hosted server.
It returns { id, component, tsx, item?, bounds? }. item is there when id is a frame.
const [hero] = Find(page, '[data-name="Hero"]')
const view = await Get(hero, { depth: 2, bounds: true })
Print(view.tsx)
return view.bounds[hero]
Use bounds to check the layout without an image, for example to find a layer that is wider than its frame.
Find
Find(scope, selector) returns the ids of the layers that match, in the order of the document. Find('Hero') with a plain name and no match as a selector gives the layers with that data-name.
scope: the same values as forTree.selector: a selector string, or a function that gets aNodeInfoand returnstruefor a match.
| Selector | Matches |
|---|---|
h1 |
Elements with that tag |
Button |
Instances of that component |
.rounded-lg |
Elements with that class |
[data-name="Price"] |
Layers with that name |
[href], [type="email"] |
Elements with that attribute, and instances with that prop |
section h2, nav > a |
Descendants, and children |
h1, h2 |
Either selector |
Use a function for other matches, such as text or classes with a variant.
const titles = Find(page, 'section h2')
const buttons = Find(page, 'Button')
const small = Find(page, (n) => n.classes.includes('text-xs'))
const signIn = Find(page, (n) => n.text === 'Sign in')
return { titles, buttons, small, signIn }
Screens, components and frames
AddScreen
AddScreen(name, tsx, options) makes a screen. It makes a component with the name name and the root tsx, and it puts a frame of it on a page. It returns { id, kind, name, component, item, root, page }. id and item are the id of the frame.
name: a component name in PascalCase. Knighn changes other names to PascalCase: "Pricing page" becomesPricingPage. Then the result has anotethat gives the component name, and the response notes say it too.tsx: the root element. Start its classes withflex size-full flex-col, and the background and text tokens.options:page,x,y,width,height,paperandmodes. The default width is 1440. Withoutheight, the frame hugs its content. Withoutxandy, Knighn puts the frame in empty space to the right of the other frames.paper:'A4'(794 by 1123),'A5'(559 by 794) or'Letter'(816 by 1056) makes a print page of that size. Knighn adds the page marginp-[20mm]to the root, unless the root has a padding class.- Knighn adds the
classNameprop to the new component.
pricing = AddScreen(
'Pricing',
`<main className="flex size-full flex-col bg-background text-foreground" />`,
{ width: 1440 }
)
return pricing
Define
Define(source) adds or replaces components. It returns their names.
sourceis TSX with one or more exported components, their props types and their variant maps.- A component with the same name as a component in the design replaces it. Its instances and frames change with it.
- Knighn puts the main of each new component on the canvas, beside the screens.
- Escape the backticks and
${of the TSX in the snippet. See TSX in a snippet.
Define(`
type PriceCardProps = {
plan?: string
price?: string
blurb?: string
featured?: boolean
className?: string
}
export function PriceCard({ plan = 'Pro', price = '$24', blurb = 'For growing teams.', featured = false, className = '' }: PriceCardProps) {
return (
<div className={\`flex flex-col gap-6 rounded-xl p-8 \${featured ? 'bg-foreground text-background' : 'bg-surface ring-1 ring-border'} \${className}\`}>
<div className="flex flex-col gap-2">
<h3 className="text-sm font-medium">{plan}</h3>
<p className="text-4xl font-semibold tracking-tight">{price}</p>
<p className="opacity-70">{blurb}</p>
</div>
<Button label="Choose plan" className="w-full" />
</div>
)
}
`)
To change the props of a component, get its source, change it and define it again:
const { tsx } = await Get('PriceCard')
Define(tsx.replace("blurb = 'For growing teams.'", "blurb = 'For teams that grow fast.'"))
Place
Place(component, options) puts one more frame of a component on a page. It returns the frame as { id, kind, name, component, item }.
options:page,x,y,width,height,propsandmodes.- Use it for a mobile or a dark version of a screen. The frames share one component, so a change to the component changes all of them.
- To make a screen that you can change on its own, make a new screen from the TSX of the old one:
AddScreen('PricingB', (await Get(pricing.root)).tsx).
Place('Pricing', { x: 1540, y: 0, width: 390 })
Place('Pricing', { x: 2030, y: 0, width: 390, modes: { theme: 'dark' } })
FindEmptySpace
await FindEmptySpace({ width, height, direction, padding, nodeId, page }) finds an empty area on a page and returns its top-left corner { x, y }.
direction: where to look. The default is'right'.padding: the smallest distance to other frames.nodeId: a frame id. Knighn looks beside this frame, so screens go in a row.
const spot = await FindEmptySpace({ width: 1440, height: 900, padding: 100 })
signup = AddScreen(
'SignUp',
`<main className="flex size-full flex-col items-center justify-center bg-background text-foreground" />`,
{ x: spot.x, y: spot.y, width: 1440, height: 900 }
)
ListTemplates
ListTemplates(options?) lists the template designs that come with Knighn. Each has id, name, summary, kind, tags and screens, the number of frames.
kind:'website','app','mobile'or'slides'.query: a word to find in the name, the summary or the tags.- The templates are a landing page, a pricing page, a dashboard, mobile app screens, a blog post, a portfolio, a settings page and a slide deck.
return ListTemplates({ kind: 'website' }).map((template) => template.id)
InsertTemplate
InsertTemplate(id, options?) adds a template to the design. Use it to start a design quickly, then change the copy and the tokens.
- Knighn adds the components of the template, and puts its frames in empty space on the page.
optionscan givepage,xandy. - Knighn adds the tokens and the fonts of the template that the design does not have. The tokens of the design stay as they are.
- When the design has a component or a variant map with the same name, the one from the template gets a number, such as
Button2.renamedlists these names. - It returns the ids of the new frames in
screensand the names incomponents. Itsidis the first new frame.
const { screens } = InsertTemplate('pricing', { page })
Screenshot(screens[0])
return screens
Change layers
Insert
Insert(parent, tsx, { index }) reads a TSX fragment and puts it into parent. It returns the new root as { id, kind, name, parent }.
parent: the id of an element, or of an instance to fill itschildren.tsx: one root element, one instance or one fragment. A prop reference such as{label}works when the parent is in a component with that prop.index: the position among the children, from 0. The default is last.- Give sections a
data-name. The response lists each new name with its id. - If the TSX has an error,
Insertthrows with the diagnostics, and the call changes nothing.
plans = Insert(
pricing.root,
`
<section data-name="Plans" className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-6 px-6 py-16 @3xl:grid-cols-3">
<PriceCard plan="Starter" price="$0" blurb="For one person." />
<PriceCard plan="Pro" price="$24" blurb="For growing teams." featured />
<PriceCard plan="Team" price="$64" blurb="For companies with many teams." />
</section>
`
)
In the TSX, Knighn writes out a .map() over a literal array (see tsx.md). You can also build repeated TSX in JavaScript in the snippet:
const questions = [
['Can I cancel at any time?', 'Yes. Your plan stops at the end of the month.'],
['Do you have a discount for charities?', 'Yes. Charities get 50% off every plan.'],
['Where is my data?', 'In Sydney, with a copy in Melbourne.']
]
faq = Insert(
pricing.root,
`
<section data-name="FAQ" className="mx-auto flex w-full max-w-3xl flex-col px-6 py-16">
<h2 className="pb-4 text-3xl font-semibold tracking-tight">Questions</h2>
${questions
.map(
([q, a]) => `
<details className="border-b border-border py-5">
<summary className="cursor-pointer font-medium">${q}</summary>
<p className="mt-2 text-muted-foreground">${a}</p>
</details>`
)
.join('')}
</section>
`
)
Replace
Replace(id, tsx) puts new TSX in place of a layer and its children. It returns the new root as { id, kind, name }; the root keeps the old id. The old layers below it and their ids go. { force: true } lets a large replace through the guard.
const [title] = Find(pricing.root, 'h1')
Replace(
title,
`<h1 className="text-center text-6xl font-semibold tracking-tight text-balance">Pricing for every team</h1>`
)
To change a large part, get its TSX, change the text and give it back:
const { tsx } = await Get(faq)
Replace(faq, tsx.replace('Questions', 'Common questions'))
Update
Update(id, patch) changes one layer or one frame.
| Field | Does |
|---|---|
classes |
Adds classes. Each class replaces the classes that set the same property with the same variants. p-6 replaces p-4, and @3xl:p-10 replaces only @3xl:p-8. |
className |
Replaces all literal classes. Parts such as ${className} stay. Use it to remove classes. |
text |
Sets the text of a text layer, or of an element that has only text in it. |
attrs |
Sets attributes, such as { href: '/pricing', 'aria-label': 'Close' }. null removes one. |
props |
Sets the props of an instance or a frame. null goes back to the default. |
name |
Sets the data-name. null removes it. |
x, y, width, height, modes |
Frames only: the position, the size and the modes of the frame. |
On an instance, classes and className change its className prop.
Update(plans, { classes: 'gap-8 @5xl:gap-10' })
const [team] = Find(plans, '[plan="Team"]')
Update(team, { props: { price: '$59', featured: false } })
const [title] = Find(pricing.root, 'h1')
Update(title, { text: 'Simple pricing', name: 'Title' })
Update(pricing.item, { height: 1400 })
Editor attributes
Some features of the Knighn app are attributes on a layer. Set them with attrs in Update, or write them in the TSX. null removes one. They do not change how the design renders, apart from the shader.
| Attribute | Does |
|---|---|
data-locked="true" |
Locks the layer and its children: the user cannot select or move them on the canvas. Your edits still work. Lock a background or a finished part. |
data-layout-grid="columns 12 24 32" |
A layout grid on a frame: 12 columns, 24 px gutters, 32 px margins. "grid 8" is a square grid of 8 px. Put it on the root layer of the frame. The user sees it with View > Layout Grids. |
data-kn-bind="convex:<table>:<id>:<field>" |
Binds a text layer or an img to a field of a document in the Convex database of the user. field can be a path, such as author.name. File > Integrations > Refresh writes the value in. |
const [root] = (await Get(pricing.item)).children
Update(root, { attrs: { 'data-layout-grid': 'columns 12 24 32' } })
const [title] = Find(pricing.root, 'h1')
Update(title, { attrs: { 'data-locked': 'true', 'data-kn-bind': 'convex:plans:k57abc:title' } })
A shader fill is a <knighn-shader> element: an animated background that the design file draws itself. To fill a layer, make the layer relative isolate and put the shader in it as the first child, with the classes absolute inset-0 -z-10 rounded-[inherit]. A shader with a size of its own (h-60 w-80) is a layer by itself.
shaderis one ofmesh-gradient,grain-gradient,dithering,halftone-dots,liquid-metal,simplex-noiseandperlin-noise.colorsis a list of colours with spaces between them (mesh-gradient,grain-gradient,simplex-noise).colorBackandcolorFrontare single colours (grain-gradient,dithering,halftone-dots,perlin-noise).halftone-dotsandliquid-metaltake an image insrc.speedis the animation speed.speed={0}gives a still picture.scaleandrotationwork on each shader.- The user changes the other values in the Fill section of the properties panel.
Update(pricing.root, { classes: 'relative isolate' })
Insert(
pricing.root,
`<knighn-shader shader="mesh-gradient" colors="#e0eaff #241d9a #f75092" speed={0.2} className="absolute inset-0 -z-10 rounded-[inherit]" />`,
{ index: 0 }
)
Move
Move(id, parent, index?) moves a layer to a new parent, or to a new position in the same parent. The default index is last.
Move(faq, pricing.root, 1)
Copy
Copy(id, parent?, { index }) copies a layer and its children. Without parent, the copy goes just after the original. It returns the copy as { id, kind, name, parent }.
const [starter] = Find(plans, '[plan="Starter"]')
const copy = Copy(starter)
Update(copy, { props: { plan: 'Business', price: '$99' } })
Delete
Delete(ids, { force }) deletes one id or a list of ids.
- A layer goes with its children.
- A call that deletes a frame needs
{ force: true }(orAllowLargeChange()), so a frame never goes by mistake. - A frame goes from its page. The component stays.
- A component goes only when no instance and no frame uses it.
Delete(Find(pricing.root, '[data-name="FAQ"]'))
Archive
Archive(screens) moves one frame or a list of frames to the Archive page of the design. It returns { page, screens }: the Archive page and the frames that moved.
- The Archive page is the last page of a design. It holds old ideas. Each new design has one, and
Archivemakes it when a design has none. - When the user tells you to keep the old version of a frame, use
Archiveand notDelete. The frame and its component stay complete. - Do not edit, lint or take screenshots of the frames on the Archive page unless the user tells you to.
Tree()andGetAppState().pagesmark the page (archive: true). - To bring a frame back, use
Move(frame, page).
const old = AddScreen('Pricing old', '<main className="flex size-full flex-col gap-6 p-10" />')
return Archive(old.item)
ProposeOptions
When the user asks for options, ideas or a brainstorm for a screen, do not change the screen. Propose a round: ProposeOptions(screen, { prompt, options, name? }). It puts a Proposals group on the canvas to the right of the screen, with the prompt and one option per TSX entry, and returns the round. The user compares the options in the Proposals tab and accepts one.
- Each option is the TSX of the whole screen. Give 2 to 4 options that differ in a clear way.
promptis the user's request in a few words.nameis the round's name (default "Round N").- A new round goes to the right of the earlier rounds of that screen.
- Do not edit the round groups or their option components by hand.
- In a project tab, use
Propose()for changes to the code, notProposeOptions.
return ProposeOptions('Home', {
prompt: 'Two hero layouts',
options: [
'<main className="flex flex-col items-center gap-6 p-16"><h1 className="text-5xl font-semibold">Ship faster</h1></main>',
'<main className="grid grid-cols-2 gap-8 p-16"><h1 className="text-4xl font-semibold">Ship faster</h1></main>'
]
})
Rounds
Rounds(screen?) lists the rounds of one screen, or of the whole design, oldest first. Each round has status (open or accepted), accepted (the option number) and options. That list is the history of the screen's proposals.
AcceptOption
AcceptOption(round, option) puts option option (1, 2, …) of a round in place of the screen's root, as one undo step, and marks the round accepted. Use it only when the user picks an option.
const round = ProposeOptions('Home', {
prompt: 'Bigger title',
options: ['<main><h1 className="text-6xl">Hi</h1></main>']
})
return AcceptOption(round, 1)
Kits and states
UseKit
UseKit(kit, { only }) defines the common components of a component library, so that instances such as <Button variant="secondary" /> look as the library draws them. kit is 'shadcn' (shadcn/ui) or 'heroui' (HeroUI). It returns the names of the components.
- The components are
Button,Badge,Card,Input,Switch,Checkbox,TabswithTab,Avatar,SeparatorandTooltip, with the variants of the library: shadcn'svariantandsize, HeroUI'scolor,variantandsize. - Knighn adds the tokens that the components use and the theme does not have, with the values of the library. The tokens of the design stay as they are.
- A component that the design has already stays as it is. The response says which ones.
onlydefines some of them:{ only: ['Button', 'Badge'] }.ButtonandCardhave a slot for an icon:icon={<Icon name="plus" className="size-4" />}.
return UseKit('shadcn')
AddTooltip
await AddTooltip(id, text, { side, gap, className }) shows a tooltip next to a layer of a screen. It returns the tooltip.
side:'top','bottom','left'or'right'. The default is'top'.gapis the distance in pixels, 8 by default.- With a renderer, the tooltip is
absolutein the root of the screen, placed from the box of the layer. The root getsrelative. - Without a renderer, the tooltip goes inside the layer, which gets
relative. - Give the layer in the screen, not a layer inside a component definition.
await AddTooltip(plans, 'Prices are in AUD and include GST', { side: 'bottom' })
AddState
AddState(screen, state, changes?) copies a screen beside it for one state, such as a hover, an open menu or a confirmation step. It returns the copy as { id, component, item, root }.
- The copy is a component named after the screen and the state:
AddState('Pricing', 'Delete confirm')makesPricingDeleteConfirm. The original stays as it is. changesmaps thedata-nameof a layer in the copy to anUpdatepatch.- Then change the copy with the other helpers:
Inserta menu or a dialog intoroot.
const confirm = AddState('Pricing', 'Hover', { Plans: { classes: 'opacity-90' } })
Insert(
confirm.root,
`<div data-name="Confirm" role="dialog" className="absolute inset-x-0 top-24 mx-auto w-96 rounded-xl bg-surface p-6 shadow-lg">Cancel your plan?</div>`
)
Tokens
GetTokens
GetTokens() returns the tokens of the design: { tokens, modes }. tokens is the @theme block. modes has the overrides of each mode set, such as { theme: { dark: { '--color-surface': '#1c1c1e' } } }.
return GetTokens()
SetTokens
SetTokens(tokens, { mode }) adds or changes tokens. It merges with the tokens that exist.
- Write each name with its
--prefix, such as'--color-accent'. - A value of
nullremoves the token. - Without
mode, it changes the@themeblock. mode: 'dark'changes the[data-theme="dark"]block.mode: { brand: 'acme' }changes the[data-brand="acme"]block.
SetTokens({
'--color-accent': '#0f766e',
'--color-accent-foreground': '#ffffff',
'--radius-lg': '12px'
})
SetTokens({ '--color-accent': '#2dd4bf', '--color-accent-foreground': '#042f2e' }, { mode: 'dark' })
SetTokens({ '--color-accent': '#e11d48' }, { mode: { brand: 'rose' } })
// A type scale for a print document: text-title, text-body and text-caption.
SetTokens({ '--text-title': '18pt', '--text-body': '10pt', '--text-caption': '8pt' })
See tokens.md for the names to use.
ImportTokens
ImportTokens(css, { darkMode }) reads the tokens of a project's CSS, such as the text of globals.css, and sets them with SetTokens. It returns { tokens, dark }.
- The custom properties of
:root,htmland@theme(also@theme inline) become base tokens. - The custom properties of
.dark,[data-theme="dark"]and:rootin@media (prefers-color-scheme: dark)become thedarkModemode,'dark'by default. - Each
var()is resolved in each mode, so--color-background: var(--background)gets the light value and the dark value. - Other rules, such as component classes and width queries, are not read. Read the CSS file and give its text: the hosted server cannot read files on your computer.
// The text of the project's globals.css.
const css = `@theme inline { --color-background: var(--background); }
:root { --background: oklch(1 0 0); }
.dark { --background: oklch(0.145 0 0); }`
const { tokens, dark } = ImportTokens(css)
Print(tokens, dark)
Check and export
Screenshot
await Screenshot(ids, { scale }) adds one PNG image for each id to the response.
ids: frames or layers. Use at most 4 in one call.scale: the default is 1.state:'hover','focus','active'or'disabled'. The picture shows each target with the classes of that state applied, such ashover:bg-blue-600. Give the layer that has the classes, such as the button, and not its screen. The layers in the target show theirgroup-hover:classes. The design does not change.- The images show the design at the end of the call.
- It needs a renderer: the app, or the daemon of the user for the hosted server. Without one, the response has a warning.
- Take one screenshot at the end of the work, of the frame or of the smallest part that shows the change.
- A frame that is larger than 1600 px on its long side is drawn at a smaller scale. Use
Exportfor the full size. - A render stops after 40 seconds. Then the response has the pictures that were ready, or a layout report from the bounds (layers past their frame or parent, cut text, overlaps). Do not try again at a smaller scale or with fewer pictures: stop, and tell the user that the screenshots do not work now.
GetAppState().capabilities.rendergives the time and the result of the recent renders.
await Screenshot([pricing.item, signup.item])
const [button] = Find(page, 'button')
if (button) await Screenshot(button, { state: 'hover' })
Lint
await Lint(ids?) returns the problems in the design, or in the frames, components and layers that you give. See tsx.md for each code.
- Each problem names its layer with
nodeId, and its component withcomponent. - Many problems have a
fix, such as the container variant formd:px-16. Apply it withUpdate. - An
infoproblem, such as an unknown class, does not break the design. Read it all the same. - The response notes say which checks Lint did and which it skipped. The layout check (
layout/past-bottom: layers past the bottom of a screen with a fixed height) needs a renderer. Without one, or when the bounds do not come in 20 seconds, Lint skips it, gives a warning, and still returns the other problems.Lint(ids, { layout: false })skips the layout check.
const problems = await Lint(pricing.item)
return problems.filter((p) => p.severity !== 'info')
Export
await Export(ids, format, path?, { scale }) exports frames, layers or components.
png,jpgandpdf: one file for each id.pathis a folder. In the app, it is under the MCP root folder. It returns the files. The default scale is 2.- On the hosted server, the daemon of the user makes the files. The response gives a download link for each file, which works for one hour, and shows each PNG and JPG as an image. When no daemon is online,
Exportgives a warning and makes no file. - A frame exports as
png,jpgorpdf. A layer inside a frame exports aspngonly. A PDF keeps the text as real text. - A PDF has one page the size of the frame. With
page, it has pages of that size:{ page: 'A4' }(or'A5','Letter'), or{ page: { width: '210mm', height: '297mm' } }. Knighn fits the frame to the page width and cuts a long frame into pages. svg: one SVG file for each frame, with the layers as the browser drew them. It works on the local server, with the design open in the Knighn app. A layer inside a frame cannot export as SVG.- In the Knighn app on the Mac,
pathcan also be a full folder path, and a relativepathis a folder in Downloads. Each file has its fullpathin the result. In the web app, each PNG, JPG and SVG file has aurl, and a PDF downloads in the browser of the user.{ page }works with the daemon only. tsx: it returns the TSX of the components as text, with the components that they use. Withpath, it also writes a.tsxfile.expo: it returns{ files, skipped }, an Expo Router screen for each screen built from theexpo-swiftuitemplate, with the real@expo/uiSwiftUI controls. See guide/expo-swiftui.md.
return await Export(pricing.item, 'png', 'exports', { scale: 2 })
const letter = AddScreen('Letter', `<main className="flex flex-col gap-[6mm] text-[10pt]" />`, {
paper: 'A4'
})
return await Export(letter.item, 'pdf', 'exports', { page: 'A4' })
return await Export('PriceCard', 'tsx')
Images and icons
UploadImage
await UploadImage(urlOrDataUrl) puts an image into Knighn storage and returns its URL: https://share.knighn.com/a/<sha256>.<ext>. The same bytes always give the same URL. Anyone with the URL can see the image.
- Use only images that the user gave you or asked for. Never make up image URLs.
const photoUrl = 'https://images.example.com/office.jpg' // a URL that the user gave you
const src = await UploadImage(photoUrl)
Insert(
signup.root,
`<img src="${src}" alt="A team at work in a bright office" className="h-120 w-full rounded-xl object-cover" />`,
{ index: 0 }
)
GenerateImage
await GenerateImage(prompt, { kind, width, height }) makes a picture with the Codex CLI on the person's Mac and returns its URL in Knighn storage, ready for <img src>. It takes about a minute.
kindis"photo"(the default: a realistic photograph with no text) or"logo"(a flat mark, centred on a plain white background).widthandheightgive the shape of the picture, not its exact size.- It works only in the Knighn app on a Mac and on the Knighn daemon. Elsewhere it says so: use
SearchPhotosor a placeholder instead. - Use it only when the person asks for a new picture.
const src = await GenerateImage('A small bakery at dawn, warm light', {
kind: 'photo',
width: 1600,
height: 900
})
Insert(
signup.root,
`<img src="${src}" alt="A small bakery at dawn" className="aspect-video w-full rounded-2xl object-cover" />`,
{ index: 0 }
)
TraceImage
await TraceImage(image, { threshold, detail, smoothness, colour, invert, replace }) traces an image layer into vector paths with potrace on the person's Mac. It puts an inline <svg> after the image, or in its place with replace: true, and returns it as { id, kind, name }. The svg keeps the classes of the image.
threshold,detailandsmoothnessgo from 0 to 1.threshold(default 0.5) sets how much of the image is dark.detail(default 0.75) keeps smaller specks when it is higher.smoothness(default 0.75) makes rounder corners.colouris the fill, such as"#1a1a1a".invert: truetraces the light parts.- For an image URL, not a layer, give
{ parent }. - It works best on a logo or a drawing with two colours. It works only in the Knighn app on a Mac and on the Knighn daemon.
const logo = await GenerateImage('A knight chess piece', { kind: 'logo' })
const img = Insert(
signup.root,
`<img data-name="Logo" src="${logo}" alt="" className="size-10" />`,
{ index: 0 }
)
await TraceImage(img, { replace: true, colour: '#111111' })
InsertIcon
await InsertIcon(parent, name, { className, index }) puts a Lucide icon into parent as an inline <svg>. It returns the icon as { id, kind, name }.
- Find names with the search tool, kind
icon. - The icon draws with
currentColor. Set its colour with a text colour class on the icon or on its parent. - Set the size with
className, such assize-4. The default issize-5.
const [cta] = Find(signup.root, 'button')
await InsertIcon(cta, 'arrow-right', { className: 'size-4' })
InsertLogo
await InsertLogo(parent, logo, { theme, wordmark, className, name, index }) puts the brand logo from svgl (svgl.app) into parent as an inline <svg> with its own colours. It returns the logo as { id, kind, name }. Use it for each company, product or tool logo. Never use an emoji or an icon for a brand.
logois the brand name, such as'Stripe', or a result ofSearchLogos.theme: 'dark'takes the version for a dark background. The default is'light'.wordmark: truetakes the logo with the name. It gives an error when svgl has no wordmark.- Set the height with
className. The default ish-8 w-auto. - Knighn removes scripts, event handlers and outside links from the SVG.
const [logos] = Find(page, '[data-name="Customer logos"]')
await InsertLogo(logos, 'Stripe', { className: 'h-8 w-auto' })
SearchLogos
await SearchLogos(query, { limit }) returns the svgl logos whose title matches, as Logo objects: { title, categories, route: { light, dark }, wordmark?, url?, brandUrl? }. The search tool finds logos too, with kind logo.
const [logos] = Find(page, '[data-name="Customer logos"]')
const [vercel] = await SearchLogos('vercel', { limit: 1 })
await InsertLogo(logos, vercel, { theme: 'dark', wordmark: true, className: 'h-6 w-auto' })
ImportWebPage
await ImportWebPage(url) loads a web page in a real browser and adds it to the page as a new screen, in empty space. Each element becomes a layer at the position that the browser measured. It returns { id, screens, report }: the first new canvas item, the ids of all of them, and the parts that the design could not keep.
- Use it only for a page that the user asked for.
widthsets the viewport width in pixels. The default is 1280.colorSchemesets theprefers-color-schemethat the page sees:'dark'(the default) or'light'. A page with a dark theme shows it. Give'light'to get the light version of the page.- It hides the page's start-up overlays: cookie and consent banners, chat widgets, first-run dialogs and pop-ups. Give
hideOverlays: falseto keep them, for example when the user asks for the cookie banner. - It works in the Knighn app and on a Mac with Knighn. Other hosts give an error.
- A Storybook story link (
http://localhost:6006/?path=/story/pages-home--default) gives the story alone, not the Storybook sidebar and toolbar. Give the link as it is. - A page behind a sign-in: give
headers(such as{ Authorization: 'Bearer …' }, sent only to the page's own origin) andcookies(a list of{ name, value, domain, path }, an object of names and values, or the text of aCookieheader,'a=1; b=2'). Only the Knighn desktop app with Chrome, Edge or Chromium, and the Mac daemon, take them. Ask the user for them; do not guess. Knighn does not log them, but do not put them in a design or a reply. - Do not rebuild a live page by hand from its source files. Import it with
ImportWebPage, then edit the screen.
const { screens, report } = await ImportWebPage({ url: 'https://example.com', width: 1440 })
Print(screens, report.lossy.length)
ImportHTML
await ImportHTML({ html, css }) adds HTML text and its CSS to the page as a new screen, in empty space, with editable layers and text. Use it when you have the HTML of a page, not only its URL. It returns { id, screens, report, method }. Given an http or https URL in place of the HTML, it captures the page as ImportWebPage does.
- The CSS can be in
<style>elements, instyleattributes, or incss. Linked stylesheets,@importrules and scripts do not load: put the CSS incss. - A local HTML file: read the file and give its text as
html. The hosted server cannot read files on your computer. - Images need an
httpsor adata:URL. A relative address does not load, andreport.lossysays so. widthsets the screen width and the width that@mediarules see. The default is 1280.namenames the screen; the default is the page<title>.colorSchemesets theprefers-color-schemethat the HTML sees:'dark'(the default) or'light'. HTML with a dark theme shows it. Give'light'to get the light version. Withmethod: 'headless', only theprefers-color-schemerules change:color-schemeandlight-dark()are not applied, and the default text and background colours stay light.methodsays how the import ran."browser": the host loaded the HTML in a browser and made a layer for each element at the position that the browser measured. The Knighn app and a Mac with Knighn do this."headless": there is no browser (the hosted server), so the CSS rules are matched to the elements and each element gets its styles as Tailwind classes. Flex and grid layouts stay layouts.::before,::afterand animations are not kept. Givemethod: 'headless'to get this result on any host.
const html = `<main class="cards"><section class="card"><h2>Pro</h2><p>For teams.</p></section></main>`
const css = `.cards { display: flex; gap: 24px; padding: 32px } .card { padding: 24px; border-radius: 12px; background: #fff }`
const { id, method, report } = await ImportHTML({ html, css, width: 1280, name: 'Pricing' })
Print(method, report.lossy)
ImportSimulator
await ImportSimulator({ device, name }) captures a booted iOS Simulator and adds two screens: the screenshot, and an editable screen made from it. device is a simulator name or udid. The default is the first booted simulator. It works only on a Mac with Knighn.
ImportSafari
await ImportSafari({ tab, width }) adds the page that is open in Safari as a new screen. The default is the current tab of the front Safari window; tab is a tab number of that window, from 1. It works only in the Knighn app on a Mac.
- When Safari allows JavaScript from Apple Events (Develop menu), the screen is the page as the person sees it: signed in, with the text in its fields.
widthis the width of the Safari window unless you give it. - When Safari does not allow it, Knighn captures the tab's address as
ImportWebPagedoes, without the sign-in. colorSchemeandhideOverlaysare as forImportWebPage.
ImportFile
await ImportFile(file, { format, name, page }) adds a Figma .fig file, a Pencil .pen file or an SVG file as new frames. It returns { id, screens, report }, as ImportWebPage does.
fileis an http or https URL, a data URL, SVG text, or a file path.- A path works in the Knighn app on the Mac (a full path), and on a design file on a Mac (a file in the folder of the design). The daemon and the web app read no path: give a URL.
- The format comes from the file name. Give
format('fig','pen'or'svg') when the name has no extension. - It works in the Knighn app and on a Mac with Knighn. The hosted server with no Mac online cannot convert a file.
report.lossylists what the design could not keep. Read it, then fix the frames.
const kit = await ImportFile('/Users/me/Designs/Brand kit.fig')
return { screens: kit.screens, lossy: kit.report.lossy.length }
Import a project or a site (knighn import)
To import a whole code project, a whole site or a design file into the user's account, run the knighn import command in a shell. Do not write a script or a tool of your own for an import, and do not import the pages one at a time with ImportWebPage. If the command cannot do the job, tell the user what is missing.
knighn import .imports a code project (Next.js, Vite, TanStack Start or Expo). It finds every route, starts the dev server and captures each page. It makes one design for each top-level route group (/salient/...gives the design "Salient"). A[slug]route gives one sample page, and a catch-all route gives every page that the project links to.knighn import https://site.comimports one page of a site as one design.--crawladds the pages of the same site that the page links to.knighn import design.figimports a.fig,.pen, SVG or image file as one design.--onemakes one design for the whole project.--only salient,radiantimports only those route groups.--name "Tailwind Plus"puts these words at the start of each design name.--as templatesaves the designs as the user's own Home templates, not as designs.--local <folder>writes.htmlfiles and saves nothing on Knighn.- The command uses the account of the Knighn API key (
KNIGHN_API_KEYor~/.knighn/worker.json). It uploads the images, fonts and thumbnails in the same run. When you run it again, it updates the same designs. When an import stops, the next run continues from the last design that it saved. - Before a large import, ask the user if each result is a template or a design, when the request does not say.
knighn import . --name "Tailwind Plus" --only salient
knighn import https://example.com --crawl
knighn import . --as template --local out
Help and state
GetAppState
await GetAppState() returns the state of the host. Call it first. The main fields are:
host:'app','daemon'or'hosted'.server:'local'(the knighn server on the Mac) or'hosted'(mcp.knighn.com).appConnected: true when the call works on the design open in the Knighn app, live.file: the name, and theidorpath, of the design.pageandpages: each page with its frames (id, component, position and size). The Archive page hasarchive: true: it holds old ideas, so do not edit it unless the user tells you to.components: the component names.selection: what the user selected, as TSX with ids.capabilities: what the host can do, such asscreenshot,layout(bounds) andexport. Whenscreenshotorlayoutis false,noRendersays why.screenshotis'slow'when the last render took 15 seconds or more, and false when the last render failed.rendergives the recent renders. Read it before your first edit.globals: the names that you kept from earlier calls.recentErrors: the number of recent errors, when there are some.files: on the hosted server, the designs of the user that are not archived, newest first.archivedFiles: on the hosted server, the archived designs. Do not edit them unless the user tells you to.
return await GetAppState()
ReadSkill
ReadSkill(path?) returns a file of this skill as text.
- No path gives
SKILL.md. 'tsx.md'gives one file.'tsx.md#subset-map'gives one section with its subsections. The part after#is the heading in lowercase, with a-for each run of other characters.- The search tool gives these paths in its results.
return ReadSkill('tsx.md#subset-map')
Search
await Search(query, { kinds, limit }) does the same as the search tool.
kinds: limit the results to some kinds:component,frame,layer,text,token,skill,style,template,file,font,icon,logo,error,place,project.- A
placeresult is a screen of the Knighn app. Itslinksopen that screen on a Mac, on iPhone or iPad and on the web. Give themaclink first on a Mac, and theweblink only when the person does not have the app. - A
projectresult is a code project. Pass its id asprojecttoexecute(see Code projects). - Each result has a
kind, anid, atitle, a shorttextandactions: the helper calls that open it, such asReadSkill("tsx.md#subset-map").
return await Search('pricing card', { kinds: ['component', 'skill'] })
GetStyle
GetStyle(name?) gives ready-made styles for a design with no brand.
- No name gives the list:
{ name, summary, bestFor }for each style. - A name gives the style:
{ name, summary, bestFor, tokens, modes, fonts, accents, guidance }. tokensis ready forSetTokens.modes.darkis ready forSetTokens(…, { mode: 'dark' }).- To change the accent colour, change
--color-accentand--color-accent-foreground.accentslists colours that suit the style.
const style = GetStyle('Editorial Serif')
SetTokens(style.tokens)
SetTokens(style.modes.dark, { mode: 'dark' })
return style.guidance
Print(...values) adds one line to the response. It prints strings as they are and other values as JSON, with spaces between them. Print short lines, not whole objects.
Print('Frames on this page:')
Print(Tree(page, { depth: 0 }))
GetErrors
await GetErrors({ limit, since, runtime }) returns recent errors from each part of Knighn: the app, the desktop shell, the hosted server, the daemon and the MCP server. Read them before you guess why a call failed.
limit: the default is 50, and the most is 200.since: a Unix time in milliseconds.runtime: the part of Knighn, such asapp,desktop,convex,daemonormcp.- Each error has
id,runtime,version,level,message,stack,context,count,firstSeenAt,lastSeenAtandresolvedAt.
return await GetErrors({ limit: 5 })
ResolveError
await ResolveError(id) marks an error as fixed. If the same error comes back, Knighn opens it again.
const [latest] = await GetErrors({ limit: 1 })
if (latest) await ResolveError(latest.id)
LinkTo
LinkTo(place) gives the links that open a place in the Knighn app. When a person asks how to do something in Knighn, give them these links. Do not give a menu path, such as Settings > Agents, without the link.
mac(knighn://go/…) opens the place in the macOS app. On a Mac, give this link first.iosopens the place in the iOS app. The iOS app opens the closest screen when it does not have that place.webopens the place in the web app, in any browser. Give it only when the person does not have the Knighn app. A design opens on the web only when the browser is signed in to the account that has it.usesays which link to give.placeis a place id, ordesign/<file id>for a design on Knighn. Search with the kindplaceto find the id, such assearch("connect claude code", { kinds: ['place'] }).- The places are:
home,new,open,templates,this-mac,get-started,logos(Logo Generation),cli-design(CLI Design),shortcuts,import/file,import/web,import/simulator,import/library,import/schema,settings/general,settings/account,settings/storage,settings/remote,settings/agents,settings/connections,settings/flagged-chats,settings/partners,settings/about,settings/file(the settings file, Mac app only),install-cli(install the knighn command line tool, Mac app only),code/open(open a code project),sign-inandsign-up.
return LinkTo('settings/agents')
OpenProject
await OpenProject(path) opens a code project folder in a tab of the Knighn app. The app must be in operation on the Mac. The link knighn://project?path=<path> does the same thing. To work on a project with no app, do not open it: pass its folder as project (see Code projects).
return await OpenProject('/Users/me/code/shop')
Select
Select(ids, { zoom }) selects layers or frames on the canvas of the user, so that the user sees where your work is. It works on the local server only, with the design open in the Knighn app.
- The app selects after it applies the edits of the call, so you can select a layer that the same call made.
- The canvas shows the page that has the first id.
{ zoom: true }also fits the view to the selection. Select([])clears the selection. The selection is not an edit, so it makes no undo step.- Use it at the end of your work, not after each edit: it moves the view of the user.
Select(pricing.root, { zoom: true })
The user's designs
These helpers work on the hosted server only. They change the designs in the Knighn account of the user, by file id. A call with one of them has no renderer, so take screenshots in a different call. Get a file id from GetAppState().files, GetAppState().archivedFiles or a search for kind file.
ArchiveDesign
await ArchiveDesign(fileId) archives a design. Knighn keeps the design, but Home and agents do not show it in their lists. Archive a design only when the user tells you to. The result is { id, name, archived }.
const { archivedFiles } = await GetAppState()
return archivedFiles
UnarchiveDesign
await UnarchiveDesign(fileId) brings an archived design back to the lists. The result is { id, name, archived }.
const [first] = (await GetAppState()).archivedFiles ?? []
if (first) return await UnarchiveDesign(first.id)
RenameDesign
await RenameDesign(fileId, name) gives a design a new name. Home and the tab of the design show the name. The result is { id, name }.
const [design] = (await GetAppState()).files ?? []
if (design) return await RenameDesign(design.id, 'Spring campaign')
DuplicateDesign
await DuplicateDesign(fileId) makes a copy of a design in the account. The result is the copy as { id, name }. To edit the copy, pass its id as fileId in your subsequent calls.
const [design] = (await GetAppState()).files ?? []
if (design) return await DuplicateDesign(design.id)
ShareDesign
await ShareDesign(fileId, on?) turns the public link of a design on, or off with false. The result is { id, name, shared, url }. Each person who has the url can see the design, so share a design only when the user tells you to. When you turn the link on again, it is the same link.
const [design] = (await GetAppState()).files ?? []
if (design) return await ShareDesign(design.id)
ListVersions
await ListVersions(fileId) returns the older versions of a design, newest first. Each version is { id, savedAt, reason, size, name, pinned }. reason says what saved it, such as 'save', 'hosted' (an agent) or 'restore'. Knighn keeps the last 20 versions.
const [design] = (await GetAppState()).files ?? []
if (design) return await ListVersions(design.id)
RestoreVersion
await RestoreVersion(fileId, versionId) makes an older version the design again. The design as it was before the restore becomes a version, so you can restore that one to go back. The result is { id, name, version }. Do not edit the same design in the same call: the call cannot save its edits after the restore.
const [design] = (await GetAppState()).files ?? []
const [last] = design ? await ListVersions(design.id) : []
if (last) return await RestoreVersion(design.id, last.id)
Comments
These helpers read and write the comments of the Comment tool in the app: the pins on the canvas, with their replies. They work on a design on Knighn: on the hosted server, and in the app when the open design is on Knighn. Other hosts give an error that says so. A comment is not part of the design, so Undo does not remove it.
A comment is { id, author, text, resolved, createdAt, page, layer, point, layerGone, replies }. layer is the id of the layer or the screen that has the pin, for this call. point is the place of a pin that is not on a layer, in canvas pixels. layerGone is true when the layer of the pin is not in the design now. Each reply is { author, text, createdAt }. author is a name.
ListComments
await ListComments({ resolved }) returns the comments of the design, oldest first. With resolved: false, it returns only the open comments. Read the open comments before you change a design that other people review.
const open = await ListComments({ resolved: false })
return open.map((comment) => ({ id: comment.id, layer: comment.layer, text: comment.text }))
AddComment
await AddComment(target, text) puts a comment on a screen or a layer. To put it at a point on a page, give { page, x, y } in canvas pixels as the target. The result is the comment. Use a comment to ask the user a question about one layer, or to tell the user what you changed there.
const [title] = Find(page, 'h1')
if (title) return await AddComment(title, 'Is this the final headline?')
ReplyToComment
await ReplyToComment(id, text) adds a reply to a comment. The result is the comment with its replies.
const [first] = await ListComments({ resolved: false })
if (first) return await ReplyToComment(first.id, 'Done: the button is larger now.')
ResolveComment
await ResolveComment(id, resolved?) resolves a comment, or opens it again with false. A resolved comment shows no pin in the app. Resolve a comment only when its work is done or the user tells you to.
const [first] = await ListComments({ resolved: false })
if (first) return await ResolveComment(first.id)
The run
Commit
Commit(name?) keeps the steps done so far. When a later step fails, Knighn applies the steps up to the last Commit() and rolls back the rest. The result is { error, kept, rolledBack } and the first warning gives the error. Commit('name') also names the point for RevertTo('name').
SetTokens({ '--color-accent': '#ff5a1f' })
Define(`export function Tag({ label = 'New' }: { label?: string }) { return <span>{label}</span> }`)
Commit('tokens and components')
pricing = AddScreen('Pricing', '<main className="flex flex-col gap-6"><Tag /></main>')
Undo
Undo(count?) undoes the last count execute calls of this session, one by default, as one new step. It returns the labels of the calls that it undid. The session keeps the history of its last 20 calls, and the hosted server and the Mac daemon keep as many of those as fit in 256 KB. For older work, use the versions of the design.
Undo(2)
Redo
Redo(count?) makes the change of the last count calls that Undo undid again, one by default. It returns their labels. A call that makes a different change after Undo empties the list of undone calls.
Redo()
RevertTo
RevertTo(name) undoes every change after Commit(name), in this call or in an earlier call of the session.
RevertTo('tokens and components')
AllowLargeChange
AllowLargeChange() lets this call delete frames, or change or remove more than 200 layers that were there before the call. Without it, such a call fails and changes nothing. { force: true } on Delete or Replace does the same.
AllowLargeChange()
Delete(Find(page, 'section'))
DryRun
DryRun() runs the snippet and applies nothing. The first warning says what the call would change: layers, frames, pages, components and tokens. Did gives each step. Screenshots show the design as the call would leave it. dryRun: true next to input does the same.
DryRun()
AddScreen('Pricing', '<main className="flex flex-col gap-6" />')
Examples
Change a design that exists. Read the outline, find the layer, change it, then look at it:
Print(Tree(page, { depth: 2 }))
const [cta] = Find(page, 'Button')
Update(cta, { props: { label: 'Try it free' } })
const [hero] = Find(page, '[data-name="Hero"]')
Update(hero, { classes: 'py-32 @5xl:py-40' })
await Screenshot(hero)
Add a dark version and a mobile version of a screen, then look at all three:
const dark = Place('Pricing', { x: 1540, y: 0, width: 1440, modes: { theme: 'dark' } })
const mobile = Place('Pricing', { x: 3080, y: 0, width: 390 })
await Screenshot([pricing.item, dark, mobile])