silt-design-mode
v0.1.22
Published
Point at any element in a running UI, say what should change, and an agent makes the change in your source. Works standalone; Silt canvases are optional.
Readme
silt-design-mode
Point at any element in a running UI, say what should change, and an agent makes the change in your source.
Works standalone — no account, no API key. Silt is optional and additive on top.
Quick start
npm i silt-design-mode
npx silt-design-mode setup # one time: agent skills, and Silt if you want it
npx silt-design-mode start # the service — leave it runningTwo spellings
| | works |
| ----------------------------- | --------------------------------------------- |
| npx silt-design-mode <verb> | always |
| npx silt <verb> | inside a project that depends on this package |
npx silt-design-mode is the one to write down, in docs and in scripts: it needs
nothing to already be true.
There is no bare silt to type, on purpose. The agent binary is downloaded and kept
current for you — it lives in ~/.silt/bin and is revalidated against the published
checksums on every run — but it is an engine this package drives, not a command with its
own audience. Every verb is per project, and npx is what makes "this project" mean the
directory you are standing in.
npx siltonly works from a project that already has this package installed. Elsewhere npm resolves the name to an unrelated abandoned package and fails, so it is not a first-install route.
Then mount it around whatever you want to be able to point at:
import { DesignMode, createLocalSink, revertLocalEdit } from 'silt-design-mode';
<DesignMode
sendOnSave
onSend={createLocalSink({ repoRoot })}
onRevert={revertLocalEdit}
repoRoot={repoRoot}
anchor="viewport"
>
{children}
</DesignMode>;Two things people get wrong here:
anchordefaults toparent, which pins the pill inside the wrapped box — on a page taller than the viewport that puts it at the bottom of the document, invisible until you scroll. Any full page wantsanchor="viewport".repoRootcan't be discovered from a browser. Pass it from build-time or server-side code.
npx silt-design-mode start — the service
One service per project, running two halves as one thing: the listener that applies edits to your source, and — only if this machine is linked to Silt — an agent scoped to this project, which is what makes it reachable from a canvas. Unlinked, it says so in one line and runs anyway.
"Project" means the nearest folder up whose package.json declares
silt-design-mode — not the node_modules location, not the git root. That folder
is the only tree it will edit.
It binds the first free port in 4747–4762, so several projects can run at once.
createLocalSink() finds it by sweeping that range; you never pass a port.
Send to Silt has one deterministic priority: the component currently selected
in Design Mode, otherwise the latest successfully edited component, otherwise the
current stamped page. It pushes that exact identity from your working tree to your
most recently updated canvas. The button calls the real MCP silt_push route
directly: it does not ask an agent to identify the component, translate the URL
into a filename, or choose a canvas. If the target is a private helper in a larger
file, the listener exposes it under its exact name only in the throwaway snapshot
sent to Silt; your source file and Git index are not changed.
Send a visual snapshot
The arrow beside Send to Silt offers Send component (the existing source/build flow) and Send snapshot. Select an element, then choose Send snapshot to capture its current DOM, author CSS, pseudo-elements, form values, fonts and images. It goes to your latest canvas without onboarding or building the repo. The main send button and its shortcut retain their existing component behavior.
Snapshots use the same canvas frame and bitmap pipeline as component builds, retain the captured theme, and reflow their layout when resized. Media queries, container queries, viewport units and root font sizing retain the source page context. CSS keyframes, animation timing, and CSS hover/focus transitions are preserved within the selected tree. JavaScript-driven interactions are not copied. They carry source identity as metadata, but have no application scripts or editable build source. Activate a snapshot on the canvas, enter Design mode, select a nested element, then drag it out or choose Take out. Extraction and placement are undoable. Older snapshots keep their captured layout; resend them with 0.1.18 or later for reflow.
Passwords are excluded. Accessible canvas/video pixels become images; protected media, embedded frames and unsupported content report a capture error. Assets must be readable by the browser, and the complete request is limited to 64 MB.
This option requires the updated Design Mode listener, Silt web service (the
silt_snap tool), and render host. Restart the listener after updating the
package. Component sending remains compatible with the existing server.
Source stamps — strongly recommended
Without this, an annotation tells the agent what you pointed at. With it, the annotation carries the file and line it lives on, and the agent stops searching. On a real codebase that's about a minute saved per edit.
It can't be done from the browser — React's element stack on a modern bundler never passes through the file the JSX is in. The compiler knows, so the build stamps it.
npx silt-design-mode setup offers this as its last step, so most people never
run anything else. To add it to a project that is already set up, or to redo it:
npx silt-design-mode wireBoth show the diff and ask before touching your config, and both take --yes.
Neither writes without a terminal to answer that prompt — scripting it means
passing --yes.
npx silt-design-mode doctor tells you whether it's on. If you'd rather do it by
hand:
Next.js
// next.config.mjs
import { withDesignMode } from 'silt-design-mode/next';
export default withDesignMode({/* your config */});Vite — list it before react(); the React plugin is also enforce: 'pre'
and its Fast Refresh preamble would shift every line number.
import designMode from 'silt-design-mode/vite';
export default defineConfig({ plugins: [designMode(), react()] });Anything else on webpack: point a rule at silt-design-mode/loader.
Both wrappers are development-only by default and cost ~2 ms per file. If a file
uses syntax we can't parse, stamping is skipped — it will never break your build,
and never emit a line number it isn't sure of. A non-JSX host (.astro, .vue,
.erb) can't be stamped at all.
The parser ships with the package, so there's nothing to install and the syntax
gate on writes is always on. If your repo has its own @babel/parser, that copy
is preferred — it's the one whose version matches what your build accepts.
Editing tiers
Each annotation takes one of two paths. With a provider key in the environment
(ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GROQ_API_KEY) a bounded
visual change is a single fast round-trip. Without one — or when the change needs
real comprehension — it delegates to your own coding agent, which is slower and
understands more. Override the runner with DM_RUNNER.
Every edit is parse-gated before it's written, and every applied edit can be reverted from its pin.
Silt (optional)
npx silt-design-mode status # what's linked, and to which instance
npx silt-design-mode login # link this project (opens a browser)
npx silt-design-mode send # share this agent session to a canvas
npx silt-design-mode push <entry> # render a local component onto a canvas
npx silt-design-mode pull <name> # cherry-pick a pushed version onto your branch
npx silt-design-mode mcp # register Silt's MCP server with your coding agent
npx silt-design-mode logout # sign out AND revoke the key (--all for every project)
npx silt-design-mode doctor # health check + detected agents
npx silt-design-mode uninstall # undo all of it — see belownpx silt-design-mode setup runs the ones you need for a first-time setup and skips whatever is
already done.
Each project gets its own revocable key, listed in Silt as <project> — <host>,
so revoking one repo leaves the others working. Keys live in your OS config dir,
never in the repo. A project with no key of its own borrows the machine one and says
so; npx silt-design-mode login there gives it its own.
logout revokes the key, it does not just forget it. The credential presents itself
to Silt and is killed by its own authority, so nothing is left live behind you and there
is no Settings visit to remember. If Silt cannot be reached, you are still logged out
locally and the key is named on screen as still active so you can revoke it by hand
— being offline is never a reason to be stuck logged in.
One thing to know either way: an agent already holding an open connection keeps the key
in memory until it stops. It is a foreground process, so Ctrl-C in the terminal running
npx silt-design-mode start ends it immediately.
Uninstalling
npx silt-design-mode uninstall # then: npm rm silt-design-modeIt undoes what installing did — the agent skills, the AGENTS.md block, the build-plugin
wiring in your next.config/vite.config, the annotation inbox — and signs out,
revoking the key and dropping the MCP registration.
It asks nothing, because you typed it. What protects you instead is that it only deletes
what it can prove it wrote: a skill file you edited is kept and named, and so is one
from an older release, since the two are indistinguishable (--force deletes those too).
The AGENTS.md block is identified by a marker pair and a sentinel string, so markers
you typed yourself around your own notes are left alone. A config that has drifted beyond
a clean reverse is skipped with instructions rather than guessed at.
--all signs out of every project on the machine, not just this one.
Left in place deliberately: the cached binary in ~/.silt/bin (machine-wide, shared by
every repo — silt uninstall removes that and the credentials dir), and node_modules,
since the command cannot remove the package it is running from.
SILT_API_URL picks the instance (default https://silt.app). The
silt-design-mode-beta command — silt-beta for short — targets beta.silt.app,
which is a separate account and separate data.
Because keys are per project, different projects can point at different instances.
Registering the MCP server
setup does this for you. mcp is the same step on its own, and --scope,
--client and --no-sign-in are its only flags:
npx silt-design-mode mcp # every project on this machine (default)
npx silt-design-mode mcp --scope local # this project only, private config
npx silt-design-mode mcp --scope project # committed .mcp.json, for the team
npx silt-design-mode mcp --client cursor # …or codex; default is claude
npx silt-design-mode mcp remove # --all for every scope--scope project never stores a key or a helper, on purpose: that scope writes a
committed .mcp.json, a long-lived key in git is an incident, and a helper would carry
one machine's absolute path to a binary nobody else has. It registers for OAuth
instead. The other two scopes live in the client's own private config.
No login is needed — OAuth in the browser works on an unlinked machine. When the project is linked, registration gives the client two ways to authenticate, and it needs both:
- a
headersHelperpointing atsilt mcp headers, which reads the credential store on every connect. This is what makes a key rotation a non-event — a re-login is picked up by itself and re-runningmcpis harmless. - a static
Authorizationheader at the default scope, because Claude Code runs a locally-scoped helper only after you have accepted the trust prompt for that folder. A project you just created has not been trusted, so without the static header the very first connection arrives with no credential at all, fails, and is remembered as needing browser sign-in.
At --scope user there is no trust gate and the server is visible from every
directory, so that scope gets the helper alone and no copy of the key.
One consequence worth knowing: at the default scope the client's own config holds a
copy of your key (private to you, not in git). silt logout and silt uninstall both
clear it for you — they drop the registration before deleting the credential store,
which is the only order in which they can still work out its name. A registration made
against a different instance, or on another machine, is the case they cannot reach:
run npx silt-design-mode mcp remove --all there.
A direct mcp run also triggers the client's own sign-in after registering, so the
browser opens while you're still at the keyboard rather than ambushing you after a
restart. --no-sign-in skips it, and an older client without that subcommand — or no
terminal at all — is detected and skipped quietly.
setup deliberately does not sign in: it sets SILT_MCP_NO_SIGN_IN=1, an env var
rather than the flag, because this host runs whatever daemon is published and an
unknown flag exits 2 before the command runs where an unknown env var is ignored.
Inside setup, sign-in could only fire when the project has no usable credential —
meaning the login step just failed, and whatever stopped that would stop this too. Run
npx silt-design-mode mcp on its own if you want the browser step.
Whichever route: the client reads its server list at startup, so the tools appear
only after it restarts (or you run /mcp).
Agent skills
Installing the package writes three skill files into your repo, so the coding agent
you already use knows the tool exists and how to drive it: one for setting it up and
running it, one for applying a batch of annotations, and one for reaching Silt itself
— which canvas tools exist and when to call them instead of the shell. Claude Code
also gets a /silt-send command for sharing the session.
npx silt-design-mode init --force rewrites them after an upgrade.
An agent only opens a skill it decides to open, and the rules that keep it off the
wrong surface are needed before that decision. So a short block also goes into
AGENTS.md, which every major agent loads at startup: what this package is, that
Silt is reached through silt_* tools rather than the shell, and that an annotation
already carries the file and line to edit. Only the text between the
<!-- BEGIN:silt-design-mode --> and <!-- END:silt-design-mode --> markers is
ours — put your own notes outside them and they are left alone, upgrade after
upgrade. There is no --force for this one, because the rest of the file is yours.
When the installer cannot be certain which bytes are its own — the markers do not
form one clean pair, or they do but the text between them was not written by the
installer — it says so and changes nothing. Same when it cannot read or write the
file at all. Refusing is the only safe answer: a repair pass would guess, and the
span it guessed about is the span most likely to hold your writing.
License
See LICENSE.
