@tyouter/dsh-recorder 0.0.0-stage → 0.1.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/HANDOFF.md ADDED
@@ -0,0 +1,115 @@
1
+ # Handoff — build a DSH recording plugin
2
+
3
+ You are taking over a prototype whose goal is: **record a DSH plugin's real interface and produce a GIF
4
+ (or stills) from it**, without hand-building a lookalike page.
5
+
6
+ Read [docs/findings.md](docs/findings.md) first; it contains the measurements. This document is the
7
+ brief: what to build, the seams that already exist, and the traps that already cost time.
8
+
9
+ ---
10
+
11
+ ## The honest starting point
12
+
13
+ Two instruments were tried against `@tyouter/dsh-notes`:
14
+
15
+ | Approach | What it produced | Verdict |
16
+ | --- | --- | --- |
17
+ | **Re-render**: a generated HTML page reusing the plugin's renderer + stylesheet, driven by `seek(t)`, captured over CDP | A perfect-looking GIF that was *almost* the app — wrong proportions, invented labels | Rejected by the user: "你的 GIF 不对啊" |
18
+ | **Real capture**: scripted driver inside the real page + window grab from outside | Real pixels, real interactions; the note count went 13 → 14 (proof it ran) | Worked, but the tooling was bolted onto the notes repo and the timing/visibility problems below were never solved |
19
+
20
+ The second approach is the right one. What is missing is the product around it.
21
+
22
+ ---
23
+
24
+ ## What to build
25
+
26
+ A plugin, `@tyouter/dsh-recorder` (name is yours), with two surfaces.
27
+
28
+ ### 1. A host tool the agent can call
29
+
30
+ ```
31
+ record_start({ target: "window" | "rect", title?, rect?: [l,t,w,h], fps?, width? })
32
+ record_stop() → { gif, frames, seconds, framesCount }
33
+ list_windows() → [{ hwnd, title, rect }]
34
+ ```
35
+
36
+ Reuse `tools/record-window.py` as the engine: it is a complete, working recorder (Win32 enumerate +
37
+ `ImageGrab` + Pillow GIF). Wrap it as a subprocess, or port the ~120 lines into the plugin's host half.
38
+ Return the GIF path so the agent can present it.
39
+
40
+ Requirements the prototype implies:
41
+
42
+ - **start/stop, not fixed duration.** The agent does not know how long a walkthrough will take.
43
+ - **a status line in the UI while recording.** The prototype was silent; the user assumed nothing was
44
+ happening ("我切过去了,那边没有开始自动录制啊").
45
+ - **explicit rect support.** Titles are not unique; the biggest match is often the wrong pane.
46
+
47
+ ### 2. A client-side driver contract
48
+
49
+ The half that must live inside the plugin under test. `src/demo-driver.js` is a working reference
50
+ implementation for one plugin; generalise the idea:
51
+
52
+ - a small **driver API** the plugin under test exposes: `startDemo(steps)`, or a generic "act like a
53
+ user" helper layered on selectors;
54
+ - a **dev flag** to arm it (the prototype used a `.demo` file in the data folder plus an env var —
55
+ the file form means arming needs no restart; the *client bundle* still needs a reload);
56
+ - `resetDemo()` so a walk can be replayed without reloading the page;
57
+ - **no network of its own**: the demo should exercise the product's real code paths, never mock them.
58
+
59
+ ### 3. Assembling outputs
60
+
61
+ - GIF: already in `record-window.py` (128-colour palette, one shared palette per recording).
62
+ - Stills: pick frames by timestamp; `tools/record-window.py` keeps every frame under `frames/`.
63
+ - Optional later: chapter cards, cursor highlight, zoom — none of this exists yet. Do not promise it in
64
+ a README before it is built; that was the mistake of the re-render approach.
65
+
66
+ ---
67
+
68
+ ## Traps, in the order they cost time
69
+
70
+ 1. **One-shot driver.** A module-level `started` flag means "run once per page load". After the first
71
+ run the UI is inert, which reads as a bug. Either reload or expose a reset.
72
+ 2. **Recording the wrong window.** The panel can be mounted while the chat pane is in front. Sample a
73
+ frame mid-recording and *look at it*; do not trust the timer.
74
+ 3. **Dynamic import does not work in a plugin client half.** The bundle is one factory function; the
75
+ driver must be concatenated into it (that is why `build.mjs` in dsh-notes has a `parts` list).
76
+ 4. **React ignores `value = …`.** Use the native setter + `dispatchEvent(new Event('input'))`.
77
+ 5. **Console encoding.** The bundled Python defaults to cp1252; `--list` crashes on a Chinese window
78
+ title without `sys.stdout.reconfigure(encoding="utf-8")`.
79
+ 6. **The GUI is 401-gated and its port changes on restart.** Do not hard-code a port; read it from the
80
+ app or pass it in.
81
+ 7. **The renderer has no debug port** unless the app is started with `--remote-debugging-port`. If you
82
+ want CDP-based capture (cleaner than screen grabbing: no focus, no occlusion, exact frames), that
83
+ restart is part of the design, not an accident.
84
+
85
+ ---
86
+
87
+ ## Suggested order of work
88
+
89
+ 1. Wrap `tools/record-window.py` behind a host tool + a `record_start/stop` pair, with a visible status.
90
+ This alone is useful to any plugin author.
91
+ 2. Move the driver into a small, documented module (`driveUI({ steps })`) and use the notes walk as the
92
+ first fixture; keep selector strings in the fixture, not in the engine.
93
+ 3. Add a "record this panel" flow: open the panel, arm the demo, start the recorder, stop on completion.
94
+ 4. Only then: overlays (cursor, zoom, chapter cards) — and only as an opt-in post-process on the frames.
95
+
96
+ ## Acceptance for v0.1
97
+
98
+ - Start a recording of a named window or rect, get a GIF back, and the GIF shows the plugin's real UI.
99
+ - Stop reliably; no orphan process; the frames folder is cleaned on the next start.
100
+ - The recording is visible in the UI while it runs.
101
+ - A replay needs no page reload (reset path exists).
102
+ - Nothing in the plugin under test changes except an opt-in dev flag.
103
+
104
+ ---
105
+
106
+ ## Files
107
+
108
+ ```
109
+ src/demo-driver.js reference driver (works against dsh-notes 1.1 selectors)
110
+ tools/record-window.py reference recorder (window/rect, fps, GIF, --list)
111
+ docs/findings.md measurements: ports, 401, window rects, what worked, what did not
112
+ ```
113
+
114
+ The notebook repo (`../dsh-notes`) no longer contains any of this; it was reverted to its product code
115
+ only. Do not add recording concerns back into it.
package/README.md CHANGED
@@ -1,3 +1,250 @@
1
- # Temporary Holding Version
1
+ # dsh-recorder
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Records a DSH plugin's **real** interface — the pixels of the running app, not a re-render — and turns
4
+ that into a GIF or stills. It also **drives** that interface like a person: typing with real input
5
+ events, clicking buttons, selecting a range of text, switching panels.
6
+
7
+ Two ways to use it:
8
+
9
+ - **The panel**: sidebar → 「录制器」, paste a script, press 「开始录制」. The page executes the script
10
+ while the real window is recorded, then shows the GIF.
11
+ - **Agent tools**: `recorder_scan` → `recorder_demo` for a one-shot recording from a conversation.
12
+
13
+ **Status: v0.1, installed and verified on this machine.** A real notes walk produced a 10.8 s,
14
+ 65-frame, 2.0 MB GIF plus three stills (`recordings/rec-20261103-142842/`). The plugin is installed as
15
+ `@tyouter/dsh-recorder` in the `desktop` profile; the row is `include:dsh-recorder`.
16
+
17
+ ## The panel
18
+
19
+ ![Recorder panel](docs/panel.png)
20
+
21
+ Sidebar → 「录制器」 opens a page with two tabs.
22
+
23
+ **录制** — the whole workflow is one centred column:
24
+
25
+ | Part | What it does |
26
+ | --- | --- |
27
+ | 录制脚本 | The script, front and centre. JSON steps or one step per line (see below); kept in `localStorage` between visits. |
28
+ | 复制给插件 agent 的 prompt | One click copies a self-contained prompt (the full spec + what to output). Paste it into the agent that owns the plugin, describe the demo, and it returns a script you paste back. `载入示例` restores the notes walk that the verified GIF came from. |
29
+ | 开始录制 | The primary action, a large button under the editor. `排练(不录制)` runs the same steps without recording (real clicks, no video); `停止` ends early. A rehearsal brings the window forward first — a background window throttles the page's timers and would freeze the walk. |
30
+ | 本次录制 | The result of this run: a large GIF preview (click it for a full-size lightbox), its length/frames/size, the path, and the per-step log. The lightbox opens at **100%** — one GIF pixel per device pixel, so the text is exactly the physical size it had on screen; `适应屏幕` fits the whole frame instead (smaller text). |
31
+
32
+ ![Full-size preview](docs/panel-preview.png)
33
+
34
+ **⚙ 设置** (top right) opens a popover with the parameters — they are no longer mixed into the main
35
+ page:
36
+
37
+ - 输出工作区 — which registered workspace's `recordings/` receives the output.
38
+ - 录制区域 — `功能区` (crop to the main panel), `整个窗口`, `自定义区域` + a CSS selector.
39
+ - 帧率 / GIF 宽度 / 截图时间点 — 6 fps. The GIF width defaults to the content's **physical** pixel
40
+ width (≈1470 on a 125% display) so the preview is never upscaled; lower it to shrink files, and
41
+ remember the encoder cannot add detail beyond the source. Stills are seconds.
42
+ - 铺满当前屏幕 — maximize on the monitor under the cursor.
43
+
44
+ ![Settings](docs/panel-settings.png)
45
+
46
+ **历史** — a grid of past recordings of the selected workspace (thumbnail, name, length, frames,
47
+ size). Click a card to open it with the GIF, meta, path and copy button. Click the preview (the
48
+ `⤢ 全尺寸` badge) to open the full-size lightbox; click the backdrop or ✕ to close.
49
+
50
+ The detail view also has **删除这段录制**: the first click arms the button, the second confirms and
51
+ removes the recording directory from disk. Only directories directly under a recordings root whose
52
+ name matches `rec-YYYYMMDD-HHMMSS` can be deleted (the route refuses anything else).
53
+
54
+ ![History](docs/panel-history.png)
55
+
56
+ ![Recorder running](docs/panel-running.png)
57
+
58
+ While a run is active the status pill counts steps (`录制中 00:01 · 3/3 click`) and the floating REC
59
+ pill below the title bar shows the clock and a stop button. A rehearsal gets the same pill in blue
60
+ (`排练 2/10`) with its own stop, so it can be ended even when the script has switched away from this
61
+ page. The panel is in the main area, so a script that opens another plugin's panel will hide it — the
62
+ pill stays available, and coming back to the recorder panel shows the finished result.
63
+
64
+ The pill is **hidden while the pointer is away from the top strip** and appears when the pointer enters
65
+ the top 84 px. That keeps it out of the recording (nothing is drawn over the page during a normal
66
+ walk), while moving the mouse to the top is the stop control when the panel is not visible. It sits at
67
+ `top: 46px` because the very top strip is the window's drag region; a click there belongs to the window
68
+ manager. Pressing 停止 hides the pill immediately while the GIF is assembled, so a long splice never
69
+ looks like an ignored click.
70
+
71
+ ### Window size
72
+
73
+ `铺满屏幕` (and, when `maximizeOnStart` is true in the patch, a moment after the plugin activates)
74
+ moves the app window to the monitor under the cursor and maximizes it, so a freshly launched Harness
75
+ fills the current screen instead of opening at its remembered size. Focusing the window for a rehearsal
76
+ never changes its size: a maximized window stays maximized, at the same rect. Window geometry is the
77
+ plugin's business; **UI scale is not** — the interface still follows the Windows display scale of that
78
+ monitor (120 DPI = 125% here). For a 1:1 UI, set that monitor to 100% or launch the app with Chromium's
79
+ `--force-device-scale-factor=1`; a page plugin cannot change the process's DPI mode.
80
+
81
+ Keep the Harness window visible while a walk runs: Chromium throttles a background window's timers,
82
+ which freezes every `sleep`/`wait` in the script (and delays a stop). The rehearsal brings the window
83
+ forward before it starts; the capture does the same by design.
84
+
85
+ ### Cursor and click effects
86
+
87
+ The driver paints the pointer into the page, so the effects are part of the recording — no
88
+ post-processing, the frames are the page:
89
+
90
+ - a short blue trail follows a move, including a jump across the screen (two breadcrumbs are laid on
91
+ the straight line when the distance is large);
92
+ - **two expanding blue rings plus a flash** mark the click;
93
+ - the clicked element gets a **temporary blue outline**, so the target is obvious even when the
94
+ pointer is small.
95
+
96
+ ![Click effect](docs/click-effect.png)
97
+
98
+ All of it is disabled under `prefers-reduced-motion` (the outline and the pointer are still drawn).
99
+
100
+ ### Recording area
101
+
102
+ `功能区` is the default and is what most demos want: the page measures the main panel's box and sends
103
+ the **insets in CSS pixels plus `devicePixelRatio`**; the Host maps them through the window's real
104
+ client area (Win32 `GetClientRect` + `ClientToScreen`) and crops every frame. The window is still
105
+ maximized for the shoot — the crop is what removes the sidebar and the app title bar, and
106
+ it stays correct on a mixed-DPI desktop because the mapping is measured, not assumed.
107
+
108
+ Because the crop is the **main panel**, the GIF's aspect ratio is the panel's, not the window's
109
+ (e.g. 1544×970 for a 1920×1020 window). The image is still scaled proportionally; if the framing has
110
+ to match the full window, pick `整个窗口`.
111
+
112
+ - `整个窗口` records the maximized window as-is (sidebar included).
113
+ - `自定义区域` takes a CSS selector and crops to that element's box; the element must exist when you
114
+ press start.
115
+ - If another app takes the foreground mid-recording, those frames show that app; the result then
116
+ carries a `⚠` warning rather than pretending the footage is clean.
117
+
118
+ ![Cropped recording](docs/crop-example.png)
119
+
120
+ The same walk in `功能区` mode: 10.6 s / 64 frames / 705 KB, against 2.0 MB for the full-window cut —
121
+ the sidebar and app title bar are simply not in the frame.
122
+
123
+ ### Script syntax
124
+
125
+ One step per line; `|` introduces the text for that step. Lines starting with `#` are comments.
126
+
127
+ ```
128
+ # 打开记事本,写一段笔记,选中一句
129
+ open notes
130
+ wait .dshn-root
131
+ click .dshn-switch[aria-checked="true"]
132
+ click .dshn-tabs button.dshn-tab:nth-of-type(1)
133
+ set .dshn-textarea |
134
+ click .dshn-textarea
135
+ type .dshn-textarea | 极简笔记\n\n这不是一个新的工具,而是一个关于文字的哲学思考。\n智能不能成为文字的主角。\n功能应当服务于思想。\n书写带来快乐。
136
+ sleep 900
137
+ select .dshn-textarea | 智能不能成为文字的主角。
138
+ sleep 1200
139
+ ```
140
+
141
+ - `open <panel>` — switch to another plugin's main panel (`notes`, `wuche`, `conversation`, …).
142
+ - `click <selector> [| text]` — click a button/tab; `text` picks among matches.
143
+ - `type <selector> | text` — type character by character (60 ms each); `\n` inserts a newline.
144
+ - `set <selector> | text` — set the value in one go (good for clearing a draft).
145
+ - `select <selector> | text` — highlight that text.
146
+ - `wait <selector> [timeoutMs]` — wait for an element (default 8 s).
147
+ - `sleep <ms>`, `key <selector> <Tab|Enter|Escape>`, `scroll <selector>`, `focus <selector>`.
148
+
149
+ A JSON array of step objects works too — starting the box with `[` switches the parser. Step objects
150
+ use the same `op` names the agent tools use, and can set `ms` on `type`.
151
+
152
+ A step whose element is missing is reported as failed and the walk continues (unless the JSON sets
153
+ `strict: true`). Selectors come from the plugin's real DOM; the agent tool `recorder_scan` prints them.
154
+
155
+ The full, versioned grammar is in [docs/script-spec.md](docs/script-spec.md). Before spending a
156
+ recording, press **排练(不录制)**: it runs the same steps for real and lists `✓`/`✗` per step, so a
157
+ wrong selector shows up in a second instead of in a wasted GIF.
158
+
159
+ ### Writing a script with another agent
160
+
161
+ The plugin's own agent knows its components and class names, but not the recorder's grammar. Hand it
162
+ the prompt:
163
+
164
+ 1. Press **复制给插件 agent 的 prompt**.
165
+ 2. Paste it into that plugin's agent conversation, replace the last paragraph with what you want to
166
+ see (`打开记录页,点 Structure,输入一句话…`), and send.
167
+ 3. The agent answers with one code block. Paste it into 录制脚本, press **排练(不录制)**, then **开始录制**.
168
+
169
+ The prompt carries the whole spec, so the other agent does not need this repository. If you also want
170
+ it to use *real* selectors, send it the output of `recorder_scan` (the agent tool) for the panel you
171
+ mean; the prompt tells it to prefer that list.
172
+
173
+ ## Agent tools
174
+
175
+ | Tool | What it does |
176
+ | --- | --- |
177
+ | `recorder_windows` | Lists recordable windows (title, screen rect, size) and reports the capture backend. |
178
+ | `recorder_scan` | Reads the live DOM and returns the visible interactive elements with real CSS selectors. |
179
+ | `recorder_start` / `recorder_stop` | Manual start/stop; `start` returns a `probe` frame to check the target. |
180
+ | `recorder_drive` | Runs steps against the live page without recording, or while a recording runs. |
181
+ | `recorder_demo` | start → drive → settle → stop in one call. |
182
+
183
+ ## How it works
184
+
185
+ - **Capturing** pixels happens in the Host half: screen capture of the real window through
186
+ `tools/record-window.py` (Win32 + Pillow). It knows nothing about DSH plugins.
187
+ - **Driving** happens in the page that owns the UI. The recorder's **own client half** lives in the
188
+ same page as every other plugin, so it operates the target's real DOM. The plugin under test does
189
+ not have to change at all — no dev flag, no injected script, no debug port.
190
+ - **Agent-triggered** runs use a job bridge: the page polls `/recorder/api/poll`, runs the handed-out
191
+ steps, and posts the report back. The **panel does not use that bridge** — it runs the same driver
192
+ locally (`POST /start` → steps → `POST /stop`), so a throttled background timer can never stop a
193
+ demo from starting. `/progress` still feeds the pill's step counter on both paths.
194
+
195
+ ```
196
+ run / recorder_demo (agent path)
197
+ │ Host: resolve window → activate → maximize → start frames
198
+ ├────────► /recorder/api/poll ──► page: run steps (type/click/select/openPanel)
199
+ │ Host: wait for the report ◄── /recorder/api/report (and /progress)
200
+ │ Host: settle → STOP → GIF assembly (Pillow) → restore window
201
+ └── { gif, stills, frames, seconds, walk: [step results] }
202
+
203
+ panel: POST /start → runSteps() in the page → POST /stop (no poll involved)
204
+ ```
205
+
206
+ Because a screen grab cannot see through another app, a run raises the window (minimize + restore,
207
+ which is the one activation Windows never refuses) and maximizes it for the length of the shoot; the
208
+ previous placement is restored afterwards. The painted cursor and the REC pill are part of the page
209
+ and are recorded in both modes.
210
+
211
+ ## Installing / updating
212
+
213
+ ```
214
+ plugin_manager(install_bundle, target = <this directory>)
215
+ ```
216
+
217
+ A newly installed bundle's **client** half appeared in the running page without a reload on this
218
+ build. Host-side JavaScript changes are **not** picked up by editing files: disable and re-enable the
219
+ row with `plugin_manager(set_plugin)`, or restart the app.
220
+
221
+ ## Limits (v0.1)
222
+
223
+ - One recording at a time; output goes to `<workspace>/recordings/rec-<stamp>/`.
224
+ - Screen capture means *the screen*: if the window is moved or covered mid-recording, the footage shows
225
+ that. `recorder_start` returns a `probe` frame for exactly this check.
226
+ - The recorded window is maximized for the shoot and restored afterwards.
227
+ - No zoom, chapter cards, or post-processed cursor: the cursor is a DOM overlay recorded with the page.
228
+ - The panel's output workspace is a registered-workspace picker; the agent tools use the calling
229
+ session's cwd.
230
+ - The plugin under test is untouched. The notes fixture clears its draft and never saves.
231
+
232
+ ## Files
233
+
234
+ ```
235
+ index.js Host half: engine wiring, tools, /recorder/api
236
+ lib/engine.js one recording + one page operation at a time; the job bridge; workspaces
237
+ lib/capture.js capture backends (Electron capturePage when available, else screen) + stills
238
+ lib/python.js locating the bundled Python and running the helper
239
+ lib/tools.js the six model-facing tools
240
+ client.js page half: poll loop, step driver, DOM scan, REC pill, cursor, recorder panel
241
+ tools/record-window.py window/rect capture, stop-file protocol, probe frames, GIF assembly
242
+ examples/notes-demo.json the verified walk as JSON steps
243
+ docs/panel.png the recorder page
244
+ docs/panel-running.png the page during a run (REC pill + step counter)
245
+ docs/crop-example.png a 功能区-mode frame (no sidebar, no title bar)
246
+ docs/click-effect.png the pointer, trail and double click ring over a clicked tab
247
+ docs/script-spec.md the versioned script grammar (also the source of the copied prompt)
248
+ HANDOFF.md the original brief (kept as the design record)
249
+ docs/findings.md measurements: what was proved, what failed, and why
250
+ ```