Skip to content

Sidebar chat#5587

Draft
alisman wants to merge 17 commits into
masterfrom
sidebar-chat
Draft

Sidebar chat#5587
alisman wants to merge 17 commits into
masterfrom
sidebar-chat

Conversation

@alisman

@alisman alisman commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Fix cBioPortal/cbioportal# (see https://help.github.com/en/articles/closing-issues-using-keywords)

Describe changes proposed in this pull request:

  • a
  • b

Checks

  • Has tests or has a separate issue that describes the types of test that should be created. If no test is included it should explicitly be mentioned in the PR why there is no test.
  • The commit log is comprehensible. It follows 7 rules of great commit messages. For most PRs a single commit should suffice, in some cases multiple topical commits can be useful. During review it is ok to see tiny commits (e.g. Fix reviewer comments), but right before the code gets merged to master or rc branch, any such commits should be squashed since they are useless to the other developers. Definitely avoid merge commits, use rebase instead.
  • Is this PR adding logic based on one or more clinical attributes? If yes, please make sure validation for this attribute is also present in the data validation / data loading layers (in backend repo) and documented in File-Formats Clinical data section!

Any screenshots or GIFs?

If this is a new visual feature please add a before/after screenshot or gif
here with e.g. Giphy CAPTURE or Peek

Notify reviewers

Read our Pull request merging
policy
. It can help to figure out who worked on the
file before you. Please use git blame <filename> to determine that
and notify them either through slack or by assigning them as a reviewer on the PR


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Copilot AI review requested due to automatic review settings May 19, 2026 23:56
@alisman alisman marked this pull request as draft May 19, 2026 23:57
</button>
<img
src={lastScreenshot}
export function getChatServerBase(): string {
const host =
typeof window !== 'undefined' ? window.location.hostname : '';
if (host.endsWith('cbioportal.org') || host.endsWith('.netlify.app')) {
return (
url.includes('/api/chat/') ||
url.includes('cbioportal-frontend-sidebar.vercel.app') ||
url.includes('tailf02841.ts.net:5174')
</button>
<img
src={lastScreenshot}
@netlify

netlify Bot commented May 20, 2026

Copy link
Copy Markdown

Deploy Preview for cbioportalfrontend ready!

Name Link
🔨 Latest commit c313fef
🔍 Latest deploy log https://app.netlify.com/projects/cbioportalfrontend/deploys/6a282375c4002c00083e5f20
😎 Deploy Preview https://deploy-preview-5587.preview.cbioportal.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new “Study Chat” sidebar integrated into the Results View. It embeds an isolated iframe-based chat UI, captures viewport screenshots for context, and adds “alteration beacons” overlays driven by a new chat-sidebar backend that fetches/uses paper text and calls Anthropic models.

Changes:

  • Add ChatSidebar host component (+ styles) that embeds the sidebar iframe and responds to screenshot requests.
  • Add AlterationBeacons overlay that fetches paper-grounded highlights and places animated beacons on matching legend/genes/tabs.
  • Add new packages/chat-sidebar (Vite/React) iframe app and packages/chat-sidebar-server (Express dev + Vercel functions) backend; update rspack devServer/proxy and add html2canvas.

Reviewed changes

Copilot reviewed 29 out of 32 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/shared/components/chatSidebar/screenshot.ts Implements viewport capture + network/view readiness helpers
src/shared/components/chatSidebar/ChatSidebar.tsx Host-side launcher/panel + iframe integration + screenshot messaging
src/shared/components/chatSidebar/ChatSidebar.scss Styles for launcher button, panel iframe, and loader chip
src/shared/components/chatSidebar/chatServerBase.ts Chooses iframe/backend base URL (prod vs dev)
src/shared/components/chatSidebar/AlterationBeacons.tsx Fetches highlight data and renders/pulses overlay beacons + tooltip
src/pages/resultsView/ResultsViewPage.tsx Mounts ChatSidebar on Results View
rspack.config.js Copies chat-sidebar dist (optional), changes dev host/publicPath, adds /api/chat proxy
packages/chat-sidebar/vite.config.ts Vite config for iframe app (relative base, HTTPS dev, API proxy)
packages/chat-sidebar/tsconfig.json TS config for iframe app
packages/chat-sidebar/src/styles.css Iframe UI styling
packages/chat-sidebar/src/main.tsx Iframe app bootstrap
packages/chat-sidebar/src/cbioportal.ts Minimal cBioPortal API helper for study metadata
packages/chat-sidebar/src/App.tsx Iframe chat UI, preset prompts, screenshot request, /api/chat calls
packages/chat-sidebar/pnpm-lock.yaml Adds per-package pnpm lockfile
packages/chat-sidebar/package.json Iframe app package definition + scripts/deps
packages/chat-sidebar/index.html Iframe HTML entry
packages/chat-sidebar/.gitignore Ignores build/dev/certs artifacts
packages/chat-sidebar-server/vercel.json Vercel build/install configuration and function maxDuration
packages/chat-sidebar-server/tsconfig.json TS config for backend
packages/chat-sidebar-server/src/server.ts Express dev server wrapping shared core logic
packages/chat-sidebar-server/src/paper.ts Fetches/derives paper context via PMC BioC + PubMed fallback
packages/chat-sidebar-server/src/cors.ts CORS helper for Vercel functions
packages/chat-sidebar-server/src/core.ts Shared Claude prompting, cost calculation, highlights JSON schema
packages/chat-sidebar-server/package.json Backend package scripts/deps (Anthropic SDK, Express, Vercel)
packages/chat-sidebar-server/api/chat/suggest.ts Vercel function handler for suggest endpoint
packages/chat-sidebar-server/api/chat/highlights.ts Vercel function handler for highlights endpoint
packages/chat-sidebar-server/api/chat/health.ts Vercel function health endpoint
packages/chat-sidebar-server/.vercelignore Excludes local artifacts from Vercel build context
packages/chat-sidebar-server/.gitignore Ignores local env/build artifacts
package.json Adds html2canvas dependency
.gitignore Ignores .claude/ and .vercel directories
Files not reviewed (1)
  • packages/chat-sidebar/pnpm-lock.yaml: Language not supported

Comment on lines +33 to +36
(function installNetworkPatch() {
const w = window as any;
if (typeof window === 'undefined' || w[PATCH_FLAG]) return;
w[PATCH_FLAG] = true;
Comment on lines +62 to +78
onMessage = async (e: MessageEvent) => {
// The iframe asks for a screenshot before each preset request so the
// model sees what the user is actually looking at.
if (
e.source !== this.iframeRef.current?.contentWindow ||
e.data?.type !== 'chat-sidebar:requestScreenshot'
) {
return;
}
const requestId = e.data.requestId;
await waitForNetworkIdle(1000);
await waitForViewReady();
const dataUrl = await captureViewport();
this.iframeRef.current?.contentWindow?.postMessage(
{ type: 'chat-sidebar:screenshot', requestId, dataUrl },
'*'
);
Comment thread rspack.config.js
{
context: ['/api/chat'],
target: 'http://127.0.0.1:4000',
pathRewrite: { '^/api/chat': '' },
Comment on lines +6 to +12
export function getChatServerBase(): string {
const host =
typeof window !== 'undefined' ? window.location.hostname : '';
if (host.endsWith('cbioportal.org') || host.endsWith('.netlify.app')) {
return 'https://cbioportal-frontend-sidebar.vercel.app';
}
return 'https://vps-870e202d.tailf02841.ts.net:5174';
Comment on lines +3 to +17
// The highlights and suggest endpoints are invoked cross-origin from the
// cBioPortal host page (e.g. cbioportal.org) as well as same-origin from the
// iframe. Echo the Origin header for any caller — these endpoints take only
// a studyId, so there's nothing user-specific to leak; the ANTHROPIC_API_KEY
// stays server-side.
export function applyCors(req: VercelRequest, res: VercelResponse): boolean {
const origin = (req.headers.origin as string) || '*';
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.status(204).end();
return true;
}
Comment on lines +207 to +214
// Continuously reposition placed beacons. Cheap for ~10 elements.
private startRepositionLoop() {
const tick = () => {
this.repositionAll();
this.rafId = requestAnimationFrame(tick);
};
this.rafId = requestAnimationFrame(tick);
}
Comment on lines +884 to +898
const studyIds = this.resultsViewPageStore.studyIds.result;
return (
<PageLayout
noMargin={true}
hideFooter={true}
className={'subhead-dark'}
>
{this.pageContent}
</PageLayout>
<>
<PageLayout
noMargin={true}
hideFooter={true}
className={'subhead-dark'}
>
{this.pageContent}
</PageLayout>
<ChatSidebar
studyId={studyIds && studyIds[0]}
genes={this.resultsViewPageStore.hugoGeneSymbols}
tab={this.resultsViewPageStore.tabId}
/>
Comment on lines +1 to +64
lockfileVersion: '9.0'

settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

importers:

.:
dependencies:
react:
specifier: ^19.0.0
version: 19.2.6
react-dom:
specifier: ^19.0.0
version: 19.2.6(react@19.2.6)
devDependencies:
'@types/react':
specifier: ^19.0.0
version: 19.2.14
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.7.0(vite@6.4.2)
typescript:
specifier: ^5.7.2
version: 5.9.3
vite:
specifier: ^6.0.7
version: 6.4.2

packages:

'@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}

'@babel/compat-data@7.29.3':
resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
engines: {node: '>=6.9.0'}

'@babel/core@7.29.0':
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
engines: {node: '>=6.9.0'}

'@babel/generator@7.29.1':
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
engines: {node: '>=6.9.0'}

'@babel/helper-compilation-targets@7.28.6':
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
engines: {node: '>=6.9.0'}

'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}

'@babel/helper-module-imports@7.28.6':
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
engines: {node: '>=6.9.0'}

'@babel/helper-module-transforms@7.28.6':
@@ -0,0 +1,10 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"installCommand": "cd ../.. && pnpm install --no-frozen-lockfile",
Comment on lines +68 to +80
// Per-process paper cache. Persistent on the Express server; per-warm-Lambda
// on Vercel (each warm function reuses; cold starts fetch fresh from PMC).
const paperCache = new Map<string, PaperContext>();

export async function getPaperContext(
studyId: string
): Promise<PaperContext> {
const cached = paperCache.get(studyId);
if (cached) return cached;
const ctx = await fetchPaperForStudy(studyId);
paperCache.set(studyId, ctx);
return ctx;
}
@alisman alisman marked this pull request as ready for review June 8, 2026 16:24
Ubuntu and others added 12 commits June 8, 2026 12:24
- packages/chat-sidebar: React 19 + Vite iframe app, served via rspack's
  CopyPlugin at /chat-sidebar/index.html. Button-triggered "Suggest insight"
  flow that renders a paper-grounded observation plus per-call cost.
- packages/chat-sidebar-server: Node + Express backend (port 4000) that
  fetches the open study's primary publication (PMC full text via the BioC
  API, fallback to PubMed abstract) and prompts Claude with a verbatim-quote
  grounding contract. Prompt-caches the paper text.
- rspack.config.js: dev-server proxy /api/chat/* -> :4000; bind to HOST env
  so the dev server is reachable over LAN/Tailscale.
- src/shared/components/chatSidebar: launcher + iframe wrapper mounted in
  ResultsViewPage. Drops credentials:include from study fetch (was breaking
  CORS against the public cBioPortal CORS=* policy).

API key lives in packages/chat-sidebar-server/.env (gitignored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- chat-sidebar-server: new /highlights endpoint returns a structured list of
  paper-grounded items via Anthropic structured outputs. Three beacon types
  in one schema:
    * alteration  -> oncoprint legend label (Amplification, Missense, ...)
    * gene        -> oncoprint gene track title (TP53, ESR1, ...)
    * tab         -> results-view tab (Survival, Mutual Exclusivity, ...)
  Each carries note + verbatim quote + importance, grounded in the paper.
  Prompt-cached on the paper text with 1h ephemeral TTL; /suggest uses the
  same TTL. Cost breakdown now reports cacheWrite5m vs cacheWrite1h.
- AlterationBeacons (new): single React component mounted by ChatSidebar.
  Fetches /api/chat/highlights, watches the DOM via MutationObserver, and
  injects absolutely-positioned HTML beacon dots overlaying the matched
  targets (SVG <text> for oncoprint, <a class="tabAnchor_*"> for tabs).
  Pulse animation runs on the rAF reposition loop directly so it cannot be
  defeated by stylesheet/SMIL/CSS-keyframes edge cases. Tab beacons use a
  Range over the link text so they snug up to the last character, not the
  padded edge of the <a> or <li>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract Express handler logic into shared core.ts so the serverless
functions in api/chat/* and the local dev server stay in sync. Add
vercel.json to bundle the chat-sidebar iframe into public/ and expose
suggest/highlights/health endpoints. Point ChatSidebar's iframe at the
deployed Vercel URL so cBioPortal embeds the hosted app and same-origin
API calls reach the functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iframe URL is now picked at runtime: prod Vercel deployment when the
host is cbioportal.org, otherwise a local Vite dev server reachable
over the tailnet at vps-870e202d.tailf02841.ts.net:5174. Vite serves
HTTPS using a Tailscale-issued cert (loaded from packages/chat-sidebar/
certs/, gitignored) and proxies /api/* to the local Express server on
:4000 so dev mirrors the same-origin assumption the iframe code makes
in prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Sidebar opens by default with the queried genes/active tab passed
  through ChatSidebar -> iframe URL. Auto-runs the "Key finding" preset
  on mount; "Cohort" and "Limitations" presets are one click away.
  Preset prompts explicitly tell Claude to stay relevant to the listed
  genes and tab.
- Move the study link into the header (paper URL on click), drop the
  separate study-meta block and the source-pill banner.
- Make /api/chat/* reachable cross-origin from the host page: shared
  CORS helper on Vercel functions, Vite dev server CORS opened with
  reflected origin, /api routes on the Express dev server prefixed to
  match production paths.
- Centralize the chat-server base URL (Vercel prod vs. tailnet dev)
  so both the iframe and AlterationBeacons resolve the same host.
- Keep iframe state across open/close by hiding the panel via [hidden]
  instead of unmounting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Host page captures the current viewport via html2canvas (excluding the
sidebar itself), downscaled to a max 1024px long side, and ships the
PNG to the iframe over postMessage. The iframe attaches it to the
suggest POST; the backend forwards it as an image content block in
the user turn so Claude can reference what the user is actually
seeing — specific genes/tracks, legend buckets, plot patterns.

Also adds a shimmer animation behind the "thinking…" bubble while a
preset is in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add a free-form chat input at the bottom of the sidebar that sends
  the user's question with the same paper/gene/tab/screenshot context
  as the preset buttons. The textarea expands on focus and collapses on
  submit; the user's last prompt is rendered as a user bubble above the
  reply.
- Move the open/close toggle to the top-right of the panel when open,
  back to a circular bottom-right launcher when closed. Skip mounting
  AlterationBeacons while the sidebar is closed. Persist open/closed
  in localStorage so it survives reloads.
- Wait for any [data-test=LoadingIndicator] (e.g. oncoprint paint) to
  clear before capturing the screenshot, and rasterize the oncoprint
  via oncoprintjs.toCanvas() into the html2canvas onclone hook so the
  WebGL surface no longer screenshots as black.
- Add a "view screenshot" debug link next to the cost line that opens
  a modal with the exact image sent to Claude.
- Switch the highlights model to Sonnet 4.6 (~50% cheaper); keep Opus
  4.7 for suggest. computeCost takes the model so each call's price
  reflects what was actually used.
- Surface a "Loading beacons…" pill in the bottom-left while the
  highlights fetch is in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… previews to Vercel

Monkey-patch fetch + XHR once at module load to track in-flight host-page
requests (skipping our own /api/chat/* traffic). Before each screenshot,
wait for 1s of continuous network idle, then for any DOM LoadingIndicator
to clear, then snap — so tabs like Mutations that finish data loading
after the initial paint don't get captured half-empty.

Also extend the prod-vs-dev chat-server base check to recognize
*.netlify.app hosts so deploy previews use the Vercel deployment instead
of the developer's tailnet host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… closed

Highlights endpoint now takes an `inventory: { alterations, genes, tabs }`
captured from the host DOM (legend labels, queried genes, present
`.tabAnchor_*` nodes). The system prompt instructs Claude to emit
highlights only for items in the inventory — anything outside has no DOM
target and would be silently dropped, so spending output budget on it is
waste. Wait for network idle before snapshotting the inventory so the
listing reflects what the user actually sees, not a half-rendered shell.

Re-fetch highlights when the queried gene set changes, not just when the
study does. Toggle `body.chat-sidebar-closed` so beacons + loader hide
via CSS when the user closes the sidebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… with per-call pricing (#5591)

* Route Claude calls through Vercel AI Gateway via the AI SDK

Replace direct @anthropic-ai/sdk usage in chat-sidebar-server with `ai@^6`
+ `zod`. LLM calls now go through the Vercel AI Gateway with plain
"anthropic/claude-opus-4.7" / "anthropic/claude-sonnet-4.6" slugs (versions
use dots, not hyphens), authenticated via the VERCEL_OIDC_TOKEN that
Vercel injects on deploy and `vercel env pull` writes to .env.local for
local dev.

Why the switch:
- Unified observability and spend tracking in one Vercel dashboard.
- Provider-agnostic — we can fail over or A/B different models by changing
  a single string instead of swapping SDKs.
- Zero markup; AI SDK preserves Anthropic prompt caching (verified: the
  1h cache_control breakpoint on the paper-text system prompt still
  produces cache_creation_input_tokens on the first call and
  cache_read_input_tokens on every subsequent call within the TTL).

Mechanics:
- runSuggest: generateText(...) with the system role inside `messages` so
  the providerOptions.anthropic.cacheControl breakpoint can attach to it.
  The previous .stream().finalMessage() pattern was internal-only — the
  server only ever returned res.json(result) — so non-streaming is fine.
- runHighlights: generateObject(...) with a Zod schema replaces the
  hand-built JSON Schema + JSON.parse path; same cache breakpoint.
- computeCost is unchanged. It reads from providerMetadata.anthropic.usage,
  which preserves Anthropic's native snake_case shape including the
  cache_creation.ephemeral_1h_input_tokens split.
- Image content uses AI SDK's {type:'image', image: dataUrl} shape; the
  data-URL is parsed for mediaType automatically.
- Server startup now requires VERCEL_OIDC_TOKEN (or AI_GATEWAY_API_KEY)
  instead of ANTHROPIC_API_KEY; .env.local is layered on top of .env so
  the local order matches what Vercel does in deployed envs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Let users switch models from the sidebar; price per call via Gateway pricing

The sidebar header now exposes a model dropdown wired to a curated
shortlist (Anthropic Opus/Sonnet/Haiku, OpenAI GPT-5.4 + mini, Gemini 3
Pro Preview, Grok 4.1 fast-reasoning, DeepSeek V4 Pro). Selection
persists in localStorage and ships with each /api/chat/suggest call.

Backend changes:
- New src/pricing.ts: lazy-load gateway.getAvailableModels() once, expose
  getShortlistedModels() for the dropdown and computeCostFromUsage(usage,
  model) keyed on Gateway-listed pricing. Hand-maintained PRICES table is
  gone — the Gateway is the authoritative source.
- runSuggest / runHighlights now accept an optional `model` Gateway slug
  and default to SUGGEST_MODEL / HIGHLIGHTS_MODEL when absent.
- Cost computation switches to the AI-SDK-normalized result.usage shape
  (inputTokens, outputTokens, inputTokenDetails.cacheReadTokens/
  cacheWriteTokens) so it works across providers, not just Anthropic.
  One known under-count: the Gateway lists Anthropic's 5m-TTL write rate
  (1.25x input) as `cacheCreationInputTokens`; our 1h-TTL writes actually
  bill at 2x. Cache writes are one-time per session, so the cost-display
  delta is small; documented inline.
- New GET /api/chat/models endpoint (Express + Vercel function) returns
  the shortlist with name + per-token pricing for the iframe's dropdown.
- suggest / highlights endpoints accept `model` in the request body.

Frontend (packages/chat-sidebar):
- App.tsx fetches /api/chat/models on mount, renders a <select> in the
  header, persists choice in localStorage, sends `model` with each suggest
  request. Per-option title shows in/out/cached per-million-token prices.
- Cost line already showed cost.total and cost.model — works unchanged
  once the backend swaps models per request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several follow-ups on top of the AI Gateway refactor, all driven by use:

Model selection propagates to beacons. The iframe owns the model dropdown
and persists it in localStorage; on mount and on every change it posts
`chat-sidebar:modelChanged` to the host. ChatSidebar mirrors that into an
@observable and passes it down to AlterationBeacons, which now re-fetches
/api/chat/highlights whenever the model changes. Previously the dropdown
only affected the iframe's runSuggest calls; beacons silently kept using
the server default (sonnet-4.6).

Auto-refire suggest on model switch. Changing the dropdown immediately
re-runs whatever the user last asked — preset or free-form prompt — with
the new model, so swapping providers gives an instant A/B comparison
without re-clicking or re-typing.

Eligibility gating. The host page renders the sidebar with an explanatory
message (and skips beacons entirely) when either:
  (a) the user has more than one study queried — grounding requires a
      single primary publication
  (b) the single study has no PMC full text — we won't ground on just an
      abstract
Backed by a new lightweight `GET /api/chat/paper-status?studyId=X`
endpoint that re-uses the in-process paper cache, so the check is fast
on warm path. Renders "Checking paper availability…" while the status
fetch is in flight to avoid flashing the iframe and yanking it.

Beacon-cost receipt. The bottom-left "Loading beacons…" pill stays
visible after load and becomes a quiet receipt: `Beacons · model · $X`.
On failure it turns red with the error in the tooltip — previously the
chip vanished silently when highlights failed.

Token-budget bump. Reasoning models (DeepSeek V4 Pro and o-series) were
exhausting the 4096 maxOutputTokens cap entirely on internal reasoning
and emitting zero text, causing generateObject to throw
NoObjectGenerated. Bumped runHighlights to 16384 and runSuggest to 4096.
Non-reasoning models only consume what they need, so the floor doesn't
inflate normal-case cost.

Layout + theming. Close button is back in the panel's top-right (small,
subtle gray ✕), model dropdown sits just to its left with a `|` divider
between them — all three elements share a 22px row at top:8 so they
align cleanly. Sidebar/launcher/iframe accents now use cBioPortal blue
#3786C2 instead of the prior #2563eb.

Cost computation. Switched from a hand-maintained PRICES table to
gateway.getAvailableModels()-provided per-token rates (lazy-loaded once
per process) so costs are correct for every model the catalog returns,
including non-Anthropic providers whose discount structures differ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… inventory

Refactor the host-side beacon code into a framework-neutral capability surface
and stand up a Model Context Protocol server over postMessage, so any
allowlisted in-browser agent (the sidebar iframe today; other agents tomorrow)
can read the current view/URL/inventory, screenshot, annotate, and URL-navigate
through one discoverable surface instead of reverse-engineering the DOM.

- inventory.ts: scrapeInventory() + legend/tab constants, extracted from
  AlterationBeacons.buildInventory().
- BeaconEngine.ts: framework-neutral beacon placement, rAF pulse/reposition
  loop, MutationObserver rescan, and tooltip — lifted from AlterationBeacons.
- PortalContext.ts: typed capability interface + HostPortalContext + the
  ViewSource adapter over ResultsViewURLWrapper. Navigation routes through
  urlWrapper.updateURL (soft, session-prop-aware), never window.location.
- portalMcpServer.ts: JSON-RPC-over-postMessage MCP shim with an origin
  allowlist, read/act capability scoping, and a writableParams allowlist.
- AlterationBeacons.tsx: slimmed to a thin wrapper (fetch highlights -> map to
  beacons -> engine.setBeacons), sharing one beacon implementation.
- ChatSidebar/ResultsViewPage: build the ViewSource from store.urlWrapper and
  start the server, gated behind localStorage chat-sidebar:mcp=1 so default
  behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alisman and others added 5 commits June 8, 2026 17:41
coadread_tcga_pub (and any cold study) intermittently reported "no full-text
paper" even though PMC full text exists. Root cause: the sidebar fires
/paper-status, /highlights, and /suggest for the same study at mount, and
getPaperContext had no in-flight dedup — all three raced into the NCBI chain at
once, a self-inflicted burst that trips NCBI's 3-req/s/IP limit. NCBI's throttle
response is HTTP 200 with an empty linkset (not a 429), so elink silently yields
no PMCID and the code falls through to the abstract. That degraded result was
then cached permanently per warm Lambda, freezing the study.

- core.ts: coalesce concurrent getPaperContext calls per study onto one Promise
  so the mount-time burst becomes a single fetch; cache 'pmc' results for good
  but give 'abstract'/'none' a 5-minute TTL so a transient miss self-heals.
- paper.ts: identify the client to NCBI (tool/email, plus api_key from
  NCBI_API_KEY when set, lifting the limit 3 -> 10 req/s); add fetchWithRetry
  with a per-attempt 8s timeout and backoff retries on socket errors / 429 / 5xx
  for elink, BioC, and the abstract fetch.

Set NCBI_API_KEY in the Vercel env for durable headroom against shared-egress
throttling. Verified coadread_tcga_pub resolves to source=pmc (PMC3401966).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The paper full-text hop still failed on Vercel even after the rate-limit fixes:
every study resolved to abstract. Instrumenting each NCBI hop from Vercel's
egress showed why — NCBI tarpits eutils elink.fcgi from cloud IPs (it hangs ~8s
and times out), while efetch on the same host responds in ~130ms. PMID->PMCID
depended solely on elink, so PMC never resolved.

Switch that hop to NCBI's ID Converter (pmc.ncbi.nlm.nih.gov/tools/idconv),
which returns the PMCID in ~50ms from Vercel and shares the host family of the
BioC full-text service (also fast from Vercel). efetch stays on eutils for the
abstract fallback.

Verified on production: coadread_tcga_pub, brca_tcga_pub, gbm_tcga_pub all
return source=pmc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lift the resource/tool catalogs into an exported getServerSpec() and commit the
generated mcp.json next to the module, so the surface can be read without a live
postMessage connection — for docs, tooling, or an agent reading ahead. A spec
test asserts mcp.json stays in sync with getServerSpec(), and MCP.md documents
both ways to obtain the spec (static file + runtime initialize/list handshake).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy the committed mcp.json into the build output via CopyRspackPlugin so the
in-page MCP surface is discoverable at https://<portal-origin>/.well-known/mcp.json
— the same origin where the postMessage MCP server runs. Source of truth stays
the committed file (kept in sync with getServerSpec() by portalMcpServer.spec.ts);
this just emits it as a static asset alongside the other copied JSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add portalWebMcp.ts, which registers the same PortalContext capability surface
as native WebMCP tools via the browser's document.modelContext API (W3C draft),
reusing getServerSpec() as the catalog. This makes the tools discoverable by
standard in-browser agents — the MCP-B bridge extension and the model-context
tool inspector today (Chrome Canary behind enable-webmcp-testing), Gemini-in-
Chrome / Edge Copilot as their consumption ships — without our bespoke
postMessage transport.

- Feature-detects document.modelContext (falls back to the deprecated
  navigator.modelContext); a no-op where the API is absent, and each
  registerTool call is wrapped so experimental API drift can't break page load.
- Registered on the host document alongside PortalMcpServer, behind the same
  opt-in flag, sharing PortalContext and the writableParams allowlist for
  navigate. Unregisters via AbortSignal on unmount.
- portalWebMcp.spec.ts covers tool registration, execute routing to
  PortalContext, readOnly filtering, and writableParams enforcement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alisman alisman marked this pull request as draft June 9, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants