Skip to content
C.W.K.
Stream
Lesson 06 of 10 · published

Anchor 6 — The Viewport Context Payload Shape

~11 min · payload, schema, context, case-study, v0.2.1

Level 0Extension Curious
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Lesson 6 zooms into the JSON object that flows through every part of ChromeEmbed: the host-context payload. Every field justified, every field a contract the panel iframe relies on."

The Full Shape

The content script's extractContext() returns:

{
  type: 'pippa:host-context',
  payload: {
    host_kind: 'web-page',
    snapshot_at: '2026-05-16T01:23:45.000Z',
    host_id: chrome.runtime.id,
    source: location.href,
    human_label: document.title || location.hostname,
    favicon: faviconHref(),
    selection,
    viewportText,
    readableText: viewportText, // compat alias
    wordCount,
    viewport: { width, height, scrollX, scrollY, devicePixelRatio },
    scrollPosition: { y: scrollY, ofPageHeight: pageHeight },
    language: document.documentElement?.lang || navigator.language || '',
    frameUrl: location.href,
    isTopFrame: window.top === window
  }
}

The Identity Fields

  • host_kind — always 'web-page' for ChromeEmbed. The PIPPA-EMBEDS framework expects this; Adobe-embed would be 'adobe-app', Calendar would be 'gcal-event', etc. Discriminator for downstream rendering.
  • host_id — the extension's runtime id. Lets cwkPippa distinguish messages from different installed instances (multi-Chrome-profile setups).
  • source — the canonical URL the user is on. cwkPippa surfaces this as 'capturing from example.com/article'.
  • human_label — fallback-chained title. Used as the chat thread label.
  • favicon — for UI affordance.

The Content Fields

  • selection — current highlighted text (or cached selection from up to 5 minutes ago).
  • viewportText — the text inside the user's current viewport, walker-extracted and spatially sorted (Lesson 3).
  • readableText — alias for viewportText. Older cwkPippa code paths read 'readableText'; the alias means ChromeEmbed doesn't break them. Plan: deprecate when cwkPippa migrates fully to 'viewportText'.
  • wordCount — quick metric for cwkPippa UI ('1,247 words in view').

The Viewport Fields

  • viewport: { width, height, scrollX, scrollY, devicePixelRatio } — physical-pixel measurements of the user's window. Lets cwkPippa adapt UI to small viewports or hi-DPI displays.
  • scrollPosition: { y, ofPageHeight } — where the user is in the page vertically, plus total page height. cwkPippa can show 'you are 60% through this article.'

The Metadata

  • snapshot_at — ISO timestamp. Lets cwkPippa display 'context from 5 seconds ago.'
  • language — useful for cwkPippa to format responses in matching locale.
  • frameUrl, isTopFrame — for the merge logic in the SW (Lesson 2). Sub-frame pushes carry these so the SW can decide whether to override the top-frame snapshot.

Why a Schema, Not 'Just Send the DOM'

The discipline of a fixed payload shape pays back when:

  • Other embeds compose. The PIPPA-EMBEDS framework expects every embed (Chrome, Adobe, Mail, Calendar) to produce host-context with the same field names. cwkPippa renders them uniformly.
  • Versioning. If you ever need to bump payload v1 → v2, you can add a version: 1 field today and check it tomorrow.
  • Compatibility. The readableText alias is exactly the kind of migration aid a fixed schema enables. Without a schema, every consumer would have to guess.
  • Observability. When cwkPippa's logs show a context payload, you can read it like a structured record instead of guessing what fields exist.

What's NOT in the Payload

Deliberately absent:

  • Full document HTML — way too large and loses the viewport semantics.
  • Screenshot — costs storage, screenshot crops can be a future field.
  • Tab history — outside ChromeEmbed's scope; cwkPippa already has its own conversation history.
  • User identity — Pippa knows who you are from the cwkPippa session, not from extension context.

Adding fields is cheap when justified by a feature; adding them speculatively makes the schema noisy and the bridge slower.

One payload shape, every field justified, every field a contract. The schema is what makes ChromeEmbed composable with other embeds; without it, each embed reinvents 'context' and cwkPippa has to special-case all of them.
The PIPPA-EMBEDS framework angle. ChromeEmbed v0.1 is the first PIPPA embed. The host-context schema it ships is the proposed contract for future embeds (Adobe, Mail, Calendar, IDE). cwkPippa renders them uniformly; the embed's only job is to populate the schema fields from whatever its native host provides. That's the win — each new embed is a content-script-and-bus shape with one payload format, not a from-scratch UI.

Code

content-script.js — the schema, exactly as the prototype writes it·javascript
// embeds/chrome/content-script.js — extractContext shaped output
function extractContext() {
  const viewportText = collectViewportText();
  const selection = currentSelectionText();
  const pageHeight = Math.max(
    document.documentElement?.scrollHeight || 0,
    document.body?.scrollHeight || 0,
    window.innerHeight || 0
  );
  return {
    type: 'pippa:host-context',
    payload: {
      host_kind: 'web-page',
      snapshot_at: new Date().toISOString(),
      host_id: chrome.runtime.id,
      source: location.href,
      human_label: document.title || location.hostname,
      favicon: faviconHref(),
      selection,
      viewportText,
      readableText: viewportText, // back-compat alias
      wordCount: viewportText ? viewportText.split(/\s+/).filter(Boolean).length : 0,
      viewport: {
        width: Math.round(window.innerWidth || 0),
        height: Math.round(window.innerHeight || 0),
        scrollX: Math.round(window.scrollX || 0),
        scrollY: Math.round(window.scrollY || 0),
        devicePixelRatio: Number((window.devicePixelRatio || 1).toFixed(2))
      },
      scrollPosition: { y: window.scrollY || 0, ofPageHeight: pageHeight },
      language: document.documentElement?.lang || navigator.language || '',
      frameUrl: location.href,
      isTopFrame: window.top === window
    }
  };
}
v0.2.1 checkpoint — ChromeEmbed payload — page facts first, tab identity second·javascript
// content script: page facts, provisional identity
const pagePayload = {
  host_kind: "web-page",
  host_id: chrome.runtime.id,
  source: location.href,
  human_label: document.title || location.hostname,
  selection,
  selectionFresh,
  viewportText,
  readableText: viewportText,
  wordCount,
  viewport: { width, height, scrollX, scrollY, devicePixelRatio },
  scrollPosition: { y: scrollY, ofPageHeight: pageHeight },
  frameUrl: location.href,
  isTopFrame: true
}

// service worker: authoritative tab-scoped identity
const decoratedPayload = {
  ...payload,
  host_id: `chrome-tab:${sessionId}:window:${tab.windowId}:tab:${tab.id}`,
  browser_session_id: sessionId,
  chrome_tab_id: String(tab.id),
  chrome_window_id: String(tab.windowId),
  chrome_tab_key: tabKey,
  incognito: Boolean(tab.incognito)
}

Exercise

Take one live pippa:host-context payload from the panel and label each field as content-script-authored or service-worker-decorated. Navigate the same tab to a new URL and compare host_id, chrome_tab_key, source, and snapshot_at. Then open the same URL in another tab and compare again.
Hint
The content script may emit chrome.runtime.id provisionally, but the panel-facing payload should carry the decorated chrome-tab key. Viewport dimensions are CSS pixels; DPR is metadata, not the identity.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.