Commit Graph

10 Commits

Author SHA1 Message Date
Karthik Kalyan 371f06e5ac feat(web): bulk-cancel selected runs from the runs table (#3349)
* 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>
2026-08-05 14:54:33 -07:00
Karthik Kalyan fe327e69e2 [world][web][cli] o11y: window-aware runs listing (#2812)
* 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>
2026-07-07 18:12:17 -07:00
Karthik Kalyan ae51f45166 web: list hooks from analytics, fetch token on demand (#2652)
* 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>
2026-07-07 12:06:41 -07:00
Karthik Kalyan 1518c48608 web: read observability list views from world.analytics when available (#2647)
* 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>
2026-07-07 11:45:07 -07:00
Mitul Shah 8393716687 Fix trace detail panel Input/Output flicker (selection-driven state machine) (#2637)
* 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>
2026-06-26 16:42:47 -04:00
Ali Waleed a0b99c8ec8 [web] Decompose workflow-api-client.ts into focused modules and add unit tests (#1070) 2026-02-24 15:34:53 -08:00
Nathan Rajlich 5213309073 Fix "dev" script in web package (#1039)
* Fix "dev" script in `web` package

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 14:24:27 -08:00
Karthik Kalyan 1c1157340d [builders][web-shared] Improvements to o11y and fixes to graph generation code path (#1031)
* [workflow o11y] rebase on latest main and keep targeted UI/builders changes

Rebase the branch intent onto latest main by preserving web-shared UI refactors and builders base-builder updates while taking main for hydration and data-fetching behavior elsewhere.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [workflow o11y] align web-shared hydration revivers with main

Revert the hydration reviver delta for URL, URLSearchParams, and Headers so web-shared matches main behavior while keeping the targeted UI/builders-only scope on this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [workflow o11y] restore PR #1017 detail-panel decoupling

Bring the web-shared trace/detail panel files back in sync with main so PR #1017 behavior is preserved and not regressed on this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [workflow o11y] restore PR #1018 react-inspector sidebar updates

Bring web-shared o11y rendering files back in sync with main so ObjectInspector-based sidebar rendering and related UI behavior from PR #1018 remain intact on this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [workflow o11y] keep custom viewers and apply inspector rendering

Preserve the branch-specific event list and stream viewer UX while applying react-inspector rendering to payload/chunk data so complex hydrated values render correctly without reverting custom UI behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [workflow o11y] trace viewer UX improvements and Geist alignment

- Add live tick animation, context menu, and cancel run support from PR #984
- Decouple side panel styling to use Geist design tokens (inline styles)
- Fix sleep span detail panel showing events instead of wait entity attributes
- Fix stream viewer flickering by removing unstable object deps
- Remove Chunks/Output toggle from stream viewer, show only chunks
- Add resolve hook modal, wake-up sleep, and cancel run plumbing
- Add loading skeleton for stream viewer

Co-authored-by: Cursor <cursoragent@cursor.com>

* Bug fixes

* Bug fixes

* Bug fixes

* Bug fixes

* Bug fixes

* Bug fixes

* Bug fixes

* Bug fixes

* Bug fixes

* Bug fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 13:27:16 -08:00
Nathan Rajlich c56dc3848e Extract browser-safe serialization format and move hydration client-side (#1015)
## Summary

Split the serialization/deserialization logic into environment-specific layers so data hydration can happen client-side in the browser. This is a prerequisite for e2e encryption where decryption keys are only available in the browser.

## Architecture

### Layer 1: `@workflow/core/serialization-format` (new, browser-safe)
- Format prefix encoding/decoding (`devl`, future `encr`, etc.)
- Generic `hydrateData()` dispatch — handles Uint8Array (v2 binary), legacy arrays (v1), and plain values
- `hydrateResourceIO(resource, revivers)` resource-type dispatcher (step/hook/event/workflow field mapping)
- `ClassInstanceRef` (plain data class, no `node:util` dependency)
- `StreamRef`, type guards (`isStreamRef`, `isStreamId`, `isClassInstanceRef`), utility functions (`extractStreamIds`, `truncateId`)
- Shared `observabilityRevivers` for stream/class/step display overrides
- 36 unit tests covering all of the above

### Layer 2: Environment-specific revivers
- **`@workflow/web-shared`** (`lib/hydration.ts`) — browser-safe revivers using `atob()` for base64, real `URLSearchParams`/`Headers`/`URL` instances, `ClassInstanceRef` for UI rendering
- **`@workflow/cli`** (`lib/inspect/hydration.ts`) — Node.js revivers using `Buffer.from()` for base64, `CLIClassInstanceRef` with `util.inspect.custom` for CLI output

Each module exports a pre-bound `hydrateResourceIO(resource)` that uses its environment's revivers.

### Removed: `@workflow/core/observability`
- Deleted `observability.ts` and `observability.test.ts` entirely (no remaining consumers)
- Removed `"./observability"` export from `@workflow/core/package.json`
- Removed the `workflow` package's `internal/observability.ts` re-export
- All functionality has been split between `serialization-format.ts` (shared types/utilities) and the environment-specific hydration modules

## Web package changes
- Server passes raw world data through without hydration (CBOR preserves `Uint8Array`)
- Client calls `hydrateResourceIO` from `@workflow/web-shared` after receiving CBOR-decoded data
- No Vite `node:*` stubs needed since `@workflow/core/serialization-format` is browser-safe
- Optimized: event hydration finds the matching event before hydrating (instead of hydrating all)
- Reduced server log noise from handled API errors (4xx errors no longer logged)

## Packages affected
- `@workflow/core` — new `serialization-format` export (with tests), removed `observability` export
- `@workflow/web-shared` — new `lib/hydration.ts` with browser-safe revivers
- `@workflow/cli` — new `lib/inspect/hydration.ts` with Node.js revivers, updated `output.ts` import
- `@workflow/web` — client-side hydration, removed server-side hydration
- `workflow` — removed `internal/observability.ts` re-export
2026-02-12 16:58:19 -08:00
Nathan Rajlich 7653e6bfdb Migrate @workflow/web from Next.js to React Router v7 (#1005)
## Summary

- Replace Next.js App Router with React Router v7.13.0 framework mode (Vite-based), eliminating the large `next` dependency from the web, CLI, and workflow metapackages
- Serve the web UI in-process from the CLI via Express instead of spawning `next start` as a child process
- Switch RPC transport from JSON to CBOR to preserve binary data types across the wire
- Replace `nuqs` URL state management with React Router's `useSearchParams`
- Replace Next.js server actions with an RPC resource route (`/api/rpc`) and a thin CBOR-based client

## Motivation

The `next` package is ~300MB installed and was the single largest dependency in the monorepo. It also required spawning a separate child process from the CLI to run the o11y web server, adding complexity around process lifecycle management, port readiness polling, and environment variable forwarding.

With React Router framework mode, the web package builds to a standard Express-compatible server bundle that the CLI can import and serve directly in its own process.

## What changed

**Framework swap (`@workflow/web`):**
- `next.config.ts` / `postcss.config.mjs` → `react-router.config.ts` / `vite.config.ts`
- `src/` directory → `app/` directory (React Router convention)
- `src/app/layout.tsx` + `layout-client.tsx` → `app/root.tsx`
- `src/app/page.tsx` → `app/routes/home.tsx`
- `src/app/run/[runId]/page.tsx` → `app/routes/run-detail.tsx`
- Path alias `@/` → `~/`
- Removed all `'use client'` / `'use server'` directives

**Data transport:**
- Server actions → RPC resource route at `/api/rpc` with CBOR encoding
- CBOR preserves `Uint8Array` and other binary types natively (no base64 overhead)
- Stream reading → dedicated `/api/stream/:streamId` resource route

**URL state:**
- `nuqs` (`useQueryState`) → `useSearchParams` from `react-router`

**Fonts:**
- `next/font/google` → Geist `.woff2` files referenced directly from `node_modules/geist` via `@font-face` in CSS

**CLI integration (`@workflow/cli`):**
- `import('@workflow/web/server').then(m => m.startServer(port))`
- No child process, no readiness polling, no cleanup handlers

**Radix UI compatibility:**
- `onSubmit` preventDefault on `AlertDialogContent` and `SheetContent` to prevent Radix's internal `<form method="dialog">` from triggering React Router route actions
- Catch-all action on root route for any stray POSTs

## Dependencies removed
- `next`, `swr`, `nuqs`, `@tailwindcss/postcss`

## Dependencies added
- `react-router` / `@react-router/dev` / `@react-router/node` / `@react-router/express` (all `7.13.0`)
- `express`, `vite`, `@tailwindcss/vite`, `cbor-x`, `isbot`, `cross-env`
- `geist` (devDep)
2026-02-12 16:30:51 -08:00