PinList

The PinList interface is the type of o.pins: a live Array of pin handles, with methods that add, change, capture and delete the pins of the playground's output. A pin points at an element or at a region of the live output and can carry a plain-text comment. People and agents use pins to say "this, here". Agent Folder Share stores each pin in the agent folder as 📍.<N>.json, plus 📍.<N>.png when it has an image.

Use o.pins from io.eval.js: write one request, wait for and read io.eval.json, and only then send the next. Top-level await and a final-expression return work. Pins shows how a person picks pins by hand, and Agent files describes the files.

Instance properties

PinList.length

A number: how many pins there are. o.pins is a live Array: length, indexing, map and iteration work, and the array is updated in place. JSON.stringify(o.pins) returns the result of o.pins.toJSON().

PinList.default

An object with the session defaults of the pin tools. Its accessors are enumerable:

scale, origin, lift, unclip, settle, background, backgroundMode

The capture options. Reading gives the effective value of the ⚙️ Image capture setting. Setting validates at once and updates the setting.

enforceAgentDefaults

A boolean value: the 🤖 Enforce as agent pin defaults checkbox.

autoTrack, autoCapture

The defaults in the header of the Pins panel. They apply to pins that a person picks, not to calls of the API.

observeMutations

A boolean value: the 🧬 switch. It is a session-wide switch, not only a default for new pins: assigning it takes effect at once for the pins that exist.

linked, comment

Read only: always true and ''.

Reading background gives 'transparent' while Custom background is off, and otherwise the remembered or theme colour. Setting it to a colour turns Custom background on and remembers the colour; setting 'transparent' turns it off explicitly.

Warning: Do not pass o.pins.default, or a spread of it, to setOptions(). It carries comment: '' and linked: true, so the call clears the comments of its targets, tries to relink detached pins, and throws for a region pin that you name as a target.

PinList.paused Read only

A boolean value: true whenever picking is off, including when Pins is closed.

PinList.curtain Read only

The overlay <div> of the playground that receives pointer events while picking, or null when Pins is closed.

Instance methods

Every method except at() and toJSON() returns a Promise that resolves to the updated payload, the same shape as toJSON() returns, or rejects with an Error. Without an active share, every method that changes pins, and pause() and resume(), rejects with Pin API requires an active Agent Folder Share. See Targets and Options for the arguments.

PinList.add()

await o.pins.add(target, options) pins the first match of each selector, one pin per item. target is a CSS selector string, or an iterable of selector strings. options may hold the pin options and the capture options. It throws Pin target not found: <selector>, and then adds nothing.

PinList.addAll()

await o.pins.addAll(selector, options) takes the same options as add() but pins every match of each selector. Every new pin gets the same comment.

PinList.addRect()

await o.pins.addRect({ x, y, width, height, comment, autoTrack, autoCapture }) pins a region, in CSS pixels of the output viewport. The values are clamped to the viewport. comment defaults to '', autoTrack to true and autoCapture to false. It throws Pin rectangle requires positive width and height.

PinList.at()

o.pins.at(target) returns the pin handle at once, or undefined. target is an integer index, where a negative one counts from the end like Array.prototype.at(), or a pin id string. Handles are cached per id, so o.pins.at(0) === o.pins[0].

PinList.setComment()

await o.pins.setComment(comment, ...targets) sets the comment of the targets. comment defaults to '', and a call without arguments clears every comment.

PinList.setOptions()

await o.pins.setOptions(options, ...targets) applies a partial { autoTrack?, autoCapture?, linked?, comment? }. The values are coerced to boolean or string. Any other key, capture options included, is ignored, and an empty object does nothing.

Without targets, linked is skipped for region pins. With explicit targets that include a region pin it throws Pin target is not element-sourced: <target>. linked: true on a detached pin looks up its element again; if that fails, the whole call rejects with Pinned element was not found; keeping it detached. and nothing changes.

PinList.capture()

await o.pins.capture(...targets, options) refreshes the images of the targets, even when their autoCapture is false. The last argument counts as options only if it contains at least one of track, scale, origin, lift, unclip, settle, background or backgroundMode. By default (track: true) the geometry is refreshed first, and an explicit target whose element cannot be found throws Pin geometry not found: <id>. With { track: false } the stored geometry is used. Without Output capture it resolves without writing images.

PinList.track()

await o.pins.track(...targets) refreshes the geometry once, without changing autoTrack. Only changed geometry is saved. Like capture(), it throws Pin geometry not found: <id> for an explicit target whose linked element is gone.

PinList.pause()

await o.pins.pause() opens Pins if needed and pauses visual picking, which hands the output back to the page. Saved pins, tracking and capture state do not change.

PinList.resume()

