@cychien/ezdesign 0.0.0-stage → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +486 -2
- package/dist/cli.mjs +5649 -0
- package/dist/constants.d.ts +45 -0
- package/dist/overlay.css +465 -0
- package/dist/overlay.js +2447 -0
- package/dist/project-root.d.ts +10 -0
- package/dist/shell.css +2482 -0
- package/dist/shell.js +4644 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.mjs +100 -0
- package/package.json +79 -4
- package/skills/ezdesign/SKILL.md +78 -0
- package/skills/ezdesign/reference/baseline.md +131 -0
- package/skills/ezdesign/reference/components.md +102 -0
- package/skills/ezdesign/reference/critique.md +129 -0
- package/skills/ezdesign/reference/design.md +247 -0
- package/skills/ezdesign/reference/inspiration.md +125 -0
- package/skills/ezdesign/reference/make.md +331 -0
- package/skills/ezdesign/reference/principles.md +130 -0
- package/skills/ezdesign/reference/product.md +127 -0
- package/skills/ezdesign/reference/stack.md +123 -0
- package/skills/ezdesign/reference/start.md +76 -0
- package/skills/ezdesign/scripts/page.d.mts +61 -0
- package/skills/ezdesign/scripts/page.mjs +908 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Justin Chien
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,487 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ezdesign
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Point at your live app. Your agent fixes it.
|
|
4
|
+
|
|
5
|
+
ezdesign turns any locally running dev server into an annotatable review surface for human ↔
|
|
6
|
+
agent iteration. The user marks up the **real page** - click an element, frame a region, select
|
|
7
|
+
some text, leave a comment - and the feedback flows to a local coding agent (Claude Code, Codex,
|
|
8
|
+
anything that can run a CLI) as structured items that resolve to **exact source locations**. The
|
|
9
|
+
agent edits the code, HMR updates the page in place, the user reviews the next round.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
$ npx -y @cychien/ezdesign@latest http://localhost:5173/pricing --agent claude # managed ACP agent
|
|
13
|
+
$ npx -y @cychien/ezdesign@latest http://localhost:5173/pricing # portable poll mode
|
|
14
|
+
$ npx -y @cychien/ezdesign@latest poll http://localhost:5173/ # agent waits for feedback
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Zero config.** A local daemon reverse-proxies your dev server and injects the annotation
|
|
18
|
+
overlay. No code changes, no build plugins required, works with any framework.
|
|
19
|
+
- **Anchored feedback.** Annotations carry a layered anchor: `file:line` (with the optional Vite
|
|
20
|
+
plugin), React component chain, `data-section`, CSS selector, text, viewport. Agents stop
|
|
21
|
+
guessing which part of the page you meant. Paste or drop a screenshot into any comment and the
|
|
22
|
+
agent gets a path to it; `/element` points a comment at a second element, so "make this match
|
|
23
|
+
that one" arrives as a `file:line` too.
|
|
24
|
+
- **Every screen at once.** Review on one device or lay all three out on a canvas - desktop,
|
|
25
|
+
iPad and iPhone side by side at their real viewports, scrolling together. An annotation stays on
|
|
26
|
+
the screen it was made on, and reaches the agent tagged with the width it was made at.
|
|
27
|
+
- **Built for the loop.** In ACP mode, ezdesign owns the agent and keeps its live work, questions,
|
|
28
|
+
and permission prompts in the review shell. Poll mode keeps the blocking structured-JSON contract
|
|
29
|
+
for any external agent or automation. HMR keeps iterations in place; the review chrome lives
|
|
30
|
+
outside the app frame, so it survives your agent's syntax errors.
|
|
31
|
+
- **Local-first.** Everything binds to 127.0.0.1. Nothing leaves your machine, except one
|
|
32
|
+
request to the npm registry every few hours to learn whether a newer version exists (opt out
|
|
33
|
+
with `EZDESIGN_NO_UPDATE_CHECK=1`).
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
Nothing to install - `npx -y @cychien/ezdesign@latest` is enough. For agents, add the bundled skill:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
npx skills add cychien/ezdesign --skill ezdesign
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Optional, for exact `file:line` anchors in Vite + React projects:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// vite.config.ts
|
|
47
|
+
import { ezdesignSource } from '@cychien/ezdesign/vite'
|
|
48
|
+
|
|
49
|
+
export default defineConfig({
|
|
50
|
+
plugins: [ezdesignSource(), react()],
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Dev-only (`apply: 'serve'`) - it never touches production builds.
|
|
55
|
+
|
|
56
|
+
## Updating
|
|
57
|
+
|
|
58
|
+
When a newer ezdesign is on npm, a card appears at the top of the review shell and one button takes
|
|
59
|
+
it. The new version is installed under `~/.ezdesign/versions/` and started as this daemon's
|
|
60
|
+
successor: the successor binds and registers first, and only once it is up does the old daemon let
|
|
61
|
+
go - so a version that fails to start leaves you on the one you had, with the error in the card.
|
|
62
|
+
Your sessions come back on the ports they held and the shell reloads itself.
|
|
63
|
+
|
|
64
|
+
An ACP agent is restarted along with the daemon and asked to resume the conversation it was having,
|
|
65
|
+
so an update no longer costs the review its context. An agent that cannot resume comes back in a
|
|
66
|
+
fresh session, the same as `/new`, and the thread says so when it happens.
|
|
67
|
+
|
|
68
|
+
The close button in the card's corner puts the offer away; the version beside the name in the
|
|
69
|
+
header turns into a pill you can click to bring it back. Nothing is installed or restarted without
|
|
70
|
+
that click.
|
|
71
|
+
|
|
72
|
+
Without the shell:
|
|
73
|
+
|
|
74
|
+
- **CLI** - always invoke it as `npx -y @cychien/ezdesign@latest`. A bare `npx @cychien/ezdesign`
|
|
75
|
+
reuses whatever version npx cached and never checks for a newer one.
|
|
76
|
+
- **Daemon** - opening a session with a different CLI version replaces the running daemon with that
|
|
77
|
+
version, under the same restore guarantee any daemon restart gets (see Configuration). A CLI
|
|
78
|
+
that reaches a daemon on another version is refused with a `409` that says how to update,
|
|
79
|
+
instead of speaking a mismatched protocol.
|
|
80
|
+
- **Skill** - `npx skills update ezdesign`, whenever you feel like it. Deliberately not part of the
|
|
81
|
+
update button: the skill's own instructions launch the CLI as `npx -y @cychien/ezdesign@latest`,
|
|
82
|
+
so an old copy still starts a current review, and everything that governs a review once it is
|
|
83
|
+
running ships with the daemon. Its reader is also an agent session ezdesign did not spawn and
|
|
84
|
+
cannot restart, so a sync would not take effect until that agent's next session either way. The
|
|
85
|
+
bundled copy carries the release it came from in its frontmatter (`metadata.version`).
|
|
86
|
+
|
|
87
|
+
The daemon asks the npm registry for the latest version at most once every four hours. Set
|
|
88
|
+
`EZDESIGN_NO_UPDATE_CHECK=1` to never ask.
|
|
89
|
+
|
|
90
|
+
## Annotating
|
|
91
|
+
|
|
92
|
+
Two modes, and the toolbar shows which one is armed. Both leave the page live underneath, and
|
|
93
|
+
`Esc` steps back out - first the open comment box, then the mode.
|
|
94
|
+
|
|
95
|
+
- **元素** (`E`) - point at one thing. Hover frames the element under the cursor, a click opens the
|
|
96
|
+
comment box on it, and selecting text instead annotates that run. This is the mode for "this
|
|
97
|
+
button", "this heading", "this sentence".
|
|
98
|
+
- **範圍** (`R`) - drag a box over an area. What the box encloses *whole* is what it means: the
|
|
99
|
+
elements it merely cuts through are looked past, down to the ones that fit - so framing a card is
|
|
100
|
+
the card, and framing a row of them is the row. The box you drew stays on screen as the comment's
|
|
101
|
+
subject, and the agent is handed the enclosing element's `file:line` plus `anchor.contains`, a
|
|
102
|
+
line per element the box held: component, `file:line`, and a snippet of that element's own text,
|
|
103
|
+
which is what tells two instances of the same component apart. This is the mode for "this whole
|
|
104
|
+
block", "these four cards", "the spacing through here".
|
|
105
|
+
|
|
106
|
+
One comment is composed at a time - across every preview on the canvas, not just the one you are
|
|
107
|
+
in - so a box drawn in one screen cannot open a second comment box behind the one you are writing.
|
|
108
|
+
|
|
109
|
+
## The comment box
|
|
110
|
+
|
|
111
|
+
Typing `/` in either comment box opens a command menu - arrow keys and Enter, or click. A slash
|
|
112
|
+
mid-word stays a slash, so urls and paths are left alone. The commands:
|
|
113
|
+
|
|
114
|
+
- **`/file`** opens the system file picker and drops what you choose in as an attachment, where the
|
|
115
|
+
slash was.
|
|
116
|
+
- **`/new`** starts a new chat: the agent's context is thrown away and the review carries on in a
|
|
117
|
+
fresh session, which is how a long review stops paying to replay its own history on every turn.
|
|
118
|
+
Only offered in [ACP mode](#acp-mode-experimental), and only in the note box below the queue. The
|
|
119
|
+
thread empties with it, back to the state it opened in - a notice explaining that the history above
|
|
120
|
+
no longer counts is still history above. The log on disk is untouched: `/new` records where the
|
|
121
|
+
visible thread starts, so the record of the review stays whole while the shell shows the fresh
|
|
122
|
+
start that was actually asked for. Every batch the agent had not finished with goes too, not just
|
|
123
|
+
the turn it was on - a question queued behind that turn was asked of a context you have just said
|
|
124
|
+
to start over from, and a fresh session answering it would be answering something else. The 待送
|
|
125
|
+
清單 is left alone: those you have not sent yet, so they were never the old context's to begin
|
|
126
|
+
with. A note you were part-way through typing survives, so `/new` and then send is one move.
|
|
127
|
+
One thing does carry over: the first batch of the new chat is handed what you asked for in the
|
|
128
|
+
earlier ones, one line each, marked as already handled. The agent works to `PRODUCT.md`, starts
|
|
129
|
+
from `DESIGN.md`, and is told to write a correction into them when it has come up before; a fresh
|
|
130
|
+
session cannot see "before" on its own, so the daemon shows it.
|
|
131
|
+
- **`$`** opens a second menu, of the skills this machine can run, and picking one
|
|
132
|
+
asks the agent to run it over the batch you are about to send. The skill shows as a pill above
|
|
133
|
+
the box until the batch goes, and on the sent bubble afterwards - which is the only place it
|
|
134
|
+
appears, because an explicitly invoked skill is expanded into the agent's prompt rather than
|
|
135
|
+
loaded through its skill tool, so nothing in the turn's activity would otherwise say it ran.
|
|
136
|
+
A skill on its own is a batch: "run this over what you can see" needs no annotation and no note.
|
|
137
|
+
|
|
138
|
+
Its own symbol rather than more rows under `/`, because the two are different kinds of thing:
|
|
139
|
+
`/file` and `/element` act on the box you are typing in, and a skill is handed to the agent.
|
|
140
|
+
Only offered in [ACP mode](#acp-mode-experimental), and only in the note box.
|
|
141
|
+
|
|
142
|
+
The list is read off disk - `.claude/skills` and `.agents/skills`, in the project and in your
|
|
143
|
+
home directory, plus `.claude/plugins/*/skills`. Skills bundled inside the Claude Code binary
|
|
144
|
+
are **not** listed: they have no file to find, and the protocol's own command list cannot tell a
|
|
145
|
+
skill from `/doctor`, so including it would mean a menu of "skills" that offers things that are
|
|
146
|
+
not skills.
|
|
147
|
+
- **`/element`** points the comment at a *second* element - "make this match that one". The page
|
|
148
|
+
stays live while you choose, so a plain click still follows links and opens menus and only
|
|
149
|
+
⌘/Ctrl+click picks; the comment box steps aside and comes back when you are done. You can cross to
|
|
150
|
+
another page to find what you meant, and ezdesign brings you back to finish the comment. The
|
|
151
|
+
element arrives as a chip carrying the same layered anchor an annotation gets, so the agent is
|
|
152
|
+
handed a `file:line` instead of a description, and `[ref 1]` in the comment text says exactly where
|
|
153
|
+
in the sentence you meant it.
|
|
154
|
+
|
|
155
|
+
The same command also takes a plain **drag**, the way a screenshot tool does: frame a box and the
|
|
156
|
+
chip points at everything inside it (no modifier needed - a press only becomes a box once it has
|
|
157
|
+
moved, so a plain click is still the page's). What the box encloses whole is what it means - the
|
|
158
|
+
elements it merely cuts through are looked past, down to the ones that fit - so framing a card is
|
|
159
|
+
the card, and framing a row of them is the row. A region resolves to their common ancestor for the
|
|
160
|
+
`file:line`, and carries `anchor.contains`, a line per element it enclosed - component, `file:line`
|
|
161
|
+
and a snippet of that element's own text, which is what tells two instances of the same component
|
|
162
|
+
apart. A frame around exactly one element is simply that element, the same as clicking it.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
Either comment box also takes an image or file, pasted or dropped in. Attachments become inline chips
|
|
166
|
+
in the text itself, so a comment can point at a file mid sentence, and backspace deletes one the
|
|
167
|
+
way it deletes a character. They are names, not previews - the box is for the comment, and a
|
|
168
|
+
thumbnail takes the room it needs. The bytes go straight to the session directory, capped at 8 MB
|
|
169
|
+
per file, and the batch hands the agent an absolute path per attachment so it opens the screenshot
|
|
170
|
+
instead of being handed base64. Deleting a queued annotation deletes its files with it; anything
|
|
171
|
+
attached and then abandoned is collected a day later.
|
|
172
|
+
|
|
173
|
+
## The queue
|
|
174
|
+
|
|
175
|
+
Queued annotations stay editable until the batch goes out. The pencil on a row turns that row's
|
|
176
|
+
comment into a field in place - nothing appears elsewhere, and there is no second place to look.
|
|
177
|
+
Editing borrows the two buttons the row already has rather than adding any: the pencil becomes the
|
|
178
|
+
tick that commits, and the cross, which dismisses either way, becomes the one that backs out. The row
|
|
179
|
+
keeps its number and its source label throughout, so what is being changed never stops being obvious.
|
|
180
|
+
|
|
181
|
+
The field is seeded with exactly what the row was showing, chips where the sentence had them, so a
|
|
182
|
+
`[ref 1]` or an attached screenshot survives a wording change instead of having to be picked again.
|
|
183
|
+
⌘/Ctrl+Enter saves, Escape cancels, and both outrank the shell's own bindings while the field is
|
|
184
|
+
open, so ⌘+Enter is this save rather than the batch send. `/file` works inside it; `/element` does
|
|
185
|
+
not - pointing a comment at a new element needs the page's own pointer routed back into a row, which
|
|
186
|
+
is a different thing from adjusting what you already wrote, so delete the chip and re-annotate
|
|
187
|
+
instead.
|
|
188
|
+
|
|
189
|
+
A cancel takes any file attached during the edit with it; the files the row already had are left
|
|
190
|
+
alone either way, and one dropped by a save is collected by the same sweep that collects an
|
|
191
|
+
attachment nothing references.
|
|
192
|
+
|
|
193
|
+
## Asking while the agent is still working
|
|
194
|
+
|
|
195
|
+
Sending mid-turn is allowed and queues - you should not have to wait for a turn to finish before
|
|
196
|
+
writing down what you just noticed. The batch goes out on its own the moment the agent comes back,
|
|
197
|
+
and your bubble appears in the thread the moment it leaves the composer, which is what says it went.
|
|
198
|
+
To make it go *now*, stop the current turn: the queued batch is delivered as soon as that turn ends.
|
|
199
|
+
`/new` does the opposite - it drops the queue along with the context.
|
|
200
|
+
|
|
201
|
+
The thread draws each answer under the question it answers, which is not always where the log put it.
|
|
202
|
+
The log itself stays append-only and stamped in real time - it is the record of the review - but a
|
|
203
|
+
batch sent mid-turn lands *between* an earlier question and its reply, so rendering the log verbatim
|
|
204
|
+
would put the agent's answer under a question it is not answering, and put the new question above
|
|
205
|
+
output that started before it was typed. Instead every entry carries the batch it belongs to, and the
|
|
206
|
+
in-flight turn is drawn in the same place its finished reply will be, so nothing moves when the reply
|
|
207
|
+
lands. Entries that answer no batch - the session notices - stay exactly where the log put them,
|
|
208
|
+
because for those the timeline *is* the meaning.
|
|
209
|
+
|
|
210
|
+
## Devices
|
|
211
|
+
|
|
212
|
+
The header carries one toggle group with two sides: a menu picking the size to preview at, and
|
|
213
|
+
all of them at once (`4`). The raised side is the one showing. The sizes are real device viewports, not bare widths - where the fold lands is
|
|
214
|
+
half of what a responsive review is looking at:
|
|
215
|
+
|
|
216
|
+
| Key | Device | Viewport |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `1` | 手機 | 375×629 - a 375×812 screen, less the browser's bars |
|
|
219
|
+
| `2` | 平板 | 1112×740 - a 10.5" tablet on its side, past the breakpoint most layouts switch on |
|
|
220
|
+
| `3` | 電腦 | 1440×788 - a 1440×900 laptop screen, less the menu bar and the browser's own |
|
|
221
|
+
|
|
222
|
+
The sizes are the **page area** a browser leaves on that device, not the screen it leaves it on -
|
|
223
|
+
so a card on the canvas stops showing the page exactly where the real thing stops showing it. On
|
|
224
|
+
its own a size takes only its width from the table and the height from your screen: one preview
|
|
225
|
+
gets the whole stage to show the page in, and the canvas is where the fold is what you are
|
|
226
|
+
checking.
|
|
227
|
+
|
|
228
|
+
The names are shelves, not machines - a 手機 row naming a handset would claim a precision the
|
|
229
|
+
row does not have, when phone widths run from 360 up past 430. The sizes are still taken from
|
|
230
|
+
real machines; the comments in `devices.ts` say which.
|
|
231
|
+
|
|
232
|
+
**`4` lays them out on one canvas** - phone and tablet side by side, the desktop on the row below.
|
|
233
|
+
The control in the stage's top corner picks which sizes are on it: the three above, plus a portrait
|
|
234
|
+
tablet (834×1018) that is off by default.
|
|
235
|
+
|
|
236
|
+
The canvas is drawn at **true size**, always. A 375 card is 375 pixels whether it is standing alone
|
|
237
|
+
or with three others beside it - a scaled 375 is not 375 to a media query, and a page measured at a
|
|
238
|
+
size nobody browses at is not a measurement. Turning a size off does not resize or rearrange the
|
|
239
|
+
ones that are left. What does not fit is what the dragging is for.
|
|
240
|
+
|
|
241
|
+
Every card is the device it names, at its own height. What keeps the canvas short enough to work
|
|
242
|
+
on is the two-row layout, not a cap on the cards - a card cut to a shared height would be lying
|
|
243
|
+
about where the fold falls, which is half of what the preview is for.
|
|
244
|
+
|
|
245
|
+
**Drag a card by its label to rearrange the canvas.** A card cannot stop just anywhere: it lands
|
|
246
|
+
top-aligned in an existing row, or on a row of its own, and a line marks where it would go -
|
|
247
|
+
upright between two cards, flat between two rows. The arrangement is remembered, and a size turned
|
|
248
|
+
on later joins as its own row at the bottom rather than reshuffling what you laid out.
|
|
249
|
+
|
|
250
|
+
**Drag the background to move the canvas.** The frames are the app: a press inside one belongs to
|
|
251
|
+
the page, so the backdrop and the gaps are what the canvas is panned by. That is also why the
|
|
252
|
+
canvas does not have to fit - what is off the edge is a drag away.
|
|
253
|
+
|
|
254
|
+
Scrolling inside any one preview scrolls the others to the same point in the page, and navigating
|
|
255
|
+
one - a link, back, forward - takes all of them: annotations are filtered by path, and three
|
|
256
|
+
previews on three pages is three separate reviews.
|
|
257
|
+
|
|
258
|
+
An annotation belongs to the screen it was made on: mark up the iPhone card and the pin lives
|
|
259
|
+
there, not on all three. The agent gets `@mobile 390x844` in the anchor either way. Everything the
|
|
260
|
+
overlay draws - the comment box, the element label, the pins - is drawn back up to full size, so a
|
|
261
|
+
canvas scaled to 60% is still one you can read and type into.
|
|
262
|
+
|
|
263
|
+
## Configuration
|
|
264
|
+
|
|
265
|
+
| Variable | Default | What it moves |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| `EZDESIGN_DATA_DIR` | `~/.ezdesign` | Session state, the daemon registry and log, versions installed by the in-shell update, and the update-check cache |
|
|
268
|
+
| `EZDESIGN_CONTROL_PORT` | `4400` | First port of the ten-port range the daemon searches for its control server |
|
|
269
|
+
| `EZDESIGN_NO_UPDATE_CHECK` | unset | Set to `1` and the daemon never asks the npm registry for the latest version, so the shell never offers an update |
|
|
270
|
+
| `EZDESIGN_INSPIRE_URL` | `https://inspire.ezdesign.dev` | The inspiration database the `inspire` commands query |
|
|
271
|
+
|
|
272
|
+
Set both together to run a second, fully isolated instance: a starting daemon adopts any live
|
|
273
|
+
daemon it finds inside its own control range, so moving the data dir alone still lands you on the
|
|
274
|
+
shared daemon. `npm run dev` sets both.
|
|
275
|
+
|
|
276
|
+
Sessions outlive the daemon that served them. On start, the daemon picks each session back up
|
|
277
|
+
from disk and re-binds it to the port it last held, so a review shell tab you already have open
|
|
278
|
+
only needs a reload, and feedback you queued before the restart is still waiting. If that port has
|
|
279
|
+
since been taken, the session moves to a free one and the CLI re-resolves it. A session that was
|
|
280
|
+
driving an ACP agent starts that agent again **and asks it to resume the conversation they were
|
|
281
|
+
having**, so the review carries on where it stopped. Whether that works is the agent's answer to
|
|
282
|
+
give - a transcript it no longer has, or an agent that cannot resume at all, leaves the review in a
|
|
283
|
+
fresh context, and only then does the thread say so.
|
|
284
|
+
|
|
285
|
+
A session belongs to one project on one origin, not to the origin alone. Dev servers all default
|
|
286
|
+
to the same port, so reviewing a second project on `localhost:5173` would otherwise inherit the
|
|
287
|
+
first one's conversation and hand its undelivered feedback to the wrong agent. The project is the
|
|
288
|
+
nearest `.git` or `package.json` ancestor of the directory you run `ezdesign` from, so switching
|
|
289
|
+
projects on a port starts a clean review and switching back finds the old one intact.
|
|
290
|
+
|
|
291
|
+
## Agent modes
|
|
292
|
+
|
|
293
|
+
ezdesign can either manage an ACP agent inside the review session or expose feedback through its
|
|
294
|
+
portable polling contract. Both modes receive the same structured feedback and exact anchors.
|
|
295
|
+
|
|
296
|
+
### ACP mode (experimental)
|
|
297
|
+
|
|
298
|
+
Pass `--agent` when opening the session:
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
npx -y @cychien/ezdesign@latest http://localhost:5173/ --agent claude
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The daemon starts the agent as a child process, sends each feedback batch as one turn, and shows
|
|
305
|
+
the agent's streaming reply, plan, tool activity, questions, and permission prompts in the review
|
|
306
|
+
shell. The completed reply is saved in the conversation, and the next queued batch is delivered
|
|
307
|
+
automatically. No second terminal or `poll` loop is needed. This is also the mode used by the
|
|
308
|
+
bundled ezdesign skill.
|
|
309
|
+
|
|
310
|
+
Three controls come with owning the agent:
|
|
311
|
+
|
|
312
|
+
- **⌘/Ctrl+.** stops the turn in flight, from anywhere including mid-sentence in the composer -
|
|
313
|
+
which is when you usually want it, since watching the agent head the wrong way is what prompts it.
|
|
314
|
+
No button: a control that is live for the few seconds a turn lasts costs the composer a permanent
|
|
315
|
+
slot to say so. The status badge reads `正在中止` until the agent answers, so the keypress is not
|
|
316
|
+
silent. Whatever it had already said stays in the thread, and the batch is not handed back: you
|
|
317
|
+
stopped it on purpose.
|
|
318
|
+
- **`/new`** in the note box clears the agent's context. See [The comment box](#the-comment-box).
|
|
319
|
+
Every conversation a review has had stays reachable: once there is more than one, a control at the
|
|
320
|
+
head of the thread lists them by time and by how much was said, and picking one shows its thread
|
|
321
|
+
and puts the agent back on it. While an earlier conversation is showing the control says so in
|
|
322
|
+
colour, because feedback sent then joins *that* conversation and nothing else on screen would
|
|
323
|
+
mention it.
|
|
324
|
+
- **The pill above the note box** names the model the next batch will be answered by, and opens
|
|
325
|
+
everything else the agent lets the session be set to - reasoning effort, permission mode, and any
|
|
326
|
+
toggle it offers. It is a permanent slot because unlike a cancel it is *state*: it says what you
|
|
327
|
+
are about to send your feedback to.
|
|
328
|
+
|
|
329
|
+
What it offers is the model and, where the agent has one, the reasoning effort. Not the permission
|
|
330
|
+
mode and not the fast-mode toggle: those are the agent's own configuration, and a review is not
|
|
331
|
+
where they should be decided. A mode the *agent* changes is still reported in the thread - some
|
|
332
|
+
models do not support every mode, and selecting one moves it as a side effect that outlives the
|
|
333
|
+
model that caused it, so the problem worth solving there was the silence rather than the missing
|
|
334
|
+
control.
|
|
335
|
+
|
|
336
|
+
A switch keeps the conversation. The model changes on the running session, so nothing is replayed
|
|
337
|
+
and nothing is forgotten - which is what makes it worth reaching for mid-review: point at the easy
|
|
338
|
+
half on a cheap model, move up for the layout problem that needs it. Sending mid-turn is allowed
|
|
339
|
+
and applies from the next turn.
|
|
340
|
+
|
|
341
|
+
What the menu holds is whatever the agent reports, not a list ezdesign keeps. That matters because
|
|
342
|
+
the list changes with the choice: pick a model with no reasoning levels and no fast mode and both
|
|
343
|
+
options leave the menu, and the agent can move a value on its own - a refusal falling back to
|
|
344
|
+
another model - which arrives here and redraws. Some models do not support every permission mode,
|
|
345
|
+
and picking one **downgrades the mode** as a side effect; showing the mode is what keeps that from
|
|
346
|
+
happening silently.
|
|
347
|
+
|
|
348
|
+
Your pick is remembered for the session and re-asserted on the next one, so `/new` and a daemon
|
|
349
|
+
restart do not hand the review back to the agent's default. Every switch goes into the thread, so
|
|
350
|
+
a review read back later says which model answered which batch.
|
|
351
|
+
|
|
352
|
+
The agent itself can be changed from the pill left of the model, which is where it belongs: the two
|
|
353
|
+
answer one question between them - what is about to read this feedback - and the model is a choice
|
|
354
|
+
*within* the agent, so changing the agent replaces the list the model was chosen from. That always
|
|
355
|
+
starts a new conversation, and the shell says so before it does it: a session id belongs to the agent that issued
|
|
356
|
+
it - Claude keeps its conversations in one store and Codex in another, and neither can resolve the
|
|
357
|
+
other's - so there is no way to hand a conversation over. Nothing ezdesign owns is lost. Every earlier
|
|
358
|
+
conversation stays in the thread's own picker with the agent that had it, and switching back finds it
|
|
359
|
+
again, resumable by the agent that remembers it.
|
|
360
|
+
|
|
361
|
+
The agent can design the way a designer works, through a five-step harness in the bundled skill:
|
|
362
|
+
product, stack, design, components, make. It is opt-in, experimental - the switch says 實驗 - and a
|
|
363
|
+
premium feature: the **增強設計** switch on its own line above the agent and the model, marked with a
|
|
364
|
+
gold sparkle, turns it on, and the choice is remembered for the project - across sessions, a dev
|
|
365
|
+
server that moved ports, `/new` and daemon restarts - except that it starts off when the project's
|
|
366
|
+
`PRODUCT.md` or `DESIGN.md` is gone, so turning it on asks again. Off, every batch is handled
|
|
367
|
+
exactly as it was before the harness existed. The harness applies only to a turn whose prompt says
|
|
368
|
+
it is on; after it has been on in a conversation, every later turn with it off is told so, because
|
|
369
|
+
the agent still remembers what it read while it was on. Two files at the project root are what it
|
|
370
|
+
works to. **`PRODUCT.md`** says what the product is for - who uses it, what it makes possible, what
|
|
371
|
+
must not change. **`DESIGN.md`** ([the format](https://github.com/google-labs-code/design.md)) says
|
|
372
|
+
what it looks like, in broad strokes - a few anchor values in YAML frontmatter and the character of
|
|
373
|
+
the language in prose, with the full scales left to the design system. When both exist, every batch
|
|
374
|
+
tells the agent to read them before a visual change, keep to `PRODUCT.md`, and take `DESIGN.md` as
|
|
375
|
+
a direction rather than a rule: a document held to the letter makes everything look alike, so the
|
|
376
|
+
agent may go past it. An ordinary change edits code only. The two files change
|
|
377
|
+
when you ask for it, when a correction has come twice, or when you state a rule - only on what you
|
|
378
|
+
actually said, never on what the agent reads into a send-back; a change that only leaves a line stale is mentioned in the reply, not edited. Between them,
|
|
379
|
+
`.ezdesign/config.json` records the stack and the browser tool the agent looks at the page with. The
|
|
380
|
+
skill needs no ezdesign session to run: installed in Claude Code or Codex and invoked on its own, it
|
|
381
|
+
walks the same steps against the project and the dev server.
|
|
382
|
+
|
|
383
|
+
Turning the switch on in a project that lacks either file asks first, in the card above the
|
|
384
|
+
composer: 啟用增強設計 - 增強設計會建立 PRODUCT.md 與 DESIGN.md,是否繼續? 取消 leaves the switch off,
|
|
385
|
+
because the harness cannot run without them. 繼續 turns it on and starts a setup turn at once, which walks the steps
|
|
386
|
+
that build the missing files before any batch is worked. Switching off again before the agent has
|
|
387
|
+
started that turn cancels it, and the thread says so. `PRODUCT.md` is written as a designer meets a
|
|
388
|
+
client: the agent reads the project's own copy and assets first, then asks, three questions a round,
|
|
389
|
+
what they could not answer - who it is for, what it makes possible, what must be preserved - each
|
|
390
|
+
led by what it inferred, so you correct rather than dictate. Nothing about feel or style is asked there; that belongs to `DESIGN.md`, which
|
|
391
|
+
is read off the running page the way a design-token extractor reads one: computed styles, counted,
|
|
392
|
+
so the colour on three hundred elements is the surface and the one on two is the accent, and only
|
|
393
|
+
the character and its anchors are written down; the format's own linter checks the result.
|
|
394
|
+
|
|
395
|
+
How the agent then works - where it looks for precedent, the principles it builds to, how it
|
|
396
|
+
writes a repeated correction back into the two files - lives in the bundled skill's `reference/`
|
|
397
|
+
files, which the daemon points it at turn by turn. A piece of design is not judged by the agent
|
|
398
|
+
that made it, since a maker praises its own work: it starts a subagent with a fresh context, which
|
|
399
|
+
looks at the agent's screenshots of the change and scores it against the reaction good work gets on
|
|
400
|
+
X and Dribbble, "super clean!", in a product held to the standard of Shopify, Stripe, Linear and
|
|
401
|
+
Vercel - useful, super clean, clear, fitting, well made - and answers ship, refine or pivot. One review; a second only after a pivot,
|
|
402
|
+
comparing the two directions. The reply never mentions the review. What you say about quality in so many words, or correct twice, is
|
|
403
|
+
written to `.ezdesign/taste.md`, which the agent builds to and the reviewer judges by,
|
|
404
|
+
so both move toward your eye. The daemon carries
|
|
405
|
+
only the facts a skill cannot know: whether the switch is on, which files exist, where the page
|
|
406
|
+
is, what the turn is for.
|
|
407
|
+
|
|
408
|
+
Three built-in profiles map short names to ACP server commands:
|
|
409
|
+
|
|
410
|
+
| Profile | Command |
|
|
411
|
+
| --- | --- |
|
|
412
|
+
| `claude` | `npx -y @agentclientprotocol/claude-agent-acp` |
|
|
413
|
+
| `codex` | `npx -y @agentclientprotocol/codex-acp` |
|
|
414
|
+
| `gemini` | `gemini --experimental-acp` |
|
|
415
|
+
|
|
416
|
+
Any other value is used as an ACP command line, so a custom server can be started with
|
|
417
|
+
`--agent 'my-acp-agent --flag'`. Only one live ACP agent can own a session. End the session or stop
|
|
418
|
+
the daemon before switching it to a different agent.
|
|
419
|
+
|
|
420
|
+
### Poll mode
|
|
421
|
+
|
|
422
|
+
Without `--agent`, the review uses the existing CLI contract:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
npx -y @cychien/ezdesign@latest http://localhost:5173/
|
|
426
|
+
npx -y @cychien/ezdesign@latest poll http://localhost:5173/
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`poll` blocks until the user sends feedback, prints the structured batch as JSON, and exits. This
|
|
430
|
+
mode remains available for scripts and integrations that consume the CLI contract directly.
|
|
431
|
+
|
|
432
|
+
## Inspiration
|
|
433
|
+
|
|
434
|
+
Before it builds a piece of design, the agent looks for precedent: a curated collection of
|
|
435
|
+
interfaces, each entry carrying the reason it works and the case where it would be the wrong
|
|
436
|
+
choice. It is searched by meaning - a description of what the piece has to show - and entries carry
|
|
437
|
+
tags in their curator's own words, which nudge a search that names one. It needs a token:
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
npx -y @cychien/ezdesign@latest login --token <token>
|
|
441
|
+
npx -y @cychien/ezdesign@latest inspire --query "a command menu over the whole product"
|
|
442
|
+
npx -y @cychien/ezdesign@latest inspire --query "an onboarding checklist" --tag "dark popover" --limit 3
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`login` writes the token to `~/.ezdesign/credentials.json`, mode 600; without `--token` it prompts,
|
|
446
|
+
and `logout` forgets it. A search takes a `--query`, `--tag`, or both. `inspire` prints a block per
|
|
447
|
+
result - the tags, `Shows:`, `Components:`,
|
|
448
|
+
`Actions:`, `Why it works:`, `Wrong when:`, and local paths to the entry's images, which are
|
|
449
|
+
downloaded once and reused after that; `(approximate)` in a heading means the entry was read off a
|
|
450
|
+
screenshot. `--json` prints the same response with those paths in place of the service's own URLs,
|
|
451
|
+
and carries what the block leaves out: the visible wording, the layout and its metrics, and the
|
|
452
|
+
relevance score. There is no fixed list of tags; the query does the finding.
|
|
453
|
+
|
|
454
|
+
Nothing about the collection ships in the package: a query returns what it asked for, and the
|
|
455
|
+
results belong in the agent's reply, not in your repository. A search that finds nothing exits 0
|
|
456
|
+
with one line, and one that cannot run - no token, a refused token, a service that cannot be
|
|
457
|
+
reached - exits 2 with one line, so the agent says so and carries on.
|
|
458
|
+
|
|
459
|
+
## Development
|
|
460
|
+
|
|
461
|
+
Working on ezdesign itself takes one command:
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
npm run dev # watch src/, serve the playground fixture, start an isolated daemon,
|
|
465
|
+
# open the review shell with an ACP agent on it
|
|
466
|
+
npm run dev -- --no-plugin # the same page without ezdesignSource(), to see fallback-only anchors
|
|
467
|
+
npm run dev:agent # a stand-in poll-mode agent, for working on the poll contract itself
|
|
468
|
+
npm run dev:cli status # the ezdesign CLI, pointed at the dev daemon instead of the real one
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
`npm run dev` opens the review in ACP mode, scoped to the fixture - the agent's working directory
|
|
472
|
+
is `fixtures/playground`, which is the project it is reviewing. Nothing else is needed; `dev:agent`
|
|
473
|
+
is only for the poll contract.
|
|
474
|
+
|
|
475
|
+
A client rebuild (`src/client/**`) only needs a browser reload; a rebuild the daemon loads
|
|
476
|
+
restarts it, and in-flight polls reconnect on their own. Dev mode keeps its daemon registry and
|
|
477
|
+
sessions in `.dev/`, so your own `~/.ezdesign` is never touched. The review target is
|
|
478
|
+
[`fixtures/playground`](fixtures/playground/README.md).
|
|
479
|
+
|
|
480
|
+
## Status
|
|
481
|
+
|
|
482
|
+
Early. On the roadmap: diff-derived Keep/Undo and layout-issue detection. ACP mode is an
|
|
483
|
+
experimental spike.
|
|
484
|
+
|
|
485
|
+
## License
|
|
486
|
+
|
|
487
|
+
MIT
|