aihand 0.0.1 → 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/README.md +136 -2
- package/dist/chunk-2NTK7H4W.js +10 -0
- package/dist/chunk-3X4FTHLC.cjs +369 -0
- package/dist/chunk-BXVNR4E2.js +399 -0
- package/dist/chunk-C7DGE6MY.cjs +1456 -0
- package/dist/chunk-DUUCVLC3.cjs +254 -0
- package/dist/chunk-FAHI53KO.cjs +125 -0
- package/dist/chunk-G7KVJ7NF.js +369 -0
- package/dist/chunk-GNEUSRGP.js +52 -0
- package/dist/chunk-IGNEAOLT.cjs +130 -0
- package/dist/chunk-IS5XFUDB.js +125 -0
- package/dist/chunk-JLYC76XL.js +2448 -0
- package/dist/chunk-KQOABC2O.cjs +52 -0
- package/dist/chunk-OVMK33AC.cjs +104 -0
- package/dist/chunk-OWYK2IGV.js +250 -0
- package/dist/chunk-PQSQN4CN.js +126 -0
- package/dist/chunk-QF6AG3M5.cjs +410 -0
- package/dist/chunk-QSAMLXML.js +1456 -0
- package/dist/chunk-VEKYRKPF.cjs +399 -0
- package/dist/chunk-Y6H7W7PI.cjs +2451 -0
- package/dist/chunk-YKSYW77R.js +410 -0
- package/dist/chunk-Z2Y65YOY.cjs +7 -0
- package/dist/chunk-ZJQRNIK7.js +104 -0
- package/dist/cli-FDS2C2CZ.cjs +651 -0
- package/dist/cli-HHRGYPSM.js +649 -0
- package/dist/cli-JQEIE7RQ.js +120 -0
- package/dist/cli-K3OS2QQH.cjs +122 -0
- package/dist/cli-OSYG6LJD.cjs +89 -0
- package/dist/cli-TXRW5PG6.js +89 -0
- package/dist/cli.cjs +81 -0
- package/dist/cli.js +81 -0
- package/dist/config-5KEQLN6L.cjs +13 -0
- package/dist/config-PJPYKDLQ.js +13 -0
- package/dist/graph-IH56SCPK.js +8 -0
- package/dist/graph-ZUXXCJ5A.cjs +8 -0
- package/dist/index.cjs +481 -0
- package/dist/index.d.cts +461 -0
- package/dist/index.d.ts +461 -0
- package/dist/index.js +479 -0
- package/dist/locate-5XFSXJ5J.cjs +15 -0
- package/dist/locate-NKSUGL3A.js +15 -0
- package/dist/refactor-5FWSZIBN.cjs +19 -0
- package/dist/refactor-BOB3SZSA.js +19 -0
- package/dist/scan-4R7GQG2W.cjs +9 -0
- package/dist/scan-VF54GAAX.js +9 -0
- package/dist/ui/probe/server.cjs +505 -0
- package/dist/ui/probe/server.js +507 -0
- package/dist/vite.cjs +12 -0
- package/dist/vite.d.cts +12 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +12 -0
- package/package.json +82 -9
- package/src/cli.ts +107 -0
- package/src/index.ts +54 -0
- package/src/read/cli.ts +650 -0
- package/src/read/compact.ts +286 -0
- package/src/read/config.ts +62 -0
- package/src/read/graph.ts +182 -0
- package/src/read/index.ts +12 -0
- package/src/read/inject.ts +121 -0
- package/src/read/locate.ts +104 -0
- package/src/read/panel.ts +335 -0
- package/src/read/pipeline.ts +78 -0
- package/src/read/refactor.ts +576 -0
- package/src/read/render.ts +1118 -0
- package/src/read/scan.ts +61 -0
- package/src/read/seam.ts +0 -0
- package/src/read/security.ts +171 -0
- package/src/read/signals.ts +333 -0
- package/src/read/state.ts +71 -0
- package/src/read/stategraph.ts +205 -0
- package/src/read/types.ts +162 -0
- package/src/read/vite.ts +77 -0
- package/src/ui/babel/line-profiler.ts +197 -0
- package/src/ui/babel/source-loc.ts +68 -0
- package/src/ui/bridge/cdp-bridge.ts +138 -0
- package/src/ui/bridge/compile-probe.ts +80 -0
- package/src/ui/bridge/transport.ts +26 -0
- package/src/ui/bridge/vite-bridge.ts +116 -0
- package/src/ui/client/client-patch.ts +899 -0
- package/src/ui/client/client.ts +2562 -0
- package/src/ui/core/action.ts +747 -0
- package/src/ui/core/candidates.ts +348 -0
- package/src/ui/core/canvas.ts +305 -0
- package/src/ui/core/check.ts +34 -0
- package/src/ui/core/compact.ts +314 -0
- package/src/ui/core/detail.ts +244 -0
- package/src/ui/core/diff.ts +253 -0
- package/src/ui/core/emit.ts +198 -0
- package/src/ui/core/knob-exec.ts +137 -0
- package/src/ui/core/perf.ts +254 -0
- package/src/ui/core/types.ts +164 -0
- package/src/ui/core/util.ts +221 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/probe/cli.ts +139 -0
- package/src/ui/probe/server.ts +468 -0
- package/src/ui/self/act.ts +47 -0
- package/src/ui/self/discover.ts +101 -0
- package/src/ui/self/grow.ts +121 -0
- package/src/ui/self/install.ts +100 -0
- package/src/ui/self/probe.ts +105 -0
- package/src/ui/self/screen-hook.ts +44 -0
- package/src/ui/self/self.ts +48 -0
- package/src/ui/self/store-refs.ts +123 -0
- package/src/ui/self/store-schema.ts +65 -0
- package/src/ui/self/synth.ts +37 -0
- package/src/ui/server/cli.ts +102 -0
- package/src/ui/server/dispatch.ts +276 -0
- package/src/ui/server/help-text.ts +237 -0
- package/src/ui/server/knob-schema.ts +87 -0
- package/src/ui/server/plugin.ts +1151 -0
- package/src/vite.ts +39 -0
- package/index.js +0 -2
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// The /__aihand/help body — the full curl playbook for the runtime probe (read state →
|
|
2
|
+
// reverse morphism → drive → chain → escape hatch). Pure: port → string, no closure deps.
|
|
3
|
+
// Lifted out of plugin.ts verbatim so the two real plugin functions lead the file.
|
|
4
|
+
export function helpText(port: number) {
|
|
5
|
+
const base = `http://localhost:${port}/__aihand`
|
|
6
|
+
return `
|
|
7
|
+
# aihand — Runtime Browser Inspector
|
|
8
|
+
|
|
9
|
+
IMPORTANT: Before debugging any UI issue, visual bug, or runtime error, ALWAYS fetch the live app state first. Do NOT guess — look at the actual browser state.
|
|
10
|
+
|
|
11
|
+
## Read state — cheapest first
|
|
12
|
+
|
|
13
|
+
\`\`\`bash
|
|
14
|
+
curl ${base}/screen # semantic layout tree (indent=nesting, 左/中/右) + {view, modal, focus, knobs} — START HERE (returns token: tN)
|
|
15
|
+
curl '${base}/screen?form=visual' # same screen as an ASCII art sketch instead of the layout tree
|
|
16
|
+
curl '${base}/screen?form=knobs' # only the control surface: clickable knobs + their store morphisms, grouped by column
|
|
17
|
+
curl '${base}/screen?since=tN' # only what moved since that token (view/modal/focus + new errors), not a full snapshot
|
|
18
|
+
curl ${base}/ui # React component tree — deep-dive when /screen isn't enough
|
|
19
|
+
curl '${base}/dom?scope=ChatInput' # semantic DOM scoped to a component — UI as text, src locations
|
|
20
|
+
curl ${base} # high-density summary (ok sections → 1 line, issues → expanded)
|
|
21
|
+
curl ${base}?full # full dump: UI tree + console + network + errors + state
|
|
22
|
+
curl ${base}/check # pass/fail health check — use after code changes
|
|
23
|
+
curl ${base}/console # console logs (errors, warnings, info)
|
|
24
|
+
curl ${base}/network # fetch/XHR requests with status and timing
|
|
25
|
+
curl ${base}/errors # uncaught errors and unhandled rejections
|
|
26
|
+
curl ${base}/state # registered store snapshots (overview)
|
|
27
|
+
curl ${base}/state/imStore # drill into one store (domain)
|
|
28
|
+
curl '${base}/state?path=imStore.conversations.0' # expand a nested value the overview collapsed to Array(N) (point)
|
|
29
|
+
curl ${base}/tabs # list live tabs (id, visible/background, title) for ?tab= addressing
|
|
30
|
+
curl ${base}/timeline # interleaved action stream across all tabs (who clicked what, in order)
|
|
31
|
+
curl '${base}/query?sel=[role=menuitem]' # this selector right now: count + each element's text/visible/attrs
|
|
32
|
+
curl ${base}/profile # performance profiler: which component/function is burning frames (+ source lines with AIPEEK_LINES=1)
|
|
33
|
+
curl ${base}/profile/reset # clear the profiler window, then reproduce the interaction
|
|
34
|
+
curl ${base}/profile/diff # closed loop: 1st call marks baseline, fix+reproduce, 2nd call → IMPROVED/REGRESSED verdict
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
\`/profile\` is a verdict, not a flame graph — read it, fix, re-read, don't eyeball frames.
|
|
38
|
+
Three tiers, escalate only as needed:
|
|
39
|
+
|
|
40
|
+
\`\`\`bash
|
|
41
|
+
# 1. Diagnose — one read names the component/function burning frames (self-time × render count)
|
|
42
|
+
curl ${base}/profile
|
|
43
|
+
|
|
44
|
+
# 2. Closed-loop A/B — proves a fix worked instead of guessing
|
|
45
|
+
curl ${base}/profile/reset # clear the window
|
|
46
|
+
# …reproduce the slow interaction (scroll, stream, type)…
|
|
47
|
+
curl ${base}/profile # 1st call = baseline snapshot
|
|
48
|
+
# …edit code + reproduce the SAME interaction…
|
|
49
|
+
curl ${base}/profile/diff # 2nd call → IMPROVED / REGRESSED / NO DATA verdict
|
|
50
|
+
|
|
51
|
+
# 3. Line-level — break a hot component down to the exact source line
|
|
52
|
+
AIPEEK_LINES=1 <your dev command> # opt-in: line instrumentation taxes every render, off by default
|
|
53
|
+
curl ${base}/profile
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
Profiling is the ONE read that needs the tab FOREGROUND + FOCUSED — the OS throttles rAF
|
|
57
|
+
for backgrounded/unfocused windows, so timing there is a lie. The profiler refuses to launder
|
|
58
|
+
that lie: a throttled window reads \`Frames: none sampled\` and a window that didn't reproduce
|
|
59
|
+
the workload reads \`NO DATA\`, never a fabricated \`0.0% dropped\` / \`IMPROVED\`. A real verdict
|
|
60
|
+
requires: tab in front, \`/profile/reset\`, then repeat the exact interaction. (\`/screen\` and
|
|
61
|
+
\`/dom\` work fine backgrounded — only profiling needs focus.)
|
|
62
|
+
|
|
63
|
+
\`/query\` is the read-side twin of click/fill's \`sel=\` — assert on a specific element
|
|
64
|
+
(how many match, its text, \`data-state\`, \`aria-*\`/\`data-*\`, value, disabled) without \`/eval\`.
|
|
65
|
+
Secret fields (password inputs, API-key/token fields) show \`‹redacted N chars›\` instead of
|
|
66
|
+
their value across \`/dom\`, \`/query\` and \`/screen\` — length stays visible, the secret doesn't.
|
|
67
|
+
|
|
68
|
+
\`/screen\` projects the whole UI to a few state variables — start there, not \`/ui\`. Append
|
|
69
|
+
\`?full\` for untruncated output. Each read prints a \`token: tN\` line; pass it back as
|
|
70
|
+
\`/screen?since=tN\` to get only the transition since that read (view/modal/focus + new
|
|
71
|
+
errors/failed requests), \`(no state change)\` if nothing moved — the cheap "what changed
|
|
72
|
+
after I acted" read, without re-paying for the unchanged 99%.
|
|
73
|
+
|
|
74
|
+
To inspect or edit a component, work top-down — the full DOM is huge, a scoped view is
|
|
75
|
+
accurate: \`/screen\` or \`/ui\` to find the component, then \`/dom?scope=<Name>\` (matches the
|
|
76
|
+
source path) or \`/dom?sel=<css>\` for just that subtree. Each line carries its source
|
|
77
|
+
location (\`@File.tsx:line\`), so the DOM view tells you exactly where to edit.
|
|
78
|
+
|
|
79
|
+
## UI → code reason (reverse morphism)
|
|
80
|
+
|
|
81
|
+
\`\`\`bash
|
|
82
|
+
curl '${base}/source?path=src/features/Sidebar/index.tsx:78' # which symbol owns this line + its callers/callees
|
|
83
|
+
curl -G ${base}/source --data-urlencode 'path=<data-insp-path>' # feed /dom's data-insp-path raw (rel:line:col:Name)
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
\`/dom\` tells you *where* a UI element lives (\`@File.tsx:line\`); \`/source\` tells you *what code
|
|
87
|
+
backs it* — the enclosing symbol, who calls it, what it calls. Paste a \`data-insp-path\`
|
|
88
|
+
(\`rel:line:col:Name\`) straight from \`/dom\`. This is the static twin of \`/open\` (which sends the
|
|
89
|
+
same coordinate to your editor): point at a rendered element → get its code reason, without grepping.
|
|
90
|
+
|
|
91
|
+
## Drive the page (acts on the currently-open tab — no separate browser)
|
|
92
|
+
|
|
93
|
+
Pass \`text=\`/\`sel=\`/\`value=\` via \`-G --data-urlencode\` — **always**, not just for CJK. A raw
|
|
94
|
+
\`?text=群聊\` is an illegal HTTP request-target, rejected with an empty 400 *before aihand runs*
|
|
95
|
+
(a wasted round-trip); \`--data-urlencode\` escapes it for you, so the same form works for \`New\`
|
|
96
|
+
and \`群聊\` alike — write it right the first time.
|
|
97
|
+
|
|
98
|
+
\`\`\`bash
|
|
99
|
+
curl -G ${base}/click --data-urlencode 'text=New' # click by visible text (or 'sel=<css>')
|
|
100
|
+
curl -G ${base}/dblclick --data-urlencode 'text=todo' # double-click (inline edit / rename / select); fires dblclick handlers click can't
|
|
101
|
+
curl -G ${base}/hover --data-urlencode 'text=More' # hover only (reveal a menu/tooltip; no click)
|
|
102
|
+
curl -G ${base}/fill --data-urlencode 'sel=textarea' --data-urlencode 'value=hi' # set input/textarea; <select> matches by option text
|
|
103
|
+
curl -G ${base}/press --data-urlencode 'key=Enter' # key on focused element (e.g. Control+a)
|
|
104
|
+
curl -G ${base}/wait --data-urlencode 'text=Done' -d 'timeout=8000' # poll until text/sel appears (gone=1 until it disappears; enabled=1 until not [disabled])
|
|
105
|
+
curl '${base}/screenshot?out=shot.png' # DOM→PNG into .aidev/ (html-to-image; lossy)
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
ASCII-only values may use the bare \`?text=New\` form, but \`-G --data-urlencode\` is never wrong —
|
|
109
|
+
prefer it so a value that turns out to contain CJK/space/\`&\` doesn't silently 400.
|
|
110
|
+
|
|
111
|
+
\`click\`/\`fill\`/\`press\` settle the DOM and append \`--- changed ---\`: only the state-machine
|
|
112
|
+
transition this action caused (\`view: a → b\`, \`modal: opened X\`, \`focus: …\`) plus any new
|
|
113
|
+
errors/failed requests — not a fresh snapshot. \`(no state change)\` means nothing moved. Read
|
|
114
|
+
the delta, then drill into /ui or /dom for detail if you need it. On a miss, the response lists
|
|
115
|
+
the reachable clickable elements so you can re-target.
|
|
116
|
+
|
|
117
|
+
Each \`click\`/\`fill\`/\`press\` response also carries a \`--- recent actions ---\` timeline:
|
|
118
|
+
the semantic page actions (yours and the user's) in order, \`T\`=trusted human / \`S\`=synthetic
|
|
119
|
+
aihand, each with its resulting UI change (\`→ 弹窗打开「…」\`/\`→ 弹窗关闭\`). Your own action is
|
|
120
|
+
bracketed by \`你当前的行为\` dividers. So if the user closed a dialog you just opened, you see
|
|
121
|
+
their \`T key:Escape → 弹窗关闭\` right after your \`S\` action — no need to query for it.
|
|
122
|
+
|
|
123
|
+
**Beyond click/fill/press** — four more interactions for what those can't reach:
|
|
124
|
+
|
|
125
|
+
\`\`\`bash
|
|
126
|
+
curl '${base}/scrollIntoView?text=Row 99' # scroll a target into view (off-screen list rows)
|
|
127
|
+
curl '${base}/drag?sel=.item&to=.slot' # synthetic pointer drag, source → destination
|
|
128
|
+
curl '${base}/drop?sel=.dropzone&files=a.png,b.pdf' # fire a file-drop (DataTransfer) on a target
|
|
129
|
+
curl '${base}/clipboard?mode=write&value=hi' # seed the clipboard (mode=read reports it back)
|
|
130
|
+
\`\`\`
|
|
131
|
+
|
|
132
|
+
\`drag\` fires a real pointer sequence (down → stepped moves past dnd-kit's activation
|
|
133
|
+
distance → up); if a dnd-kit reorder doesn't take, retry the same gesture via \`realclick\`
|
|
134
|
+
(trusted events). \`drop\` delivers the drop event with the named files (synthetic Files have
|
|
135
|
+
no byte content — fine for triggering handlers, not for real uploads). \`clipboard\` needs the
|
|
136
|
+
tab focused (browser security) and says so plainly when it isn't, rather than hanging.
|
|
137
|
+
|
|
138
|
+
A control tagged \`{needs-trusted?}\` in \`/screen\` or \`/dom\` opens a popup (\`aria-haspopup\`)
|
|
139
|
+
that a synthetic click may not trigger — reach for \`realclick\` on it from the start instead
|
|
140
|
+
of discovering it via a dead click. (Right-click-only menus carry no DOM marker, so they
|
|
141
|
+
still surface only on a miss — use \`realclick\` with \`button=right\` there.)
|
|
142
|
+
|
|
143
|
+
**Green channel — replay a knob's morphism, no DOM.** \`/screen?form=knobs\` lists every
|
|
144
|
+
control with its store morphism (\`「群聊」→ appUIStore.{ mode=im }\`). \`/action?knob=<label>\`
|
|
145
|
+
replays that morphism by writing the store directly — bypassing selector resolution, disabled
|
|
146
|
+
gating, and the silent-false-success a covered \`/click\` can return. It's smoother than
|
|
147
|
+
\`/click\`+\`/fill\` for any control whose effect is a store write:
|
|
148
|
+
|
|
149
|
+
\`\`\`bash
|
|
150
|
+
curl -G ${base}/action --data-urlencode 'knob=群聊' # literal: appUIStore.mode='im'
|
|
151
|
+
curl -G ${base}/action --data-urlencode 'knob=深度研究' # toggle: flips the bool
|
|
152
|
+
curl -G ${base}/action --data-urlencode 'knob=最大Token' --data-urlencode 'value=8' # param: needs ?value=
|
|
153
|
+
curl ${base}/action?knob=@k3 # @kN: addressable ref from /screen, no CJK escaping
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
Every \`/screen\` projection tags each knob with a stable \`@kN\` ref (the number in \`?form=knobs\`,
|
|
157
|
+
shared across the ASCII art / tree / legend). \`/action?knob=@k3\` resolves it back to the label —
|
|
158
|
+
saving the CJK \`--data-urlencode\` round-trip and any \`&file=\` disambiguation. A stale/unknown
|
|
159
|
+
\`@kN\` is refused (422 listing the current numbers) — re-read \`/screen\` for fresh ones; it never
|
|
160
|
+
silently hits the wrong knob.
|
|
161
|
+
|
|
162
|
+
Returns the same \`--- changed ---\` trajectory a \`/click\` does (waitForStable + diffScreen),
|
|
163
|
+
not a full state dump. A knob whose value depends on runtime context (\`e.target.value\`, a
|
|
164
|
+
mapped array, a method with args) is **refused** (422 + reason) — fall back to \`/click\`/\`/fill\`
|
|
165
|
+
there; it never fake-replays. A label two components share returns 422 + candidates; add
|
|
166
|
+
\`&file=<path-fragment>\` to disambiguate.
|
|
167
|
+
|
|
168
|
+
**Multiple tabs.** Every read/drive command takes \`?tab=<id>\` to address one specific tab —
|
|
169
|
+
including a **background** one (you can drive the Chat tab while the user is looking at a
|
|
170
|
+
different tab). Run \`${base}/tabs\` to see the live ids. With one tab open, omit \`?tab=\` and
|
|
171
|
+
it just works. With several tabs open and no \`?tab=\`, the command returns \`409\` + the tab
|
|
172
|
+
list (rather than randomly hitting one) — pick an id from it and retry with \`?tab=\`.
|
|
173
|
+
|
|
174
|
+
**Multiple servers (federation).** When several dev servers run at once — a micro-frontend,
|
|
175
|
+
separate front/back servers, or a teammate's machine — every command also takes
|
|
176
|
+
\`?host=<host:port>\` to reach a *sibling* aihand. The plugin you curl reverse-proxies the
|
|
177
|
+
request to that peer (server-side, no browser): \`${base}/screen?host=localhost:5174\` reads
|
|
178
|
+
the app on :5174; combine with \`?tab=\` to point at one tab over there
|
|
179
|
+
(\`?host=192.168.1.9:5173&tab=t3\`). Omit \`?host=\` and it's the local server as always. There's
|
|
180
|
+
no registry — you name the peer, so list its tabs with \`/tabs?host=<host:port>\` first.
|
|
181
|
+
|
|
182
|
+
**Cross-tab timeline.** \`${base}/timeline\` interleaves the semantic actions of *every* tab
|
|
183
|
+
in time order — each line \`<tab> [T|S] <action> → <ui change>\` (\`T\`=trusted human,
|
|
184
|
+
\`S\`=synthetic aihand). The per-action \`--- recent actions ---\` tail only shows the acting
|
|
185
|
+
tab; \`/timeline\` is the group view, so an A/B comparison across two tabs (drive A, watch B
|
|
186
|
+
react) is one read. \`?tab=<id>\` filters to one tab's history.
|
|
187
|
+
|
|
188
|
+
**Chain — a whole interaction in one round-trip.** POST a JSON array; runs in sequence,
|
|
189
|
+
each step settles before the next, stops on first failure. A failed step carries the same
|
|
190
|
+
\`clickable:\` fallback list a single-shot \`/click\` gives, and a trailing
|
|
191
|
+
\`[n..m] skipped (… not run)\` line names every step the fail-fast stop left unrun — the page
|
|
192
|
+
is parked in a half-done state, not rolled back (page actions aren't transactional):
|
|
193
|
+
|
|
194
|
+
\`\`\`bash
|
|
195
|
+
curl -X POST ${base}/chain -d '[
|
|
196
|
+
{"type":"knob","knob":"群聊"},
|
|
197
|
+
{"type":"assert","screen":"appUIStore.mode","equals":"im"},
|
|
198
|
+
{"type":"click","sel":"button[title=\\"知识库\\"]"},
|
|
199
|
+
{"type":"wait","text":"Done"},
|
|
200
|
+
{"type":"fill","sel":"textarea","value":"hi"},
|
|
201
|
+
{"type":"assert","screen":"<a domain key from /screen>","equals":"false"},
|
|
202
|
+
{"type":"press","key":"Enter"}
|
|
203
|
+
]'
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
The \`screen\` key in an \`assert\` must be a real field from \`/screen\`'s \`domain:\` block (e.g.
|
|
207
|
+
\`streaming\`, \`appUIStore.mode\`) — copy it verbatim, the names are app-specific. A wrong key
|
|
208
|
+
fails with \`no domain key "X" — available: …\` listing every real one.
|
|
209
|
+
|
|
210
|
+
A \`knob\` step (\`{type:"knob",knob:"<panel label>"[,value,file]}\`) replays a panel morphism
|
|
211
|
+
via the green channel — the store-direct optimal path \`/action?knob=\` takes, now inside a
|
|
212
|
+
batch. Prefer it over \`click\` when a knob exists: no selector resolution, no overlay gating.
|
|
213
|
+
|
|
214
|
+
\`assert\` is the chain's mid-step judge: \`{type,screen,equals}\` checks a domain variable
|
|
215
|
+
(from the app's \`window.__AIPEEK_SCREEN__\`), or \`{type,sel,equals}\` an element's text. On
|
|
216
|
+
mismatch the chain stops and reports \`asserted X=="Y", actual "Z"\` — a test, not a guess.
|
|
217
|
+
Domain variables also show up in \`/screen\`'s \`domain:\` block and in every \`--- changed ---\`
|
|
218
|
+
diff (e.g. \`流式中: false → true\`) — the app's own state machine, which a DOM-only inspector
|
|
219
|
+
can't see. The app opts in by setting \`window.__AIPEEK_SCREEN__ = () => ({...})\`.
|
|
220
|
+
|
|
221
|
+
**Escape hatch.** \`curl '${base}/eval?code=...'\` (or POST the code as the body) runs arbitrary
|
|
222
|
+
JS in the page and returns the result — for what the typed endpoints can't do (install listeners,
|
|
223
|
+
read closures, probe event flow). For count/text/state/attr assertions reach for \`/query\` first.
|
|
224
|
+
When the code is a hand-rolled \`/state\` (or the selector is illegal), the response tail carries a \`hint:\` line pointing at the typed twin.
|
|
225
|
+
|
|
226
|
+
aihand auto-detects errors after HMR and prints them to the terminal — watch for \`[aihand]\` messages.
|
|
227
|
+
|
|
228
|
+
**Iterating on aihand itself.** Editing this server's own code (\`plugin.ts\` and anything in the Vite
|
|
229
|
+
config dependency graph) makes Vite AUTO-restart the dev server in-process — terminal prints
|
|
230
|
+
\`…changed, restarting server…\` then \`server restarted.\`, PID unchanged. You do NOT restart it by
|
|
231
|
+
hand and you do NOT ask the user to; just edit and wait ~2-4s, then poll \`${base}/tabs\` for the page
|
|
232
|
+
to re-register (it self-heals via an unthrottled heartbeat). The client probe (\`client.ts\`) instead
|
|
233
|
+
hot-swaps in place keeping app state. Server stdout goes to the user's terminal (you can't read it) —
|
|
234
|
+
to make server-side state curl-observable, add a temporary debug route, let Vite auto-restart, then
|
|
235
|
+
remove it.
|
|
236
|
+
`
|
|
237
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// aihand/vite — 旋钮语义的 build 时投影,活的 /screen 的输入端零件。
|
|
2
|
+
//
|
|
3
|
+
// 对偶于 aiself storeSchema(状态/输出端):那个把 store 的值域从类型注解长出,这个把
|
|
4
|
+
// 旋钮的态射(拨它 → store.{field=to})从组件 onClick 长出。两者都是「build 时 AST、
|
|
5
|
+
// 运行时纯 JSON」—— tree-sitter 只在这里(Node 侧)跑,浏览器只收一张 join 表。
|
|
6
|
+
//
|
|
7
|
+
// 为什么 build 时:extractKnobs 走 web-tree-sitter,绝不进浏览器(同 aiself §0.3.0 铁律)。
|
|
8
|
+
// /screen 运行时只有 DOM 候选(label + @File:line);语义这一半在这里备好,运行时按
|
|
9
|
+
// join 键 label+file 拼回去。join 键不是 file:line —— host 行 ≠ handler 行,组件边界处错位。
|
|
10
|
+
|
|
11
|
+
import type { KnobArity, KnobOp } from '../../read/panel.js'
|
|
12
|
+
import { globSync } from 'glob'
|
|
13
|
+
import { buildPanel, classifyKnob, fmtTransitions } from '../../read/panel.js'
|
|
14
|
+
|
|
15
|
+
const VIRTUAL = 'virtual:aihand-knobs'
|
|
16
|
+
const RESOLVED = '\0' + VIRTUAL
|
|
17
|
+
|
|
18
|
+
interface Options {
|
|
19
|
+
// 扫哪些组件文件(glob,相对 vite root)。默认全 src 下的 tsx/jsx。
|
|
20
|
+
include?: string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 一个旋钮在浏览器侧需要的最小投影:渲染好的态射 + store + 坐标 + 可执行算子。
|
|
24
|
+
// key 由 label+file 拼成(join 键),client 用 DOM 候选的 label+file 查它。
|
|
25
|
+
// transitions(人读串)供 /screen morphism 渲染;ops/arity/executable 供绿色通道 executeKnob 直接执行。
|
|
26
|
+
export interface KnobProjection {
|
|
27
|
+
transitions: string // 'mode=im' / 'showSideBar=!showSideBar'
|
|
28
|
+
store: string
|
|
29
|
+
line: number
|
|
30
|
+
ops: KnobOp[] // 结构化算子(field+kind+arity+to/reason),executeKnob 按 kind 分派
|
|
31
|
+
arity: KnobArity // 整钮:nullary(直接 replay) / param(需 ?value=)
|
|
32
|
+
executable: boolean // false = 任一 op 依赖运行时上下文,诚实拒绝
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 旋钮 join 键:label + file。label 为 null(纯图标旋钮)时退回 tag,仍带 file 区分。
|
|
36
|
+
// 与 client 侧从 @File:line 取 file 段、从 DOM 取 label 的拼法必须一致。
|
|
37
|
+
function keyOf(label: string | null, tag: string, file: string): string {
|
|
38
|
+
return `${label ?? `<${tag}>`} ${file}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function knobSchema(options: Options = {}) {
|
|
42
|
+
const include = options.include ?? ['src/**/*.tsx', 'src/**/*.jsx']
|
|
43
|
+
let cache: Record<string, KnobProjection> | null = null
|
|
44
|
+
|
|
45
|
+
const build = async (): Promise<Record<string, KnobProjection>> => {
|
|
46
|
+
if (cache)
|
|
47
|
+
return cache
|
|
48
|
+
const files = globSync(include, { nodir: true, ignore: ['**/node_modules/**'] })
|
|
49
|
+
const knobs = await buildPanel(files)
|
|
50
|
+
const byKey: Record<string, KnobProjection> = {}
|
|
51
|
+
for (const k of knobs) {
|
|
52
|
+
// filePath 是 glob 给的相对/绝对路径;client 侧的 file 来自 babel data-insp-path,
|
|
53
|
+
// 通常是末两段(如 Sidebar/index.tsx)。统一成末两段做 join 键。
|
|
54
|
+
const file = k.filePath.split('/').slice(-2).join('/')
|
|
55
|
+
const { ops, arity, executable } = classifyKnob(k.transitions)
|
|
56
|
+
byKey[keyOf(k.label, k.tag, file)] = {
|
|
57
|
+
transitions: fmtTransitions(k.transitions),
|
|
58
|
+
store: k.store,
|
|
59
|
+
line: k.line,
|
|
60
|
+
ops,
|
|
61
|
+
arity,
|
|
62
|
+
executable,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
cache = byKey
|
|
66
|
+
return byKey
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name: 'aihand:knob-schema',
|
|
71
|
+
resolveId(id: string) {
|
|
72
|
+
if (id === VIRTUAL)
|
|
73
|
+
return RESOLVED
|
|
74
|
+
},
|
|
75
|
+
async load(id: string) {
|
|
76
|
+
if (id !== RESOLVED)
|
|
77
|
+
return
|
|
78
|
+
const knobs = await build()
|
|
79
|
+
return `export const knobs = ${JSON.stringify(knobs)}\n`
|
|
80
|
+
},
|
|
81
|
+
// 组件改动 → 失效缓存,HMR 重投影(onClick 改了 → 态射跟着变)。
|
|
82
|
+
handleHotUpdate(ctx: { file: string }) {
|
|
83
|
+
if (ctx.file.endsWith('.tsx') || ctx.file.endsWith('.jsx'))
|
|
84
|
+
cache = null
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|