Skip to content

Fix recording pipeline and deterministic replay on browser-use 0.13 / Chrome 137+ - #166

Open
Sangaibisi wants to merge 8 commits into
browser-use:mainfrom
Sangaibisi:fix/recording-and-replay-browser-use-0.13
Open

Fix recording pipeline and deterministic replay on browser-use 0.13 / Chrome 137+#166
Sangaibisi wants to merge 8 commits into
browser-use:mainfrom
Sangaibisi:fix/recording-and-replay-browser-use-0.13

Conversation

@Sangaibisi

@Sangaibisi Sangaibisi commented Aug 10, 2026

Copy link
Copy Markdown

Fixes #165

On a fresh clone with the pinned browser-use 0.13.4, recording captures zero events and deterministic replay fails on several step types. This PR restores the full record → build → replay (no-AI) pipeline; each commit is self-contained:

1. fix(recorder) — recorder extension never loads

  • browser-use's default extensions append a second --load-extension flag which overrides ours (Chrome honors the last one). Disable default extensions for the recording profile.
  • Branded Google Chrome 137+ removed --load-extension support altogether. Prefer a Playwright-bundled Chromium / Chrome for Testing binary when present (macOS/Linux/Windows cache paths), overridable via WORKFLOW_USE_RECORDER_BROWSER; falls back to browser-use's default with a warning.

2. fix(schema) — validator 422s every incremental recording update

validate_ends_with_extract rejected essentially every WORKFLOW_UPDATE streamed by the extension mid-recording, so the recording server never stored any data and create-workflow hung after the browser closed. The validator is relaxed; no-AI workflows are also valid without a trailing extract step.

3. fix(workflow) — deterministic replay on browser-use 0.13

  • Stop stripping cssSelector/xpath from step params: the click/input/key_press/select_change action models require cssSelector (extras are ignored via extra='ignore'), so every selector-based deterministic action failed validation.
  • Named-key crash: dict.get's default is evaluated eagerly, so ord('ENTER') raised even though 'Enter' is in the key map.
  • Replace the hand-rolled Input.dispatchKeyEvent (stale CDP session → -32601 method not found) with page.press().
  • Key-press verification treated navigation as failure (element gone → retry pressing Enter repeatedly). A URL change now counts as success.
  • Retry semantic-mapping extraction briefly when it comes back empty (page mid-navigation) instead of failing the step.

4. fix(cli,backend) — no-AI paths shouldn't require BROWSER_USE_API_KEY

  • cli.py: NameError: llm_instance when LLM init failed; now degrades gracefully.
  • backend: from browser_use.browser.browser import Browser no longer exists in 0.13; and the eager ChatBrowserUse instantiation made every endpoint 500 without an API key. LLM is now optional.

Verified

  • Recording: extension service worker present on the CDP debug port; step events received by the server; create-workflow-no-ai produced a valid semantic workflow from a real user recording.
  • Replay: run executes navigation/input/key_press/click/scroll steps of the recorded workflow deterministically (no API key), including Enter-triggered navigation.

🤖 Generated with Claude Code


Summary by cubic

Restores the full record → build → deterministic replay pipeline on browser-use 0.13, adds a GUI “Record New Workflow” button backed by new recording APIs, and hardens task/recording state so status/cancel work across requests and concurrent runs. No‑AI workflows now run without BROWSER_USE_API_KEY.

  • New Features

    • GUI recording: sidebar button starts/stops recording, polls /recordings/status, and selects the saved file when done.
    • Backend recording: /recordings/start|stop|status run RecordingService, convert to semantic, and save into ./tmp; zero‑step stops cancel cleanly.
  • Bug Fixes

    • Recorder: disable default extensions so our --load-extension wins; prefer Playwright-bundled Chromium/Chrome for Testing (newest revision) on Chrome 137+; override via WORKFLOW_USE_RECORDER_BROWSER.
    • Backend: share one WorkflowService to preserve in‑memory task/recording state; create Browser/WorkflowController per execution; prune finished tasks; record load errors in task status.
    • Schema: relax “must end with extract” so incremental recording updates are accepted.
    • Workflow: keep cssSelector/xpath; use page.press() for keys; treat URL change as success; retry semantic mapping during navigation.
    • CLI/Backend: optional LLM; run-workflow guards on missing LLM; update imports for browser-use 0.13.
    • UI: use GET /api/workflows/metadata so the sidebar shows real names; keep metadata cache in sync after saves; record button stop/polling logic is more robust.

Written for commit 8defbb4. Summary will update on new commits.

Review in cubic

Sangaibisi and others added 5 commits August 11, 2026 00:46
…-use 0.13

