mesurer-solid 0.1.0 → 0.1.1-beta.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.
@@ -28,10 +28,14 @@ The page is the shared state boundary. Mesurer never needs to know which chat, t
28
28
 
29
29
  ## Install the portable Agent Skill
30
30
 
31
+ Use the stable package by default:
32
+
31
33
  ```bash
32
- npx --yes --package=mesurer-solid@beta mesurer-skill install
34
+ npx --yes --package=mesurer-solid mesurer-skill install
33
35
  ```
34
36
 
37
+ Use `mesurer-solid@beta` only when intentionally validating a prerelease.
38
+
35
39
  The installed skill is self-contained:
36
40
 
37
41
  ```text
@@ -41,7 +45,7 @@ The installed skill is self-contained:
41
45
  └── inject-script.js
42
46
  ```
43
47
 
44
- The skill defines the same context-first workflow described here.
48
+ The skill defines the same context-first workflow described here, including the distinction between Mesurer's optional human screenshot plugin and screenshots owned by the agent's outer browser harness.
45
49
 
46
50
  ## Reuse a live human instance first
47
51
 
@@ -58,7 +62,7 @@ if (hasMesurer) {
58
62
  }
59
63
  ```
60
64
 
61
- If Mesurer exists, use that exact instance. The person may already have selected elements, placed guides, measured gaps, held distances, enabled rulers/X-ray, or saved annotations. Read that state before changing it.
65
+ If Mesurer exists, use that exact instance. The person may already have selected elements, placed guides, measured gaps, held distances, enabled rulers/X-ray, saved annotations, or kept a screenshot preview open. Read and preserve that state before changing it.
62
66
 
63
67
  The injector also reuses a live injected instance by default. Deliberate destructive replacement requires:
64
68
 
@@ -97,6 +101,14 @@ await browser.evaluate(() => window.__MESURER__.ready())
97
101
 
98
102
  Do not create a second browser/CDP connection, Mesurer server, special app build, or source mutation merely to inspect a page the harness already controls.
99
103
 
104
+ Normal injection keeps screenshot capture disabled unless requested:
105
+
106
+ ```js
107
+ window.__MESURER_CONFIG__ = { screenshot: true }
108
+ ```
109
+
110
+ The first-party Chrome extension enables the screenshot plugin automatically because its human-facing camera tool can use the extension's visible-tab capture bridge.
111
+
100
112
  ## Capability contract
101
113
 
102
114
  After `ready()`:
@@ -123,7 +135,7 @@ Copy Selection
123
135
  Add Note
124
136
  ```
125
137
 
126
- There is no `send`, `screenshots`, or `sendContext` delivery capability.
138
+ There is no `send`, `screenshots`, or `sendContext` **delivery capability**. The optional `mesurer.screenshot` plugin is a separate human capture tool/service and does not add image delivery to the context API.
127
139
 
128
140
  ## Context acquisition precedence
129
141
 
@@ -326,9 +338,32 @@ expected target/guide/measurement missing
326
338
 
327
339
  If the requested result remains numerically wrong, continue editing.
328
340
 
329
- ## Screenshots complement context
341
+ ## Human screenshot plugin vs agent screenshot evidence
342
+
343
+ Mesurer has two intentionally different screenshot paths.
344
+
345
+ ### Human capture tool
346
+
347
+ The optional `mesurer.screenshot` plugin gives the person a camera tool inside Mesurer:
348
+
349
+ ```ts
350
+ import { mountMeasurer } from "mesurer-solid"
351
+ import { screenshotPlugin } from "mesurer-solid/screenshot"
352
+
353
+ const mesurer = mountMeasurer({
354
+ plugins: [screenshotPlugin()],
355
+ })
356
+ ```
357
+
358
+ The user can drag a viewport region, capture a real HiDPI-aware PNG, optionally copy/download it, keep a persistent draggable thumbnail, and open a larger Copy/Save viewer. Normal browser hosts use `getDisplayMedia()`; the first-party Chrome extension uses `chrome.tabs.captureVisibleTab()` through its isolated-world bridge and therefore avoids the screen-share chooser.
330
359
 
331
- Mesurer supplies capture scope; the outer harness supplies real pixels:
360
+ For advanced mounted integrations, the typed `MesurerScreenshotService` is available from the plugin host under service id `screenshot`. It is not part of `window.__MESURER__`'s context/delivery capability surface.
361
+
362
+ Agents should preserve an existing human screenshot preview unless the task explicitly asks them to test, close, replace, or otherwise manipulate the screenshot feature.
363
+
364
+ ### Agent verification screenshot
365
+
366
+ For coding-agent verification, the outer harness should normally continue to own screenshot bytes so the task can control the exact browser, viewport, timing, and artifact destination. Mesurer supplies clean capture scope/presentation:
332
367
 
333
368
  ```js
334
369
  const plan = await window.__MESURER__.capturePlan({ scope: "selection" })
@@ -348,6 +383,8 @@ Mesurer context → exact geometry, box model, styles, distances, overflow
348
383
  real screenshot → composition, hierarchy, clipping, color, visual judgment
349
384
  ```
350
385
 
386
+ Do not replace exact Mesurer measurements with pixel estimates from either screenshot path.
387
+
351
388
  ## Source-mounted usage
352
389
 
353
390
  When Mesurer is intentionally mounted from application code:
@@ -367,7 +404,7 @@ The same API is available on `mesurer.agent` and, when configured, `window.__MES
367
404
  const context = await mesurer.agent.select("#target")
368
405
  ```
369
406
 
370
- No transport callback is involved.
407
+ Screenshot capture can be composed independently with `screenshotPlugin()` when the host wants the human camera tool. No transport callback is involved.
371
408
 
372
409
  ## Low-level inspection
373
410
 
@@ -391,7 +428,7 @@ Prefer `context()`, `select()`, and `review()` for visual development because th
391
428
  A good harness-level visual completion loop is:
392
429
 
393
430
  ```text
394
- 1. discover/reuse Mesurer
431
+ 1. discover/reuse Mesurer and preserve human state
395
432
  2. consume existing human context
396
433
  3. if target ambiguous, ask user to select
397
434
  4. otherwise select known affected rendered target(s) when needed
@@ -399,8 +436,10 @@ A good harness-level visual completion loop is:
399
436
  6. edit normal source
400
437
  7. wait for stable render
401
438
  8. get fresh review/context; use select() for known changed targets
402
- 9. optionally capture real screenshot
439
+ 9. optionally capture a real screenshot through the outer harness
403
440
  10. iterate until rendered evidence supports the claim
404
441
  ```
405
442
 
443
+ When the task is specifically testing Mesurer's screenshot plugin, exercise its camera/preview/viewer path as the feature under test; otherwise do not substitute it for the harness's normal screenshot primitive.
444
+
406
445
  **Context is the output of the Mesurer step, not an optional side effect.**
package/README.md CHANGED
@@ -6,13 +6,23 @@ The renderer is implemented privately in Solid 2, but consumers can use Solid 1/
6
6
 
7
7
  ## Install
8
8
 
9
+ Stable releases use the `latest` dist-tag:
10
+
11
+ ```bash
12
+ bun add -d mesurer-solid
13
+ # or
14
+ npm install -D mesurer-solid
15
+ ```
16
+
17
+ Use the `beta` tag only when you intentionally want to test a prerelease:
18
+
9
19
  ```bash
10
20
  bun add -d mesurer-solid@beta
11
21
  # or
12
22
  npm install -D mesurer-solid@beta
13
23
  ```
14
24
 
15
- > Prereleases through `0.1.0-beta.11` used the old scoped package name. New releases use `mesurer-solid`.
25
+ Prereleases through `0.1.0-beta.11` used the old scoped package name; current releases use `mesurer-solid`.
16
26
 
17
27
  ## Mount the base inspector
18
28
 
@@ -108,6 +118,14 @@ Injection installs `contextPlugin()` by default. To deliberately inject only the
108
118
  window.__MESURER_CONFIG__ = { context: false }
