* 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>
* fix(sveltekit): patch server chunks with rollup-renamed __filename bindings
The adapter-node chunk patch skipped any chunk matching
/\b(const|let|var)\s+__(file|dir)name\b/ — but $ is not a regex word
character, so rollup-renamed declarations like `__filename$1` (produced
when adapter-node re-bundles the intermediate server output and our
banner's declaration collides) satisfied the check. Chunks that declared
only a renamed binding while a bundled CJS dependency referenced the
bare `__filename` were skipped, and the production server crashed at
boot (observed on main with the TypeScript compiler bundled via
cosmiconfig through @workflow/world-postgres).
Anchor both regexes with (?![\w$]) so renamed identifiers no longer
match. Verified: the sveltekit workbench production server now boots and
serves health checks (with and without a base path), and queue
deliveries from start() succeed.
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
* fix(sveltekit): keep the TypeScript compiler out of the server bundle
Since the world-target injection change, the sveltekit workbench's
hooks.server.ts imports @workflow/world-postgres, whose dependency chain
(graphile-worker -> cosmiconfig) reaches cosmiconfig's TS-config loader.
At runtime that loader's require('typescript') is lazy and never fires,
but SvelteKit bundles the whole chain into the server and rollup's CJS
conversion hoists it into an eager top-level evaluation — executing the
entire TypeScript compiler at boot and crashing the server
("__filename is not defined" inside the bundled compiler).
Alias 'typescript' to a stub module in the SvelteKit plugin, following
the existing pg-native pattern. Server output shrinks from 36MB to 11MB
and boots cleanly.
Verified: sveltekit workbench production build boots, serves
flow?__health (200), and start() runs execute with clean queue
deliveries. The chunk-patch regex fix from the previous commit stays as
hardening for any other CJS dependency that references __filename.
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
* Update .changeset/fix-sveltekit-filename-chunk-patch.md
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
---------
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
* cli: restore dynamic world loading for community backends
Since #2752 moved world selection to static injection, setupCliWorld only
constructed the vercel, local, and postgres worlds explicitly and threw
'Unsupported workflow backend' for anything else — breaking community
worlds (e.g. @workflow-worlds/turso) that previously loaded through the
dynamic createWorld() in @workflow/core/runtime.
Generalize the postgres-only dynamic path: any backend other than
vercel/local is now resolved from the user's project directory and loaded
via its createWorld() export, matching @workflow/web's world construction
(#2804), with clear errors when the package is missing or does not export
createWorld().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* cli: fall back to default.createWorld for CJS world packages
Review feedback on #2804: 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.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update .changeset/cli-generic-world-backends.md
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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>
The workbench workflows are shared across apps (nitro-v2/workflows
symlinks into files shared with workbench/example), and the shared
99_e2e.ts workflow imports @repo/lib/steps/paths-alias-test. nitro-v2's
tsconfig was missing the @repo/* path alias that nitro-v3 already has,
so building nitro-v2 with the Vercel preset failed at esbuild
resolution:
../example/workflows/99_e2e.ts: ERROR: Could not resolve
"@repo/lib/steps/paths-alias-test"
Mirror nitro-v3's alias. Verified NITRO_PRESET=vercel pnpm build now
succeeds (was failing on main before this change).
* 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>
* Add workflow analytics world APIs
* cli: read list views from world.analytics when available
inspect list views (runs, steps, events, hooks, sleeps) now read from the
optional world.analytics namespace when the active backend provides one,
falling back to the runtime storage APIs otherwise. Payload and detail views
are unchanged. Deprecate --with-data for list views; payloads are viewable
per-resource via 'inspect <resource> <id>'.
* cli: keep hook listing on the runtime storage API
The analytics read path omits ownerId (and the secret hook token), so routing
hook listing through it silently drops the ownerId column. Keep inspect hooks
on the runtime APIs, consistent with the web observability UI. Runs, steps,
events, and sleeps continue to use the analytics read path when available.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Handle analytics access metadata in CLI
* test(cli): preserve analytics pageInfo in json output
* fix(cli): paginate analytics sleeps output
* fix(cli): correct deprecation message flag name to --withData
The list-view deprecation warning referenced '--with-data', but the actual
oclif flag is '--withData' (with '-d' alias); '--with-data' errors with
"Nonexistent flag". Fix the warning text, the doc comment, and the changeset
to reference the real flag name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): preserve inspect json array output
* fix(cli): fall back when analytics lists are empty
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(world-vercel): send x-vercel-queue-region on proxy-mode queue sends
Token/proxy clients (CLI, dashboards, external scripts) send queue
messages through api.vercel.com's /v1/workflow proxy, where the fixed
resolveBaseUrl override replaces the queue SDK's own
region -> <region>.vercel-queue.com base-URL resolution — so every
proxied send landed on the region-less VQS host, while the direct
in-function path already dials the regional host (iad1 today).
The proxy now routes sends to the region's VQS dataplane when the
x-vercel-queue-region header is present (vercel/api#79056, deployed).
Set it on every proxy-mode send from the region the QueueClient is
constructed with, giving proxy clients parity with the direct path.
Today that region is the static 'iad1'; when per-send region
resolution lands (#1981), the header automatically carries the
tag/option-derived region — this change is the transport mechanism,
not the policy.
Tests: proxy-mode construction carries the header matching the
client's region; the direct path doesn't.
* chore: trim comments, drop private repo reference
* chore: changeset for proxy-mode x-vercel-queue-region header
* doen
* Fix: Old test file `test/format-duration-precise.test.ts` still asserts two-decimal output ("45.20s", "1m 0.00s") that no longer matches the trimmed-zero output of `formatDurationPrecise`, so `pnpm test`/CI fails.
This commit fixes the issue reported at packages/web-shared/test/format-duration-precise.test.ts:15
## Bug
The PR changed `formatDurationPrecise` (in `packages/web-shared/src/lib/utils.ts`) to trim trailing zeros by wrapping the fractional value in `Number(x.toFixed(fractionDigits))`:
```ts
if (normalizedMs < MS_IN_MINUTE) {
return `${Number((normalizedMs / MS_IN_SECOND).toFixed(fractionDigits))}s`;
}
...
parts.push(`${Number(seconds.toFixed(fractionDigits))}s`);
```
`Number("45.20")` → `45.2`, `Number("0.00")` → `0`, so whole/half seconds now render without padding.
A new test file `packages/web-shared/src/lib/utils.test.ts` reflects this behavior, but the pre-existing `packages/web-shared/test/format-duration-precise.test.ts` was left untouched and still asserts the **old** padded output.
## Concrete trigger
Reproduced the actual function output (integer decomposition + trimmed zeros):
| Input | Old assertion | New actual output |
|-------|---------------|-------------------|
| `45200` | `45.20s` | `45.2s` |
| `999.6` | `1.00s` | `1s` |
| `999.5` | `1.00s` | `1s` |
| `59999` | `1m 0.00s` | `1m 0s` |
| `59995` | `1m 0.00s` | `1m 0s` |
| `119999` | `2m 0.00s` | `2m 0s` |
| `3659999` | `1h 1m 0.00s` | `1h 1m 0s` |
| `86459999` | `1d 1m 0.00s` | `1d 1m 0s` |
The root `vitest.config.ts` uses default include globs, so `test/*.test.ts` runs and these assertions fail, breaking CI. (I couldn't run vitest directly in the sandbox because dev deps weren't installed / `vitest/config` unresolved, so I reproduced the exact function logic in a standalone Node script to confirm the outputs.)
## Fix
Updated the stale assertions in `test/format-duration-precise.test.ts` to the trimmed-zero outputs, and adjusted the file-level doc comment (which referenced `"1m 0.00s"` / `"60.00s"`) to describe the current behavior.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: mitul-s <mitulxshah@gmail.com>
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
* Statically inject workflow world target
* Fix static world injection in host bundles
* Fix static world injection gaps
* Fix Vite Nitro server startup
* Fix Nitro pg-native aliasing
* Fix static world target CI gaps
* Fix static world dev rebuild gaps
* Avoid broad runtime alias in Nitro
* Refresh Next dev route for step HMR
* Externalize Nest target world
* Use canary HMR rediscovery timeout
* Bundle local world in Nest builds
* Dedupe world target helpers and fix SvelteKit chunk patch guard
* 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>
* Optimize Next dev HMR rebuilds
* Fix Next dev HMR CI coverage
* Gate dev HMR logs behind opt-in flag
* Match workflow dev build logs to Next style
* Fix Next dev HMR changed-file classification
* Fix Windows port detection
* Relax HMR log wait in dev e2e
* Avoid canary workflow execution cache flakes
* Allow slower Turbopack HMR propagation in e2e
* Scope canary HMR fuzz execution assertions
* fix(web-shared): align metadata panel styling with attributes section
Render hook metadata as key-value rows inside a DetailCard, matching
the Attributes section instead of a raw JSON block with a separate label.
* fix(web-shared): align top detail panel rows with attributes styling
Reuse DetailKeyValueRow for Module, Step ID, timestamps, etc. — same
tighter spacing, typography, and no dividers as the Attributes section.
* fix(web-shared): collapse top detail rows into Metadata with mono values
Wrap Module, Step ID, timestamps, etc. in a Metadata DetailCard and
render all values in monospace, including copyable paths and IDs.
* fix(web-shared): use contained header style for Metadata section
Add a contained DetailCard variant with rounded bg header and drop my-2
in favor of py-2 padding on the section wrapper.
* refactor(web-shared): drop reserved badge and use cn for row classes
Remove ReservedBadge and showReservedBadge; keep reserved-key sorting only.
Use cn() for conditional mono font classes in DetailKeyValueRow.
* refactor(web-shared): drop contained DetailCard variant
Use the default section DetailCard for Metadata, matching Attributes.
* feat(web-shared): extend cn with custom tailwind-merge class groups
Add a dedicated cn module that understands text-heading, text-label,
text-copy, text-button, and material utilities when merging classes.
* chore: note cn tailwind-merge update in changeset
* fix(web-shared): remove my-2 from DetailCard summary rows
Move vertical spacing to py-2 on the section container instead.
* fix(web-shared): scope Metadata spacing override
Restore shared DetailCard summary spacing for Input, Output, Events, and
Attributes while keeping Metadata tighter with a summaryClassName override.
* refactor(web-shared): use CVA variants for detail row value styling
Replace the mono boolean and one-off Metadata summary override with CVA-backed row variants and semantic mono row wrapper.
* fix(web-shared): use section padding for DetailCard spacing
Move spacing from summary margins to section padding and add content top
spacing so collapsed and expanded detail cards both breathe consistently.
* fix(web-shared): remove expanded Metadata content gap
Keep DetailCard's default expanded content spacing for data panels, but let
Metadata rows start directly below the title via contentClassName merge.
* refactor(web-shared): use Tailwind classes for disabled detail card
* refactor(web-shared): use DetailCard compound content
* refactor(web-shared): rebuild DetailCard as a compound component
Replace the monolithic DetailCard (summary/trailing/disabled props plus
child-type reflection for content) with a context-driven compound API:
DetailCard + DetailCard.Trigger + DetailCard.Content. Drop the dead
trailing branch, expose data-slot/data-state, and migrate all call sites.
* refactor(web-shared): rename DetailCard to Collapsible
It's a generic collapsible section, not a card-specific component. Rename
the component, its parts, data-slots, and the file accordingly.
* refactor(web-shared): split Collapsible into all-in-one + parts
Export a batteries-included <Collapsible label> for the common case so
consumers don't recompose the trigger/content every time, plus
CollapsibleRoot/CollapsibleTrigger/CollapsibleContent for the few call
sites that need to override part styling. Drop the dot-notation namespace.
* refactor(web-shared): move Collapsible into ui directory
It's a generic UI primitive, not sidebar-specific.
* refactor(web-shared): import cn from lib/cn directly
Drop the cn re-export from lib/utils; consumers import it from its
actual source instead of routing through utils.
* refactor(web-shared): drop cn tailwind-merge changes from this PR
Move the extended cn (lib/cn) work to a separate PR; this branch keeps
using the existing cn from lib/utils.
* ship it
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>