Two independent bugs made recording capture zero events:

1. browser-use's default extensions append their own --load-extension
   flag after the profile args. Chrome only honors the last occurrence,
   so the recorder extension was silently dropped. Disable default
   extensions for the recording profile so our flag wins.

2. Branded Google Chrome 137+ removed support for --load-extension
   entirely. When browser-use picks the installed Chrome, the extension
   never loads and no events reach the recording server. Prefer a
   Playwright-bundled Chromium / Chrome for Testing binary when one is
   available, overridable via WORKFLOW_USE_RECORDER_BROWSER.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WorkflowDefinitionSchema required the last step to be extract /
extract_page_content. The extension streams WORKFLOW_UPDATE events
step-by-step while the user records, so virtually every update ended in
click/input/navigation and was rejected with 422 by the recording
server — the recorder never received any workflow data and
create-workflow hung forever after the browser closed.

Relax the validator: an extract-terminated workflow cannot be enforced
at parse time without breaking recording, and no-AI workflows are valid
without a trailing extract step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _run_deterministic_step stripped cssSelector/xpath from step params,
  but the click/input/key_press/select_change action models require
  cssSelector — every selector-based deterministic action failed
  validation. Keep those fields (extras are ignored via extra='ignore').

- Semantic key_press crashed on named keys: dict.get's default is
  evaluated eagerly, so ord('ENTER') raised before the lookup.

- The hand-rolled Input.dispatchKeyEvent call used a stale CDP session
  and failed with 'method not found'. Use page.press(), which handles
  named keys, combos and session setup.

- The key_press verifier required the pressed element to still be
  visible, so keys that navigate (Enter on a search box) always failed
  verification and were re-pressed. Treat a URL change as success.

- An empty semantic mapping usually means the page is mid-navigation;
  retry extraction a few times before failing the step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- cli.py crashed with NameError when LLM init failed and the user
  declined to enter an API key: llm_instance was left undefined.
  Degrade gracefully — no-AI commands don't need an LLM.

- backend imported Browser from the pre-0.13 module path
  (browser_use.browser.browser) and crashed on startup; and it
  instantiated ChatBrowserUse eagerly, so the API served 500s without
  BROWSER_USE_API_KEY even for listing/executing deterministic
  workflows. Make the LLM optional.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sidebar items only received metadata for the currently selected
workflow, so every other row rendered a permanent 'Loading workflow…'
placeholder until clicked. Fetch metadata for all listed workflows and
pass it through the existing allWorkflowsMetadata prop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Sangaibisi

Copy link
Copy Markdown
Author

Pushed one more commit: the GUI sidebar rendered a permanent "Loading workflow…" placeholder for every non-selected workflow (metadata was only fetched for the selected one, and the existing allWorkflowsMetadata prop was never populated). The sidebar now fetches metadata for all listed workflows and shows their real names.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="ui/src/components/workflow-layout.tsx">

<violation number="1" location="ui/src/components/workflow-layout.tsx:174">
P2: This effect fetches the *entire* workflow document (including every step) for all workflows — N parallel GETs on mount and again whenever `workflows` changes — but the Sidebar/WorkflowItem only use the result to render each non-selected row's name and version (the details/edit panel always reads the selected workflow's separate `workflowMetadata`). Fetching a full document per workflow just to display a name is wasteful and can fan out a large number of requests for repos with many workflows. Consider using a lighter metadata/list endpoint (or deriving names from the existing /api/workflows response) instead of downloading each full workflow, and it's worth logging failures in the `catch` so a failed fetch doesn't silently leave the row stuck on the 'Loading workflow…' placeholder.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread ui/src/components/workflow-layout.tsx
Comment thread ui/src/components/workflow-layout.tsx Outdated
const entries = await Promise.all(
workflows.map(async (name) => {
try {
const { data } = await fetchClient.GET("/api/workflows/{name}", {

@cubic-dev-ai cubic-dev-ai Bot Aug 10, 2026

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.

P2: This effect fetches the entire workflow document (including every step) for all workflows — N parallel GETs on mount and again whenever workflows changes — but the Sidebar/WorkflowItem only use the result to render each non-selected row's name and version (the details/edit panel always reads the selected workflow's separate workflowMetadata). Fetching a full document per workflow just to display a name is wasteful and can fan out a large number of requests for repos with many workflows. Consider using a lighter metadata/list endpoint (or deriving names from the existing /api/workflows response) instead of downloading each full workflow, and it's worth logging failures in the catch so a failed fetch doesn't silently leave the row stuck on the 'Loading workflow…' placeholder.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ui/src/components/workflow-layout.tsx, line 174:

<comment>This effect fetches the *entire* workflow document (including every step) for all workflows — N parallel GETs on mount and again whenever `workflows` changes — but the Sidebar/WorkflowItem only use the result to render each non-selected row's name and version (the details/edit panel always reads the selected workflow's separate `workflowMetadata`). Fetching a full document per workflow just to display a name is wasteful and can fan out a large number of requests for repos with many workflows. Consider using a lighter metadata/list endpoint (or deriving names from the existing /api/workflows response) instead of downloading each full workflow, and it's worth logging failures in the `catch` so a failed fetch doesn't silently leave the row stuck on the 'Loading workflow…' placeholder.</comment>

<file context>
@@ -159,6 +162,37 @@ const WorkflowLayout: React.FC = () => {
+      const entries = await Promise.all(
+        workflows.map(async (name) => {
+          try {
+            const { data } = await fetchClient.GET("/api/workflows/{name}", {
+              params: { path: { name } },
+            });
</file context>
Fix with cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread workflows/cli.py
Comment thread workflows/workflow_use/recorder/service.py Outdated
Sangaibisi and others added 2 commits August 11, 2026 01:01
get_service() constructed a fresh WorkflowService per request, so
in-memory task state (active_tasks, cancel_events) was lost immediately:
/tasks/{id}/status always 404'd and cancel never worked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a 'Record New Workflow' button to the sidebar. The backend gains
recording endpoints (/recordings/start|stop|status) that drive
RecordingService in-process, convert the captured recording via
convert_recorded_workflow_to_semantic, and save it into ./tmp so it
appears in the list immediately.

Stopping with zero captured steps cancels the session outright instead
of hitting the recorder's wait-forever finalizer path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Sangaibisi

Copy link
Copy Markdown
Author

Two more commits:

  • fix(backend): share one WorkflowService instance across requestsget_service() built a fresh WorkflowService per request, so in-memory task state was lost immediately and /tasks/{id}/status always returned 404 (cancel was equally broken).
  • feat(gui): record new workflows from the GUI — a Record New Workflow button in the sidebar drives new /api/workflows/recordings/start|stop|status endpoints; the captured recording is converted with convert_recorded_workflow_to_semantic and saved into ./tmp, appearing in the list immediately. Stopping with zero captured steps cancels the session instead of hanging in the recorder's wait-forever finalizer.

Happy to split the GUI feature into its own PR if you'd prefer to keep this one fixes-only.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="workflows/backend/routers.py">

<violation number="1" location="workflows/backend/routers.py:30">
P1: Concurrent `/execute` requests can run another task’s workflow and close each other’s browser. Keep task/recording state shared, but create workflow/browser execution state per task (or serialize execution).</violation>

<violation number="2" location="workflows/backend/routers.py:30">
P2: Completed task results now accumulate for the backend process lifetime, so repeated executions cause unbounded memory growth. Expire or cap completed `active_tasks` entries after a status-retention window.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

return WorkflowService()
global _service
if _service is None:
_service = WorkflowService()

@cubic-dev-ai cubic-dev-ai Bot Aug 10, 2026

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.

P1: Concurrent /execute requests can run another task’s workflow and close each other’s browser. Keep task/recording state shared, but create workflow/browser execution state per task (or serialize execution).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At workflows/backend/routers.py, line 30:

<comment>Concurrent `/execute` requests can run another task’s workflow and close each other’s browser. Keep task/recording state shared, but create workflow/browser execution state per task (or serialize execution).</comment>

<file context>
@@ -18,9 +19,16 @@
-	return WorkflowService()
+	global _service
+	if _service is None:
+		_service = WorkflowService()
+	return _service
 
</file context>
Fix with cubic

return WorkflowService()
global _service
if _service is None:
_service = WorkflowService()

@cubic-dev-ai cubic-dev-ai Bot Aug 10, 2026

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.

P2: Completed task results now accumulate for the backend process lifetime, so repeated executions cause unbounded memory growth. Expire or cap completed active_tasks entries after a status-retention window.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At workflows/backend/routers.py, line 30:

<comment>Completed task results now accumulate for the backend process lifetime, so repeated executions cause unbounded memory growth. Expire or cap completed `active_tasks` entries after a status-retention window.</comment>

<file context>
@@ -18,9 +19,16 @@
-	return WorkflowService()
+	global _service
+	if _service is None:
+		_service = WorkflowService()
+	return _service
 
</file context>
Fix with cubic

Comment thread workflows/backend/service.py Outdated
Comment thread ui/src/components/record-button.tsx Outdated
- backend: create Browser/WorkflowController per execution instead of
  sharing one instance, so concurrent /execute runs can't close each
  other's browser (cubic P1).
- backend: prune finished tasks beyond MAX_FINISHED_TASKS so a long-lived
  process doesn't grow active_tasks unboundedly (cubic P2).
- backend: guard RecordingService.browser with getattr — stopping right
  after start no longer 500s before the browser is assigned (cubic P2).
- backend: load-error path now records failed status + error instead of
  only printing and returning.
- recorder: sort Playwright cache dirs by numeric revision, not
  lexicographically (chromium-1017 must beat chromium-999) (cubic P3).
- cli: run-workflow now guards on missing LLM and exits cleanly instead
  of passing llm=None into the agent path (cubic P2).
- ui: add GET /api/workflows/metadata (one request, name/description/
  version/input_schema only) and use it for the sidebar instead of
  fetching every full workflow document (cubic P2).
- ui: update the sidebar metadata cache after a metadata save so renamed
  labels don't revert when another workflow is selected (cubic P2).
- ui: record-button stop() decides polling from the response, not the
  stale render-closure status (cubic P3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Sangaibisi

Copy link
Copy Markdown
Author

Thanks for the review @cubic-dev-ai — addressed all of it in 8defbb4:

  • P1 concurrent /execute closing each other's browserBrowser/WorkflowController are now created per execution instead of shared.
  • P2 unbounded active_tasks growth → finished tasks pruned beyond MAX_FINISHED_TASKS (100).
  • P2 stop-right-after-start 500RecordingService.browser accessed via getattr.
  • P2 run-workflow with llm=None → guards on missing LLM and exits cleanly.
  • P2 sidebar fetching full documents → new GET /api/workflows/metadata returns only name/description/version/input_schema in a single request; sidebar uses that.
  • P2 renamed labels revert → sidebar metadata cache is updated on save.
  • P3 lexicographic Playwright revision sort → now compares the revision numerically (chromium-1017 > chromium-999).
  • P3 record-button stale-closure polling → decides from the /stop response.

Also moved off the shared browser_instance/controller_instance fields entirely.

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Thanks for the review @cubic-dev-ai — addressed all of it in 8defbb4:

  • P1 concurrent /execute closing each other's browserBrowser/WorkflowController are now created per execution instead of shared.
  • P2 unbounded active_tasks growth → finished tasks pruned beyond MAX_FINISHED_TASKS (100).
  • P2 stop-right-after-start 500RecordingService.browser accessed via getattr.
    ...

@Sangaibisi All eight items check out in the code — this looks fully addressed.

Quick pass through each:

  • P1run_workflow_in_background now builds Workflow.load_from_file(..., browser=Browser(), controller=WorkflowController()) locally per execution instead of reusing module-level instances, and close_browser_at_end=True still closes that per-run browser. Concurrent runs won't step on each other.
  • P2 prune_prune_finished_tasks() runs at the top of each launch and drops finished/pending entries beyond MAX_FINISHED_TASKS, oldest first. It's called before the new task is marked running, so the executing task isn't pruned mid-flight. Good.
  • P2 getattrgetattr(self.recording_service, 'browser', None) guards the pre-launch window. Also nice that you swallow errors on browser.stop() so a halt mid-launch degrades cleanly.
  • P2 llm=Nonerun_workflow_command exits with a clear message instead of threading llm=None through the agent path. The module-level guard on llm_instance is None is the right place.
  • P2 metadata endpoint/metadata returns only name/description/version/input_schema in one request, and this comment is worth keeping: it's registered before GET /{name}, otherwise 'metadata' would be captured as a workflow name.
  • P2 label revert — the sidebar cache is merged on save, so a rename no longer flips back.
  • P3 numeric sort_revision() parses the numeric suffix, so chromium-1017 beats chromium-999. Lexicographic would've silently picked the wrong binary.
  • P3 stale closurestop() now derives polling from the /stop response (next.status === "saving"), not the render-closure status.

One deliberate note, not a blocker: the per-execution Browser() means every run spins up its own browser (necessary for isolation), so startup latency per task goes up slightly — but that's the correct trade given the original race.

No outstanding concerns from my side. Good cleanup.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

2 issues found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="workflows/backend/service.py">

<violation number="1" location="workflows/backend/service.py:70">
P2: Cancelling tasks can be pruned before their cancellation handler finishes when another execution starts, losing status and raising `KeyError`; keep `cancelling` entries until they reach a terminal state.</violation>
</file>

<file name="ui/src/components/workflow-layout.tsx">

<violation number="1" location="ui/src/components/workflow-layout.tsx:189">
P2: This replaced the typed OpenAPI client call (previously `fetchClient.GET(...)`) with a raw `fetch` to a hardcoded `http://localhost:8000`, duplicating the base URL that already lives in `ui/src/lib/api/index.ts` and bypassing the `$api`/typed client used everywhere else in this component. Because the response is cast blindly (`as Array<{ file: string } & WorkflowMetadata>`), `data.workflows.map(...)` will throw if the response shape changes or an error body is returned. Since the new backend endpoints aren't in the checked-in `openapi.json`/`apigen`, they were forced to bypass the typed client. Consider regenerating the OpenAPI client (`npm run type-gen-update`) so `/api/workflows/metadata` is represented, then call it through `fetchClient`/`$api` (or a shared base-URL constant) instead of a hardcoded URL, and validate `res.ok` before parsing.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic


def _prune_finished_tasks(self) -> None:
"""Cap remembered finished tasks so long-lived processes don't grow unboundedly."""
finished = [tid for tid, info in self.active_tasks.items() if info.status not in ('running', 'pending')]

@cubic-dev-ai cubic-dev-ai Bot Aug 11, 2026

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.

P2: Cancelling tasks can be pruned before their cancellation handler finishes when another execution starts, losing status and raising KeyError; keep cancelling entries until they reach a terminal state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At workflows/backend/service.py, line 70:

<comment>Cancelling tasks can be pruned before their cancellation handler finishes when another execution starts, losing status and raising `KeyError`; keep `cancelling` entries until they reach a terminal state.</comment>

<file context>
@@ -64,6 +65,13 @@ def __init__(self) -> None:
 
+	def _prune_finished_tasks(self) -> None:
+		"""Cap remembered finished tasks so long-lived processes don't grow unboundedly."""
+		finished = [tid for tid, info in self.active_tasks.items() if info.status not in ('running', 'pending')]
+		excess = len(finished) - self.MAX_FINISHED_TASKS
+		for tid in finished[:max(0, excess)]:  # dict preserves insertion order → oldest first
</file context>
Suggested change
finished = [tid for tid, info in self.active_tasks.items() if info.status not in ('running', 'pending')]
finished = [tid for tid, info in self.active_tasks.items() if info.status not in ('running', 'pending', 'cancelling')]
Fix with cubic

let cancelled = false;
(async () => {
try {
const res = await fetch("http://localhost:8000/api/workflows/metadata");

@cubic-dev-ai cubic-dev-ai Bot Aug 11, 2026

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.

P2: This replaced the typed OpenAPI client call (previously fetchClient.GET(...)) with a raw fetch to a hardcoded http://localhost:8000, duplicating the base URL that already lives in ui/src/lib/api/index.ts and bypassing the $api/typed client used everywhere else in this component. Because the response is cast blindly (as Array<{ file: string } & WorkflowMetadata>), data.workflows.map(...) will throw if the response shape changes or an error body is returned. Since the new backend endpoints aren't in the checked-in openapi.json/apigen, they were forced to bypass the typed client. Consider regenerating the OpenAPI client (npm run type-gen-update) so /api/workflows/metadata is represented, then call it through fetchClient/$api (or a shared base-URL constant) instead of a hardcoded URL, and validate res.ok before parsing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ui/src/components/workflow-layout.tsx, line 189:

<comment>This replaced the typed OpenAPI client call (previously `fetchClient.GET(...)`) with a raw `fetch` to a hardcoded `http://localhost:8000`, duplicating the base URL that already lives in `ui/src/lib/api/index.ts` and bypassing the `$api`/typed client used everywhere else in this component. Because the response is cast blindly (`as Array<{ file: string } & WorkflowMetadata>`), `data.workflows.map(...)` will throw if the response shape changes or an error body is returned. Since the new backend endpoints aren't in the checked-in `openapi.json`/`apigen`, they were forced to bypass the typed client. Consider regenerating the OpenAPI client (`npm run type-gen-update`) so `/api/workflows/metadata` is represented, then call it through `fetchClient`/`$api` (or a shared base-URL constant) instead of a hardcoded URL, and validate `res.ok` before parsing.</comment>

<file context>
@@ -173,30 +179,24 @@ const WorkflowLayout: React.FC = () => {
-          Object.fromEntries(entries.filter((e): e is NonNullable<typeof e> => e !== null))
-        );
+      try {
+        const res = await fetch("http://localhost:8000/api/workflows/metadata");
+        const data = (await res.json()) as {
+          workflows: Array<{ file: string } & WorkflowMetadata>;
</file context>
Fix with cubic

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.

Recording captures zero events: extension never loads (browser-use 0.13 / Chrome 137+) and schema validator 422s every WORKFLOW_UPDATE

1 participant