Skip to content

feat: add record-replay system for playground HTTP calls#3336

Open
hellovai wants to merge 6 commits intocanaryfrom
vbv/record-replay-system-for-playground-http-calls
Open

feat: add record-replay system for playground HTTP calls#3336
hellovai wants to merge 6 commits intocanaryfrom
vbv/record-replay-system-for-playground-http-calls

Conversation

@hellovai
Copy link
Copy Markdown
Contributor

@hellovai hellovai commented Apr 7, 2026

Record-Replay System | Artifacts | Task

What problems was I solving

When iterating on BAML functions that make multiple LLM calls (chains, retries, conditional branches), every click of "Run" re-executes all HTTP fetches from scratch. If the user is only changing the prompt for one call in a chain, they still wait for every other call to complete. There's no way to "pin" a previous response and skip the network round-trip.

This PR adds automatic HTTP response recording and user-controlled replay to the BAML playground. After running a function once, users can pin any recorded response so that subsequent runs with the same request key return instantly — eliminating redundant LLM calls during prompt iteration.

What user-facing changes did I ship

  • Automatic recording: Every HTTP response flowing through the runtime is recorded transparently (no user action needed)
  • REPLAYED badge: Fetch log rows that were served from cache show a purple "REPLAYED" badge with near-zero duration
  • Replay Manager dialog: A new toolbar button (rotate icon) opens a dialog where users can browse recorded requests organized by domain/endpoint, inspect request/response bodies, and toggle replay on/off per request
  • Toggle replay: Selecting a recorded response and enabling its switch causes all future matching requests (same method + URL + headers + body) to be served instantly from cache
  • Both runtime paths: Works identically in VS Code (LSP/WebSocket path) and promptfiddle.com (WASM/Web Worker path)

How I implemented it

Core Infrastructure (Rust — sys_ops/src/replay.rs)

New 635-line module with:

  • RecordReplay<K, V> — generic recording/pinning/lookup store with record(), get_pinned(), set_pinned(), and snapshot() methods
  • RequestKey — SHA-256 hash of (method, url, sorted_headers, body) for content-addressable request matching
  • RecordedResponse — stores status, headers, body, and URL
  • ReplayHttp — wraps any dyn IoNamespaceHttp to intercept send() (checks for pinned replay hit) and text() (records response after successful fetch). Uses ReplayResponseBody marker in _body for replay hits and pending_recordings map for send()text() correlation via Arc::as_ptr
  • ReplayFetchEvent + on_replay callback — notifies the UI when a replay hit occurs
  • 10 unit tests covering record/lookup, multiple recordings per key, header-order independence, snapshot, and pin/unpin

LSP Path Wiring (baml_lsp_server/)

  • lib.rsReplayStoreMap type alias for per-project replay stores; build_playground_sys_ops() now takes a replay_store and wraps PlaygroundHttp in ReplayHttp with an on_replay callback that broadcasts FetchLogNew + FetchLogUpdate events
  • playground_http.rsnext_fetch_id changed from owned AtomicU64 to shared Arc<AtomicU64> so ReplayHttp can allocate IDs for replayed calls without collision
  • playground_server.rsWsState gains replay_stores field; new ToggleReplay and RequestReplayState WebSocket message handlers
  • playground_ws.rs — New WsInMessage::ToggleReplay and WsInMessage::RequestReplayState variants; FetchLogNew gains optional replayed field; new WsOutMessage::ReplayState

WASM Path Wiring (bridge_wasm/src/lib.rs)

  • BamlWasmRuntime gains replay_store field
  • Constructor wraps WasmHttp in ReplayHttp with a replay_notify JS callback that posts replay events to the main thread via js_sys::Array
  • New toggleReplay() and replayState() methods exposed to JS via #[wasm_bindgen]

Frontend (TypeScript)

  • worker-protocol.ts — New ReplayGroup, ReplayRecording types; FetchLogEntry gains replayed and pinned fields; new toggleReplay and requestReplayState worker messages
  • baml-lsp-worker.tsreplay_notify callback in WASM callbacks; toggleReplay and requestReplayState message handlers
  • WebSocketRuntimePort.ts — Maps toggleReplay/requestReplayState to/from WebSocket; transforms replayState and fetchLogNew.replayed in both directions
  • ExecutionPanel.tsx — Purple "REPLAYED" badge on fetch log rows (all 3 render sites); Replay Manager button in toolbar; state management for replayEntries
  • ReplayManagerPopover.tsx (new, 397 lines) — Full Replay Manager dialog with domain→endpoint→request tree navigation (left panel) and response inspector with replay toggle (right panel)
  • components/ui/switch.tsx (new) — Radix-based Switch component for replay toggle
  • ApiKeysDialog.tsx — Suppresses password manager autofill on API key inputs (quality-of-life fix)

Deviations from the plan

Implemented as planned

  • Phase 1: Core RecordReplay<K,V> store, RequestKey (SHA-256), RecordedResponse, ReplayResponseBody, ReplayHttp wrapper in sys_ops
  • Phase 1: LSP path wiring with shared Arc<AtomicU64> fetch ID allocator
  • Phase 2: set_pinned()/get_pinned(), replay-hit path in send(), ToggleReplay/RequestReplayState WebSocket protocol
  • Phase 3: REPLAYED badge on fetch log rows with purple #7c3aed styling at all 3 render sites
  • Phase 4: WASM path — ReplayHttp wrapping WasmHttp, toggleReplay/replayState on BamlWasmRuntime, replay_notify callback

Deviations/surprises

  • Plan called for pin icons on each fetch log row; implementation uses a separate Replay Manager dialog instead — provides richer browsing/inspection UX without cluttering the compact fetch log rows
  • RecordedEntry has a recorded_at timestamp (using web-time for WASM compat) — not in plan but needed for the "time ago" display in the Replay Manager
  • on_replay callback pattern for broadcasting replay-hit fetch log events — plan didn't specify how replay hits would appear in the fetch log, this approach keeps ReplayHttp decoupled from any specific transport

Additions not in plan

  • Full Replay Manager dialog with tree navigation (domain → endpoint → request) and request/response body inspector
  • RequestDisplayInfo and snapshot types (ReplayGroupSnapshot, RecordingSnapshot) for the popover data model
  • Switch UI component
  • ApiKeysDialog.tsx password-manager suppression (unrelated fix)

Items planned but not implemented

  • Pin icon on fetch log rows — replaced by the Replay Manager dialog approach
  • Shared FetchLogRow component extraction — plan called for refactoring the 3 identical fetch log render sites into a shared component; this was deferred (REPLAYED badge was added inline at all 3 sites)

How to verify it

Setup

git fetch
git checkout vbv/record-replay-system-for-playground-http-calls

Manual Testing (VS Code / LSP path)

  • Open a BAML project in VS Code with a multi-step function
  • Run the function — observe fetch log rows appear normally
  • Click the rotate (↺) icon in the toolbar to open the Replay Manager
  • Verify recorded requests appear organized by domain/endpoint
  • Select a request, enable the replay toggle, close the dialog
  • Re-run the same function — pinned fetches should show "REPLAYED" badge with ~0ms duration
  • Open Replay Manager, disable the toggle, re-run — fetch should go live again

Manual Testing (promptfiddle.com / WASM path)

  • Same workflow as above but on promptfiddle.com
  • Verify toggleReplay and replayState work through the Web Worker

Automated Tests

cd baml_language
cargo test -p sys_ops replay

Description for the changelog

Add record-replay system to the playground: HTTP responses are automatically recorded during function runs, and users can pin recorded responses via the new Replay Manager dialog for instant replay on subsequent runs — eliminating redundant LLM calls during prompt iteration.

Summary by CodeRabbit

  • New Features

    • HTTP request recording and per-project replay with pin/unpin controls.
    • "Record / Replay" manager UI and dialog from the execution panel to view, pin, and replay recordings.
    • Live replay notifications to the UI when pinned responses are served.
    • Fetch logs now mark replayed requests.
  • Improvements

    • API Keys dialog: added browser autofill suppression attributes for improved privacy.

hellovai added 5 commits April 6, 2026 22:29
…Phase 1)

Add RecordReplay<K,V> generic store, RequestKey, RecordedResponse types,
and ReplayHttp wrapper implementing IoNamespaceHttp + IoClassHttpResponse.
Wire ReplayHttp into the LSP path wrapping PlaygroundHttp with shared
fetch_id allocator. Every HTTP response is now automatically recorded
in a per-project replay store.
… (Phase 2)