109
119
  ```
110
120
 
121
+ Screenshot capture remains opt-in for normal injection:
122
+
123
+ ```js
124
+ window.__MESURER_CONFIG__ = { screenshot: true }
125
+ ```
126
+
127
+ The first-party Chrome extension enables screenshot capture automatically.
128
+
111
129
  ## Direct context API
112
130
 
113
131
  With the context plugin loaded:
@@ -248,9 +266,41 @@ const after = await window.__MESURER__.select([
248
266
 
249
267
  For meaningful visual work, fresh Mesurer context/review is part of completion. Lint, typecheck, tests, and build are implementation checks, not rendered proof.
250
268
 
251
- ## Clean screenshot evidence
269
+ ## Optional screenshot plugin
270
+
271
+ Screenshot capture is a removable first-party plugin instead of permanent core state:
272
+
273
+ ```ts
274
+ import { mountMeasurer } from "mesurer-solid"
275
+ import { screenshotPlugin } from "mesurer-solid/screenshot"
276
+
277
+ const mesurer = mountMeasurer({
278
+ plugins: [
279
+ screenshotPlugin({
280
+ copy: true,
281
+ download: false,
282
+ }),
283
+ ],
284
+ })
285
+ ```
286
+
287
+ The camera tool lets the user drag a viewport region. Mesurer captures the real visible page, converts CSS viewport coordinates to the captured bitmap scale so Retina/HiDPI crops remain exact, temporarily hides its control chrome from the pixels, then restores the previous inspector presentation.
288
+
289
+ A successful capture can automatically copy PNG data to the clipboard and/or download a PNG according to persistent plugin settings. Those output operations are best-effort: if clipboard or download access is unavailable, the captured PNG is still kept for preview/viewer use and Mesurer reports the available result instead of discarding it.
290
+
291
+ When `contextPlugin()` and/or `screenshotPlugin()` are mounted, Settings → General → Plugins exposes compact persisted controls for the human-facing plugin surface. Context can hide or restore its toolbar/annotation controls without removing the typed context service. Screenshot can hide or restore the camera tool and control automatic copy, automatic download, and whether measurement presentation is included in captured pixels.
292
+
293
+ After capture, Mesurer shows a persistent draggable thumbnail. A new thumbnail starts in the bottom-right with an 8px viewport inset. Dragging preserves the existing viewport-clamping behavior, so the preview stays inside that safe boundary. The thumbnail can be dismissed, dragged around the viewport, right-clicked with the browser's native image context menu, or clicked to open a larger viewer. The viewer preserves native image right-click behavior and adds explicit Copy, Save, and Close controls. Escape or backdrop click closes the viewer without discarding the thumbnail. A short status message confirms whether the screenshot was copied, saved, captured, or could not complete an optional output.
294
+
295
+ Normal browser hosts use `getDisplayMedia()` and reuse a live capture stream to avoid prompting for every region. The first-party Chrome extension uses `chrome.tabs.captureVisibleTab()` through its isolated-world extension bridge, so its screenshot path does not open the screen-share chooser and does not require a broad `<all_urls>` permission.
296
+
297
+ Programmatic mounted users can get the typed `MesurerScreenshotService` from the plugin host with service id `screenshot`. `start()` opens region selection, `cancel()` closes it, `capture(rect)` captures an exact CSS-pixel viewport rectangle, `settings()` reads copy/download preferences, and `setSettings()` updates those persistent preferences.
298
+
299
+ Screenshot does not claim the global `C` shortcut because the context workflow already uses `C` and `Shift+C`.
300
+
301
+ ## Clean screenshot evidence for agents
252
302
 
253
- Mesurer plans the evidence frame; the existing browser harness takes the real screenshot:
303
+ The optional screenshot plugin is a human capture tool, not an agent-delivery channel. Agent verification can continue to let the existing browser harness own deterministic task screenshots while Mesurer plans a clean evidence frame:
254
304
 
255
305
  ```js
256
306
  const plan = await window.__MESURER__.capturePlan({ scope: "selection" })
@@ -262,7 +312,7 @@ try {
262
312
  }
263
313
  ```
264
314
 
265
- Use screenshots for visual composition and Mesurer context for exact numeric claims.
315
+ Use screenshots for visual composition and Mesurer context for exact numeric claims. A screenshot plugin image does not create a `screenshots` delivery capability on `window.__MESURER__` and does not replace context/select/review evidence.
266
316
 
267
317
  ## Context UI
268
318
 
@@ -279,7 +329,7 @@ Those remain the three human context controls. `select()` is a programmatic agen
279
329
  ## Portable Agent Skill
280
330
 
281
331
  ```bash
282
- npx --yes --package=mesurer-solid@beta mesurer-skill install
332
+ npx --yes --package=mesurer-solid mesurer-skill install
283
333
  ```
284
334
 
285
335
  The installer leaves:
@@ -291,7 +341,7 @@ The installer leaves:
291
341
  └── inject-script.js
292
342
  ```
293
343
 
294
- The skill teaches the context-first workflow, including when to consume human selection, when to ask for a selection, when to self-select changed targets, multi-selection reads, and fresh post-edit verification. See [`AGENT_INTEGRATION.md`](./AGENT_INTEGRATION.md).
344
+ The skill teaches the context-first workflow, including when to consume human selection, when to ask for a selection, when to self-select changed targets, multi-selection reads, fresh post-edit verification, and the distinction between the optional human screenshot plugin and harness-owned agent screenshot evidence. See [`AGENT_INTEGRATION.md`](./AGENT_INTEGRATION.md).
295
345
 
296
346
  ## Low-level agent API
297
347
 
@@ -316,13 +366,14 @@ import {
316
366
  } from "mesurer-solid/core"
317
367
  ```
318
368
 
319
- Plugins can contribute tools, commands, hooks, overlays, settings, state, services, history/persistence, renderer-owned UI, and lifecycle cleanup.
369
+ Plugins can contribute tools, commands, hooks, overlays, settings, state, services, history/persistence, renderer-owned UI, and lifecycle cleanup. Plugin-defined toggle settings can register through the plugin host and appear in the canonical General → Plugins Settings section without hard-coding the plugin into Mesurer's renderer.
320
370
 
321
371
  ## Public surface
322
372
 
323
373
  ```text
324
374
  mesurer-solid
325
375
  mesurer-solid/core
376
+ mesurer-solid/screenshot
326
377
  mesurer-solid/inject
327
378
  mesurer-solid/inject-script
