Skip to content

weft v0.12.0 — phase 3 of the devtools plan: the panel, @weftgo/devtools, the scope header, the live grant

Choose a tag to compare

@wajihtba wajihtba released this 09 Oct 10:14
· 275 commits to main since this release

0.12.0 — 2026-10-09

Phase 3 of the devtools plan (the panel): @weftgo/devtools on npm,
scope detection (explicit, response headers through weft/scope, DOM
markers, the page URL, the fallback), the panel's host API, layout,
theme, keyed renderer, views, honesty and the Request tab, Studio's
Request pane, and the live grant. Tags core/v0.12.0 (the
weft.version literal only; core's API is unchanged since 0.10.1) and
v0.12.0 — a minor bump, as ?token= is refused and mount() may
return null (Changed — breaking, Changed — migration).

Added

  • The panel's size: studio/dist/panel/panel.js ships at 62,591 B
    gzip (61.1 KiB, 18.9 KiB of headroom under the 80 KiB cap), the last
    row of the append-only ledger studio/web/panel-budget.json; every
    panel build prints the per-item table from it. The KiB figures in the
    entries below are each item's delta, or the panel's size when that
    item landed — intermediate, not the release's.

  • Review fixes (panel): the size table and budget.test.ts measure
    panel.js with its version stamp normalized to the fixed-length
    placeholder "v0.00.0" (normalizeStamp), so a version bump moves no
    ledger byte; the panel finds its own <script> tag by data-weft,
    else by the src that resolves to the bundle's import.meta.url —
    never by a generic data-endpoint/data-token word, so a
    third-party widget's tag ahead of it no longer becomes its
    configuration; nothing renders after a disconnect (the tree's filter
    debounce and "copied" timer are cleared, no observer on a detached
    sentinel); the turn filter's prompts, the reported parked calls and
    failures and the header rung's paths are cleared on every start and
    capped (oldest first); the server's overflow frame reopens a live
    lane on its own bounded backoff (1 s, doubling while overflows repeat
    within a minute, a minute at most) instead of at once; viewport
    resizes are coalesced to one redraw per animation frame; with two
    panels on a page only one (the global's, else the first connected)
    answers Alt+W; external keys are looked up as own properties only
    (weft.content: "constructor" is no mode, an unknown cause no
    cause); a data-weft-scope marker on or inside a
    data-weft-untrusted element is ignored, and the README says markers
    must not appear in untrusted HTML.

  • Review fixes (npm): @weftgo/devtools publishes public
    (publishConfig.access), with prepublishOnly running
    npm-package.ts --check, which now also fails on a missing exports
    or types target, an unguarded panel import, or a README naming
    another version (the README no longer hard-codes one); engines
    (node >= 18), homepage, bugs, keywords, and types for
    ./panel.js. mount() waits for <body> (called from <head> it
    returns the element and appends it on DOMContentLoaded), is
    idempotent (a second call reuses the element it made and applies the
    new options) and returns null with enabled: false or on a server;
    the root entry is import-safe without a DOM (ssr-guard.js /
    ssr-unguard.js around the panel.js import; every export a no-op);
    enabled on mount() and the React/Vue/Svelte helpers; Position
    is the panel's whole placement union (left-dock, top-dock,
    bottom-dock added) and mount() takes mode, push and zIndex
    (written as data-mode, data-push, data-z-index). Breaking:
    mount() returns WeftDevtoolsElement | null.

  • Review fixes (Studio UI): make devtools-vite-check is pinned
    — examples/devtools-vite names vite 8.3.4 and jsdom 30.1.2 exactly
    and commits its package-lock.json (installed by npm ci; the packed
    tarball goes in after, --offline --no-save, and stays out of
    package.json and the lockfile) — and runs in CI (job
    devtools-vite, after studio); check.ts waits for the panel's
    /api/meta request with a deadline instead of a fixed 300 ms and
    gives the bundle's import.meta the built module's URL. The run page
    re-reads a finished run's spans for the invoke_agent span only after
    a running→finished transition it saw (a run opened cold reads them
    once). While a run runs, a request row ahead of its transcript batch
    says "bytes when the transcript is read" (no_transcript), not the
    gap badge (messagesSent's new optional running). The result-cap
    badge shows the bytes cut (truncated · 2.0 KiB cut) and an unrun
    call's max_tokens · call never executed. A badge's reason and fix
    are its accessible description (aria-describedby to a hidden
    element), said once; its name is the label. The Studio suites' real
    sleeps are fake timers advanced past the 2 s poll or deadline-polled
    conditions.

  • Review fixes (verification): imported through @weftgo/devtools
    (bundled into an app's chunk) the panel no longer takes the app's
    entry <script> for its own tag — its import.meta.url is the
    chunk's — so the endpoint default stays the page's directory (it was
    /assets/, with a panel-config.json asked there and the app tag's
    data-* read as configuration): the package's ssr-guard.js marks
    the import before panel.js evaluates (ssr-unguard.js removes the
    mark) and the element class's npmEntry keeps it, and under it rung 4
    is data-weft alone; the standalone panel.js still matches its own
    src. make devtools-vite-check keeps the app's entry tag in a
    second variant (page /app/) and asserts the endpoint. The panel's
    Request tab passes running to messagesSent, so it draws the
    neutral no_transcript where the run page does (a parity fixture
    pins it). A badge's reason and fix are inline visually hidden text
    after the label on both surfaces (the panel's .weft-sr, the run
    page's sr-only) — part of its accessible name, replacing the
    aria-describedby above; title stays for the pointer. The overflow
    backoff measures calm from the reopen, so a sustained overflow holds
    at the minute cap. Alt+W prefers a live, page-mounted panel (the
    global's if it is one, else the first connected one; the dormant or
    self-mounted docks only when there is none), so markup a page adds
    later can be toggled. A run opened cold within 30 s of its end (the
    row's finished) polls its spans for the invoke_agent span inside
    that window.

  • The devtools panel's Request tab (plan E1.2): the run page's
    Request pane in the panel, one step at a time (J/K, the tab's step
    buttons, a step card; ⤢ carries it) — the chips "changed by
    PrepareStep", "prompt changed at this step", "overridden by
    experiment" and "catalog changed at this step" from the request
    record's hashes, the system prompt with its bounded diff (vs the
    previous step; at the first step vs the registered instructions when
    the panel can read /api/manifest), the messages sent (count · bytes,
    the last three inline, the rest in a D4 JSON tree inside the tab), the tool
    catalog (description, policy chips, schema tree), params with "adapter
    default", tool choice, thinking, the attempts, a subagent call's child
    request and the compaction marker. Every absent block is a badge; a
    read-scoped token sees hidden and nothing else, and asks for no
    prompt. The panel gate checks the studio-local trim's chip and diff
    live. +5.5 KiB gzip (estimate +4 KiB). Review fixes: a derived
    prompt or catalog record (a body that did not parse) is badged
    derived and never diffed over, on both surfaces (promptText says
    undefined for it); the manifest is read per endpoint and token, asked
    again 30 s after a failure and once when it does not list the run's
    manifest hash (a redeploy); a subagent's "read its turn" says when the
    child's history is unreachable and hands off to Studio; the tab's step
    is the one ⤢ carries and J/K start from, walking the steps the record
    names too; a Studio that serves no request record is not_recorded
    with the new obsdb cause not_served (/api/meta's
    capabilities_off.requests words first); the panes share their words
    through lib/requests.ts and the stripped words of the tools route
    (+0.6 KiB: the tab is +6.1 KiB, 0.1 KiB past half again its estimate).

  • The devtools panel's honesty (plan D5): every badge the panel
    draws comes from the A3 table through one module,
    src/panel/badges.ts (<span class="weft-badge" data-hole="…">, the
    table's label, the reason and fix as its title — the run page's span;
    badge(hole, note) is what the Request tab calls): on the turn's
    header, step cards, tool calls (a result cap's cut is truncated,
    cause result_cap; an unrun call of a max_tokens step is
    max_tokens), compaction markers and request lines. The table's
    causes (log_cap, result_cap, no_public_id, no_spans,
    dev_token_only) join lib/honesty.ts as CAUSES, checked against
    the golden. The footer's content line ("content on · 2 events
    shortened (24.6 KiB cut)", "content off · weft.Content(false)" /
    "· otel.NoContent()") replaces the "content is stripped" suffix; turn
    rows carry "scripted (0 tokens)", "fork of s_…#e_…" and "experiment
    of t3" chips from the run row (its model, weft.session.forked_from,
    weft.forked_from). parity.test.ts proves every hole of the table
    shows on both the panel and the run page for the same run; a grep test
    keeps hole words out of every other panel source. The panel's badge
    attribute is now data-hole (was data-weft-hole), as the run page's. Review fixes: a
    finished step no request row names is gap on both surfaces (one
    reason, lib/requests.ts); the run page's result-cap badge is the
    table's (truncated/result_cap, max_tokens with no fix for an
    unrun call; HoleBadge takes a cause); a badge's reason and fix
    ride in its accessible text on both surfaces; the content line is said
    only on evidence, and none reads "captured none (weft.Content(false),
    or no destination takes content)" in the panel and in /api/meta's
    note; "scripted (0 tokens)" only at zero usage; the session lookup's
    own reason/fix win; the parity test compares both surfaces' badge sets
    over a hole-free baseline.

  • The devtools panel's views (plan D4): the open turn has tabs —
    Story (the step story), Request (the run page's Request
    pane, one step at a time — E1.2, above),
    Timeline (the spans waterfall at full width over a time axis in
    ms, or, on a run without spans, its steps and tool calls over the
    event sequence) and Raw: a hand-written JSON tree
    (src/panel/tree.ts, no library) of {doc, events, transcript, spans} (and requests when the panel holds them) with per-node
    collapse (aria-expanded, →/←), lazy children (only open nodes
    are built; 200 children, then "+N more"; a string over 2,048 characters shows
    "+N bytes"), a filter over keys and values that opens the path to
    each match and counts them, copy-node, copy-all (the clipboard,
    else the text selected to copy by hand) and a <run id>.json
    download. / focuses the Raw tab's filter, else the turn list's.
    ARIA tabs (role="tablist", ←/→/Home/End); the tab is
    remembered as tab in localStorage["weft.devtools"] (additive to
    v1; raw mirrors it). The turn list gets a filter box (text over
    the id, the error and the prompts the panel has read; a status;
    has error — "n of m turns") and pages older turns through the runs
    API's before=/before_id= cursor as it scrolls to its end (an
    IntersectionObserver on a sentinel; a button where there is none,
    and in the narrow dropdown): no 50-turn cap, "all n turns loaded".
    r returns to the tab before Raw (the header's raw is
    aria-pressed), the raw filter is debounced (100 ms) and a redraw
    re-walks nothing; the Story tab's node is kept across tab switches.
    +5.2 KiB gzip of its +5 KiB (with its review fixes).

  • The devtools panel renders incrementally and is read by assistive
    tech
    (plan D3): a draw patches the dock in place — keyed by run id
    for turn rows and by step ordinal for step cards — so a streaming
    step re-renders only its own card, and focus, the caret, scroll and
    opened <details> survive a redraw because their nodes do (the
    manual restore is gone). The dock is role="complementary"
    (aria-label="weft devtools"), the turn list a role="list" of
    listitems with a roving tabindex (↓/↑ move it; j/k still
    select), the running step's text the one aria-live="polite"
    region, the expanders carry aria-expanded, every icon button an
    aria-label; Tab wraps inside an open, focused float (never when
    docked; Esc lets go). An axe-core run over every mode in both
    themes reports zero violations (a11y.test.ts; axe is a
    devDependency, never in panel.js), the experiment drawer and the
    ? overlay included. The streaming text is aria-busy while it
    grows and a step's end is said once on a hidden role="status" line;
    per-call lines and dropdown options are keyed; the Studio app's
    Tailwind no longer scans the panel's sources (@source not "./panel"). +1.5 KiB gzip.

  • The devtools panel's theme (plan D2): light and dark, following
    the host page without configuration — data-theme="light|dark"
    (also weft:theme, the script tag, mount({theme})) wins, then the
    user's choice from the header's ◐ button (stored as theme in
    localStorage["weft.devtools"]; auto clears it), then auto: <html data-theme>, <html class="dark|light">, <html>'s computed
    color-scheme, prefers-color-scheme, else dark. A host toggle and a
    system change are followed (passive, removed on disconnect); the
    result is data-theme-resolved on the element. The colours are the
    Studio app's palette (studio/web/src/lib/palette.ts, held in
    lockstep with src/styles.css by a test), contrast-checked to WCAG AA
    in both themes, and every colour, radius and font is a --weft-*
    custom property on the panel's :host — weft-devtools { --weft-bg: … } overrides it. The panel's old dark-only --w-* variables are
    gone.

  • The devtools panel's layout (plan D1): the dock floats (dragged
    by its header, resized from its corner, 360×280 up to the viewport
    minus 16 px, clamped on every window resize) or docks to any side —
    left, right, bottom, top — resized along its edge; collapsed
    it is the pill (now with the current turn's tokens in→out, — while
    it runs), and data-mode="hidden" draws nothing while the API and
    events keep working. data-position gains left-dock, top-dock and
    bottom-dock; data-mode, data-push="true" (pads <html> on the
    docked side — the bottom sheet by its 70vh — through
    --weft-devtools-inset, restored exactly; off by default; a fixed or
    sticky composer uses the variable in its own CSS) and data-z-index / --weft-z are new. Under 640 px of
    panel the turn column is a dropdown; under a 480 px viewport the panel
    is a bottom sheet. The placement, the selected turn and the raw view
    are remembered per origin in localStorage["weft.devtools"] ({v: 1, …}; localStorage.weft_debug=1 is migrated into it). Keys: Alt+W
    stays the one global; Alt+Shift+W (next layout), Esc, j/k
    (turns), J/K (steps), g s (open in Studio), r, ? and the
    reserved / fire only with focus inside the panel — the panel no
    longer hears bare keys pressed on the page's body. Every panel build
    prints the per-item size table from the append-only
    studio/web/panel-budget.json ledger against the 80 KiB cap.

  • The devtools panel's host API (plan C4.2): the <weft-devtools>
    element's open(), close(), toggle(), isOpen, scope(s) (a
    Scope or its string form; scope(null) hands the scope back to the
    ladder; a session-only scope resolves through
    GET /api/sessions/{id}/public_id for setup A and the dev token, and
    says why when it cannot), select(runId, step?) (a run the list does
    not show joins it when it is the conversation's, else "run r_… not in
    this conversation"), on(event, cb) and studioLink(runId, step?)
    (through lib/links.ts, never a token). The panel dispatches
    weft:run {runId, status, publicId?, sessionId?, step?} per status
    transition, weft:parked {runId, callId, ackId, name} once per
    parked call (ackId is the call id its approval takes) and
    weft:error {message, runId} once per failed run — bubbling,
    composed, asynchronous, only for the conversation followed. The
    script-tag install publishes the API as window.weft.devtools while a
    panel is connected (never replacing another library's window.weft;
    data-global="off" / weft:global opt out); @weftgo/devtools adds
    select, isOpen and studioLink and no global.
    examples/studio-local's page drives the panel from its own
    "debug this" buttons and a "report this run" link, and a refund
    question parks its turn on a weft.RequireApproval tool. The host API
    costs +3.1 KiB gzip (ledger row "C4.2 fixes": 45,664 B). Review fixes:
    scope() reads {session} alone and sessionId/runId aliases and
    says a scope naming neither; scope(null) restores what mount()
    named; a frozen window.weft is said in the footer; a conversation
    returned to does not re-report its runs; an ordinal the run lacks is
    said and the last step carried; a lookup or run read Studio did not
    answer says so; a host call waits 10 s at most for a hung start;
    error.runId is required; the panel gate (scripts/panel-gate.ts) drives studio-local's
    own page script (--page-script); a failed deny-first in the example
    answers 409 instead of queueing.

  • examples/devtools-vite (the phase 3 gate's first clause): a
    minimal Vite app that installs the packed @weftgo/devtools and
    shows the panel with no <script> tag; make devtools-vite-check
    builds it and proves the panel mounts (jsdom, a fake /api/meta)
    and that the installed panel.js is the served one by sha256.

  • The devtools panel's DOM-marker rung and scope switcher (plan
    C3.3): the panel follows data-weft-scope markers anywhere on the
    page — the attribute the framework helpers already set — read through
    one MutationObserver (debounced) and one passive focusin listener,
    no global touched; with several, the marker around the focused
    element wins. A header switcher (aria-label="conversation") lists
    every conversation known — explicit, markers, headers, each once, at
    most 20 — with its source and a live dot, and choosing one rescopes.
    data-detect accepts markers and headers,markers; the rung is on
    by default except under a read-scoped panel token off loopback, and
    the footer says detect: markers / headers+markers.

  • The devtools panel's URL rung, live fallback and activity pill
    (plan C3.4): ?weft_scope= (read first) or #weft_scope= on the host
    page — the scope's string form, URL-decoded — scopes the panel, below
    an explicit data-scope/scope() and above the marker and header
    rungs; it is re-read on hashchange/popstate (passive listeners,
    removed on disconnect), never written, and never gated (data-detect
    off turns rungs 2–3 off only; the footer says detect: url). With no
    scope from any rung the header reads "no conversation detected on this
    page · how to scope" (the one-line fixes behind the link) and the
    latest list follows /api/live?agent=<the newest run's agent> through
    a grant instead of the 10 s poll, which stays only while no stream
    covers the list (a 403 or a panel token: "streaming needs the server
    token · polling"; a second agent: said). The collapsed pill pulses
    (not under prefers-reduced-motion) and shows the running run's step
    count ("● 3", aria-label "weft devtools · running, step 3") from
    the streams the panel already holds.

  • Studio's Request pane (plan E1.1): each run-page step card's
    request now carries the chips the request record's hashes decide —
    "changed by PrepareStep" (the step's system_hash moved from the
    previous step's with the tool set unchanged, or at the first step is
    not the run's instructions_hash plus the offered tools'
    PromptSnippets as a manifest verified for the run — by its
    manifest_hash — names them), the neutral "prompt changed at this
    step" when a changed tool set may explain the move, "overridden by
    experiment" (the run's invoke_agent span carries
    weft.override.instructions) and "catalog changed at this step";
    nothing is decided from an unverified weft.json or a ToolSource
    tool — and a bounded line diff of the system prompt against the
    previous step, or at the first step against the registered agent's
    instructions when they differ (never over a cut record), params, tool
    choice and thinking as rows ("adapter default" when nil), and the
    messages sent (count and bytes, the last three inline, the rest as a
    raw tree) from the transcript the page already holds. The story view
    reads runs/{id}/spans once the run ends (under the requests
    capability) for the override fingerprint; the time axis stays the
    trace view's. A read-scoped token still sees only the hidden badge.
    examples/studio-local's agent gains a PrepareStep that trims a
    first-step paragraph from its system prompt from step 1 on
    (TestPrepareStepTrimsThePrompt pins the request record's shape).

  • GET /api/sessions/{id}/public_id (plan C4.1): the reverse of
    GET /api/public/{public_id} — {"session_id", "public_id"}, the
    public id thread.PublicID stamped on the session's turns
    (MAX(weft.public_id) over its top-level turns: a session whose turns
    carry several answers the greatest, unbadged), for the devtools'
    scope() given a session id and no public id. A session whose turns
    carry none answers 200 with "public_id": "" and the not_recorded
    badge (cause no_public_id, fix thread.Create(…, thread.PublicID(id)), or set weft.public_id on every turn);
    an unknown session is 404 not_found. The dev (server) token's and
    setup A's open API's alone: every panel token, read or playground,
    is 403 with the hidden badge (cause dev_token_only) before the
    session is looked up. In the always-on read group, so /api/meta's
    capabilities are unchanged. obsdb gains the two causes
    (CauseNoPublicID, CauseDevTokenOnly) in HoleCauses() and
    studio/testdata/holes.golden.json's causes.

  • @weftgo/devtools on npm (plan C1, assembled in
    studio/web/npm, unpublished until a release publishes it): its
    panel.js is /studio/panel.js byte for byte (sha256 equal, gated
    by make studio-check). import "@weftgo/devtools" mounts the
    panel on a page with no <script> tag. The typed entry exports
    mount, scope, open, close, toggle, on, serializeScope
    and parseScope (the data-weft-scope marker:
    pub_…;session=…;flow=…;run=…), and the host API's select,
    isOpen and studioLink (C4.2, above). /react, /vue and
    /svelte export marker helpers, not components. Zero runtime
    dependencies; the version is the weft version. scope() passes the
    whole scope to the panel (C3.2, below), and on() subscribes to the
    weft:run, weft:parked and weft:error events the panel
    dispatches (C4.2, above).
    make devtools-npm builds the package and dry-runs npm pack.

  • weft/scope (plan C3.1): the Go side of the devtools' Scope.
    scope.Scope{PublicID, SessionID, FlowID, RunID} with String(),
    IsZero() and scope.Parse(string) Scope, the same string form as
    the web side's serializeScope/parseScope
    (pub_…;session=…;flow=…;run=…, values percent-encoded, a lenient
    parser that never fails). One golden, studio/testdata/scope.golden.json,
    is read by both test suites. scope.Header(next, func(*http.Request) scope.Scope) http.Handler sets the Weft-Scope response header on
    the app's own chat endpoint, and scope.Set(w, s) sets it once the
    run id is known. Both append Weft-Scope to
    Access-Control-Expose-Headers, and the header never carries a
    token. examples/studio-local's /run answers Weft-Scope: pub_demo;run=<id>. The panel reads the header (C3.2, below). The
    package sits beside the root rather than in it: the root package is
    a generated facade over core, which never imports net/http.

  • The live grant (plan C5): POST /api/live-grant (capability
    live, authenticated like every API route) takes one /api/live
    selector and its kinds (JSON body or query) and answers {sig, exp}; GET /api/live?<selector>&kinds=…&sig=<sig> opens that one
    stream as the identity that asked. The sig is HMAC-SHA256 over the
    identity, the selector, the kinds set and the expiry (60 s, never
    past a panel token's own); it is refused (401) on another selector
    or kinds set, tampered or expired. A panel token is granted only a
    stream inside its public id. Without a Token the key is random per
    process, so setup A takes the same path. A grant is a 60 s window,
    not a single-use ticket: any number of streams may open with its sig
    while it lasts (an EventSource reconnect reuses the URL), each
    running until closed, so a leaked grant URL is live for 60 s — the
    grant answer is no-store and Studio never logs the URL. A panel
    token's stream — opened with a grant or with the bearer — ends at
    the token's expiry with one event: expired frame; a server token's
    stream does not.

  • The panel follows a whole scope, and detects it from response
    headers
    (plan C3.2). Detection rung 1 is the explicit forms:
    data-scope="pub_…;session=…;flow=…;run=…" on the element, the
    weft:scope meta tag or the script tag, window.__WEFT__ = { scope }
    (or { publicId }), and mount({scope}) / scope() from
    @weftgo/devtools. The public id selects the conversation; run pins
    the selected turn and its live tail once, or the panel says run r_… not in this conversation; session narrows the turn list to the runs
    that carry that weft.session.id; flow is a header chip and filters
    nothing yet. Rung 2 reads the Weft-Scope header (weft/scope) of the
    page's own fetch responses by wrapping window.fetch: same-origin
    responses, or cross-origin ones between two loopback origins; headers
    and URL only, never bodies, never sends, never the panel's own Studio
    requests; it chains to whatever fetch it found, is installed only
    once Studio has answered and is restored on disconnect. The next
    turn's header of the same conversation re-pins its run unless the
    user has clicked a turn since, and another conversation is followed
    only from the request path that set the current one. It is on by
    default only with the page and the endpoint on loopback and no token
    or a dev token, opt-in elsewhere with data-detect="headers" (or
    mount({detect})), and data-detect="off" turns detection off.
    Under a panel token the page's fetch is never touched unless asked. It is fetch-only,
    because a page cannot read an EventSource's headers, and WebSocket is
    never wrapped. The footer names the choice: detect: headers | off | explicit | none. examples/studio-local's tag drops data-public-id, and
    its page's first /run scopes the panel to pub_demo. Panel after
    C3.2: 34.5 KiB gzip (was 32.0) — an intermediate size; the release's
    is the first entry above.

Changed

  • data-public-id is deprecated in favour of data-scope (plan
    C3.2). It is still read, as the scope with only its public id, at
    every rung where it was read before (weft:public-id too), and
    data-scope wins at the same rung. mount({publicId}) keeps working
    beside the new mount({scope}).

Changed — breaking

  • A token in a URL is refused: the ?token= query parameter is
    refused on every /api route, in every setup (401, never echoed,
    naming the grant); a token travels in
    Authorization: Bearer only. Migration: an EventSource (or any
    client that put the token in the URL) requests a grant with POST /api/live-grant and opens /api/live?…&sig=. The #token=
    fragment of weft open / weft studio --open links is unaffected:
    it never reaches the server.
  • Both web clients request a live grant (plan C5.2): the Studio UI
    and the devtools panel open every live stream with POST /api/live-grant (bearer in the header) and a sig, and put no
    token in any URL; the UI adopts a link's token from its #token=
    fragment only (a ?token= is stripped, not kept) and drops the
    unused exportUrl.

Changed — migration

One line per change above that asks something of a caller:

  • ?token= is refused on every /api route: send Authorization: Bearer <token>, and open /api/live with a POST /api/live-grant
    sig; the #token= fragment of weft open / weft studio --open
    links stays (it never reaches the server).
  • data-public-id (and weft:public-id, mount({publicId})) is
    deprecated: write data-scope="pub_…" (weft:scope,
    mount({scope})); the old forms are still read.
  • /api/meta's content note for none now reads "the latest run
    captured no content: weft.Content(false) on the agent, or no
    destination takes content" (its fix names both remedies): a client
    that matched the old "the agent captured no content" text reads the
    none mark instead.
  • The panel's badge attribute is data-hole (was data-weft-hole):
    a host selector or test reading the old name reads the new one.
  • mount() from @weftgo/devtools returns WeftDevtoolsElement | null — null with enabled: false or on a server: check the
    result before calling methods on it.
  • The panel no longer adopts a <script> tag because it carries a
    generic data-endpoint/data-token: a tag whose src is not the
    bundle's own URL (an inlined or re-served bundle, a loader) needs
    data-weft on it, or a higher rung (meta tags, the element,
    mount()).