await o.pins.resume() resumes picking. Nothing else changes.

PinList.delete()

await o.pins.delete(...targets) deletes the targets, or every pin when there are none, without asking. Pass the targets as separate arguments: o.pins.delete(0, 2).

PinList.toJSON()

Returns the payload of all pins at once. It works without a share and then has an empty pins array.

Targets and options

The methods share three kinds of argument.

Targets

setComment(), setOptions(), capture(), track() and delete() take their targets as rest arguments, for example delete(0, 2). All but delete() also accept a single array, for example capture([0, 2], { scale: 3 }). A target is an index, where a negative one counts from the end, or a pin id. No targets means all pins. An unknown target throws Pin target not found: <target>.

Pin options

Used by add(), addAll() and addRect(), and by setOptions(), which takes the first four. A call that omits autoTrack or autoCapture uses the values below, not the defaults of the Pins panel header.

comment

A string, '' by default. Plain text; other values are converted with String(), and null and undefined become ''. Comments carry notes from people to agents and from agents to people.

autoTrack

A boolean value, true by default. For region pins and detached pins: a tracked pin stays attached to the same spot of the scrolled output document, following the scroll position of the output window, but not scrolling inside nested scroll containers. An untracked pin stays fixed in the viewport. Linked element pins ignore it, because their geometry is always tracked.

autoCapture

A boolean value, false by default. true refreshes the pin's PNG automatically when the output changes. false keeps the current image. A forced capture() works either way.

linked

A boolean value, true by default, for element pins only. A linked pin finds its live element again and follows it. A detached pin (linked: false) keeps the element's data but behaves like a region pin.

pierceDepth

Only for add(), addAll() and setElement(): how far a selector reaches into shadow roots and frames.

Omitted

A plain querySelector() in the output document.

0

The output document only.

-1 (or null or '')

Unlimited.

A number n

At most n shadow or frame boundaries.

The output document is searched first, then nested open shadow roots and same-origin iframe documents, in document order. add() stops at the first root with a match. Closed shadow roots and inaccessible frames are skipped. The depth field in the Pins panel affects picking by a person only, never the selectors of the API.

Capture options

Options for the image of a pin. They need Output screen capture. With Enforce as agent pin defaults checked, which is the starting state, a call inherits every option it omits from the ⚙️ Image capture settings. With it unchecked, omitted options use the built-in defaults below. Options you pass always win, and never change the settings. Scale and backgrounds apply to linked element pins; region and detached pins keep their original crop.

scale

A number greater than 0, 1 by default. The element is temporarily enlarged. The applied scale is the smallest of the requested scale and the scales at which the element still fits the visible output with a 16 px margin; it is also limited by enclosing iframes. The result is in image.capture.appliedScale.

origin

'auto' by default, or a CSS transform-origin value. With 'auto' the element is scaled about its centre and shifted to fit inside the visible output with 8 px margins.

lift

A boolean value, true by default: temporarily sets position: relative (only if the element is static) and a very high z-index, so the element is not covered. It applies to scaled captures only.

unclip

A boolean value, true by default: lets the enlarged element show beyond ancestors that would clip it. It applies to scaled captures only. Real scroll containers are left alone and listed in clippedBy. If unclipping moves anything, the capture fails with Unclipping an idle overflow container changed the pin layout or scroll position.

settle

A number of milliseconds from 0 to 2000, 160 by default: a delay that starts after two animation frames, for scaled and background captures. 0 removes the delay only; the checks for fresh pixels still run.

background

'transparent' by default, or an absolute sRGB colour: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(), rgba() or color(srgb …). Named colours, hsl() and oklch() are rejected. Hex colours are trimmed, lowercased and expanded: '#fff' becomes '#ffffff'.

With Enforce on and Custom background left automatic, an effective scale other than 1 also turns a background on: the remembered colour, else white or black from the colour scheme of the output. Pass background: 'transparent' to keep the painting of the page.

backgroundMode

'backdrop' by default, or 'element'. Backdrop places the colour behind the target and hides the paint around it, which also works for masked icons and inner SVG shapes. Element sets the target's own background-color and keeps its background images.

track

Only for capture(): a boolean value, true by default. false uses the stored geometry instead of refreshing it first.

A wrong value throws one of Scale must be a positive number., Settle delay must be between 0 and 2000 ms., Origin must be auto or a CSS transform-origin value., lift must be true or false., unclip must be true or false., Background must be transparent or an absolute sRGB color. or Background method must be element or backdrop. The built-in defaults are { scale: 1, origin: 'auto', lift: true, unclip: true, settle: 160, background: 'transparent', backgroundMode: 'backdrop' }.

Pin handles

o.pins.at(i) and o.pins[i] return a handle for one pin.

id, kind, selector, label, tag, domPath, rootPath, rect, viewport, url, title, createdAt, timestamp, innerText, outerHTML, computedStyles, cssRules Read only

The fields of the pin, described under Payload and pin object. There is no linked getter: use .pin.linked or .toJSON().linked.

index, pin Read only

Not enumerable. index is the current position of the pin, or -1 after it was deleted. pin is the live pin object.

comment, autoTrack, autoCapture

Assigning one queues a setComment() or setOptions() update. The assignment cannot be awaited, and a failure is only logged to the console. Use the methods when you need to wait.

setComment()

await pin.setComment(comment). Without an argument it clears the comment.

setOptions()

await pin.setOptions(options). Without an argument it does nothing.

capture()

await pin.capture(options). Refreshes the image of this pin.

track()

await pin.track(). Refreshes the geometry of this pin.

setElement()

await pin.setElement(target, options) points the pin at the first match of the selector target, with an optional pierceDepth. The pin keeps its id, createdAt, autoTrack and autoCapture. options.comment replaces the comment, and otherwise it is kept. Capture options apply to the new image. A detached element pin stays detached, and a region pin becomes a linked element pin.

setRect()

await pin.setRect(rect). On a region pin, a missing x, y, width or height keeps its value, so setRect({ width: 240 }) resizes and setRect({ x, y }) moves. On an element pin, width and height are required (x and y default to 0), and the pin becomes a region pin and loses its element data. rect.comment replaces the comment. autoTrack and autoCapture are kept.

up()

await pin.up(delta) moves a linked element pin to an ancestor, along the path recorded when it was pinned: the element, its ancestors, up to the output <html>, crossing shadow hosts and frames. delta defaults to 1 and goes through Math.trunc(Number(delta)); NaN counts as 1, and 0 or null does nothing. A negative -n jumps to the position length − n, so up(-1) is the outermost ancestor. Positions are clamped. On region and detached pins it does nothing. A vanished element throws Pin target disconnected: <id>. Comment, autoTrack, autoCapture and the linked state are kept.

down()

await pin.down(delta): the same in the other direction. down(-n) jumps to position n − 1, so down(-1) returns to the original element. After the output reloads, the path is rebuilt from the element the pin points at now, so down() cannot go back below it.

delete()

await pin.delete() deletes this pin.

toJSON()

Returns the pin object, or null.

Handle methods return a Promise of the payload, except toJSON(). setElement(), setRect(), up() and down() keep the image and the target together. If the pin already has a PNG, they need Output capture (Output screen capture is required to update this pin image.) and commit only after the new image has been captured; if that fails, the old target and image stay. Without a PNG, the update commits at once, and an image is captured when Output capture is active. A second update of the same pin while one is pending throws Pin update is already pending.

Payload and pin object

toJSON(), and every method that returns a Promise, produce this payload:

js
{
  version: 1, type: 'rtcode.pins', source: 'output',
  route,       // Code.D[0], or null
  work,        // the name of the work folder, or null
  pinSchema: 5, url, title, updatedAt,
  latestId,    // the pin added or updated last, while it exists; else the last pin's id; else null
  image: null,
  pins: [ /* pin objects */ ]
}

A pin object has these keys. Unknown keys are dropped. All geometry is in CSS pixels of the output.

KeyMeaning
idpin_<time in base 36>_<6 base-36 characters>. Stable through reordering, renumbering, moves and retargeting.
kind'element' for a pin on an element, 'rect' for a region.
selectorAn approximate label: tag#id, else tag.firstClass, else tag. The id and class are not escaped, so it is not always a valid or unique selector. null for a region.
labelThe innerText with collapsed whitespace, cut to 40 characters plus …, or the selector when there is no text. 'Rect' for a region.
tagThe lowercase tag name, or 'rect'.
domPathThe labels from the outermost ancestor to the element, joined with > , including #shadow-root and #document markers. null for a region.
rootPathFor element pins: an array of segments [{ selector, enter: 'shadow' | 'frame' }, …, { selector }]. A pin finds its element again through it, then through a unique domPath, then through a unique selector.
rect{ x, y, width, height }, relative to the output viewport.
viewport{ width, height, scrollX, scrollY, devicePixelRatio } of the output window at the last report. It is a record; overlays and crops use the live layout, so they stay correct at any zoom.
url, titleThose of the output document.
createdAt, timestampThe first creation time, and the time of the latest full snapshot of the element.
commentThe plain-text comment.
innerText, outerHTMLThe text and the full serialized element. outerHTML can be large. Empty for a region.
computedStylesThe non-default values of 44 layout and paint properties. {} for a region.
cssRules[{ selector, css, media? }] from readable stylesheets: the document's, adopted ones and those of the element's shadow root. Cross-origin sheets are skipped. [] for a region.
autoTrack, autoCaptureAlways present. See Pin options.
linkedElement pins only.

In the agent folder, 📍.<N>.json wraps each pin with the route, the work folder, index and total, and an image entry with the capture details; see Agent files.

Errors

MessageWhen
Pin API requires an active Agent Folder Share.A method that changes pins, pause() or resume() was called without a share.
Pin requires the inline output iframe.The output is not the embedded frame. Pins target only the inline output.
Pin target not found: <x>A selector matched nothing, or a target is not an index or id of a pin.
Pin rectangle requires positive width and height.addRect() or setRect() with an empty rectangle.
Pin target is not element-sourced: <target>setOptions({ linked }) with an explicit region pin.
Pinned element was not found; keeping it detached.linked: true on a detached pin whose element is gone.
Pin geometry not found: <id>capture() or track() for an explicit pin whose element is gone.
Pin target disconnected: <id>up() or down() on a pin whose element vanished.
Pin update is already pending.A second setElement(), setRect(), up() or down() on the same pin.
Output screen capture is required to update this pin image.A change to a pin that has an image, without Output capture.

The messages of the capture options are listed there. A capture can fail for other reasons as well, for example Timed out waiting for a fresh capture frame. A failed capture keeps the previous PNG of the pin. If the pin has none yet, the error is stored and written as captureError in its JSON until a later capture succeeds.

Usage notes

Examples

A page to pin

An element, a list and an open shadow root. The root color-scheme matters: the automatic background of an enlarged capture follows the colour scheme of the output. Open the page and start Agent Folder Share. For images, press Ctrl+Alt+P / ⌘+Alt+P (Pins + Output) and pick this tab in the browser's capture dialog.

html
<style>
  :root    { color-scheme: light dark; }
  main     { display: grid; gap: 1rem; padding: 1rem; }
  #badge   { justify-self: start; padding: .25rem .6rem; border-radius: 1rem; background: oklch(.75 .15 150); }
  x-card   { display: block; }
</style>
<main>
  <h1>Pins demo</h1>
  <span id=badge>new</span>
  <ul id=list><li>One<li>Two<li>Three</ul>
  <x-card></x-card>
</main>
<script>
  customElements.define('x-card', class extends HTMLElement {
    connectedCallback() { this.shadowRoot ?? this.attachShadow({ mode: 'open' }).append(Object.assign(document.createElement('button'), { id: 'inner', textContent: 'Buy' })) }
  })
</script>

Adding a pin and returning what you need

js
const { pins } = await o.pins.add('#badge', { comment: 'Make this pill smaller' })
pins.map(({ id, label, rect, comment }) => ({ id, label, rect, comment }))

Pinning every list item and a region

js
await o.pins.addAll('#list li', { comment: 'row' })
await o.pins.addRect({ x: 0, y: 0, width: 320, height: 80, comment: 'header area', autoTrack: false })
o.pins.map(p => `${p.index} ${p.kind} ${p.label}: ${p.comment}`)

Reaching into an open shadow root

Without pierceDepth, '#inner' is searched in the output document only and fails with Pin target not found: #inner.

js
await o.pins.add('#inner', { pierceDepth: -1, comment: 'inside x-card' })

Walking up to an ancestor and back

js
await o.pins.at(-1).up()      // button#inner → x-card
await o.pins.at(-1).down(-1)  // back to the original button

An enlarged capture

This needs Output screen capture, started by a person. Pin 0 must be a linked element pin; region and detached pins keep their plain crop. Afterwards 📍.0.json has image.capture.appliedScale, the scale fitted to the visible output, and 📍.0.png holds the image.

js
await o.pins.at(0).capture({ scale: 8, settle: 0 })

Session defaults for agents

js
o.pins.default.scale = 3                     // omitted agent options inherit it while Enforce is on
o.pins.default.enforceAgentDefaults = false  // agents fall back to the built-ins: scale 1, transparent

Reading what a person pinned

js
o.pins.toJSON().pins.map(({ id, kind, selector, label, comment, autoCapture }) => ({ id, kind, selector, label, comment, autoCapture }))

Specifications

Not part of any specification. o.pins is an RTCode.io playground feature. Images use screen capture, which needs the Screen Capture API, and the share that stores them needs FileSystemObserver.

Availability

FeaturePlaygroundRTEdge.netNotes
o.pins and its methodsYesNoNeeds Agent Folder Share: Chrome or Edge on desktop, in a top-level playground. Pins are never deployed.
Pin imagesPartlyNoNeed Output screen capture, which a person starts. Without it pins and their JSON still work.
PinList.toJSON()YesNoWorks without a share and then returns no pins.

See also