* Fix empty trace and events tabs on older runs
A run's events were read from a different source than the rest of the run
detail view, one with a shorter retention window and a small ingestion
delay. Past that window the trace and events tabs came up empty even
though the run's data was still retained, and the events tab's own id
search would find events the list above it was not showing. Inside the
window, a run still executing could show gaps.
All the run-scoped reads now come from the same source as the rest of the
view. The runs list and hooks list are unchanged: they span runs and are
fine where they are.
The events tab's id search now stops after fewer pages before reporting a
truncated result.
Removes the fetchSteps server action and its /api/rpc method. The trace
viewer has built its spans from events since the observability
data-fetching refactor, which left fetchSteps the only /api/rpc method
with no caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Describe the read paths without backend internals
The comments explaining why these reads moved named the plan field that
gates the analytics window, its per-plan day counts, storage TTLs, index
choices and page-scan mechanics. This repo is the client SDK, so those
belong on the service side, not here — the reason a caller needs is that
the analytics namespace is a metadata mirror with a shorter retention
window and asynchronous ingestion.
Also drops the claim that this listing feeds the graph tab, which is
disabled in run-detail-view.tsx.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Validate world.analytics arguments up front
Every analytics method now checks its arguments before making a request
and throws a RangeError naming the limit it broke: the ids, the
pagination limit against the cap for that listing, and the attribute
filter's pair count, key length and value size. Because analytics is an
optional capability, callers wrap it in a catch, which turned an invalid
argument into what looked like an empty result rather than an error.
Two arguments that used to be dropped silently now fail too. A limit of 0
fell back to the default page size, and a startTime without a matching
endTime turned a listing you meant to bound into an unbounded one that
looked like a normal answer.
Exports ANALYTICS_RUN_SCOPED_PAGE_LIMIT, ANALYTICS_PAGE_LIMIT and
ANALYTICS_MAX_ATTRIBUTE_FILTERS so callers can check the bounds
themselves.
Deprecates analytics.events.listByCorrelationId() in favour of
analytics.events.list({ runId, correlationId }), which issues the same
request and also accepts an eventType filter. It keeps its own
implementation rather than delegating: list() treats correlationId as
optional and skips an empty one, so a delegation would turn an empty id
into an unfiltered listing of the run.
Documents every analytics method in the reference. events.getMany() was
missing entirely, seven methods shared one code block with no parameters
or return shapes, and none of the limits were written down.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Parse attribute key timestamps as UTC
firstSeenAt and lastSeenAt were the only analytics timestamps still on a
plain date coercion. The values arrive without a timezone designator, so
that read them in the process's local zone and every other field in the
namespace read them as UTC — a seven-hour skew on those two fields alone
for anyone running outside UTC.
The added test fails without the fix under TZ=America/Los_Angeles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: drop the deprecated correlation-id listing from the reference
A reference page should describe the API you should reach for. The
deprecation notice lives on the method itself, so editors surface it
where it matters without the page advertising a method nobody should
start using. Also drops it from the page-limit table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Close two gaps in the analytics argument guards
Run ids were validated with workflowRunIdSchema while every other id used
a regex mirroring the backend. Those disagree: z.ulid() accepts a
lowercase body and a first character above 7, and the backend accepts
neither, so the most-used parameter had the leakiest guard and still
produced the 400 this is meant to prevent. Run ids now use the same
pattern as the rest.
A supplied-but-empty filter value was also still being dropped —
correlationId, the optional runId scope on hooks.get, and workflowName
all tested truthiness. Dropping one widens the result set rather than
narrowing it, so an empty correlationId listed the whole run and an
empty workflowName listed every workflow. That is the same failure the
limit and time-window guards were added to prevent, and the comment on
listByCorrelationId already described the hazard. They now compare
against undefined, so an empty id throws and an empty name is forwarded
for the backend to match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Raise argument rejections as a typed, non-retryable error
The guards threw bare RangeErrors, which left a caller — or an agent
driving this API — parsing English to decide whether to fix the call or
retry it. They now raise WorkflowWorldError with
code: 'INVALID_ARGUMENT', the code the rest of this client already uses
for its transport and throttle failures, so the retry decision is a
field lookup. normalizeEventIds moves with them rather than staying the
one guard that throws a different type.
Also sharpens the four messages that made a caller do the work:
a half-open window now names the bound that is missing rather than
restating the rule, an inverted window prints both ends, and the
attribute-value and event-id batch errors report the size measured
rather than only the bound they broke.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Name the method and the field on an argument rejection
Two things a caller could not get without reading prose. The same guard
runs behind several methods, so `runId must be a workflow run id` was
identical whether it came from events.list or steps.get — fine with a
stack, lossy once the error has crossed a log line. And the offending
argument was only available as the first token of the message, which is
the part most likely to be reworded.
Messages now open with the method, and WorkflowWorldError carries an
optional `field`:
analytics.runs.list: pagination.limit must be an integer between 1
and 100 (received 9999)
→ code: 'INVALID_ARGUMENT', field: 'pagination.limit'
`field` is additive on the error class and set only by these guards, so
nothing that reads the existing properties changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cli): bulk-cancel runs in a single operation
Replace the per-run cancel loop in `workflow cancel` with one `cancelRuns`
call, validate `--limit` (1-500), print a compact outcome summary with
per-run lines for surfaced failures, and exit nonzero only when a run fails.
The bulk logic lives in a dependency-injected `performBulkCancel` helper so it
is unit-testable without an oclif harness.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): address bulk cancel review feedback
* feat(web): bulk-cancel selected runs in a single request
Thread a bulkCancelRuns action through the server action, RPC route,
rpc-client, and client wrappers, backed by core's cancelRuns. The runs table
now cancels the selected pending/running runs in one call, caps a batch at
BULK_CANCEL_MAX_RUN_IDS (disabling the button with guidance above the cap),
and reports a single outcome-summary toast covering only the categories that
occurred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci: stop deploying changeset-release/main, run its e2e against production
The changesets action force-pushes `changeset-release/main`, and it can
point at exactly main's HEAD SHA. Vercel keeps one commit status per
project per SHA, so when both a production deployment (from main) and a
preview deployment (from changeset-release/main) are built for the same
commit, whichever finishes last owns the status. On 2026-07-30 the
preview finished last, so `vercel/wait-for-deployment-action` — which
reads the deployment ID out of that status — handed production e2e runs
a preview deployment ID and forked runs across environments.
Disable git deployments for that branch in every Vercel project rooted
in this repo, and give the changeset PR's Vercel e2e lanes a deployment
to test that actually exists: main's production deployment for the PR's
base SHA, resolved by SHA so a mid-flight production build is waited out
rather than silently replaced by an older one.
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
* ci: resolve changeset-release e2e deployments with the wait action, tokenless
Per review: with changeset-release/main no longer deployed, main SHAs
can never again be deployed to a second environment of these projects,
so the per-SHA commit status the action reads is unambiguous for
exactly this lane. Reuse vercel/wait-for-deployment-action with
environment: production and sha pinned to the PR base SHA instead of
the Vercel-API polling script, drop the script and its VERCEL_TOKEN
usage, and inherit the action's inactive/skipped-build handling.
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
---------
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
* Fix Biome lint violations and add Biome CI check
Biome was not configured to respect .gitignore, so ~92% of the 13,355
reported diagnostics came from gitignored build artifacts. Enable VCS
integration (useIgnoreFile), apply safe auto-fixes across the repo, fix
the remaining mechanical errors by hand, downgrade judgment-call a11y /
dangerouslySetInnerHTML rules to warnings, and add a 'biome ci' job to
the Lint workflow so violations block PRs going forward.
* Use an empty changeset (no behavior change, no release needed)
* feat(nitro): embed observability dashboard in-process at /_workflow
Serve the @workflow/web observability UI inside the Nitro process at a
configurable route (default /_workflow) instead of spawning a separate
web server and 302-redirecting to it. Enabled in dev, omitted from
production builds by default (so prod bundles carry no @workflow/web
import). Never mounted on Vercel deploys (use the hosted dashboard).
- @workflow/web: add a framework-neutral `@workflow/web/handler`
(createWorkflowWebHandler) that serves SSR + static client assets +
RPC as one Web Request->Response handler under a runtime basename
(asset manifest URLs + publicPath are reprefixed so the dashboard is
self-contained under its mount). Add `@workflow/web/registry` for
embedded-dashboard discovery; make the RPC/stream client basename-aware.
- @workflow/nitro: mount the handler in-process (Nitro v2 h3 + v3 native
paths), gated by a new `dashboard` option (default = dev).
- @workflow/cli: `workflow web` / `inspect --web` defer to a running
embedded dashboard instead of starting a redundant server; pass
`--standalone` to force the standalone UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(nitro): normalize dashboard path once, use isNitroV2() helper
Address review feedback on the embedded dashboard:
- Normalize the dashboard mount path in one place before it feeds both
the Nitro route registration (`[path, path + '/**']`) and the handler
`basename`. Force a single leading slash, strip trailing slashes, and
reject the root mount, so a custom `path` can't make the route and the
handler's internal `normalizeBasename` disagree.
- Replace the handler-level `!nitro.routing` v2 checks with the existing
`isNitroV2()` helper for consistent v2/v3 detection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
* feat: decrypt sealed payloads in the dashboard and CLI
Without this, any payload another run sealed to this one renders as a lock
icon with no way to open it — a visible regression for anyone debugging a
run that received a cross-deployment hook resumption. The user is entitled
to read the data and has already supplied the key; only the plumbing was
missing.
`hydrateDataWithKey` now delegates to the envelope layer, which dispatches
on the format prefix, instead of unconditionally running AES-GCM. All four
o11y key-resolution sites (web-shared hydration, the web stream reader,
and both CLI `--decrypt` paths) resolve the full capability rather than
just the symmetric key. Each already had the raw 32 bytes in hand, so this
costs one extra derivation and no additional requests.
A caller that supplies only a symmetric key still gets the ciphertext
placeholder for sealed payloads rather than a decryption error, since that
key never could have opened them.
**Browser bundling.** The obvious import for the new helper is
`@workflow/core/serialization`, but that module graph reaches `node:util`
and `node:async_hooks` and cannot be bundled for the browser — which is
what `@workflow/core/serialization-format` exists to avoid. The key
helpers are re-exported from that browser-safe entrypoint instead, and the
two browser consumers import from there; the CLI keeps the direct import
since it runs on Node. Verified by walking the built import graph: the
entrypoint reaches 6 modules and zero Node built-ins.
Unrelated: `pnpm --filter @workflow/web build` currently fails on `main`
too (`reducers/common.js` importing `node:util`). Turbo caching had been
hiding it; touching core caused a cache miss that surfaced it. Not
addressed here.
* review: narrow the o11y decrypt key type and dedupe an import
- `hydrateDataWithKey` accepted `PayloadKey`, which includes `SealTarget`.
A seal target holds only a public key, so it can open neither scheme —
passing one compiled fine and then always failed at runtime. Added a
`DecryptionKey` alias (`CryptoKey | RunPayloadKeys`) and narrowed the
signature, so that misuse is now a compile error. A `@ts-expect-error`
test pins the guarantee.
- `hydrateResourceIOAsync` dynamically imported
`@workflow/core/serialization-format` twice. Destructure both bindings
from the single existing import instead.
* review: record @workflow/web in the changeset
This PR changes the dashboard's stream reader
(`packages/web/app/lib/hooks/use-stream-reader.ts`) so it dispatches on the
envelope format and can read sealed (`encp`) frames, but the changeset listed
only core, web-shared and cli.
`@workflow/web` is published, so without an entry the change would still ship —
just as an incidental dependency bump, with nothing in that package's release
notes explaining that sealed-stream decryption landed.
* web: infinite scroll for the runs table
Replace Previous/Next cursor paging with front-style infinite scroll:
a useInfiniteList hook accumulates cursor pages with per-run dedup and
generation-guarded resets, and useLoadMoreOnScroll drives loadMore from
an IntersectionObserver sentinel (400px prefetch margin, guarded against
double-fetch, observed against the table's scroll container). Footer now
shows the loaded count and the analytics lookback window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* web: back the runs infinite list with SWR so tab switches serve from cache
Rewrite useInfiniteList on useSWRInfinite: pages are keyed by
[cacheKey, cursor] in SWR's global cache, so unmount/remount (switching
tabs) restores fetched pages instantly instead of refetching. Revalidation
is conservative because analytics list queries are expensive:
revalidateFirstPage and revalidateIfStale are off; freshness comes from
the Refresh button and the visibility-change auto-reload, which map to
reload() (reset to first page + revalidate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* world: expose startTime/endTime on analytics runs listing
The workflow-server /v2/analytics/runs endpoint has accepted a bounded
startTime/endTime window since it shipped, and is significantly faster
with one (the window prunes the ClickHouse scan: ~2s for 12h vs ~8s for
the default 30-day entitlement window). The world client never exposed
the params, so the CLI and web UI could only issue windowless requests.
Pass them through so clients can send bounded windows (e.g. a period
picker like front's workflows o11y).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* web: front-style period picker for the runs list
Add a time-window picker (1h/6h/24h/3d/7d/30d, default 24h, URL-backed
via ?period=) that sends an explicit startTime/endTime window through
fetchRuns -> world.analytics.runs.list, keeping the ClickHouse scan
bounded. The window is frozen per selection/refresh so all cursor pages
share the same bounds, and it participates in the SWR cache key.
Plan tiers are honored data-driven from the server's pageInfo: presets
longer than the plan's observability lookback are disabled in the picker
(labeled Observability Plus when an upgrade is available), and a 402
observability-upgrade-required response renders through the existing
upgrade-required error handling. The footer now labels the selected
window instead of the plan lookback. The runtime (local) fallback path
ignores the window since the storage API has no time filter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* web: allow status filtering without a workflow name filter
The status dropdown was disabled on Vercel backends until a workflow was
selected — a limitation of the runtime DynamoDB API's index design. The
runs list now reads via world.analytics, whose ClickHouse query filters
derived status independently of workflowName, so drop the guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* cli: time-window flags for runs listing; widen name lookups past the default window
The analytics backend now defaults windowless runs listings to the
trailing 24h. Replicate the web's window support in the CLI:
- 'workflow inspect runs' gains --since/--until (relative durations like
30m/12h/7d/2w, or timestamps) which are sent as an explicit
startTime/endTime window. Out-of-plan windows surface through the
existing observability-upgrade-required handling; non-analytics
backends warn that the flags are ignored.
- 'workflow start <name>' resolves the workflow's latest run via a
windowless (default-window) listing and now retries across the plan's
whole observability window on a miss, so names idle for more than a
day keep resolving.
- Bulk 'workflow cancel' matches across the plan window up front — a run
can sleep or wait on a hook for days without recent events, so the
default recent window must not bound cancellation matching.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: tighten changeset descriptions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* web: persist frozen listing windows across remounts; minimize lockfile diff
Address review findings:
- The frozen startTime/endTime lived in component state, but RunsTable
fully remounts on tab switches, so every remount minted a new SWR cache
key — the cached-pages restore never hit and cache entries grew
unboundedly (one per key, including every 5s local-backend poll tick).
Move the frozen windows to a module-scope store keyed by period: a
remount reuses the stored window (same cache key, instant restore), and
the window only advances on explicit refresh/reload. Non-analytics
backends now send no window at all (the runtime APIs ignore it anyway),
which also hides the period picker and window label there.
- Regenerate pnpm-lock.yaml from main so the diff contains only the swr
addition (plus its own use-sync-external-store dependency), dropping
the unrelated docs-importer radix-ui re-resolutions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* web: construct worlds explicitly instead of via static-injection stub
Since #2752, createWorld() from @workflow/core/runtime is a stub that
throws unless a framework build plugin aliases it to a world package.
The CLI was migrated to construct worlds directly, but @workflow/web
still called the stub, so 'workflow web' crashed with 'Workflow target
world was not statically injected' for local and postgres backends
(the vercel path was unaffected since it constructs the world directly).
Mirror the CLI: import the local world statically and resolve any other
configured world package from the inspected project's directory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* web: fall back to default.createWorld for CJS world packages
Review feedback: import() of a require-resolved CJS entry relies on
cjs-module-lexer to surface named exports; fall back to
mod.default.createWorld when detection fails. Mirrored in the CLI in #2806.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add workflow analytics world APIs
* web: read observability list views from world.analytics when available
Route the runs/steps/events/hooks list server actions through the optional
world.analytics namespace when the backend provides one, falling back to the
runtime storage APIs otherwise. Events listing only uses the analytics path
when no payload data is requested. Detail/get actions, streams, and mutations
are unchanged.
* web: keep events and hooks list reads on the runtime storage API
The Events tab and trace viewer derive step names and wait resumeAt from
resolved event payloads, and the hooks table needs the secret token and
ownerId for its resume/copy-token actions. The metadata-only analytics rows
do not carry these, so only the runs and steps list views use world.analytics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* web: read events list from world.analytics with a runtime-shape remap
The events list/trace consumers read only top-level eventType, correlationId,
and createdAt; event payloads are loaded lazily per event via fetchEvent(...,
'all') on the runtime path. Map the flat analytics event rows into the runtime
Event shape (reconstructing eventData.stepName) so fetchEvents and
fetchEventsByCorrelationId can use the analytics read path when available.
Hooks remain on the runtime path (they need the secret token + ownerId).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* web: list hooks from world.analytics, fetch token on demand
The hooks list now reads from the metadata-only world.analytics namespace
when the backend provides one (falling back to the runtime storage APIs
otherwise). A hook's secret token is no longer shipped in list rows — it is
fetched one hook at a time via world.hooks.get only when the user copies the
token or resumes the hook, keeping the secret out of bulk list responses.
Adds a fetchHookToken server action + RPC, a HookListItem type (Hook without
token), and a lazy HookTokenCell for the copy-token affordance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Handle analytics access metadata in web
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add workflow analytics world APIs
* web: read observability list views from world.analytics when available
Route the runs/steps/events/hooks list server actions through the optional
world.analytics namespace when the backend provides one, falling back to the
runtime storage APIs otherwise. Events listing only uses the analytics path
when no payload data is requested. Detail/get actions, streams, and mutations
are unchanged.
* web: keep events and hooks list reads on the runtime storage API
The Events tab and trace viewer derive step names and wait resumeAt from
resolved event payloads, and the hooks table needs the secret token and
ownerId for its resume/copy-token actions. The metadata-only analytics rows
do not carry these, so only the runs and steps list views use world.analytics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* web: read events list from world.analytics with a runtime-shape remap
The events list/trace consumers read only top-level eventType, correlationId,
and createdAt; event payloads are loaded lazily per event via fetchEvent(...,
'all') on the runtime path. Map the flat analytics event rows into the runtime
Event shape (reconstructing eventData.stepName) so fetchEvents and
fetchEventsByCorrelationId can use the analytics read path when available.
Hooks remain on the runtime path (they need the secret token + ownerId).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Handle analytics access metadata in web
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* nice
* refactor(web-shared): simplify resizable detail panel internals
Inline single-use constants in DraggableBorder and make comments
self-contained.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix compressed workflow data display
* Add OSS web no-key hydration regression
* Scope compression normalization to read paths; tidy hydration
Address review feedback on the compressed-data fix:
- world-vercel: keep gzip/zstd decompression on the o11y/display read
paths (getStep/getRun/getEvent/getWorkflowRunEvents/getHook) but not on
the runtime event-append path (world.events.create, createStep,
updateStep). That path is runtime-only and re-hydrates every payload via
the decompress-aware helpers, so decompressing at the adapter was
redundant work on the TTFB-sensitive run_started/inline-delta path and
skewed the runtime's deserialize compression telemetry to `codec: none`.
deserializeStep is now shape-only; normalizeStepData runs in the read
filter. Adds a regression test pinning the write-path pass-through.
- serialized-data: drop dead `errorRef`/`metadataRef` normalization (refs
are descriptor objects, never compressed byte payloads).
- web: in the wait-entity path, filter events by correlationId before
hydrating so an encryption key doesn't decrypt the whole event page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add selection-driven span-detail primitives
Extract the run/step/hook/sleep fetch+hydrate core out of useWorkflowResourceData
into a plain async fetchSpanDetailResource (no React state), and add a
selection-driven state machine in web-shared:
- deriveSpanDetailView / resourceNeedsFetchedDetail: pure view-model deriver
whose status (idle/loading/ready/error) is a function of (selection, fetched
detail), so it can never lag the selection.
- useSelectedSpanDetail: fetches a selected span's detail directly with a
request-token to drop stale/out-of-order responses.
* Drive trace detail panel from the span-detail state machine
Replace the cross-package selection round-trip (EntityDetailPanel useEffect ->
onSpanSelect -> page spanSelection state -> useWorkflowResourceData -> context)
with a single injected fetchSpanDetail capability:
- EntityDetailPanel consumes useSelectedSpanDetail; its loading state now stays
in phase with the selected span, so Input/Output no longer vanish and pop back
in while navigating.
- SidebarDataContext drops spanDetailData/Loading/Error + onSpanSelect for a
single fetchSpanDetail; RunDetailView injects it and drops the duplicate
spanSelection state.
- WorkflowTraceViewer / RunTraceView take fetchSpanDetail too.
* Test span-detail view-model transitions; add changeset
Cover deriveSpanDetailView (idle/loading/ready/error, stale-detail rejection,
hooks ready inline) and resourceNeedsFetchedDetail.
* Trim redundant/narrative comments in span-detail state machine
Comment-only cleanup: drop PR-narration and cross-file duplication from the
deriveSpanDetailView / useSelectedSpanDetail / EntityDetailPanel / fetchSpanDetail
doc comments, keeping the non-obvious intent (request-token, error scoping,
decrypt closure).
* delete pointless coments
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>