agent-ui-kit 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 arsen-ask-lx
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 ADDED
@@ -0,0 +1,96 @@
1
+ # agent-ui-kit
2
+
3
+ **Point at an element in your running app, say what is wrong — the note lands
4
+ in a file your coding agent reads.**
5
+
6
+ Between "this button here" and `Rail.tsx:69` somebody has to build a bridge.
7
+ Without one, every conversation about the UI is made of prose descriptions,
8
+ and prose is read two ways.
9
+
10
+ It is a Vite plugin: it lives in the dev server and there is
11
+ nothing to import in your source.
12
+
13
+ ```bash
14
+ npm i -D agent-ui-kit
15
+ ```
16
+
17
+ ```js
18
+ // vite.config.js
19
+ import { defineConfig } from "vite";
20
+ import react from "@vitejs/plugin-react";
21
+ import { agentUiKit } from "agent-ui-kit";
22
+
23
+ export default defineConfig({
24
+ plugins: [react(), agentUiKit()],
25
+ });
26
+ ```
27
+
28
+ That is the whole installation. Hold **Alt** — the element under the cursor
29
+ lights up. Click — a field opens. Type, press **Enter**.
30
+
31
+ ```markdown
32
+ ## the channel list slides under the profile instead of scrolling
33
+
34
+ - **where:** `ChatScreen › Rail › RoomList › SidebarSection`
35
+ - **what:** `<button>` — «Channels»
36
+ - **classes:** `flex min-w-0 flex-1 items-center gap-1 rounded px-2.5 py-1 …`
37
+ - **page:** `/c/01a0814d-3bd0-708e-bed0-b1be60d2bbee`
38
+ - **when:** 2026-09-08 18:20:30
39
+ ```
40
+
41
+ Then you say to your agent: *"work through NOTES.md"*. The component chain
42
+ plus the class list is enough to find the source line with a single search.
43
+
44
+ ## Why it is a file and not an MCP server
45
+
46
+ An MCP server would mean either the agent polling for notes or another daemon
47
+ to keep running. A file needs neither: the agent reads it when it looks, and
48
+ **any** agent can — Claude Code, Cursor, Codex, Copilot, or a human with
49
+ `git diff`. Nothing here is tied to one vendor.
50
+
51
+ You walk the screen and leave ten notes in a row; nobody should wait while
52
+ each one is processed. The file collects, the agent works through the list
53
+ afterwards.
54
+
55
+ ## Options
56
+
57
+ ```js
58
+ agentUiKit({
59
+ file: "NOTES.md", // where notes are collected
60
+ key: "alt", // "alt" | "ctrl" | "meta"
61
+ color: "#e5484d", // highlight colour
62
+ placeholder: "what is wrong? Enter — save, Esc — cancel",
63
+ route: "/__agent-ui-kit", // change only on a collision
64
+ })
65
+ ```
66
+
67
+ ## What it reads off the element
68
+
69
+ - **the component chain** — pulled from React's own fibers on the DOM node
70
+ (`ChatScreen › Rail › RoomList`). React only, and only in dev;
71
+ - **tag, classes, `id`, `data-testid`, a little text** — for everything else
72
+ and for every other framework.
73
+
74
+ If no component names are found the note is still recorded, with the tag, the
75
+ classes and the text. Tooling that dies together with somebody else's
76
+ undocumented detail is worse than no tooling at all.
77
+
78
+ ## Dev only
79
+
80
+ The plugin declares `apply: "serve"`. The client is injected by the dev
81
+ server as a virtual module, so there is nothing to import in your source and
82
+ nothing to remember to strip from a production build.
83
+
84
+ ## Three things learned the hard way
85
+
86
+ They are in the source as comments, and they are why this is a package rather
87
+ than a snippet:
88
+
89
+ - **no `prompt()` / `confirm()`** — a native dialog freezes the page and
90
+ breaks any automation driving the browser from outside;
91
+ - **the modifier-click belongs to the plugin entirely** — otherwise a note
92
+ about a button also presses that button;
93
+ - **the field closes on click-outside, not on blur** — `blur` fires before
94
+ Enter gets a chance to run, and ate what was typed.
95
+
96
+ MIT.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "agent-ui-kit",
3
+ "version": "0.1.0",
4
+ "description": "Point at an element in your running app, say what is wrong — the note lands in a file your coding agent reads.",
5
+ "keywords": [
6
+ "vite-plugin",
7
+ "ai",
8
+ "agent",
9
+ "feedback",
10
+ "annotate",
11
+ "claude",
12
+ "cursor",
13
+ "devtools"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./src/index.d.ts",
20
+ "default": "./src/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "src"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "peerDependencies": {
30
+ "vite": ">=5"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/arsen-ask-lx/agent-ui-kit.git"
35
+ },
36
+ "homepage": "https://github.com/arsen-ask-lx/agent-ui-kit#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/arsen-ask-lx/agent-ui-kit/issues"
39
+ }
40
+ }
package/src/client.js ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Browser side of agent-ui-kit.
3
+ *
4
+ * Hold the modifier key — the element under the cursor lights up.
5
+ * Click — a small field opens. Type, press Enter — the note is appended
6
+ * to a file on disk, together with a description of what you pointed at.
7
+ *
8
+ * WHY THIS EXISTS. Between "this button here" and `Rail.tsx:69` somebody
9
+ * has to build a bridge. Without one, every conversation about the UI is
10
+ * made of prose descriptions, and prose is read two ways.
11
+ *
12
+ * ⚠️ DEV SERVER ONLY. The plugin declares `apply: "serve"`, so nothing
13
+ * here reaches a production bundle.
14
+ *
15
+ * ⚠️ NO `prompt()` / `confirm()`. A native dialog freezes the whole page
16
+ * and breaks automation that drives the browser from outside — which is
17
+ * exactly the audience this tool serves.
18
+ */
19
+
20
+ /* global __AGENT_UI_KIT_OPTIONS__ */
21
+ const OPTIONS = __AGENT_UI_KIT_OPTIONS__;
22
+
23
+ /**
24
+ * Whose node is this — the chain of framework components, read off the
25
+ * DOM node itself.
26
+ *
27
+ * ⚠️ READ FROM THE FRAMEWORK, NOT FROM THE BUILD. The first attempt wrote
28
+ * source positions into the markup with a Babel plugin, and broke against
29
+ * `@vitejs/plugin-react` 6, which is built on oxc — the Babel hook points
30
+ * are simply gone.
31
+ *
32
+ * React in dev mode hangs a `__reactFiber$…` property (random suffix) on
33
+ * every DOM node. Walking it upwards yields `ChatScreen › Rail › RoomList`
34
+ * which, together with the class list, is enough to find the source line
35
+ * with a single search.
36
+ *
37
+ * ⚠️ THESE ARE FRAMEWORK INTERNALS AND THEY MAY VANISH. That is why there
38
+ * is not one "just in case" guard here: if no names are found, the note is
39
+ * still recorded — with the tag, the classes and the text. Tooling that
40
+ * dies together with somebody else's undocumented detail is worse than no
41
+ * tooling at all.
42
+ */
43
+ function fiberOf(node) {
44
+ const key = Object.keys(node).find((one) => one.startsWith("__reactFiber$"));
45
+ return key ? (node[key] ?? null) : null;
46
+ }
47
+
48
+ function nameOf(fiber) {
49
+ if (typeof fiber.type !== "function") return undefined;
50
+ return fiber.type.displayName ?? fiber.type.name;
51
+ }
52
+
53
+ function ownersOf(node) {
54
+ const names = [];
55
+ let fiber = fiberOf(node);
56
+ // A depth limit on purpose: trees get deep and we only want the nearest.
57
+ for (let step = 0; fiber && step < 40 && names.length < 4; step++) {
58
+ const name = nameOf(fiber);
59
+ if (name && !names.includes(name)) names.unshift(name);
60
+ fiber = fiber.return ?? null;
61
+ }
62
+ return names.length > 0 ? names.join(" › ") : null;
63
+ }
64
+
65
+ /** How the element is recognised by eye: tag, a little text, classes. */
66
+ function describe(node) {
67
+ return {
68
+ tag: node.tagName.toLowerCase(),
69
+ classes: node.getAttribute("class") ?? "",
70
+ sample: (node.textContent ?? "").trim().slice(0, 60),
71
+ id: node.id || "",
72
+ testId: node.getAttribute("data-testid") ?? "",
73
+ };
74
+ }
75
+
76
+ /**
77
+ * ⚠️ COLOURS ARE SELF-CONTAINED, NOT TAKEN FROM THE HOST'S THEME.
78
+ * An earlier version used the host project's CSS custom properties. That
79
+ * is right inside one project and wrong in a package: in a project that
80
+ * has no such tokens the highlight is invisible, and an invisible
81
+ * highlight is indistinguishable from a broken plugin.
82
+ */
83
+ function frameFor(node) {
84
+ const frame = document.createElement("div");
85
+ const at = node.getBoundingClientRect();
86
+ frame.style.cssText = [
87
+ "position:fixed",
88
+ `left:${at.left}px`,
89
+ `top:${at.top}px`,
90
+ `width:${at.width}px`,
91
+ `height:${at.height}px`,
92
+ `border:2px solid ${OPTIONS.color}`,
93
+ "border-radius:4px",
94
+ `background:${OPTIONS.color}1a`,
95
+ "pointer-events:none",
96
+ "z-index:2147483646",
97
+ ].join(";");
98
+ return frame;
99
+ }
100
+
101
+ /**
102
+ * The note field, next to the element. Resolves with the text or null.
103
+ *
104
+ * ⚠️ ENTER IS HANDLED EXPLICITLY, NOT THROUGH IMPLICIT FORM SUBMISSION.
105
+ * Implicit submit depends on how many fields the form has and whether it
106
+ * has a button; an end-to-end run caught this — the field closed and the
107
+ * note went nowhere.
108
+ *
109
+ * ⚠️ CLOSING IS ON CLICK-OUTSIDE, NOT ON BLUR. `blur` fires before Enter
110
+ * gets a chance to run and ate what was typed. Same defect, other half.
111
+ */
112
+ function ask(node) {
113
+ return new Promise((done) => {
114
+ const at = node.getBoundingClientRect();
115
+ const box = document.createElement("div");
116
+ box.style.cssText = [
117
+ "position:fixed",
118
+ `left:${Math.max(8, Math.min(at.left, window.innerWidth - 348))}px`,
119
+ `top:${Math.min(at.bottom + 8, window.innerHeight - 90)}px`,
120
+ "z-index:2147483647",
121
+ "padding:8px",
122
+ "border-radius:12px",
123
+ "background:#1b1b1e",
124
+ "box-shadow:0 8px 24px rgb(0 0 0 / 40%)",
125
+ "font:14px system-ui,sans-serif",
126
+ ].join(";");
127
+
128
+ const field = document.createElement("input");
129
+ field.placeholder = OPTIONS.placeholder;
130
+ field.style.cssText = [
131
+ "width:300px",
132
+ "padding:8px 10px",
133
+ "border:0",
134
+ "border-radius:8px",
135
+ "background:#0f0f11",
136
+ "color:#f1f1f3",
137
+ "font:inherit",
138
+ "outline:none",
139
+ ].join(";");
140
+
141
+ box.append(field);
142
+ document.body.append(box);
143
+ field.focus();
144
+
145
+ let closed = false;
146
+ const close = (answer) => {
147
+ if (closed) return;
148
+ closed = true;
149
+ box.remove();
150
+ document.removeEventListener("mousedown", outside, true);
151
+ done(answer);
152
+ };
153
+ function outside(event) {
154
+ if (!box.contains(event.target)) close(null);
155
+ }
156
+
157
+ field.addEventListener("keydown", (event) => {
158
+ // Swallow everything: the page underneath must neither hear the
159
+ // typing nor read Escape as "close the menu".
160
+ event.stopPropagation();
161
+ if (event.key === "Enter") {
162
+ event.preventDefault();
163
+ close(field.value.trim() || null);
164
+ }
165
+ if (event.key === "Escape") close(null);
166
+ });
167
+
168
+ document.addEventListener("mousedown", outside, true);
169
+ });
170
+ }
171
+
172
+ /**
173
+ * ⚠️ ARMED EXACTLY ONCE PER PAGE LIFETIME. Hot reload re-executes the
174
+ * module, and without this mark the handlers stacked up: one click opened
175
+ * three fields on top of each other and the page eventually locked up.
176
+ *
177
+ * The mark lives on `window`, not in the module: after a reload the module
178
+ * is new and so are its variables. Only what lives outside survives.
179
+ */
180
+ const ONCE = "__agentUiKitStarted";
181
+
182
+ function held(event) {
183
+ return OPTIONS.key === "alt"
184
+ ? event.altKey
185
+ : OPTIONS.key === "ctrl"
186
+ ? event.ctrlKey
187
+ : event.metaKey;
188
+ }
189
+
190
+ function start() {
191
+ if (window[ONCE]) return;
192
+ window[ONCE] = true;
193
+
194
+ let frame = null;
195
+ let asking = false;
196
+
197
+ const clear = () => {
198
+ frame?.remove();
199
+ frame = null;
200
+ };
201
+
202
+ document.addEventListener(
203
+ "mousemove",
204
+ (event) => {
205
+ if (asking) return;
206
+ if (!held(event)) return clear();
207
+ const node = document.elementFromPoint(event.clientX, event.clientY);
208
+ clear();
209
+ if (!node) return;
210
+ frame = frameFor(node);
211
+ document.body.append(frame);
212
+ },
213
+ true,
214
+ );
215
+
216
+ document.addEventListener("keyup", () => {
217
+ if (!asking) clear();
218
+ });
219
+
220
+ document.addEventListener(
221
+ "click",
222
+ (event) => {
223
+ if (!held(event) || asking) return;
224
+ const node = event.target;
225
+ if (!node) return;
226
+
227
+ // The modifier-click belongs to us entirely: the application must not
228
+ // hear it, or a note about a button would also press that button.
229
+ event.preventDefault();
230
+ event.stopPropagation();
231
+
232
+ asking = true;
233
+ void (async () => {
234
+ const text = await ask(node);
235
+ asking = false;
236
+ clear();
237
+ if (!text) return;
238
+
239
+ const note = { text, where: ownersOf(node), url: location.pathname, ...describe(node) };
240
+ try {
241
+ const answer = await fetch(OPTIONS.route, {
242
+ method: "POST",
243
+ headers: { "content-type": "application/json" },
244
+ body: JSON.stringify(note),
245
+ });
246
+ // Say it out loud on success too: silence is indistinguishable
247
+ // from loss, and loss is precisely what we already missed once.
248
+ if (answer.ok) console.info(`[agent-ui-kit] noted: ${text} → ${note.where ?? note.tag}`);
249
+ else console.warn(`[agent-ui-kit] not saved: server answered ${answer.status}`);
250
+ } catch {
251
+ console.warn("[agent-ui-kit] not sent: the dev server did not answer");
252
+ }
253
+ })();
254
+ },
255
+ true,
256
+ );
257
+
258
+ console.info(`[agent-ui-kit] on — hold ${OPTIONS.key} to highlight, ${OPTIONS.key}+click to leave a note`);
259
+ }
260
+
261
+ start();
package/src/index.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ export interface AgentUiKitOptions {
4
+ /** Where notes are collected. Default: `NOTES.md` in the project root. */
5
+ file?: string;
6
+ /** Which key to hold while pointing. Default: `alt`. */
7
+ key?: "alt" | "ctrl" | "meta";
8
+ /** Highlight colour. Default: `#e5484d`. */
9
+ color?: string;
10
+ /** Placeholder in the note field. */
11
+ placeholder?: string;
12
+ /** Where the browser posts notes. Change only on a collision. */
13
+ route?: string;
14
+ }
15
+
16
+ export function agentUiKit(options?: AgentUiKitOptions): Plugin;
17
+ export default agentUiKit;
package/src/index.js ADDED
@@ -0,0 +1,144 @@
1
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ /**
6
+ * agent-ui-kit — point at an element, say what is wrong, the note lands
7
+ * in a file your coding agent reads.
8
+ *
9
+ * WHY A FILE AND NOT A CONSOLE LINE. You walk the screen and leave ten notes
10
+ * in a row; nobody should wait while each is processed. The file collects
11
+ * them, the agent works through the list afterwards.
12
+ *
13
+ * WHY NOT MCP. An MCP server would mean the agent polling, or another daemon
14
+ * to run. A file needs neither: the agent reads it when it looks, and any
15
+ * agent can — this is not tied to one vendor.
16
+ *
17
+ * ⚠️ DEV SERVER ONLY (`apply: "serve"`). Nothing here reaches a production
18
+ * build: the client is injected by the dev server and the route lives in its
19
+ * middleware.
20
+ */
21
+
22
+ const CLIENT = "\0virtual:agent-ui-kit/client";
23
+
24
+ const DEFAULTS = {
25
+ /** Where notes are collected. Markdown: read by a human and by an agent. */
26
+ file: "NOTES.md",
27
+ /** Which key to hold. `alt` | `ctrl` | `meta`. */
28
+ key: "alt",
29
+ /** Highlight colour. Self-contained on purpose — see client.js. */
30
+ color: "#e5484d",
31
+ /** Placeholder in the note field. */
32
+ placeholder: "what is wrong? Enter — save, Esc — cancel",
33
+ /** Where the browser posts notes. Change only on a collision. */
34
+ route: "/__agent-ui-kit",
35
+ };
36
+
37
+ /**
38
+ * One note, one entry.
39
+ *
40
+ * Components come first and on their own line: they are what you open the
41
+ * file by, so they must be findable by eye rather than read out of a
42
+ * paragraph. Classes are the second key — they locate the exact line inside
43
+ * the file.
44
+ */
45
+ function entry(note) {
46
+ const when = new Date().toISOString().replace("T", " ").slice(0, 19);
47
+ const where = note.where || "component not identified";
48
+ const what = `\`<${note.tag}>\`${note.sample ? ` — «${note.sample}»` : ""}`;
49
+ return [
50
+ ``,
51
+ `## ${note.text}`,
52
+ ``,
53
+ `- **where:** \`${where}\``,
54
+ `- **what:** ${what}`,
55
+ `- **classes:** \`${note.classes || "—"}\``,
56
+ note.id ? `- **id:** \`${note.id}\`` : null,
57
+ note.testId ? `- **data-testid:** \`${note.testId}\`` : null,
58
+ `- **page:** \`${note.url}\``,
59
+ `- **when:** ${when}`,
60
+ ``,
61
+ ]
62
+ .filter((line) => line !== null)
63
+ .join("\n");
64
+ }
65
+
66
+ async function readBody(request) {
67
+ const chunks = [];
68
+ for await (const chunk of request) chunks.push(chunk);
69
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
70
+ }
71
+
72
+ /**
73
+ * @param {Partial<typeof DEFAULTS>} [options]
74
+ */
75
+ export function agentUiKit(options = {}) {
76
+ const settings = { ...DEFAULTS, ...options };
77
+ const notes = resolve(process.cwd(), settings.file);
78
+
79
+ return {
80
+ name: "agent-ui-kit",
81
+ apply: "serve",
82
+
83
+ // The client is a virtual module: nothing to copy into the host project,
84
+ // and nothing to keep in sync with it.
85
+ resolveId(id) {
86
+ return id === CLIENT.slice(1) ? CLIENT : null;
87
+ },
88
+
89
+ async load(id) {
90
+ if (id !== CLIENT) return null;
91
+ const source = await readFile(new URL("./client.js", import.meta.url), "utf8");
92
+ const runtime = {
93
+ key: settings.key,
94
+ color: settings.color,
95
+ placeholder: settings.placeholder,
96
+ route: settings.route,
97
+ };
98
+ return source.replace("__AGENT_UI_KIT_OPTIONS__", JSON.stringify(runtime));
99
+ },
100
+
101
+ // ⚠️ INJECTED BY THE PLUGIN, NOT IMPORTED BY THE HOST. One line in the
102
+ // config is the whole installation; a project that has to import a client
103
+ // file also has to remember to strip it from production.
104
+ transformIndexHtml() {
105
+ return [
106
+ {
107
+ tag: "script",
108
+ attrs: { type: "module", src: `/@id/${CLIENT.slice(1)}` },
109
+ injectTo: "body",
110
+ },
111
+ ];
112
+ },
113
+
114
+ configureServer(server) {
115
+ server.middlewares.use(settings.route, (request, response) => {
116
+ if (request.method !== "POST") {
117
+ response.statusCode = 405;
118
+ response.end();
119
+ return;
120
+ }
121
+ void (async () => {
122
+ try {
123
+ const note = await readBody(request);
124
+ await mkdir(dirname(notes), { recursive: true });
125
+ await appendFile(notes, entry(note), "utf8");
126
+ // Say it in the dev-server log too: the person sees the note did
127
+ // not fly off into nowhere without opening the file.
128
+ server.config.logger.info(`[agent-ui-kit] ${note.text} → ${note.where || note.tag}`);
129
+ response.statusCode = 204;
130
+ response.end();
131
+ } catch (error) {
132
+ // Not swallowed: a note that failed to save must say so, or the
133
+ // person will believe it was taken into account.
134
+ server.config.logger.error(`[agent-ui-kit] not saved: ${String(error)}`);
135
+ response.statusCode = 500;
136
+ response.end();
137
+ }
138
+ })();
139
+ });
140
+ },
141
+ };
142
+ }
143
+
144
+ export default agentUiKit;