FrontonGPT — Browser extension demo

Drive FrontonGPT from your page

The Frontiers Assistant browser extension installs a public window.AssistantGPT on every page it activates on, so any Frontiers product can open the sidebar, run a prompt, or pin an agent / model / thread with one async call. Below, every block exercises one method against the live API and shows the streamed result.

Extension not detected. The interactive calls below will reject until the Frontiers Assistant extension is installed. Install from the Chrome Web Store.

1. Run a prompt

AssistantGPT.processPrompt({ text, … }) opens the sidebar, posts the prompt into chat, and streams tokens back via the 'text-delta' event (bracketed by 'text-start' / 'text-end'). The transcript on the right is wired to those events in real time. Required: one of model_identifier or agent_identifier so the extension knows what to route the turn to.

Try: "Draft a one-paragraph cover letter for a manuscript on cortical plasticity."

Every call starts a fresh thread, mirroring openModel / openAgent. There's no opt-out — adopters who need to append to an existing thread should call openThread({ thread_uid }) instead.

Live transcript

idle

Token deltas via AssistantGPT.on('text-delta', ({ content }) => …); the badge above flips on 'text-start' / 'text-end' (one each per stream).

— waiting for tokens —
JavaScript
// Open the sidebar, post a prompt, and stream tokens back.
await window.AssistantGPT.processPrompt({
  text: 'Draft a one-paragraph cover letter for a manuscript on cortical plasticity.',
  model_identifier: 'claude-sonnet', // or: agent_identifier: 'submission-assistant'
})

// Consume the streamed answer (events mirror AI SDK v5 stream parts).
window.AssistantGPT.on('text-start', ({ id }) => console.log('start', id))
window.AssistantGPT.on('text-delta', ({ content }) => { transcript += content })
window.AssistantGPT.on('text-end', ({ id }) => console.log('done', id))

2. Open an existing thread

AssistantGPT.openThread({ thread_uid }) opens the sidebar and navigates it straight to a specific thread. Useful for deep-links from email digests, audit logs, or any place that wants to send the user back to the conversation that produced an artefact.

JavaScript
// Deep-link the sidebar straight to an existing conversation.
await window.AssistantGPT.openThread({ thread_uid: 'tx-abc123' })

3. Open with a pinned agent or model

openAgent({ agent_identifier }) spins up a fresh thread with the named agent preselected; openModel({ model_identifier }) does the same for models. Authors of guided submission flows can route the visitor straight into the right agent without making them hunt for the picker.

Context-only launch. Pass an optional context to openAgent or openModel to open pre-loaded with structured page state but a clean, empty chat — the context rides the first turn the user sends. No seeded / visible user message (unlike processPrompt, which requires non-empty text).
JavaScript
// Pin a server-managed agent (it owns its own model + tools).
await window.AssistantGPT.openAgent({ agent_identifier: 'submission-assistant' })

// Context-only launch: pre-load the agent with structured context but
// a clean, empty chat. The context rides the first turn the user sends
// (no seeded message — unlike processPrompt, which requires text).
await window.AssistantGPT.openAgent({
  agent_identifier: 'submission-assistant',
  context: { url: location.href, step: 'review' },
})

// …or pin a specific model for a fresh thread — also accepts context
// for the same clean-chat, context-only launch.
await window.AssistantGPT.openModel({ model_identifier: 'gpt-5-thinking' })
await window.AssistantGPT.openModel({
  model_identifier: 'gpt-5-thinking',
  context: { url: location.href },
})

4. Pass context with a prompt

processPrompt accepts an optional context: Record<string, unknown>. The extension forwards it through into the assistant's runtime, so an agent can read structured page state (current URL, selected article, submission step…) instead of guessing from the prompt text alone.

Want context but no visible prompt? Use openAgent({ agent_identifier, context }) instead — it opens the agent with a clean, empty chat and attaches the context to the first turn the user actually sends.

Try: "Summarise the article I'm looking at" with { "url": location.href, "doi": "10.3389/fnins.2026.123456" } as context.
JavaScript
// Forward structured page state alongside the prompt.
await window.AssistantGPT.processPrompt({
  text: 'Summarise the article I am currently looking at.',
  model_identifier: 'claude-sonnet',
  context: {
    url: location.href,
    doi: '10.3389/fnins.2026.123456',
    section: 'methods',
  },
})

5. Open the panel, a skill, or a template

open() opens the sidebar with no navigation (must run inside a user gesture); close() dismisses it. openSkill({ skill_name }) and openTemplate({ template_identifier }) open the sidebar and preselect a composer skill / prompt template.

JavaScript
// open() must run inside a user gesture (click / keydown).
document.querySelector('#ask-frontongpt').addEventListener('click', () => {
  window.AssistantGPT.open()
})

await window.AssistantGPT.close()
await window.AssistantGPT.openSkill({ skill_name: 'Literature review' })
await window.AssistantGPT.openTemplate({ template_identifier: 'summarise' })

// Drop a draft into the composer WITHOUT sending it.
await window.AssistantGPT.prefillPrompt({ text: 'Draft a reply to this thread.' })

// …optionally attach context — stashed on the active session, it rides
// the turn the user sends after editing the draft.
await window.AssistantGPT.prefillPrompt({
  text: 'Draft a reply to this thread.',
  context: { url: location.href },
})

6. Receive host events

When an agent has the Host Events tool enabled (Settings > Agents), it can drive THIS page from the sidebar: it calls emit_host_event (fire-and-forget) or request_host_event (two-way) and the extension relays it to the active tab. Subscribe once with AssistantGPT.on('event', (name, data, reply) => …) and branch on name; for two-way calls return a value with reply(payload). The four cards below are wired to concrete event names — run a prompt in section 1 (routed to an agent that has the tool) and watch them react.

a. Scroll-to-section navigation emit_host_event

Fire-and-forget. The agent picks the section from the conversation; the handler calls scrollIntoView on the matching element and flashes it.

Try: "Take me to the journals" (show_journals) or "Show me the FAQ" (show_faq)

Journals

Open-access journals across the sciences. When the agent calls show_journals, this card scrolls into view and flashes.

FAQ

Review timelines, copyright, withdrawal policy. Ask "what's the average review time?" to scroll here via show_faq.

JavaScript
// 6a. Fire-and-forget navigation — scroll to a section.
window.AssistantGPT.on('event', (name) => {
  if (name === 'show_journals') {
    document.querySelector('#section-journals')
      ?.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }
  if (name === 'show_faq') {
    document.querySelector('#section-faq')
      ?.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }
})

b. Prefill a form emit_host_event({ data })

Events can carry a data payload. The author describes their paper in chat; the agent emits prefill_submission with whatever it gathered, and the handler maps it onto the form fields.

Try: "I'd like to submit a neuroscience paper. I'm Dr. Maria Sato, maria@univ.test, title 'Cortical plasticity in adult learning'."

Last submission

— nothing yet —
JavaScript
// 6b. Fire-and-forget WITH a data payload — map onto form fields.
window.AssistantGPT.on('event', (name, data) => {
  if (name !== 'prefill_submission') return
  if (typeof data?.name === 'string') form.name.value = data.name
  if (typeof data?.email === 'string') form.email.value = data.email
  if (typeof data?.journal === 'string') form.journal.value = data.journal
  if (typeof data?.title === 'string') form.title.value = data.title
})

c. Read page state back request_host_event

When the agent needs a value from the page it calls request_host_event and waits. The handler calls reply({ items, count }) and the agent uses it verbatim in the next turn.

Try: "What's in my reading list?" (read_reading_list, round-trip) or "Add Frontiers in Neuroscience to my list" (add_to_reading_list, emit).

Browse journals

  • Frontiers in NeuroscienceFNINS

    Neuroscience

  • Frontiers in Plant ScienceFPLS

    Plant Science

  • Frontiers in Artificial IntelligenceFRAI

    AI / Machine Learning

  • Frontiers in MedicineFMED

    Medical Sciences

  • Frontiers in Environmental ScienceFENVS

    Environmental Science

Your reading list

Empty — ask the agent to add a journal.

Count0

Events: add_to_reading_list({ code }), clear_reading_list, read_reading_list (request).

JavaScript
// 6c. Two-way request — reply with a plain, cloneable object.
window.AssistantGPT.on('event', (name, data, reply) => {
  switch (name) {
    case 'add_to_reading_list': // emit (fire-and-forget)
      if (data?.code) addJournal(data.code)
      break
    case 'clear_reading_list':  // emit
      readingList = []
      break
    case 'read_reading_list':   // request — reply within 10s
      reply({ items: readingList, count: readingList.length })
      break
  }
})

d. Async reply request_host_event

Handlers don't have to reply synchronously — they can await work first, as long as they call reply within the 10s default timeout. Flip the role below, then ask the agent.

Try: "What's my role?" (get_user_role) — cycle the role, then ask again to see the answer change.
Current visitor:Anonymous
JavaScript
// 6d. Async two-way request — await work, then reply.
window.AssistantGPT.on('event', async (name, data, reply) => {
  if (name !== 'get_user_role') return
  const role = await fetchCurrentRole() // any async work within 10s
  reply(role) // primitives are fine; objects must be plain/cloneable
})
JavaScript
// Subscribe once. The 'event' handler is special — it receives
// (name, data, reply), not a single detail object.
window.AssistantGPT.on('event', (name, data, reply) => {
  switch (name) {
    case 'show_journals':
      document.querySelector('#journals')?.scrollIntoView({ behavior: 'smooth' })
      break
    case 'prefill_submission':
      form.name.value = data?.name ?? ''
      form.email.value = data?.email ?? ''
      break
    case 'read_reading_list':
      // request_host_event: reply within 10s with a plain (cloneable)
      // object — Vue refs/reactives throw DataCloneError over postMessage.
      reply({ items: readingList, count: readingList.length })
      break
  }
})

7. Direct JSAPI controls

Raw calls without form wrappers. Useful for one-off testing and for poking at edge cases (e.g. ping() after the extension has been disabled mid-session).

JavaScript
// Availability probe — resolves with { extensionVersion } or rejects.
const { extensionVersion } = await window.AssistantGPT.ping()

await window.AssistantGPT.openThread({ thread_uid: 'demo-thread' })
await window.AssistantGPT.openAgent({ agent_identifier: 'submission-assistant' })
// Context-only launch — clean chat; context rides the first user turn.
await window.AssistantGPT.openAgent({ agent_identifier: 'submission-assistant', context: { url: location.href } })
await window.AssistantGPT.openModel({ model_identifier: 'gpt-5-thinking' })
await window.AssistantGPT.openModel({ model_identifier: 'gpt-5-thinking', context: { url: location.href } })
await window.AssistantGPT.open()
await window.AssistantGPT.close()
await window.AssistantGPT.openSkill({ skill_name: 'Literature review' })
await window.AssistantGPT.openTemplate({ template_identifier: 'summarise' })
await window.AssistantGPT.prefillPrompt({ text: 'Draft a reply to this thread.' })
// …prefill with context stashed on the active session for the next send.
await window.AssistantGPT.prefillPrompt({ text: 'Draft a reply to this thread.', context: { url: location.href } })

API surface

Every public method on window.AssistantGPT, with the full set of supported fields. The live demos above exercise each one against the running extension.

AssistantGPT.ping()#

Lightweight handshake. Resolves with { extensionVersion } if the content script is alive; rejects after the 2s handshake timeout otherwise. Cheap availability probe — use it to decide whether to render the install-the-extension CTA.

No parameters. No required fields.

JavaScript
const { extensionVersion } = await window.AssistantGPT.ping()
AssistantGPT.processPrompt({ … })#

Opens the sidebar, posts a prompt into chat, and streams tokens back via the 'text-delta' event (with matching 'text-start' / 'text-end' lifecycle markers). The promise resolves as soon as the bridge accepts the request; the actual completion happens asynchronously inside the sidebar.

textrequired
string — the prompt the assistant should answer.
template_identifier
string? — pre-canned template the assistant should use (e.g. summarise, translate).
model_identifierone of
string — pick a specific model for this turn (e.g. claude-sonnet, gpt-5-thinking). One of model_identifier / agent_identifier is required.
agent_identifierone of
string — pin a specific agent for this turn (the agent owns its own model). One of model_identifier / agent_identifier is required.
context
object? — structured page state forwarded to the agent (URL, DOI, current section, form values…). Plain JSON only.

Routing requirement. Every processPrompt call must include one of model_identifier (a specific model) or agent_identifier (a pinned agent — the agent then owns its own model). The validator in helpers-extension.js rejects calls that supply neither with a clear error at the call site, so host pages can't accidentally race against an un-initialised sidebar's default selection. Pinning an agent is usually preferable: it keeps tool / instruction / model choice on the server-managed agent row, not on the host code that fires the call.

Always fresh thread. Every call creates a new thread, mirroring openModel / openAgent. There is no opt-out — append-to-current-thread is not supported on this surface. Host pages that want to land a prompt in an existing thread should call openThread({ thread_uid }) instead.

JavaScript
await window.AssistantGPT.processPrompt({
  text: 'Summarise this article for a general audience.',
  agent_identifier: 'submission-assistant', // or model_identifier
  template_identifier: 'summarise',          // optional
  context: { url: location.href },           // optional, plain JSON
})
AssistantGPT.openThread({ thread_uid })#

Opens the sidebar and navigates straight to an existing thread. Useful for deep-links from email digests, audit trails, or any UI that wants to send the user back to the conversation that produced an artefact.

thread_uidrequired
string — the thread to open.
JavaScript
await window.AssistantGPT.openThread({ thread_uid: 'tx-abc123' })
AssistantGPT.openAgent({ agent_identifier, context? })#

Opens the sidebar with a fresh thread and pre-selects the named agent in the composer. Use to route a visitor straight into the right agent without making them hunt for the picker.

agent_identifierrequired
string — the agent to pin (e.g. submission-assistant).
context
Record<string, unknown> — optional structured context. When provided, the agent opens with a clean, empty chat and the context is attached to the first turn the user sends (no seeded / visible message). This is the recommended way to open an agent pre-loaded with context but a clean chat — unlike processPrompt, which requires non-empty text that renders as a user message.
JavaScript
await window.AssistantGPT.openAgent({ agent_identifier: 'submission-assistant' })

// Context-only launch — clean, empty chat; context rides the first user turn.
await window.AssistantGPT.openAgent({
  agent_identifier: 'submission-assistant',
  context: { url: location.href }, // optional, plain JSON
})
AssistantGPT.openModel({ model_identifier, context? })#

Same shape as openAgent but for models. Spins up a fresh thread so the new model applies in a clean context.

model_identifierrequired
string — the model to pin (e.g. gpt-5-thinking).
context
Record<string, unknown> — optional structured context. When provided, the model opens with a clean, empty chat and the context is attached to the first turn the user sends (no seeded / visible message). Mirrors openAgent's context.
JavaScript
await window.AssistantGPT.openModel({ model_identifier: 'gpt-5-thinking' })

// Context-only launch — clean, empty chat; context rides the first user turn.
await window.AssistantGPT.openModel({
  model_identifier: 'gpt-5-thinking',
  context: { url: location.href }, // optional, plain JSON
})
AssistantGPT.open()#

Opens the sidebar in the current window with no navigation (lands on the default chat tab). Must be called in response to a user gesture (click / keydown) — Chrome consumes the gesture when the panel is currently closed, so calling it outside a gesture handler will fail to open the panel.

No parameters. No required fields.

JavaScript
// Must be called from inside a user-gesture handler.
button.addEventListener('click', () => window.AssistantGPT.open())
AssistantGPT.close()#

Closes the sidebar in the current window. No-op if the panel is already closed.

No parameters. No required fields.

JavaScript
await window.AssistantGPT.close()
AssistantGPT.openSkill({ skill_name })#

Opens the sidebar and pre-selects the named skill in the composer. The skill is a composer adornment (not a routing target), so this applies to the active session rather than spinning up a fresh thread. No-op if the skill isn't in the workspace's skill catalog.

skill_namerequired
string — the skill to pre-select (matches the skill's display name).
JavaScript
await window.AssistantGPT.openSkill({ skill_name: 'Literature review' })
AssistantGPT.openTemplate({ template_identifier })#

Same shape as openSkill but pre-selects a prompt template in the composer, matched by its identifier. No-op if the identifier isn't in the prompt-template catalog.

template_identifierrequired
string — the prompt template to pre-select (e.g. summarise).
JavaScript
await window.AssistantGPT.openTemplate({ template_identifier: 'summarise' })
AssistantGPT.prefillPrompt({ text, context?, model_identifier?, agent_identifier?, template_identifier? })#

"processPrompt that stops before sending": drops the given text into the composer without firing a turn — the user reviews / edits before hitting send. Replaces any existing draft. Accepts the same optional setup fields as processPrompt.

textrequired
string — the draft to drop into the composer.
model_identifier
string? — optional model to pin. Routing is optional here (no turn is fired); when supplied, a fresh thread opens so the model applies cleanly. Model wins if both model + agent are given.
agent_identifier
string? — optional agent to pin; also opens a fresh thread when supplied.
template_identifier
string? — optional prompt template to preselect.
context
Record<string, unknown> — optional structured context stashed on the session; it rides the turn the user sends after reviewing / editing the draft. With no routing fields the draft lands in the active thread.
JavaScript
// Draft into the ACTIVE thread (no routing) — user reviews & sends.
await window.AssistantGPT.prefillPrompt({ text: 'Draft a reply to this thread.' })

// "processPrompt that stops before sending": same fields, but nothing
// is sent. A model/agent opens a FRESH thread with that route pinned;
// context rides the turn the user sends after editing the draft.
await window.AssistantGPT.prefillPrompt({
  text: 'Summarise the article I am looking at.',
  agent_identifier: 'submission-assistant', // or model_identifier
  template_identifier: 'summarise',          // optional
  context: { url: location.href },           // optional, plain JSON
})
AssistantGPT.on(event, handler) / .off(event, handler)#

Subscribe to events the extension fires back to the host page. on() returns an unsubscribe function for convenience; off() works too. Pure synchronous registration — no promise.

The 'text-*' events mirror AI SDK v5's stream-part naming so adopters who already consume AI SDK streams recognise the shape.

'text-start'
Fires once at the open of each streaming turn from a processPrompt call from this page. Handler receives { id }. Use to flip a "typing…" indicator, lock a form, start an analytics timer.
'text-delta'
Streamed token deltas from the most recent processPrompt. Handler receives { id, content }. Fires many times per turn, between matching 'text-start' / 'text-end' markers.
'text-end'
Fires once when the streaming turn completes (cleanly or on error). Handler receives { id }. Always preceded by exactly one 'text-start' for the same id.
'event'
An LLM-driven host event from the Host Events tool. Unlike every other event, the handler receives (name, data, reply) — not a single detail object. emit_host_event is fire-and-forget (ignore reply); request_host_event is two-way — call reply(payload) to send a value back to the agent (first-wins). Fires for any sidebar chat, delivered to whichever tab is active when the event fires.
'disconnected'
Fired when the extension context is invalidated mid-page (user updated the extension, browser killed the process). Host page should fall back to the install/refresh CTA.
JavaScript
// Streamed-answer lifecycle (single detail object per event).
const off = window.AssistantGPT.on('text-delta', ({ id, content }) => {
  transcript += content
})

// LLM-driven host events get the special (name, data, reply) shape.
window.AssistantGPT.on('event', (name, data, reply) => {
  if (name === 'get_user_role') reply(currentUser?.role ?? null)
})

window.AssistantGPT.on('disconnected', () => showInstallCta())

off() // unsubscribe (or: AssistantGPT.off('text-delta', handler))

Field names match the wire format forwarded over the postMessage bridge. The source of truth is apps/assistant-extension/src/inject/helpers-extension.js (validators) and apps/assistant-extension/src/content-script/pageApi.ts (sanitizer + dispatch).

© 2026 Frontiers Media SADemo harness for the Frontiers Assistant browser extension.
JSAPI event sink (latest 10)0
(no events yet — ping the extension or send a prompt)