Add ToggleReplay WebSocket message for pin/unpin commands. Implement
replay-hit path in ReplayHttp::send() that returns cached responses
instantly without network calls. Wire on_replay callback to emit
FetchLogNew (with replayed flag) and FetchLogUpdate via broadcast.
Add toggleReplay protocol types and wire through WebSocketRuntimePort.
Add pin/unpin button and purple REPLAYED badge to all three fetch log
render sites in ExecutionPanel. Add no-op toggleReplay handler in
WASM worker for exhaustiveness.
…e 4)

Wrap WasmHttp in ReplayHttp with fetch_id starting at 1M to avoid
collision with JS-side IDs. Add replay_notify JS callback for posting
fetch log events on replay hits. Add toggle_replay wasm_bindgen method
to BamlWasmRuntime. Wire toggleReplay in worker.
…spector

Replace inline pin icons with a Record/Replay dialog accessible from the
status bar. The dialog has a left/right split layout:
- Left: domain → endpoint → request navigation tree
- Right: response list with replay toggle, request/response body inspector

Backend changes:
- Add RequestDisplayInfo, snapshot structs, and snapshot() enumeration API
- Store timestamps (via web-time) and request metadata per recording
- Fix fetch_id race condition by pre-allocating IDs in send()
- Add RequestReplayState/ReplayState WS message pair
- Add replayState() wasm_bindgen method

Frontend changes:
- Add ReplayGroup/ReplayRecording types and wire through both transports
- Create ReplayManagerPopover.tsx with 4-level tree view
- Add shadcn Switch component
- Suppress browser password manager on API Keys dialog inputs
- Remove Pin/PinOff icons and onTogglePin from ExecutionPanel
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Apr 8, 2026 0:42am
promptfiddle Ready Ready Preview, Comment Apr 8, 2026 0:42am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Adds HTTP record-and-replay support across the playground: new replay store and HTTP wrapper, LSP server integration with per-project replay maps and WS handlers, WASM bridge callbacks and exports, worker/protocol and UI components for inspecting/pinning replayed responses.

Changes

Cohort / File(s) Summary
Workspace Dependencies
baml_language/Cargo.toml, baml_language/crates/sys_ops/Cargo.toml
Added crates required for replay (e.g., hex, sha2, serde/derive, web-time).
Core Replay Module
baml_language/crates/sys_ops/src/lib.rs, baml_language/crates/sys_ops/src/replay.rs
New replay module: generic RecordReplay<K,V> store, HTTP specialization (RequestKey, RecordedResponse, ReplayResponseBody), ReplayHttp wrapper that serves pinned responses or records new ones, snapshot API, correlation logic, optional on_replay callback, and unit tests.
LSP Server: orchestration
baml_language/crates/baml_lsp_server/src/lib.rs
Introduced ReplayStoreMap alias; run_server now creates per-project RecordReplay instances, inserts them into a shared map, and builds sysops with replay-enabled HTTP and per-sysops fetch-id allocators.
Playground HTTP state
baml_language/crates/baml_lsp_server/src/playground_http.rs
PlaygroundHttpState now accepts external Arc<AtomicU64> fetch-id allocator (new signature changed) and exposes response_to_fetch as pub; initial FetchLogNew emission includes replayed: None.
WS server handlers
baml_language/crates/baml_lsp_server/src/playground_server.rs
Threaded replay_stores: ReplayStoreMap into WS state and router; added ToggleReplay handler to pin/unpin recordings and RequestReplayState to snapshot/send per-session replay state.
WS protocol types
baml_language/crates/baml_lsp_server/src/playground_ws.rs
Added WsInMessage variants ToggleReplay and RequestReplayState; extended WsOutMessage::FetchLogNew with optional replayed: Option<bool> and added ReplayState output variant.
WASM bridge
baml_language/crates/bridge_wasm/src/lib.rs
Added replay_notify JS callback in WasmCallbacks; BamlWasmRuntime holds replay store, exposes toggleReplay(fetch_id, pinned) and replayState() exports, and wires on_replay to call replay_notify.
Worker integration
typescript2/app-promptfiddle/src/playground/baml-lsp-worker.ts
Added replay_notify handler to emit fetchLogNew entries with replayed: true; added worker RPC handling for toggleReplay and requestReplayState, and posting back replayState.
Worker ↔ Main protocol
typescript2/pkg-playground/src/worker-protocol.ts
Extended FetchLogEntry with optional replayed/pinned; added ReplayRecording and ReplayGroup types; added toggleReplay/requestReplayState WorkerInMessage variants and replayState WorkerOutMessage variant.
WebSocket runtime port
typescript2/pkg-playground/src/ports/WebSocketRuntimePort.ts
Mapped new replayed flag in fetchLogNew; added server↔client replayState handling and client→server toggleReplay/requestReplayState mapping.
Replay UI
typescript2/pkg-playground/src/ReplayManagerPopover.tsx, typescript2/pkg-playground/src/ExecutionPanel.tsx
New ReplayManagerDialog component and UI controls: dialog, tree/grouping, response list, pin toggle; ExecutionPanel adds a button to open dialog, handles replayState, shows REPLAYED badges, and sends toggleReplay/requestReplayState messages.
UI components / minor UI changes
typescript2/pkg-playground/src/components/ui/switch.tsx, typescript2/pkg-playground/src/components/ApiKeysDialog.tsx
Added Switch component with sizing prop; added autofill-suppression attributes to ApiKeysDialog inputs.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser Client
    participant Worker as LSP Worker (WASM)
    participant Server as LSP Server
    participant Store as ReplayStore
    participant HTTP as ReplayHttp

    Note over Client,HTTP: Recording Phase
    Client->>Worker: perform HTTP request
    Worker->>Server: sys_ops request
    Server->>HTTP: send(request)
    HTTP->>HTTP: compute RequestKey
    HTTP->>Store: check pinned (miss)
    HTTP->>HTTP: allocate fetch_id
    HTTP->>HTTP: delegate to inner HTTP
    HTTP-->>Server: response placeholder
    Server->>HTTP: text() -> actual body
    HTTP->>Store: record(fetch_id, key, response metadata)
    Server-->>Worker: response logged
    Worker-->>Client: emit FetchLogNew

    Note over Client,HTTP: Pin & Replay Phase
    Client->>Worker: toggleReplay(fetchId, pinned=true)
    Worker->>Server: WsInMessage::ToggleReplay
    Server->>Store: set_pinned(fetch_id, true)
    Client->>Worker: send same HTTP request
    Worker->>Server: sys_ops request
    Server->>HTTP: send(same_request)
    HTTP->>HTTP: compute RequestKey
    HTTP->>Store: check pinned (hit)
    Store-->>HTTP: return RecordedResponse
    HTTP->>HTTP: on_replay callback (emit event)
    HTTP-->>Server: synthetic response (replay)
    Server-->>Worker: response logged with replayed=true
    Worker-->>Client: emit FetchLogNew(replayed: true)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding a record-replay system for HTTP calls in the playground, which matches the primary focus of this comprehensive changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vbv/record-replay-system-for-playground-http-calls

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 7, 2026

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 15 untouched benchmarks
⏩ 105 skipped benchmarks1


Comparing vbv/record-replay-system-for-playground-http-calls (01fc300) with canary (3aa278c)

Open in CodSpeed

Footnotes

  1. 105 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
typescript2/pkg-playground/src/components/ApiKeysDialog.tsx (1)

146-152: 🧹 Nitpick | 🔵 Trivial

Consider adding autofill suppression to import Textarea.

While password managers rarely interact with textareas, the .env import field can contain sensitive API keys. Adding autoComplete="off" would provide consistency with the other inputs and an extra layer of protection.

🔒 Optional enhancement
 <Textarea
   value={importText}
   onChange={(e) => setImportText(e.target.value)}
   placeholder={"Paste .env contents here...\nKEY=value\nANOTHER_KEY=value"}
   rows={5}
   className="text-[11px] font-vsc-mono resize-none"
+  autoComplete="off"
 />

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6c17e53-11bf-4f66-9ffc-ad2edcd821f6

📥 Commits

Reviewing files that changed from the base of the PR and between 411f3ad and 7b40007.

⛔ Files ignored due to path filters (1)
  • baml_language/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • baml_language/Cargo.toml
  • baml_language/crates/baml_lsp_server/src/lib.rs
  • baml_language/crates/baml_lsp_server/src/playground_http.rs
  • baml_language/crates/baml_lsp_server/src/playground_server.rs
  • baml_language/crates/baml_lsp_server/src/playground_ws.rs
  • baml_language/crates/bridge_wasm/src/lib.rs
  • baml_language/crates/sys_ops/Cargo.toml
  • baml_language/crates/sys_ops/src/lib.rs
  • baml_language/crates/sys_ops/src/replay.rs
  • typescript2/app-promptfiddle/src/playground/baml-lsp-worker.ts
  • typescript2/pkg-playground/src/ExecutionPanel.tsx
  • typescript2/pkg-playground/src/ReplayManagerPopover.tsx
  • typescript2/pkg-playground/src/components/ApiKeysDialog.tsx
  • typescript2/pkg-playground/src/components/ui/switch.tsx
  • typescript2/pkg-playground/src/ports/WebSocketRuntimePort.ts
  • typescript2/pkg-playground/src/worker-protocol.ts

Comment on lines 22 to 27
pub struct PlaygroundHttpState {
broadcast_tx: broadcast::Sender<WsOutMessage>,
next_fetch_id: AtomicU64,
next_fetch_id: Arc<AtomicU64>,
/// Maps response body pointer → (call_id, fetch_id) for response_text tracking.
response_to_fetch: std::sync::Mutex<HashMap<usize, (u64, u64)>>,
pub response_to_fetch: std::sync::Mutex<HashMap<usize, (u64, u64)>>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Keep response_to_fetch out of the public API.

Exposing a mutable Mutex<HashMap<...>> field makes this fetch/body correlation invariant externally mutable. If the replay wiring only needs it inside baml_lsp_server, pub(crate) or a small accessor keeps the API surface tighter.

🔒 Suggested change
 pub struct PlaygroundHttpState {
     broadcast_tx: broadcast::Sender<WsOutMessage>,
     next_fetch_id: Arc<AtomicU64>,
     /// Maps response body pointer → (call_id, fetch_id) for response_text tracking.
-    pub response_to_fetch: std::sync::Mutex<HashMap<usize, (u64, u64)>>,
+    pub(crate) response_to_fetch: std::sync::Mutex<HashMap<usize, (u64, u64)>>,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub struct PlaygroundHttpState {
broadcast_tx: broadcast::Sender<WsOutMessage>,
next_fetch_id: AtomicU64,
next_fetch_id: Arc<AtomicU64>,
/// Maps response body pointer → (call_id, fetch_id) for response_text tracking.
response_to_fetch: std::sync::Mutex<HashMap<usize, (u64, u64)>>,
pub response_to_fetch: std::sync::Mutex<HashMap<usize, (u64, u64)>>,
}
pub struct PlaygroundHttpState {
broadcast_tx: broadcast::Sender<WsOutMessage>,
next_fetch_id: Arc<AtomicU64>,
/// Maps response body pointer → (call_id, fetch_id) for response_text tracking.
pub(crate) response_to_fetch: std::sync::Mutex<HashMap<usize, (u64, u64)>>,
}

Comment on lines +429 to +439
WsInMessage::ToggleReplay {
project,
fetch_id,
pinned,
} => {
let stores = state.replay_stores.lock().unwrap();
if let Some(store) = stores.get(&project) {
store.write().unwrap().set_pinned(fetch_id, pinned);
} else {
tracing::warn!("ToggleReplay: no replay store for project {project}");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle unknown fetch_ids explicitly.

set_pinned() already tells you when the requested recording is missing. Ignoring that return value turns stale replay-manager state into a silent no-op.

💡 Suggested change
         WsInMessage::ToggleReplay {
             project,
             fetch_id,
             pinned,
         } => {
             let stores = state.replay_stores.lock().unwrap();
             if let Some(store) = stores.get(&project) {
-                store.write().unwrap().set_pinned(fetch_id, pinned);
+                if !store.write().unwrap().set_pinned(fetch_id, pinned) {
+                    tracing::warn!("ToggleReplay: unknown fetch_id {fetch_id} for project {project}");
+                }
             } else {
                 tracing::warn!("ToggleReplay: no replay store for project {project}");
             }
         }

Comment on lines +164 to +171
replay_store: std::sync::Arc<
std::sync::RwLock<
sys_ops::replay::RecordReplay<
sys_ops::replay::RequestKey,
sys_ops::replay::RecordedResponse,
>,
>,
>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scope replay state per project in the WASM runtime.

This stores one replay_store on BamlWasmRuntime and exposes toggleReplay() / replayState() without a project. If the runtime ever has multiple playground projects loaded, recordings and pin toggles will bleed across projects even though the main-thread protocol already sends project.

Also applies to: 232-237, 464-487

Comment thread baml_language/crates/sys_ops/src/replay.rs Outdated
Comment thread baml_language/crates/sys_ops/src/replay.rs
Comment thread typescript2/pkg-playground/src/ExecutionPanel.tsx
Comment on lines +29 to 30
| { type: 'replayState'; entries: unknown[] };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Decode replay state in one shared helper.

This snake_case → camelCase mapper is duplicated here and in typescript2/app-promptfiddle/src/playground/baml-lsp-worker.ts on Lines 679-697. A shared raw wire type plus normalizer in pkg-playground would remove the unknown[]/any casts and keep the WebSocket and WASM paths from drifting.

Also applies to: 322-340

Comment thread typescript2/pkg-playground/src/ReplayManagerPopover.tsx
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 7, 2026

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 5.7 MB 5.7 MB +58.1 KB (+1.0%) OK
bridge_cffi-stripped Linux 5.7 MB 5.7 MB +36.1 KB (+0.6%) OK
bridge_cffi macOS 4.7 MB 4.6 MB +96.3 KB (+2.1%) OK
bridge_cffi-stripped macOS 4.7 MB 4.7 MB +38.5 KB (+0.8%) OK
bridge_cffi Windows 4.7 MB 4.6 MB +83.3 KB (+1.8%) OK
bridge_cffi-stripped Windows 4.7 MB 4.7 MB +36.5 KB (+0.8%) OK
bridge_wasm WASM 3.1 MB 3.0 MB +95.5 KB (+3.2%) OK

Generated by cargo size-gate · workflow run

…esponsive dialog

- Record SysOpOutput::Ready(Ok) responses in ReplayHttp::text() (sync backends)
- Send full request/response bodies in snapshot instead of 1000-char previews
- Add aria-label to Replay Manager icon button for accessibility
- Make Replay Manager dialog width responsive for narrow viewports
- Add TODO comment for per-project WASM replay store scoping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
baml_language/crates/bridge_wasm/src/lib.rs (1)

164-176: ⚠️ Potential issue | 🟠 Major

Keep replay storage project-scoped in the WASM runtime.

BamlWasmRuntime still owns one shared replay_store, and toggleReplay() / replayState() never take a project key. If a single runtime holds more than one project, recordings and pins will bleed across projects even though the worker protocol is already project-scoped.

Also applies to: 205-207, 469-491


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2b528822-61fd-4d0f-8e18-6b4f3b31576d

📥 Commits

Reviewing files that changed from the base of the PR and between 7b40007 and 01fc300.

📒 Files selected for processing (7)
  • baml_language/crates/bridge_wasm/src/lib.rs
  • baml_language/crates/sys_ops/src/replay.rs
  • typescript2/app-promptfiddle/src/playground/baml-lsp-worker.ts
  • typescript2/pkg-playground/src/ExecutionPanel.tsx
  • typescript2/pkg-playground/src/ReplayManagerPopover.tsx
  • typescript2/pkg-playground/src/ports/WebSocketRuntimePort.ts
  • typescript2/pkg-playground/src/worker-protocol.ts

Comment on lines +176 to +183
let mut sorted_headers: Vec<_> = request.headers.iter().collect();
sorted_headers.sort_by_key(|(k, _)| k.as_str());
for (k, v) in &sorted_headers {
hasher.update(k.as_bytes());
hasher.update(b":");
hasher.update(v.as_bytes());
hasher.update(b"\0");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize header names before hashing.

HTTP header names are case-insensitive. Hashing Authorization and authorization differently means the same request can miss an existing replay pin after a harmless casing change.

💡 Suggested fix
-        let mut sorted_headers: Vec<_> = request.headers.iter().collect();
-        sorted_headers.sort_by_key(|(k, _)| k.as_str());
-        for (k, v) in &sorted_headers {
-            hasher.update(k.as_bytes());
+        let mut sorted_headers: Vec<_> = request
+            .headers
+            .iter()
+            .map(|(k, v)| (k.to_ascii_lowercase(), v))
+            .collect();
+        sorted_headers.sort_by(|(a, _), (b, _)| a.cmp(b));
+        for (k, v) in &sorted_headers {
+            hasher.update(k.as_bytes());
             hasher.update(b":");
             hasher.update(v.as_bytes());
             hasher.update(b"\0");
         }

Comment on lines +421 to +649
#[cfg(test)]
mod tests {
use super::*;

fn dummy_display() -> RequestDisplayInfo {
RequestDisplayInfo {
method: "GET".to_string(),
url: "https://example.com".to_string(),
body: String::new(),
}
}

#[test]
fn test_record_and_lookup() {
let mut store = RecordReplay::new();
let key = RequestKey([0u8; 32]);
store.record(key.clone(), 1, "response body".to_string(), dummy_display());

assert!(store.get_pinned(&key).is_none());

assert!(store.set_pinned(1, true));
let pinned = store.get_pinned(&key).unwrap();
assert_eq!(pinned.value, "response body");
assert_eq!(pinned.fetch_id, 1);

assert!(store.set_pinned(1, false));
assert!(store.get_pinned(&key).is_none());
}

#[test]
fn test_multiple_recordings_per_key() {
let mut store = RecordReplay::new();
let key = RequestKey([0u8; 32]);
store.record(key.clone(), 1, "first".to_string(), dummy_display());
store.record(key.clone(), 2, "second".to_string(), dummy_display());

assert!(store.set_pinned(2, true));
let pinned = store.get_pinned(&key).unwrap();
assert_eq!(pinned.value, "second");

assert!(store.set_pinned(1, true));
let pinned = store.get_pinned(&key).unwrap();
assert_eq!(pinned.value, "first");
}

#[test]
fn test_request_key_deterministic() {
let req = owned::http::Request {
method: "POST".to_string(),
url: "https://api.openai.com/v1/chat".to_string(),
headers: indexmap::IndexMap::from([
("content-type".to_string(), "application/json".to_string()),
("authorization".to_string(), "Bearer sk-xxx".to_string()),
]),
body: r#"{"model":"gpt-4","messages":[]}"#.to_string(),
};
let key1 = RequestKey::from_request(&req);
let key2 = RequestKey::from_request(&req);
assert_eq!(key1, key2);
}

#[test]
fn test_request_key_header_order_independent() {
let req1 = owned::http::Request {
method: "POST".to_string(),
url: "https://api.example.com".to_string(),
headers: indexmap::IndexMap::from([
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string()),
]),
body: "body".to_string(),
};
let req2 = owned::http::Request {
method: "POST".to_string(),
url: "https://api.example.com".to_string(),
headers: indexmap::IndexMap::from([
("b".to_string(), "2".to_string()),
("a".to_string(), "1".to_string()),
]),
body: "body".to_string(),
};
assert_eq!(
RequestKey::from_request(&req1),
RequestKey::from_request(&req2)
);
}

#[test]
fn test_request_key_different_body() {
let req1 = owned::http::Request {
method: "POST".to_string(),
url: "https://api.example.com".to_string(),
headers: indexmap::IndexMap::new(),
body: "body1".to_string(),
};
let req2 = owned::http::Request {
method: "POST".to_string(),
url: "https://api.example.com".to_string(),
headers: indexmap::IndexMap::new(),
body: "body2".to_string(),
};
assert_ne!(
RequestKey::from_request(&req1),
RequestKey::from_request(&req2)
);
}

#[test]
fn test_set_pinned_unknown_fetch_id() {
let mut store: RecordReplay<RequestKey, String> = RecordReplay::new();
assert!(!store.set_pinned(999, true));
}

// ---------------------------------------------------------------------------
// snapshot() tests
// ---------------------------------------------------------------------------

fn make_key(s: &str) -> RequestKey {
let mut hasher = sha2::Sha256::new();
hasher.update(s.as_bytes());
RequestKey(hasher.finalize().into())
}

fn make_display(method: &str, url: &str) -> RequestDisplayInfo {
RequestDisplayInfo {
method: method.to_string(),
url: url.to_string(),
body: String::new(),
}
}

fn make_response(status: i64, body: &str) -> RecordedResponse {
RecordedResponse {
status,
headers: indexmap::IndexMap::new(),
body: body.to_string(),
url: "https://example.com".to_string(),
}
}

#[test]
fn snapshot_returns_all_groups_with_display_info() {
let mut store = RecordReplay::new();
let key_a = make_key("request-a");
let key_b = make_key("request-b");

store.record(
key_a.clone(),
1,
make_response(200, "resp1"),
make_display("POST", "https://a.com"),
);
store.record(
key_a,
2,
make_response(200, "resp2"),
make_display("POST", "https://a.com"),
);
store.record(
key_b,
3,
make_response(404, "not found"),
make_display("GET", "https://b.com"),
);

let snap = store.snapshot();
assert_eq!(snap.len(), 2);

let group_a = snap
.iter()
.find(|g| g.display.url == "https://a.com")
.unwrap();
assert_eq!(group_a.display.method, "POST");
assert_eq!(group_a.recordings.len(), 2);
assert_eq!(group_a.pinned_fetch_id, None);

let group_b = snap
.iter()
.find(|g| g.display.url == "https://b.com")
.unwrap();
assert_eq!(group_b.recordings.len(), 1);
assert_eq!(group_b.recordings[0].status, 404);
}

#[test]
fn snapshot_shows_pinned_fetch_id() {
let mut store = RecordReplay::new();
let key = make_key("req");
store.record(
key.clone(),
10,
make_response(200, "ok"),
make_display("GET", "https://x.com"),
);
store.record(
key,
11,
make_response(200, "ok2"),
make_display("GET", "https://x.com"),
);
store.set_pinned(11, true);

let snap = store.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].pinned_fetch_id, Some(11));
}

#[test]
fn display_info_stored_only_on_first_record() {
let mut store = RecordReplay::new();
let key = make_key("req");
store.record(
key.clone(),
1,
make_response(200, "a"),
make_display("POST", "https://first.com"),
);
store.record(
key,
2,
make_response(200, "b"),
make_display("POST", "https://second.com"),
);

let snap = store.snapshot();
// Display info should be from the first recording, not overwritten.
assert_eq!(snap[0].display.url, "https://first.com");
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add unit coverage for ReplayHttp's wrapper branches.

These tests stop at RecordReplay / RequestKey. The highest-risk logic is in ReplayHttp::send and ReplayHttp::text—especially the SysOpOutput::Ready(Ok(_)) recording path, replay-hit synthetic bodies, and pending correlation—and those branches are still untested.

As per coding guidelines, "Prefer writing Rust unit tests over integration tests where possible" and "Always run cargo test --lib if you changed any Rust code."

Comment on lines +216 to +233
const [selectedRecFetchId, setSelectedRecFetchId] = useState<number | null>(null);
const [activeTab, setActiveTab] = useState<"request" | "response">("request");
const isEnabled = group.pinnedFetchId != null;

const effectiveSelectedId = selectedRecFetchId
?? group.pinnedFetchId
?? group.recordings[0]?.fetchId
?? null;

const selectedRec = group.recordings.find((r) => r.fetchId === effectiveSelectedId) ?? null;

const handleToggle = (checked: boolean) => {
if (!checked) {
if (group.pinnedFetchId != null) onToggleReplay(group.pinnedFetchId, false);
} else if (effectiveSelectedId != null) {
onToggleReplay(effectiveSelectedId, true);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remount RightPanel when the selected request changes.

RightPanel keeps selectedRecFetchId and activeTab in local state, but the component is reused across different groups. After selecting a recording in group A and then switching to group B, effectiveSelectedId can still point at A's fetchId, so enabling replay in B can pin A's response.

💡 Suggested fix
-          {selectedGroup ? (
-            <RightPanel group={selectedGroup} onToggleReplay={onToggleReplay} />
+          {selectedGroup ? (
+            <RightPanel key={selectedGroup.key} group={selectedGroup} onToggleReplay={onToggleReplay} />
           ) : (

Also applies to: 386-387

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant