Skip to content

Releases: itsonu/flutter-lamp

v0.20.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 12:16

The dashboard becomes a runtime observability surface rather than a telemetry
viewer, and one real defect in the tool accounting is fixed. Nine peer tabs
become four destinations; nothing about the collectors, the store or the MCP
tool surface changed.

Fixed - the tool accounting was fabricating a zero

  • A tool call whose handler threw was never counted. The cost meter wrapped
    registration and recorded after the handler returned, so the SDK's conversion
    of a throw into an error result took the call out of the accounting
    altogether. Measured against a live app: five tools called with no isolate
    attached, four of them erroring, all four missing — the session reported
    0 calls, 0 errors. A dashboard rendering that would have shown a confident
    zero while nothing worked. There is a mutation-checked regression test: remove
    the catch and it fails.

Fixed - readings that were not what they claimed

  • The frame and heap charts printed the drawing ceiling under the word
    "max".
    The plotting area is padded 15% above the data so the series does not
    touch the top edge, and that padded number was the caption. A frame chart
    whose real peak was 41.0ms captioned 47.1; the heap chart captioned a
    maximum above the heap capacity it reported four inches higher, which is
    physically impossible in a memory tool. The caption now gives the observed
    maximum, the last value and the sample count. Pinned by a check that runs the
    shipped drawing code against known data.
  • 0 was printed where nothing could have been measured. With no runtime
    attached, Exceptions and Network showed a zero rather than naming which
    kind of nothing applied.
  • Readings taken before a disconnect looked current. Frame, heap and network
    figures kept their live typography beside a NOT CONNECTED badge; the
    staleness stamp reached two of six statistics and now reaches all of them.
  • Pause asserted the stream was RECEIVING while every reading behind it
    was frozen
    — byte-identical output measured 19.5 seconds apart while the
    server processed 32 further tool calls. STREAM and VIEW are now separate
    states, because both are true at once.
  • The dashboard claimed seven collectors were ACTIVE with the VM
    unreachable.
    Collector health is last-known and was never invalidated.
  • "last event never" appeared beside a list of events, because the
    last-event time was set on live pushes only and never seeded from the
    snapshot.

Fixed - the stream was unusable under load

  • The row you were reading moved 2,416px down the page in six seconds.
    Entries are prepended newest-first, so every arrival pushed the thing being
    read further away, at roughly 400px/s. The stream now anchors on a row rather
    than on scroll position: at the top it follows, scrolled away it pins and
    reports what arrived (N new entries — jump to latest). Measured after: 0px
    drift following, 0.2px pinned. The browser's own overflow-anchor had to be
    disabled, because it was fighting the explicit anchor and silently pushing a
    following view off the top.
  • A typo could not be corrected in the search field. The input is rebuilt on
    every keystroke and a refocus put the caret at the end of the string; caret
    and selection are now preserved exactly.
  • Over half the visible stream was the dashboard's own heap poller — 52-67%
    of the newest 400 rows, with zero frame, log or network rows visible while
    hundreds of frames sat captured. Consecutive samples now roll up the way
    reconnect churn already did, every original one disclosure away.
  • Errors named three unrelated things at once — MCP tool failures, janky
    frames and app exceptions — and a 41.8ms frame rendered in the same red as a
    crash. It is gone; the filters now each return exactly what they say.

Added - what the process knows about itself

  • Tool-call telemetry reaches the browser. The cost meter gains per-tool
    total duration and last-called time plus a window of recent calls, and the
    dashboard broadcasts it. All of it was already collected and none of it was
    reachable outside runtime_status.
  • The connected agent is named. src/core/mcpClient.ts reads the
    clientInfo the client sends at initialize, so the dashboard can say
    "Claude Code 2.1.0" instead of asserting that "an agent" is present. Shown as
    self-reported, because that is what it is.
  • A telemetry message on the dashboard WebSocket carrying server identity,
    tool accounting, collector health, unobservable categories and retention.
    Diffed before sending, so an idle dashboard stays silent.

Changed - the dashboard

  • Four destinations instead of nine. Overview (a session report), Activity
    (one stream with source and category filters), Performance (frame analysis),
    Diagnostics (topology, health, capability, blind spots). Logs, Network,
    Exceptions, Timeline and the MCP call log became filters; three of them had
    been rendering the identical empty sentence in a 120px panel. Inspector became
    a section of Diagnostics, because it is a capability list.
  • The header carries three independent states — runtime, MCP, stream —
    because a single badge could not express "the app is gone but MCP is serving
    an agent", which is a real and common state and is why the old header said
    "app not connected" while 239 tool calls were being served.
  • Findings state the evidence and, where the evidence contains one, the
    remedy.
    The VM Service diagnosis event carries concrete transport advice;
    the finding renders it verbatim. Remediation is never synthesised. Findings
    are ordered most severe first, from the severity already assigned.
  • Keyboard operable throughout. 418 interactive elements previously had no
    keyboard path. The tab strip is one tab stop with arrow-key movement; the
    activity stream is one tab stop with a roving cursor, so several hundred rows
    never become several hundred tab stops. Focus survives the re-render.
  • Tab, filter and search live in the URL, so a filtered view can be reloaded
    or shared, and a drill-through is reversed by the browser's own Back button.
  • Non-text contrast. Every boundary was one token at 1.33:1. Control
    boundaries now clear 3:1 (WCAG 1.4.11) while structural edges and row
    separators stay quiet, because a card edge is not a component boundary.
  • Export JSON is now Export events, with its scope stated: this browser's
    buffer, not the session. The full export remains the export_session tool.

Added - documentation

  • PRODUCT.md records the product truth every design pass had been
    re-deriving: two audiences of equal weight, the positioning, what is
    observable, what is not, and the principles — including "the dashboard must
    never imply a fact that the runtime cannot actually observe".
  • docs/Data-Path.md traces the topology and proves it rather than
    asserting it: an MCP tool call's own event was observed arriving on the
    dashboard socket with the same event id.
  • docs/DESIGN.md replaces docs/Design.md, which documented the
    seven-tab dashboard, Google Fonts and tokens that no longer exist.

Known limitation, unchanged in this release

The frame budget is a fixed 16.7ms (60fps). On a faster display the jank count
is conservative, and both the finding and the Performance view say so rather
than presenting it as absolute.

v0.19.1

Choose a tag to compare

@github-actions github-actions released this 28 Aug 06:02

Documentation only — no runtime change. Published because README.md ships
inside the package, so the corrections below were not visible to anyone reading
0.19.0 on npm.

Fixed - README claims that had gone stale

  • "Framework errors (Flutter.Error) are always captured regardless." Not
    true, and measured untrue: the widget inspector posts that event only while
    structured error reporting is on, which the framework disables in profile mode
    and on the web. On those targets a thrown framework error leaves no trace at
    all. The same claim had already been corrected in
    docs/Implementation-Notes.md and was left standing in the README.
  • The retention list named five categories and omitted three (state,
    rebuilds, navigation), and gave only the 60fps frame figure. A 25-minute run
    measured eviction beginning at 4 minutes under a 10fps workload, and the note
    that every diagnosis carries a retention warning once eviction starts was
    missing entirely.
  • Security did not mention the VM Service credential scrub, nor that
    FLUTTER_LAMP_REDACT=off deliberately does not disable it. That distinction is
    the point of the fix in 0.19.0: the switch exists so a developer can read their
    own app's headers and log text, not so the server will hand out the key to the
    app it is attached to.
  • "Agents should not call all sixteen" — the table lists 22.
  • The roadmap listed state-management activity and navigation as next; both
    shipped in 0.17.0.
  • Three tool rows described output that grew in 0.19.0: runtime_status (session
    cost, VM-to-host clock offset), diagnose_runtime (stable cause label,
    unobservable coverage), get_state_activity (Provider and Bloc, not only
    Riverpod).

The README's tool table and the server's tools/list were then diffed rather
than eyeballed: 22 each, with no entry on either side missing from the other.

v0.19.0

Choose a tag to compare

@github-actions github-actions released this 28 Aug 05:49

A minor release, not a patch: Diagnosis gains fields and two tools gain output.
It also carries a security fix — 0.18.1 and earlier can leak the VM Service
credential into tool output when the observed app logs its own debug-service URI,
which a web target does on startup.
That token authorises evaluate, so
upgrade rather than pin.

Added - Phase D, evaluation

The diagnosis engine is now measured against recorded sessions instead of only
against its own unit tests. Every other test here builds a store by hand and
checks what the engine does with it; that measures the code against its author's
expectations, which is not the same as measuring it against an app that actually
misbehaved.

  • eval/incidents/*.json - golden incidents: a real session captured from a
    running app with export_session in full mode, plus what the right answer
    is. Eight so far, across three targets — an Android device over adb, a Windows
    desktop build, and Chrome — because a target is part of what a diagnosis has to
    cope with, not an implementation detail. Two deliberately straddle the jank threshold: 74/381 frames
    (19.4%) must stay unknown, 48/240 (20.0%) must be diagnosed as jank. The
    pairing is the point - together they pin the boundary, which is the part of a
    heuristic that drifts. Two unrelated incidents would not. The other two are a
    ranking pair: both contain an exception and jank over threshold, and their
    right answers are opposite, so no fixed ordering of the two hypotheses can
    satisfy both. The fifth covers network: one endpoint returns 500 four times
    while another returns 200 four times, so connectivity demonstrably works and
    the finding is specific to an endpoint. The sixth covers memory: twelve heap
    samples rising monotonically from 89MB to 217MB with no drop anywhere, which
    is the argument — a healthy app under load produces a sawtooth, and twelve
    consecutive rises is not one. externalUsageMB is 0 throughout, so the growth
    is Dart heap rather than external allocation accounted elsewhere, and
    heapCapacityMB climbs with it, so the VM was expanding for live data rather
    than sitting on uncollected garbage. Every CauseKind now has a recording.

    The seventh is a second negative, and it pairs with the first on a different
    axis. One abstains because the jank ratio is too low (19.4% of 381 frames);
    the new one abstains despite a ratio of 100% — one janky frame out of one —
    because the sample is too small. Dropping the three-frame floor to one turns
    a single 90.4ms startup frame into a confident jank verdict, which would be the
    most embarrassing false positive available: technically accurate, useless, and
    true of every app that has ever started. Neither negative catches the other's
    bound.

    The eighth is the first recorded on a non-native target: the same workload on
    Chrome, where two collectors genuinely cannot see their domain. jank is still
    the right answer (30 of 52 frames, build-bound at 48.1ms against 5.4ms), and
    the diagnosis must also report exception and network as unobservable
    rather than merely empty. Expectation gains an optional unobservable field
    and replay feeds the recorded collector health back in, so that is scored
    behaviour rather than a claim in prose.

  • Two ranking policies pinned by unit test, both found by mutation rather
    than by reading the code. Sustained heap growth must not outrank an evidenced
    jank pattern — promoting memory above jank passed the entire eval suite
    unnoticed, because no recording constrains that edge and a recording that
    would needs a session where the ground truth is genuinely arguable (if
    allocation pressure caused the collection pauses that caused the jank, memory
    is the cause and jank the symptom). And heap growth is never described as a
    leak: the restraint in Growth is not proof of a leak is part of the
    contract, not a wording accident.

  • probe/flaky-server.mjs and a network scenario in bloc_probe. Offline
    and deterministic — an incident that depends on someone else's server is not
    reproducible, and a probe should not make outbound requests to record a
    fixture.

  • Header redaction is now proven on a recorded artifact, not only in a unit
    test. The probe sends Authorization: Bearer … on every request; the recorded
    incident contains "authorization": "[REDACTED]", lists authorization under
    redactedHeaders, and the token string appears nowhere in the file. The
    untested claim was never the redaction function — it was that redaction runs
    at capture, so the artifact a developer pastes into a chat is clean.

  • src/eval/replay.ts - hydrates a recorded session and re-runs the
    diagnosers. Nothing new had to be invented to record an incident:
    export_session was already versioned, carried every event and diagnosis, and
    had its shape pinned by a test.

  • src/eval/score.ts - top-1 accuracy, evidence recall, false-confidence
    rate, unknown precision, dangling-evidence count.

  • A CI gate, asymmetric on purpose. Accuracy has a floor; false confidence
    has a ceiling of zero
    . A tool that says "unknown" when it cannot tell costs a
    developer nothing - they go and look themselves, which is what they were doing
    anyway. One that says "the network call caused your jank" at 85% confidence
    when it did not sends them where the bug is not, and spends the credibility
    that makes the correct answers worth reading.

  • RuntimeStore.hydrate - loads already-stamped events back in unchanged.
    Not add(), which mints fresh ids from a counter: replaying through it would
    renumber everything, so every exc_00042 a diagnosis cited would point
    somewhere else and evidence precision would be meaningless.

Verified by mutation rather than assumed, because a gate that only ever passes
is decoration. Loosening the jank threshold to 15% makes the 19.4% session get a
confident jank verdict and trips three tests including false confidence.
Tightening it to 25% makes the 20.0% session abstain and trips two - notably
not false confidence, because abstaining when jank was real is wrong, not
confidently wrong. On the exception path: blinding the exception detector, or
letting jank outrank it, trips false confidence both times; removing only the
stack-trace confidence bonus trips the band and leaves false confidence at zero.
Same asymmetry, from the other side. On the ranking: restoring the unconditional
exception priority trips false confidence at 25%, while dropping jank's strength
below the confidence threshold trips status and band with false confidence still
at 0% - abstention and confident wrongness scored apart, which is the whole
point of the metric.

Verified

  • A 25-minute session, past every ring capacity. Every recorded incident is
    under a minute and reports evicted: 0, so retention had never been exercised
    against a real app — and the frame ring holds 1,000 events, roughly 100 seconds
    at 10fps. Held a session for 25 minutes, sampling every 60s: frames cap at
    1,000 at minute 4, eviction climbs to 6,168 frames and 5,167 state events by
    minute 25, and from the moment it starts the diagnosis carries the retention
    note. Truncation is not silent. The diagnosis stayed stable across 75 tool
    calls with no drift.

    Two things it does not show, stated because the run looks more conclusive than
    it is. The jank ratio held at 31% throughout, but the workload is uniform — so
    this shows the mechanism survives eviction, not that the ratio would reflect a
    session whose behaviour changed once the denominator wraps. And the probe for
    the server's own memory returned nothing, so there is no data on process
    growth over the run.

Added - verification

  • probe/sweep-live.mjs — calls every read-only tool against a running app
    and exits non-zero if any errored or if the VM Service credential turned up in
    a response. verify-release.sh checks the package; nothing checked the
    behaviour, and both defects that made 0.18.0 unusable were found by hand
    exactly this way. Verified to fail, not assumed to: removing the credential
    scrub makes it report credential leaks: 1 -> get_logs on a web target, and it
    is what caught the get_timeline defect above.

    Its limits are written down rather than left to be discovered. The credential
    check is opportunistic — it can only see a leak the app actually logged, and
    only while that line is still in the backlog DDS replays on connect. Reading
    collector health off runtime_status instead of runtime_health made it
    silently print nothing at first, which is the same class of bug it exists to
    catch.

Added - coverage

  • coverage.unobservable on every diagnosis: of the empty categories, those
    nothing could have seen, because the collector responsible is degraded or
    unavailable on this target. empty is unchanged, so nothing about its meaning
    moved — the split is the point. An agent reading empty: [exception, network]
    on a web target would conclude the app threw nothing and made no failing
    requests; both are unfounded there, and the second is reassurance drawn from a
    question that was never asked.

    Measured rather than reasoned about: on Chrome network is unavailable (no
    dart:io) and exception is degraded (structured error reporting is off off
    native), while navigation is empty simply because the app did not navigate.
    Three empty categories, two different reasons, and the diagnosis now says
    which is which. A category that is blind but still holds events is not counted
    as a hole.

Added - cost

  • What this server costs the caller is now measured. Every response is input
    tokens on the agent's next turn, and that was previously guessed at. Measured
    against a live app with ~1,700 events retained: tools/list is 16.7kB
    (~4,200 tokens) before any work happens, `diagnose...
Read more

v0.18.1

Choose a tag to compare

@github-actions github-actions released this 26 Aug 15:51

A security and correctness fix over 0.18.0, which shipped before either defect
was found. 0.18.0 should not be used: it leaks the VM Service auth token
into tool output, and it makes the app under observation throw.

Fixed - found by running against a real app

Two defects that 168 passing tests did not catch, found within minutes of
pointing the server at a live Flutter app (device A015, Android 16, Flutter
3.44.1). Both were mock-shaped blind spots: a mock does not care what a
parameter is called, and a unit test does not read a whole tool response.

  • The Widget Inspector calls used the wrong parameter name, and the failure
    landed inside the app being observed.
    get_widget_tree and
    get_selected_widget passed groupName; Flutter requires objectGroup.
    getRootWidgetSummaryTree is registered through
    _registerObjectGroupServiceExtension, which does parameters['objectGroup']!,
    and getSelectedSummaryWidget through _registerServiceExtensionWithArg,
    which asserts the key is present. So the tool did not merely fail to read the
    tree — it threw an exception inside the app, which then surfaced in
    get_exceptions and anchored what_changed. A diagnosis tool contaminating
    its own evidence is worse than one that returns an error. disposeGroup had
    the name right all along, which is why the group lifecycle tests passed.
  • what_changed leaked the VM Service auth token. The 0.18.0 audit fixed
    export_session and asserted, in this changelog, that redaction at capture
    meant credentials never enter the store. That was wrong, and this is where:
    ConnectionManager.open() wrote the raw URI into a system event's message
    and data.wsUri, and returned it from connect_vm. Found live at
    $.system[0].message. The path segment of a VM Service URI authorises
    evaluate — arbitrary Dart execution in the app. Fixed where the value
    enters rather than at each consumer; host and port survive.
  • Token redaction no longer honours FLUTTER_LAMP_REDACT=off. That flag is
    a choice about observed evidence — headers, log text. Nobody asking to see
    their own request headers asked for a remote-code-execution credential in
    every export and on every dashboard. Evidence redaction still opts out.

Regression guards for both, each verified to fail when the bug is reintroduced.
They assert at the source boundary, because neither faulty call is reachable
from a unit test: registerTools holds a connection singleton, and
ConnectionManager needs a live VmService.

v0.18.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 09:34

Merges the probe-app branch, and corrects a measurement in 0.17.0's favour.

Added

  • probe/ - two self-driving probe apps (riverpod_probe on
    flutter_riverpod 3.4.2, bloc_probe on flutter_bloc 9.1.1) and
    probe/measure.mjs, the harness. Each app runs a five-phase workload and
    prints a phase marker before every phase, so an event on the VM Service can
    be attributed to what caused it. Claims about state-management observability
    are now re-checkable instead of remembered.
  • export_session (P3) - the session as versioned JSON. full archives
    everything; brief carries the diagnoses plus only the events their evidence
    cites. One measured example, on probe/riverpod_probe: 22 events / 19KB
    against 417 / 152KB. The ratio depends entirely on how much the session
    captured and how much of it the diagnoses cite.
  • get_state_activity - query state-change activity directly rather than
    only through findings.

Fixed

  • Bloc: both earlier conclusions were wrong, in opposite directions. The
    merged branch concluded stock Bloc was invisible - 143 transitions, zero
    events, no ext.bloc.* RPC. All of that is true, and the conclusion still did
    not follow: flutter_bloc 9.1.1 depends transitively on provider (its own
    pubspec.lock), and provider posts provider:provider_changed, so a
    flutter_bloc app is not silent.

    The correction then overstated in the other direction. Measured on the probe:
    20 transitions against ~1,220 provider events, because stormWatchers = 60
    widgets each watch the bloc. provider:provider_changed fires once per
    notified dependent, not once per transition
    - so the count measures how many
    widgets were notified, Bloc transition counts cannot be recovered from it, and
    Bloc itself remains uninstrumented for the VM Service.

    What is now claimed, and no more: there is no ext.bloc.* RPC; flutter_bloc
    depends on provider; provider emits change notifications; therefore a Bloc app
    on that dependency path exposes state-related activity indirectly. Bloc
    internals are not directly observable, and an app avoiding provider-backed
    lookup would be silent here. probe/EVIDENCE.md records each measurement with
    its kind (observed / inferred / documented) and what it does not establish.

Fixed - audit pass

  • The session export leaked the VM Service auth token. export_session
    carried session.wsUri verbatim, and the path segment of a VM Service URI is
    a credential granting evaluate, i.e. arbitrary Dart execution in the running
    app - in an artifact whose stated purpose is to be attached to bug reports.
    Redaction now happens inside exportSession itself rather than at one call
    site, so the artifact is safe regardless of caller. Host and port survive.
  • stateFinding reported saturated activity as a discovery. With continuous
    state churn - exactly what a rebuild storm produces - every frame falls inside
    the 1s window and the ratio reads 100% whether or not jank is related. It now
    computes the same ratio for smooth frames as a control and withholds the
    finding when the difference is under 15 points. Documented as a correlation
    heuristic, with the symmetric window called out: it cannot show the state
    change came first.
  • stateFinding was invoked twice, duplicating itself in every performance
    diagnosis - a merge artifact git resolved silently.
  • The reconnection give-up event was emitted asynchronously, behind an adb
    subprocess call added in 0.15.0. The most important record in a session's
    timeline was gated on an optional external tool that can block, making the
    test flaky (1 run in 3) and the behaviour genuinely wrong. The terminal event
    is now recorded synchronously; the transport explanation follows as a separate
    enriching event.
  • Corrected three places that contradicted each other on Bloc after the merge:
    performance.ts claimed Bloc announces on the Extension stream, and
    stateActivity.ts claimed Bloc is invisible.

Changed

  • diagnose_performance's state finding now measures the fraction of janky
    frames falling within 1s of state activity, rather than comparing raw volumes
    • a falsifiable claim instead of two counts side by side. Scored 0.5, below
      every causal finding, because churn and expensive builds both follow the same
      tap.
  • The state collector registers its handler before subscribing, per 0.16.0. The
    branch predated that fix and would have reintroduced it.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 23 Aug 16:28

Storage release. Fixes a bug that silently destroyed the evidence the project
exists to preserve.

Fixed

  • Frame events no longer evict every other kind of evidence. The store was
    one shared 5,000-event buffer. FrameCollector writes an event per frame, so
    at 60fps frames filled the entire buffer in about 83 seconds and then evicted
    everything older — the exceptions, network requests and logs you were actually
    chasing. Two minutes into a session, the exception from minute one was gone.
    Each category now has its own ring buffer, so a noisy stream can only evict
    itself. Defaults: 3,000 logs, 1,000 exceptions, 1,000 network, 1,000 frames,
    500 system, overridable through the RuntimeStore constructor.
  • Insertion is O(1). The buffer was trimmed with splice(0, n) on every
    insert past capacity, copying the whole array 60 times a second inside the
    tool meant to diagnose performance problems. Replaced with a circular buffer:
    200,000 events went from 9,943ms to 81ms, a 122x improvement.

Added

  • runtime_status now reports retention: per-category capacity, how many
    events are retained, how many were evicted, and the timestamp of the oldest
    event still held. A capped buffer is fine; a silently capped one leaves an
    agent reasoning over truncated history without knowing it.
  • CATEGORIES is exported from core/events.ts as a runtime value, with the
    Category type derived from it so the two cannot drift.

Compatibility

No tool renamed, removed, or reshaped. RuntimeStore.query() returns the same
most-recent-first ordering, now merged across the per-category buffers.
runtime_status.retention is additive.

v0.2.0 — security release

Choose a tag to compare

@itsonu itsonu released this 22 Aug 11:44

Security release. Both issues were live defects in 0.1.0 — upgrade if you ran the dashboard or inspected authenticated requests.

Credentials no longer leave the app. get_network and diagnose_runtime previously returned full headers for failing requests, sending Authorization bearer tokens and Cookie / Set-Cookie values to the AI model and to every browser watching the dashboard. Redaction now runs at capture, so secrets never enter the event store at all.

The dashboard WebSocket no longer accepts cross-origin connections. Binding to 127.0.0.1 does not protect a WebSocket — browsers exempt WebSocket from the same-origin policy, so any page you had open could connect to ws://127.0.0.1:7373/ws and read your entire runtime stream. The handshake now requires a per-process token that only same-origin script can read.

No tool was renamed, removed, or reshaped. Credential header values read [REDACTED], with the withheld names listed in data.redactedHeaders.

Also adds SECURITY.md and docs/Improvement-Plan.md — a full audit of the v0.1.0 implementation with a prioritized backlog.

Full detail in CHANGELOG.md.

v0.1.0 — first public release

Choose a tag to compare

@itsonu itsonu released this 22 Aug 10:05

First public release of Flutter Lamp — an MCP server that gives your AI live eyes on a running Flutter app through the Dart VM Service.

12 MCP tools: VM connect, logs, exceptions with reconstructed stack traces, network (Dio & package:http), frame timings with jank detection, widget tree, selected widget, memory, VM timeline, evidence-first diagnose_runtime, and a live browser dashboard on http://127.0.0.1:7373.

Full detail in CHANGELOG.md.