328
379
  ```
@@ -2,8 +2,9 @@ import type { MesurerPlugin } from "./core";
2
2
  import { type MesurerAnnotation, type MesurerCapturePlanV1, type MesurerContextRequest, type MesurerContextV1, type MesurerReviewV1 } from "./context";
3
3
  export declare const MESURER_CONTEXT_PLUGIN_ID = "mesurer.context";
4
4
  export declare const MESURER_CONTEXT_SERVICE_ID = "context:v1";
5
+ export declare const MESURER_CONTEXT_SETTINGS_STATE_ID = "mesurer.context.settings";
5
6
  export type MesurerContextPluginOptions = {
6
- /** Render Copy Context, Copy Selection, Add Note, and annotation UI. Defaults to true. */
7
+ /** Initial human-facing Context UI state. The Settings toggle can change it at runtime. Defaults to true. */
7
8
  ui?: boolean;
8
9
  };
9
10
  export type MesurerContextService = {
package/dist/core.d.ts CHANGED
@@ -2,8 +2,8 @@ export type Registration = {
2
2
  readonly dispose: () => void;
3
3
  };
4
4
  export type PluginId = string;
5
- type PluginScalar = string | number | boolean | null;
6
- type PluginValue = PluginScalar | PluginValue[] | {
5
+ export type PluginScalar = string | number | boolean | null;
6
+ export type PluginValue = PluginScalar | PluginValue[] | {
7
7
  [key: string]: PluginValue;
8
8
  };
9
9
  export type PluginStateSnapshot = {
@@ -23,12 +23,39 @@ export type ToolContribution = {
23
23
  };
24
24
  active?: () => boolean;
25
25
  disabled?: () => boolean;
26
+ hidden?: () => boolean;
26
27
  };
28
+ export type SettingsToggleContribution = {
29
+ type: "toggle";
30
+ id: string;
31
+ label: string;
32
+ description?: string;
33
+ value(): boolean;
34
+ set(value: boolean): void | Promise<void>;
35
+ disabled?: () => boolean;
36
+ };
37
+ export type SettingsControlContribution = SettingsToggleContribution;
27
38
  export type SettingsContribution = {
28
39
  id: string;
29
40
  label: string;
30
41
  order?: number;
31
42
  builtin?: string;
43
+ controls?: SettingsControlContribution[];
44
+ };
45
+ export type SettingsControlDescription = {
46
+ type: "toggle";
47
+ id: string;
48
+ label: string;
49
+ description: string | undefined;
50
+ value: boolean;
51
+ disabled: boolean;
52
+ };
53
+ export type SettingsDescription = {
54
+ id: string;
55
+ label: string;
56
+ order?: number;
57
+ builtin?: string;
58
+ controls: SettingsControlDescription[];
32
59
  };
33
60
  export type OverlayContribution = {
34
61
  id: string;
@@ -50,6 +77,7 @@ export type MesurerPluginContext = {
50
77
  register<T extends PluginValue>(definition: StateSliceDefinition<T>): Registration;
51
78
  get<T extends PluginValue>(id: string): T | undefined;
52
79
  update<T extends PluginValue>(id: string, update: (value: T) => T): void;
80
+ subscribe(listener: () => void): Registration;
53
81
  };
54
82
  tool: {
55
83
  register(contribution: ToolContribution): Registration;
@@ -98,7 +126,7 @@ export type MesurerPluginDescription = {
98
126
  order?: number;
99
127
  builtin?: string;
100
128
  }>;
101
- settings: SettingsContribution[];
129
+ settings: SettingsDescription[];
102
130
  overlays: OverlayContribution[];
103
131
  state: Array<{
104
132
  id: string;
@@ -151,4 +179,3 @@ export declare function createMesurerPluginHost(): MesurerPluginHost;
151
179
  export declare function createMesurerRuntime(options?: {
152
180
  plugins?: MesurerPlugin[];
153
181
  }): Promise<MesurerPluginHost>;
154
- export {};
package/dist/core.js CHANGED
@@ -125,7 +125,13 @@ function i() {
125
125
  return p.has(t.id) || p.set(t.id, t.initial), r;
126
126
  },
127
127
  get: k.get,
128
- update: k.update
128
+ update: k.update,
129
+ subscribe(e) {
130
+ let t = i.on("changed", (t) => {
131
+ (t.reason === "state" || t.reason === "history") && e();
132
+ });
133
+ return n({ dispose: t });
134
+ }
129
135
  },
130
136
  tool: { register: (t) => n(_(o, e, t)) },
131
137
  settings: { register: (t) => n(_(s, e, t)) },
@@ -203,8 +209,18 @@ function i() {
203
209
  requires: e.requires ?? [],
204
210
  provides: e.provides ?? []
205
211
  })),
206
- tools: I.tools().map(({ active: e, disabled: t, icon: n, ...r }) => r),
207
- settings: I.settings(),
212
+ tools: I.tools().map(({ active: e, disabled: t, hidden: n, icon: r, ...i }) => i),
213
+ settings: I.settings().map(({ controls: e = [], ...t }) => ({
214
+ ...t,
215
+ controls: e.map((e) => ({
216
+ type: e.type,
217
+ id: e.id,
218
+ label: e.label,
219
+ description: e.description,
220
+ value: e.value(),
221
+ disabled: e.disabled?.() ?? !1
222
+ }))
223
+ })),
208
224
  overlays: I.overlays(),
209
225
  state: [...f.values()].map(({ id: e, history: t, persist: n }) => ({
210
226
  id: e,