Repository navigation
weft v0.12.0 — phase 3 of the devtools plan: the panel, @weftgo/devtools, the scope header, the live grant
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.jsships 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 ledgerstudio/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.tsmeasure
panel.jswith 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 bydata-weft,
else by thesrcthat resolves to the bundle'simport.meta.url—
never by a genericdata-endpoint/data-tokenword, 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'soverflowframe 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); adata-weft-scopemarker on or inside a
data-weft-untrustedelement is ignored, and the README says markers
must not appear in untrusted HTML. -
Review fixes (npm):
@weftgo/devtoolspublishes public
(publishConfig.access), withprepublishOnlyrunning
npm-package.ts --check, which now also fails on a missingexports
ortypestarget, an unguarded panel import, or a README naming
another version (the README no longer hard-codes one);engines
(node >= 18),homepage,bugs,keywords, andtypesfor
./panel.js.mount()waits for<body>(called from<head>it
returns the element and appends it onDOMContentLoaded), is
idempotent (a second call reuses the element it made and applies the
new options) and returnsnullwithenabled: falseor on a server;
the root entry is import-safe without a DOM (ssr-guard.js/
ssr-unguard.jsaround thepanel.jsimport; every export a no-op);
enabledonmount()and the React/Vue/Svelte helpers;Position
is the panel's whole placement union (left-dock,top-dock,
bottom-dockadded) andmount()takesmode,pushandzIndex
(written asdata-mode,data-push,data-z-index). Breaking:
mount()returnsWeftDevtoolsElement | null. -
Review fixes (Studio UI):
make devtools-vite-checkis pinned
—examples/devtools-vitenames vite 8.3.4 and jsdom 30.1.2 exactly
and commits itspackage-lock.json(installed bynpm ci; the packed
tarball goes in after,--offline --no-save, and stays out of
package.jsonand the lockfile) — and runs in CI (job
devtools-vite, afterstudio);check.tswaits for the panel's
/api/metarequest with a deadline instead of a fixed 300 ms and
gives the bundle'simport.metathe 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
gapbadge (messagesSent's new optionalrunning). The result-cap
badge shows the bytes cut (truncated · 2.0 KiB cut) and an unrun
call'smax_tokens · call never executed. A badge's reason and fix
are its accessible description (aria-describedbyto 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 — itsimport.meta.urlis the
chunk's — so the endpoint default stays the page's directory (it was
/assets/, with apanel-config.jsonasked there and the app tag's
data-*read as configuration): the package'sssr-guard.jsmarks
the import beforepanel.jsevaluates (ssr-unguard.jsremoves the
mark) and the element class'snpmEntrykeeps it, and under it rung 4
isdata-weftalone; the standalonepanel.jsstill matches its own
src.make devtools-vite-checkkeeps the app's entry tag in a
second variant (page/app/) and asserts the endpoint. The panel's
Request tab passesrunningtomessagesSent, so it draws the
neutralno_transcriptwhere 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'ssr-only) — part of its accessible name, replacing the
aria-describedbyabove;titlestays 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'sfinished) 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 seeshiddenand 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
derivedand never diffed over, on both surfaces (promptTextsays
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 isnot_recorded
with the new obsdb causenot_served(/api/meta's
capabilities_off.requestswords first); the panes share their words
throughlib/requests.tsand 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 istruncated,
causeresult_cap; an unrun call of amax_tokensstep is
max_tokens), compaction markers and request lines. The table's
causes (log_cap,result_cap,no_public_id,no_spans,
dev_token_only) joinlib/honesty.tsasCAUSES, 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.tsproves 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 nowdata-hole(wasdata-weft-hole), as the run page's. Review fixes: a
finished step no request row names isgapon both surfaces (one
reason,lib/requests.ts); the run page's result-cap badge is the
table's (truncated/result_cap,max_tokenswith no fix for an
unrun call;HoleBadgetakes acause); a badge's reason and fix
ride in its accessible text on both surfaces; the content line is said
only on evidence, andnonereads "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}(andrequestswhen 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 astabinlocalStorage["weft.devtools"](additive to
v1;rawmirrors 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'sbefore=/before_id=cursor as it scrolls to its end (an
IntersectionObserveron a sentinel; a button where there is none,
and in the narrow dropdown): no 50-turn cap, "all n turns loaded".
rreturns to the tab before Raw (the header'srawis
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 isrole="complementary"
(aria-label="weft devtools"), the turn list arole="list"of
listitems with a roving tabindex (↓/↑move it;j/kstill
select), the running step's text the onearia-live="polite"
region, the expanders carryaria-expanded, every icon button an
aria-label;Tabwraps inside an open, focused float (never when
docked;Esclets go). An axe-core run over every mode in both
themes reports zero violations (a11y.test.ts; axe is a
devDependency, never inpanel.js), the experiment drawer and the
?overlay included. The streaming text isaria-busywhile it
grows and a step's end is said once on a hiddenrole="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"
(alsoweft:theme, the script tag,mount({theme})) wins, then the
user's choice from the header's◐button (stored asthemein
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 isdata-theme-resolvedon the element. The colours are the
Studio app's palette (studio/web/src/lib/palette.ts, held in
lockstep withsrc/styles.cssby 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), anddata-mode="hidden"draws nothing while the API and
events keep working.data-positiongainsleft-dock,top-dockand
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) anddata-z-index/--weft-zare 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 inlocalStorage["weft.devtools"]({v: 1, …};localStorage.weft_debug=1is 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.jsonledger against the 80 KiB cap. -
The devtools panel's host API (plan C4.2): the
<weft-devtools>
element'sopen(),close(),toggle(),isOpen,scope(s)(a
Scopeor its string form;scope(null)hands the scope back to the
ladder; a session-only scope resolves through
GET /api/sessions/{id}/public_idfor 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)andstudioLink(runId, step?)
(throughlib/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 (ackIdis 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 aswindow.weft.devtoolswhile a
panel is connected (never replacing another library'swindow.weft;
data-global="off"/weft:globalopt out);@weftgo/devtoolsadds
select,isOpenandstudioLinkand 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 aweft.RequireApprovaltool. The host API
costs +3.1 KiB gzip (ledger row "C4.2 fixes": 45,664 B). Review fixes:
scope()reads{session}alone andsessionId/runIdaliases and
says a scope naming neither;scope(null)restores whatmount()
named; a frozenwindow.weftis 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.runIdis 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/devtoolsand
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 installedpanel.jsis the served one by sha256. -
The devtools panel's DOM-marker rung and scope switcher (plan
C3.3): the panel followsdata-weft-scopemarkers anywhere on the
page — the attribute the framework helpers already set — read through
one MutationObserver (debounced) and one passivefocusinlistener,
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-detectacceptsmarkersandheaders,markers; the rung is on
by default except under a read-scoped panel token off loopback, and
the footer saysdetect: 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 explicitdata-scope/scope()and above the marker and header
rungs; it is re-read onhashchange/popstate(passive listeners,
removed on disconnect), never written, and never gated (data-detect
offturns rungs 2–3 off only; the footer saysdetect: 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 underprefers-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'ssystem_hashmoved from the
previous step's with the tool set unchanged, or at the first step is
not the run'sinstructions_hashplus 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
readsruns/{id}/spansonce the run ends (under therequests
capability) for the override fingerprint; the time axis stays the
trace view's. A read-scoped token still sees only thehiddenbadge.
examples/studio-local's agent gains a PrepareStep that trims a
first-step paragraph from its system prompt from step 1 on
(TestPrepareStepTrimsThePromptpins 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 idthread.PublicIDstamped 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 thenot_recorded
badge (causeno_public_id, fixthread.Create(…, thread.PublicID(id)), or setweft.public_idon every turn);
an unknown session is 404not_found. The dev (server) token's and
setup A's open API's alone: every panel token, read or playground,
is 403 with thehiddenbadge (causedev_token_only) before the
session is looked up. In the always-on read group, so/api/meta's
capabilities are unchanged.obsdbgains the two causes
(CauseNoPublicID,CauseDevTokenOnly) inHoleCauses()and
studio/testdata/holes.golden.json'scauses. -
@weftgo/devtoolson npm (plan C1, assembled in
studio/web/npm, unpublished until a release publishes it): its
panel.jsis/studio/panel.jsbyte for byte (sha256 equal, gated
bymake 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
andparseScope(thedata-weft-scopemarker:
pub_…;session=…;flow=…;run=…), and the host API'sselect,
isOpenandstudioLink(C4.2, above)./react,/vueand
/svelteexport marker helpers, not components. Zero runtime
dependencies; the version is the weft version.scope()passes the
whole scope to the panel (C3.2, below), andon()subscribes to the
weft:run,weft:parkedandweft:errorevents the panel
dispatches (C4.2, above).
make devtools-npmbuilds the package and dry-runsnpm pack. -
weft/scope(plan C3.1): the Go side of the devtools' Scope.
scope.Scope{PublicID, SessionID, FlowID, RunID}withString(),
IsZero()andscope.Parse(string) Scope, the same string form as
the web side'sserializeScope/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.Handlersets theWeft-Scoperesponse header on
the app's own chat endpoint, andscope.Set(w, s)sets it once the
run id is known. Both appendWeft-Scopeto
Access-Control-Expose-Headers, and the header never carries a
token.examples/studio-local's/runanswersWeft-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 itskinds(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 aTokenthe 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 (anEventSourcereconnect reuses the URL), each
running until closed, so a leaked grant URL is live for 60 s — the
grant answer isno-storeand 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 oneevent: expiredframe; 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:scopemeta tag or the script tag,window.__WEFT__ = { scope }
(or{ publicId }), andmount({scope})/scope()from
@weftgo/devtools. The public id selects the conversation;runpins
the selected turn and its live tail once, or the panel saysrun r_… not in this conversation;sessionnarrows the turn list to the runs
that carry thatweft.session.id;flowis a header chip and filters
nothing yet. Rung 2 reads theWeft-Scopeheader (weft/scope) of the
page's ownfetchresponses by wrappingwindow.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 whateverfetchit 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 withdata-detect="headers"(or
mount({detect})), anddata-detect="off"turns detection off.
Under a panel token the page'sfetchis never touched unless asked. It is fetch-only,
because a page cannot read anEventSource's headers, and WebSocket is
never wrapped. The footer names the choice:detect: headers | off | explicit | none.examples/studio-local's tag dropsdata-public-id, and
its page's first/runscopes the panel topub_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-idis deprecated in favour ofdata-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-idtoo), and
data-scopewins at the same rung.mount({publicId})keeps working
beside the newmount({scope}).
Changed — breaking
- A token in a URL is refused: the
?token=query parameter is
refused on every/apiroute, in every setup (401, never echoed,
naming the grant); a token travels in
Authorization: Beareronly. Migration: anEventSource(or any
client that put the token in the URL) requests a grant withPOST /api/live-grantand opens/api/live?…&sig=. The#token=
fragment ofweft open/weft studio --openlinks 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 withPOST /api/live-grant(bearer in the header) and asig, 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
unusedexportUrl.
Changed — migration
One line per change above that asks something of a caller:
?token=is refused on every/apiroute: sendAuthorization: Bearer <token>, and open/api/livewith aPOST /api/live-grant
sig; the#token=fragment ofweft open/weft studio --open
links stays (it never reaches the server).data-public-id(andweft:public-id,mount({publicId})) is
deprecated: writedata-scope="pub_…"(weft:scope,
mount({scope})); the old forms are still read./api/meta'scontentnote fornonenow 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
nonemark instead.- The panel's badge attribute is
data-hole(wasdata-weft-hole):
a host selector or test reading the old name reads the new one. mount()from@weftgo/devtoolsreturnsWeftDevtoolsElement | null—nullwithenabled: falseor on a server: check the
result before calling methods on it.- The panel no longer adopts a
<script>tag because it carries a
genericdata-endpoint/data-token: a tag whosesrcis not the
bundle's own URL (an inlined or re-served bundle, a loader) needs
data-wefton it, or a higher rung (meta tags, the element,
mount()).