Skip to content

context-object-spec/web-adapter

Repository files navigation

@context-object-spec/web-adapter

npm version npm downloads Playground deployment License: Apache-2.0

Turn browser text selections into portable, source-grounded COS Context Objects for AI features, editors, annotations, and developer tools.

Try the live Playground · npm · COS documentation · COS on GitHub

If your product reacts to text selection, COS Web Adapter gives you the source-grounded context that selection.toString() leaves behind.

Before and after COS

A browser selection normally becomes an isolated string:

{ "text": "$49" }

COS Web Adapter keeps the selection connected to its source and deterministic structure:

{
  "version": "0.2",
  "source": { "type": "webpage", "title": "Pricing" },
  "selection": { "text": "$49" },
  "context": {
    "scope": "container",
    "segments": [
      {
        "type": "table",
        "text": "$49",
        "selectedText": "$49",
        "relations": [
          { "type": "row_header", "label": "Pro" },
          { "type": "column_header", "label": "Monthly price" }
        ]
      }
    ]
  }
}

The adapter does not summarize content, infer intent, generate prompts, call an AI service, or transmit data. It extracts deterministic evidence from the current document and returns a plain JavaScript object.

Where it fits

  • “Ask AI about this selection” experiences
  • in-page copilots and contextual toolbars
  • browser extensions and reading tools
  • document comments, highlights, and annotations
  • knowledge-base citations and research tools
  • rich-text editors and documentation systems
  • source-grounded search, RAG, and agent inputs
  • structured selections from tables, lists, headings, code, and forms

It converts a browser selection into a lean COS Context Object:

window.getSelection()
        ↓
Web Adapter
        ↓
ContextObject
        ├── version
        ├── source
        ├── selection
        ├── context
        │   ├── scope
        │   ├── segments[]
        │   └── relations?
        ├── meta
        └── extensions[]

Install

npm install @context-object-spec/web-adapter

Quick start

import { adaptCurrentSelection } from "@context-object-spec/web-adapter";

document.addEventListener("mouseup", () => {
  const cos = adaptCurrentSelection({ inputType: "mouse" });
  console.log(cos);
});

Use the events that match your interface. pointerup covers mouse, pen, and touch interactions; selectionchange also detects keyboard-driven selections. Debounce selectionchange in interfaces that perform expensive downstream work.

document.addEventListener("selectionchange", () => {
  const cos = adaptCurrentSelection();
  if (cos) console.log(cos);
});

Scoped Usage

Use scoped APIs when only one area of the page should produce Context Objects:

import { adaptCurrentSelectionWithin } from "@context-object-spec/web-adapter";

const editor = document.getElementById("editor");

editor?.addEventListener("mouseup", () => {
  const cos = adaptCurrentSelectionWithin(editor, {
    inputType: "mouse"
  });

  if (!cos) return;
  console.log(cos);
});

The adapter returns null when the current selection is outside the scope, crosses the scope boundary, or is collapsed.

Same-origin iframe documents can be used as scopes:

const frameDocument = iframe.contentDocument;

frameDocument?.addEventListener("mouseup", () => {
  const cos = adaptCurrentSelectionWithin(frameDocument, {
    inputType: "mouse"
  });
});

Cross-origin iframe selections cannot be read by the parent page because of browser security boundaries.

Playground

Try the live COS Web Adapter Playground, or run it locally.

From this package directory:

npm run dev:playground

Then open:

http://localhost:4173/playground/

dev:playground starts TypeScript watch mode and the no-cache playground server together.

React

import { useCallback, useState } from "react";
import { adaptCurrentSelection, type AdapterResult } from "@context-object-spec/web-adapter";

export function SelectionInspector() {
  const [context, setContext] = useState<AdapterResult | null>(null);

  const capture = useCallback(() => {
    setContext(adaptCurrentSelection({ inputType: "mouse" }));
  }, []);

  return (
    <main onMouseUp={capture}>
      <p>Select text here.</p>
      <pre>{JSON.stringify(context, null, 2)}</pre>
    </main>
  );
}

Vue

import { ref } from "vue";
import { adaptCurrentSelection, type AdapterResult } from "@context-object-spec/web-adapter";

export function useSelectionContext() {
  const context = ref<AdapterResult | null>(null);

  function capture() {
    context.value = adaptCurrentSelection({ inputType: "mouse" });
  }

  return { context, capture };
}

API

createWebAdapter(options?): WebAdapter
createScopedWebAdapter(scope, options?): WebAdapter
adaptCurrentSelection(options?): AdapterResult | null
adaptCurrentSelectionWithin(scope, options?): AdapterResult | null
adaptSelection(selection, options?): AdapterResult | null
adaptSelectionWithin(scope, selection, options?): AdapterResult | null
isSelectionInside(scope, selection): boolean

Useful options:

adaptCurrentSelection({
  inputType: "mouse",
  includeSelectedHtml: true,
  includeContainerText: true,
  maxContainerTextLength: 4000
});

Recipes

Copyable, type-checked integration recipes are available for:

They are vendor-neutral: your application controls transport, storage, authentication, and model choice.

Output Shape

selection.text is exactly what the user selected.

source describes where the selection came from:

{
  "type": "webpage",
  "id": "https://example.com/article",
  "uri": "https://example.com/article",
  "title": "Pricing Guide",
  "app": "browser",
  "language": "en"
}

context.segments[] is the main value of COS. Each segment is a source-grounded piece of nearby context:

Segments with selectedText contain the actual Selection. Segments without selectedText are deterministic nearby context, such as sibling list items, neighboring paragraphs, or cells from the same table row.

{
  "version": "0.2",
  "source": {
    "type": "webpage",
    "title": "Product Table"
  },
  "selection": {
    "text": "$19",
    "range": {
      "startOffset": 0,
      "endOffset": 3,
      "startNodeId": "price",
      "endNodeId": "price",
      "direction": "forward"
    }
  },
  "context": {
    "scope": "container",
    "segments": [
      {
        "id": "price",
        "type": "table",
        "text": "$19",
        "selectedText": "$19",
        "relations": [
          { "type": "column_header", "label": "Base price" },
          { "type": "row_header", "label": "Starter" }
        ]
      }
    ]
  },
  "meta": {
    "adapter": "web-adapter",
    "stages": ["selection", "source", "context", "relations"]
  }
}

For inline prose selections, a segment includes bounded before/after text:

{
  "scope": "inline",
  "segments": [
    {
      "id": "intro",
      "type": "paragraph",
      "text": "The adapter should preserve nearby context without turning the selection into a prompt.",
      "selectedText": "nearby",
      "beforeText": "The adapter should preserve ",
      "afterText": " context without turning the selection into a prompt."
    }
  ]
}

For selections that cross multiple logical containers, context.scope is multi-container, and each covered container becomes its own segment:

{
  "scope": "multi-container",
  "segments": [
    {
      "type": "paragraph",
      "selectedText": "adapter should preserve nearby context",
      "text": "The adapter should preserve nearby context without turning the selection into a prompt."
    },
    {
      "type": "list",
      "selectedText": "Selections can begin in ordinary prose.",
      "text": "Selections can begin in ordinary prose.",
      "relations": [
        { "type": "list_item", "label": "item 1" }
      ]
    }
  ]
}

The adapter only emits deterministic structure signals. It does not summarize, classify intent from prose, or generate prompts.

Strong HTML signals may appear as segment.role:

  • code_snippet: <pre> or <code>
  • definition: <dfn>
  • reference: <cite> or <a href>
  • output: <output> or <samp>

Deterministic relationships may appear in context.relations or segment.relations:

  • section
  • row_header
  • column_header
  • label
  • list_item

Browser-specific DOM paths and selected HTML are kept in the io.github.context-object-spec.browser Extension for debugging and traceability.

Browser and privacy boundaries

  • Modern browsers with the DOM Selection and Range APIs are supported.
  • A parent page cannot inspect selections inside a cross-origin iframe.
  • Collapsed selections and selections outside a configured scope return null.
  • All processing is local and synchronous; the adapter performs no network requests.
  • Applications remain responsible for obtaining appropriate user consent before storing or transmitting Context Objects.

COS resources

Licensed under Apache-2.0. Copyright 2026 uncosy.

About

Turn browser text selections into portable, source-grounded COS Context Objects for AI features, editors, annotations, and developer tools.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors