From 411a83b125d30b9fd003061a0e64e590582e59be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:36:40 -0400 Subject: [PATCH 1/3] feat: browser automations for scheduled, credential-backed evidence capture (#3417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(api): add credential-backed auto re-login for browser automations Browser automations can now sign back in on their own when a vendor session expires, instead of always requiring a person to re-authenticate. - Store a profile's login (username, password, optional authenticator setup key) in a per-organization 1Password vault via a new credential-storage service and a POST profiles/:id/credentials endpoint. - Resolve those credentials just-in-time during a run through the previously-stubbed credential vault adapter, now backed by 1Password, and drive sign-in with Stagehand variable substitution so secret values are never sent to the model. - Retry once with a freshly generated one-time code to cover the rotation boundary; fall back to human re-authentication when a login needs a step the agent cannot complete (SMS/email/push/SSO). - Let the scheduler attempt a run for a needs-reauth profile that has stored credentials, and mark a profile verified again after a successful run. - Load the 1Password SDK lazily and mark it external for Trigger.dev builds; the feature stays inert unless OP_SERVICE_ACCOUNT_TOKEN is set. Adds unit tests for the credential resolver and the re-login flow. * feat(app): capture vendor login credentials in the connect flow Adds the first step of the browser-automation connect wizard: a credentials form (username, password, optional authenticator setup key) shown before the live sign-in. On submit it resolves the auth profile and stores the login via the credentials endpoint so scheduled and manual runs can sign in on their own. Credential storage failures are non-fatal and fall back to manual sign-in. Part of the Browser Automations redesign (connect-a-login flow). Adds unit tests for the form (fields, validation, submit, password toggle). * feat(app): redesign browser-automation first-time setup screen Replaces the bare "enter a URL" prompt with the three-step explainer (01 Connect a login / 02 Describe what to capture / 03 Evidence on schedule), a single "Connect a vendor login" action, and the 1Password reassurance line. The URL is now collected in the connect credentials form rather than here. Part of the Browser Automations redesign (first-time setup). * feat(app): use split-card layout for browser-automation setup screen Switches the first-time setup screen to the designer's 2c variant — a split card with the pitch and "Connect a Vendor Login" action on the left and a quiet "How it works" rail (01/02/03) on the right. Stacks to one column on mobile. * feat(api): detect vendor login methods to power smart connect Adds a login-analysis backend: POST /v1/browserbase/analyze-login opens the vendor sign-in page in a throwaway cloud browser, detects the supported login methods (password / SSO / passkey, identifier type, extra fields), and returns a recommendation (ready / works-with-check-ins / manual). Reads a public page only — no credentials — and always degrades to a manual fallback if it can't be read. Foundation for the smart-connect flow. Unit tests for the recommendation logic and the analyzer service. * feat(app): smart connect flow (analyze -> sign in -> capture) Replaces the connect flow with the "Browser-First Split" (1C) design: a stepper rail plus a browser stage that opens the vendor URL, runs AI login-method detection (/analyze-login), shows a recommendation (ready / works-with-check-ins / manual), opens the live sign-in, then captures the reusable credentials (username, password, authenticator setup key) after verification. Removes the superseded credentials-first form. Adds tests for the capture form. The SMS-only -> "enable an authenticator app" recommendation is deferred to the live sign-in step (2FA method isn't visible before sign-in). * feat(app): group automations by connection with per-connection status Rewrites the automations list to group automations under their vendor connection. Each connection shows its status (Connected / Needs reconnect / Needs your action / Not connected) with a one-click Reconnect for the ones that need a human. Adds a useBrowserProfiles hook to load connection statuses and wires reconnect through the existing live sign-in. * feat: capture arbitrary extra login fields (workspace, subdomain, etc.) Extends credential capture beyond username/password/TOTP to any site-specific fields a login needs. The capture form gains an "add a field" control; the API stores extras as custom 1Password fields, resolves them at run time (via items.get, additive to the existing resolve path so the standard path is unchanged), and the sign-in agent fills each by label using variable substitution (only labels reach the model, never values). * fix(api): degrade login analysis to manual when Browserbase is unavailable Move Browserbase session/context creation inside the try/catch so a missing BROWSERBASE_API_KEY (or any Browserbase outage) returns the manual-entry fallback instead of a 500 at the analyze step. Documents the required BROWSERBASE_API_KEY / BROWSERBASE_PROJECT_ID in .env.example. * chore(api): log full error/stack when login analysis falls back to manual * fix(app): require a real login URL in connect flow; clearer 'no form detected' copy * feat(browserbase): run vendor login analysis as a background task Move the connect-flow login analysis off the HTTP request path into a Trigger.dev task so the browser + AI work can't be cut short by a request or browser timeout. The analyzer also auto-navigates to the sign-in page, so a customer can paste a homepage or dashboard URL instead of the exact login page. - analyze-login now returns a run handle ({ runId, publicAccessToken }) - the connect flow subscribes with useRealtimeRun and picks up the result when the run completes, falling back to manual entry on failure * fix(app): accept any vendor URL in connect flow, not the exact login page Users shouldn't have to hunt down the precise /login URL. Accept a bare domain or homepage (e.g. notion.so), add the scheme automatically, and let the analyzer navigate to the sign-in page itself. Relabel the field to "Vendor website" and update the copy to match what we actually do. * fix(app): resume vendor connect flow after navigation; static https prefix The connect flow kept all state in component-local React state, so leaving the page (client-side navigation or in-app tab switch) unmounted it and lost the in-flight analysis — the background task kept running, but the UI forgot about it and forced a restart. Persist the analysis phase to sessionStorage (per task, 15-minute window) and re-subscribe to the run on return. Also show a static "https://" prefix on the URL field so users type just the domain; a pasted full URL has its scheme stripped so it isn't doubled. * fix(app): render detected login methods as proper labels (SSO, not Sso) CSS `capitalize` only uppercases the first letter, so the SSO acronym showed as "Sso". Map method keys to human labels instead. * feat(browserbase): sign in for the user from a chooser instead of a raw browser Detection now drives the connect flow instead of just describing it. After the check, the user picks how to sign in: - Email & password (recommended): they fill our form once and we perform the sign-in for them in the background, reusing the same credential auto-fill the scheduler uses. No typing into the raw browser, and no entering credentials twice. If an automated step can't complete (CAPTCHA, email/SMS code, SSO), we hand the live browser back to finish that one login. - SSO / passkey: the live browser, which those genuinely need. The automated sign-in runs as a background Trigger.dev task so the browser + AI work can't be cut short by a request or browser timeout. Credentials are never persisted client-side; only the analysis phase is resumable. * fix(app): move "how it works" to the left on the browser automations setup card * fix(app): drop the redundant "detected" panel once the method chooser is shown The chooser already lists each sign-in method, so repeating them in the rail's detected panel was redundant. Keep the panel only as the placeholder/skeleton during the check; hide it from the choose step onward. * fix(app): declutter sign-in details form with progressive disclosure Default to just username + password. Two-factor moves behind a "this login uses an authenticator app" checkbox, with copy that makes clear it wants the one-time setup key (not the rotating 6-digit code). Extra site fields open automatically when detected (workspace, subdomain, …) and are otherwise a collapsed manual fallback. * fix(app): give the live sign-in browser more room; calmer message when auto sign-in isn't provisioned Enlarge the embedded browser (taller iframe, trimmed padding on the sign-in step) so it uses the available space. When the credential vault isn't configured on an environment, fall back to manual sign-in with an informational message instead of surfacing a raw error. * docs(api): clarify OP_SERVICE_ACCOUNT_TOKEN — what it enables and where to get it * feat(browserbase): classify why an automated sign-in couldn't complete After filling stored credentials, read the resulting page and classify it — logged in, wrong username/password, two-factor required, or a human challenge — instead of a single logged-in/not check. The connect flow uses this to route the user precisely: wrong password sends them back to the form with a clear message; a 2FA prompt or CAPTCHA hands them the live browser (and, for 2FA, suggests adding an authenticator setup key for unattended runs). * feat(browserbase): watch the automated sign-in and take over in place Run the automated sign-in on a live browser the user can watch, instead of an invisible background session. The session is created up front and shown as a live view; the background task drives it (fill credentials, handle 2FA) while the user watches. If it can't finish — 2FA prompt, a challenge, or an unclear state — the same browser stays open on exactly that page so the user finishes in place and confirms. Wrong password still routes back to the form to fix the stored value. Also navigate to the actual sign-in form first (shared with login analysis), so a homepage/dashboard URL doesn't leave the fill on the wrong page. * fix(browserbase): fill each sign-in field in its own action Stagehand's act() performs one action per call, so a single "enter username and password and submit" instruction only filled the username — the password was never typed and the form never submitted, so the sign-in stalled (and, sharing this code, so did the scheduler's auto re-login). Split it into atomic steps: each extra field, username, an optional Next/Continue for two-step logins, password, submit, then the one-time code. Secrets still go through variable substitution and never reach the model as text. * fix(app): keep the browser open on any sign-in failure instead of a toast When the automated sign-in didn't succeed, the flow bounced the user back to the form with a toast — hiding what actually happened (a real error, a 2FA prompt, or a site rate-limit/verify step after repeated attempts). Now every non-success keeps the same live browser open (take-over) with an in-view caption explaining the state, plus a "Re-enter details" action to correct stored credentials. Success still closes the session and connects. * fix(app): don't act on a stale run result when starting a new sign-in useRealtimeRun keeps emitting the previous run's terminal result for a render after switching runs. On a retry that made the effect close the freshly created session and re-route on the old outcome — so the new sign-in's task then failed with "session is missing a connect URL". Ignore any run state whose id isn't the current run's, release a prior session before starting a new one, and stop Trigger from blindly retrying the browser sign-in/analysis tasks. * fix(app): guide 2FA take-over to the authenticator code (passkeys can't work here) Passkeys need a hardware authenticator on the user's own device, so they hang forever in the remote browser. On a 2FA hand-over, tell the user to use their authenticator app's code (via "More options") instead. Also extract the sign-in session lifecycle into a focused hook to keep the flow component under budget. * fix(app): keep the 2FA take-over hint vendor-neutral Drop the site-specific button name from the take-over caption so it reads correctly for any vendor, not just one whose 2FA page happens to say "More options". * fix(app): warn up front when a vendor only offers non-unattended sign-in If a site's only detected methods are SSO or passkey, tell the user before they pick that it can't run on a schedule unattended (we keep the session alive and email for a manual refresh). Clarify the passkey option needs their device each time. Vendor-neutral. * fix(app): drop the "what we detect" placeholder panel from the connect rail Detected methods are now shown as the chooser options, so the rail's placeholder/skeleton panel is redundant. Remove it (and the now-unused detecting/showDetectPanel props). * feat(app): show who's in control of the live sign-in browser Add a banner + colored frame around the live view: a blue "AI is signing you in — sit tight" while the automation drives, and an amber "Your turn — " when the user needs to act. Removes the ambiguity of not knowing whether to wait or to do something. * feat(app): narrate the automated sign-in in real time The sign-in task now publishes live, plain-language progress ("Opening the sign-in page…", "Entering your password…", "Checking two-factor…") via run metadata, and the connect flow shows it under the "AI is signing you in" banner. The user can follow exactly what the automation is doing, moment to moment. * feat(app): redesign the live sign-in as an activity panel (design 1b) Replace the single status banner with the designer's two-pane card: the live cloud browser on the left, and a "Sign-in activity" panel on the right — a who's-in-control pill, a step-by-step timeline of what the AI is doing (new StepList), and the action the user needs to take (enter a 2FA code, or finish a sign-in the AI can't complete). The sign-in task now emits a step timeline via run metadata instead of a single status string; the flow renders these steps full-width and keeps them visible through the take-over. * feat(browserbase): manage connections — edit and remove (design 1f) Add connection management: PATCH /profiles/:id (edit name / sign-in URL; moving to a different host marks it needs-reconnect) and DELETE /profiles/:id (removes the profile and best-effort deletes its stored login from 1Password). Surface it in the connection header via a ⋯ menu — an inline edit popover (name + URL) and a guarded remove dialog that states what stops running. Reconnect already existed. Backend covered by tests. * fix(app): don't show a fresh evidence task as already connected Connection status is org-level (any verified profile), so a brand-new task was inheriting "connected" from a connection made on another task and skipping the setup/how-it-works step. Gate the empty states on this task instead: a task with no automations of its own shows the onboarding, and only advances to the "add your first automation" state after the user connects here. * fix(app): taller live browser; connected copy points to the next step Give the live sign-in browser more of the available space (iframe 460→640px, up to 80vh). And reword the connected screen so it doesn't imply runs are already happening — it now says the session is saved and prompts the user to add their first instruction (what to capture). * feat(app): "Browser evidence" section — 1n header + 3-step empty state Start the section redesign (1n): rename to "Browser evidence" with a status badge and a plain "what this does" subtitle, and replace the onboarding with the 3-step explainer (Connect once → Add instructions → Evidence lands here). Rename the create action to "Add instruction". Connection card + run history come next. * feat(app): restructure browser evidence active view into connection + instructions Split the grouped list into a labelled "Connection" card (avatar, display name, hostname, status pill, reconnect, manage menu) followed by an "Instructions" list, matching the section IA design. Establishes the per-connection presentation the instruction composer will consume. * feat(app): add run-history strip and run-detail overlay to browser evidence Below each connection's instructions, show a horizontal strip of recent run screenshots with pass/fail badges and timestamps. Clicking a tile opens a detail overlay with the full screenshot, the check verdict, any failure reason, timing, and re-run / open / download actions. * feat(api): live test-run for browser instructions before saving Add an ad-hoc test endpoint that runs a not-yet-saved instruction against a connection's live session and streams its progress, so the composer can show the AI working before the instruction is committed to the schedule. Reuses the same evidence runner scheduled runs use and persists nothing — no automation, no run record. The execution engine now emits each stage through an optional callback so the run's activity can be streamed live. * feat(app): split composer — write, watch the AI test it live, then save Replace the create/edit dialog with a two-pane instruction composer: write the instruction on the left, test it against the connection and watch the AI work live on the right, then save once it passes. Drops the separate name and schedule fields (name derives from the instruction; schedule lives on the task), and defaults the starting URL from the connection with an advanced override. * feat(app): edit instruction cadence inline; remove old config dialog Since the composer no longer carries a schedule field, each instruction row now has an inline cadence picker (daily…yearly) that saves on change — so an instruction's schedule stays editable after the modal moved out. Remove the now-unused create/edit dialog the composer replaced. * test(api): use a valid failure code in the instruction-test spec * fix(api): capture the browser at 1920x1080 for a sharper live view Our panels are well under 1920px wide, so a larger native viewport lets the embedded live view (and evidence screenshots) downscale instead of upscale — upscaling was what looked soft, especially on HiDPI displays. Browserbase exposes no device-pixel-ratio setting, so viewport size is the only lever. * feat(app): gentle success beat when a connection signs in Instead of snapping from the live browser straight to the connected screen, hold a brief "Signed in" confirmation over the still-visible browser, then fade into the connected screen — a softer hand-off. * feat(app): make the pass/fail check first-class with example chips Surface the pass/fail check by default (drop the opt-in toggle and OPTIONAL label) since evidence instructions usually want a verdict; leave it blank to capture a screenshot only. Add clickable example chips under both the instruction and check fields — a short label inserts a full example — shown while the field is empty and hidden once the user starts typing. * fix(app): bind new instructions to the just-connected vendor; align activity header The composer resolved the connection from a stale default host, so a freshly connected vendor could be tested against the wrong site's saved profile. Pass the connected URL up from the connect flow so instructions bind to the vendor the user actually connected. Also align the "Sign-in activity" header with the browser chrome bar (same 34px row). * fix(api): stop re-login from misreading a successful sign-in as failed After signing in, the run navigated back to the entered URL before checking the result — but some apps always show a login page at that URL, so a successful sign-in read as "did not complete" and forced a needless re-auth. Verify on the page the site lands on after login instead, and detect the signed-in state by the absence of a sign-in prompt rather than an avatar/profile marker (many apps have none), which was causing false negatives. * feat(app): enlarge the test-run live view to fill its panel Give the live view a 16:9 frame matching the capture resolution so it fills the panel width instead of leaving wide side gaps, and stays sharp (downscaled). * fix(api): don't call a login page a "session expired" The step fired whenever the first page looked like a sign-in prompt — including sites that always show login at the root, where nothing had expired. Reword it to state plainly that we're not signed in on this page and are signing in. * feat(app): clean up the connection chip and frame the pass/fail check Show the connection as its hostname with an avatar and a status-driven dot (green verified / amber needs-reconnect / grey unverified) instead of the raw profile name with an always-green dot. Frame the check as "PASS when / FAIL otherwise", so the pass/fail logic reads at a glance without a redundant second condition. * fix(api): give test runs more time and nudge the agent to be efficient Sign-in alone can eat ~40s, so a 4-minute cap cut multi-step lookups off mid-run. Raise the budget and tell the agent to find its own way and read what's already on screen instead of over-navigating — the common cause of hitting the limit. * feat(app): align the first field with the live view and clarify copy Line the instruction field up with the top of the live browser panel. Explain that the AI finds its own way (no exact steps needed) and what the advanced start-page override is for, and surface a clear, actionable message when a test run times out. * feat(api): make the navigation model swappable and let it self-correct Keep navigation screenshot-based but make the computer-use model configurable via env (Claude or OpenAI, no per-site tuning) and default to the newest Claude our Stagehand supports. Give the agent more steps and a generous time budget so it can recover from a wrong turn on a complex site, and tell it to verify the page matches the request and fix a wrong item before finishing. The page-reading model is a separate env knob too. * feat(api): default navigation to OpenAI's computer-use model Use openai/computer-use-preview for screenshot-based navigation by default — it's purpose-built for GUI agents — with a safe fallback to Claude when no OpenAI key is set. Page reading / verdicts stay on a general model. * feat(api): fall back to Claude at runtime if the navigation model fails If the primary computer-use model is unavailable mid-run (preview access, rate limits, upstream errors), retry navigation once on a Claude computer-use model instead of failing the run. Use claude-sonnet-4-6 for the fallback — sonnet-5 isn't a computer-use model Stagehand supports. * feat(api): default navigation to GPT-5.6 Terra Switch the default computer-use model to openai/gpt-5.6-terra — the balanced tier of OpenAI's newest family, strong on browser automation at about half Sol's cost. Still env-swappable (e.g. gpt-5.6-sol for max accuracy) with the Claude fallback intact. * fix(api): capture the named item and judge the check against that target Two evidence-quality fixes: tell the agent to open the specific item an instruction names so the screenshot shows just that (not a whole list), and pass the instruction into the evaluator so a pass/fail check is judged about the intended target — not any matching value that happens to appear elsewhere on the page (which let an unrelated product's price satisfy the check). * fix(app): align the instruction field with the live view top The left field label and the right "Test run" header had different heights (the header carries a status pill), so the textarea and the browser view didn't line up. Fix both headers to the same height so they share one baseline. * feat(app): drop the manual "start from a specific page" option The AI is meant to find its own way, so the composer no longer asks for a start page — instructions always run from the connection. Removes the advanced override and its field. * feat: target the authenticated home so runs reuse the session When automated sign-in succeeds, capture the page the app lands on and use it as the connection's URL. Runs then open the authenticated app directly instead of a login page, so the persisted session is reused and credentials aren't re-entered every time. Genuine expiry still redirects to login and re-authenticates. * fix(api): capture the full page so evidence has no empty gap Screenshots were viewport-height, so a short page left a tall band of empty viewport below the content — a wide gap above the audit overlay. Capture the full page instead: the overlay sits right under the real content and the evidence is complete. * feat(app): one flow after connect, and support multiple vendors After a successful sign-in, go straight into writing the first instruction instead of bouncing back to a "create" button. And make multiple connections first-class: each connection has its own "Add instruction", and a "Connect another vendor" button adds more vendors — any number of connections, each with any number of instructions. * fix(app): stop offering passkey as a sign-in method Passkeys are device-bound and can't complete in Comp's cloud browser, so offering one was a dead end. Remove passkey from the method chooser. If a site only supports passkeys, show a plain "can't be connected here" message instead of a sign-in that can never finish. * feat: drive SSO up to the identity provider automatically Picking SSO no longer just dumps you in a blank browser. The AI opens the vendor, clicks through to your identity provider, and streams the steps — then hands the live browser over for you to finish the IdP login (which we can't hold or automate). If the saved session is still valid it signs in with no interaction at all. Reuses the live sign-in run via a mode flag. * feat(app): smart one-click reconnect for expired connections Reconnect no longer dumps you into a fully manual browser. A password connection re-signs-in automatically with the stored credentials (no interaction); an SSO connection has the AI drive to the identity provider so you only finish there. Reuses the sign-in run and just refreshes the list when done. * feat(api): render human-facing sign-in at a smaller viewport for legibility Sign-in, take-over, SSO and reconnect sessions now use a 1280x800 viewport so the page renders larger (and stays crisp) in the embedded live view — easier to read and fill. Unattended evidence runs keep the 1920x1080 viewport for sharp full-page screenshots. Viewport is now a parameter, defaulting to capture. * feat(app): animated activity border showing who's driving the live view Add a glowing border around the live browser: a primary-color glow while the AI is acting, amber when it's the user's turn. Makes it obvious at a glance whether to wait or take over, on the connect sign-in, the instruction test run, and execution/auth views. Decorative and click-through, so take-over still works. * fix(app): make the activity border visible and stop it lagging The ring used a mid-tone that vanished on dark pages, and animating box-shadow repainted every frame over the live iframe (the lag). Use a clear green (amber for your-turn), draw the ring as a static box-shadow that's always visible, and pulse opacity instead — GPU-cheap, no repaint. Respects reduced-motion. * fix(api): reduce vendor-page motion so the live view stops looking laggy Inject reduced-motion styles into every session page so a vendor's own looping animations (hero backgrounds, marquees, spinners) don't constantly repaint the streamed live view — that churn is what read as lag. Also steadies evidence screenshots. Best-effort and applied at session start. * fix(app): make the activity border render reliably via inline styles The border's box-shadow lived in globals.css, but the rule wasn't making it into the compiled stylesheet (only the class name did), so the ring never showed. Move the ring to an inline box-shadow with an explicit color (bright green for AI, amber for your turn) and use Tailwind's animate-pulse — inline styles can't be stripped or go stale. Drop the now-unused global CSS. * feat(app): show the activity border only while the AI is driving Render the green live-view ring only during AI control (sign-in, test run, execution). On the user's turn there's no ring, so the two states read differently at a glance. * feat(app): implement the "breathing glow" AI control ring Adopt direction 1 from the ai-control-ring design: a constant green ring with a soft inner glow that breathes (opacity-only, so the live iframe never repaints). Adapted for our overflow-hidden live-view container — glow inward instead of the design's outer halo, and keep the static ring inline so it's always visible. * fix(app): soften the AI control ring Drop the hard solid green line for a feathered, semi-transparent glow that breathes at the edges, with a faint always-on haze so it stays gentle but present. Still opacity-only (no iframe repaint). * feat(app): add the "AI is controlling" pill to the control ring Add the design's status pill (green, pulsing dot) inside the live view. Placed at the bottom-center rather than the design's top-outside spot, which our overflow-hidden container would clip and which clashes with the browser chrome bar. Pulse honors reduced-motion; still click-through. * feat(app): show a "Your turn" pill when the user is driving Symmetry for the live view: AI control shows the green glow + "AI is controlling", and the user's turn now shows an amber "Your turn" pill (no glow — the ring stays reserved for the AI, so the two states read differently). Applies to the connect take-over and the manual auth view. * fix(app): drop the ring border and fix the sign-in method chooser Remove the green border line from the AI control ring, leaving only the soft breathing glow bleeding in from the edges. Make the sign-in method options feel clickable: pointer cursor, a hover/pressed background, and a nudged arrow. On SSO, switch to the live view the moment it's clicked (it shows "Opening…" until the session is ready) instead of waiting for the background run to start, so the click has instant feedback. * fix(app): fade the control ring in instead of popping it in The overlay appeared instantly while the live view was still loading, so it looked abrupt. Ease it in over ~1.4s (honors reduced-motion) so it proceeds in with the page. * fix(app): confirm before discarding an in-progress live sign-in Closing the live sign-in card used to tear the session down on a single click, so a stray click mid-login wiped the user's progress. Now, while a live session is active (signing-in / takeover / manual sign-in), the close button asks for confirmation first; outside a live session it still closes immediately. * fix(app): show the control ring on browser load and hug the view Gate the ring on the iframe's load event so it fades in with the page instead of appearing over a blank frame (the fixed timer fired during loading, so it looked instant). Wrap the ring around the browser view only, not our chrome bar. * fix(app): drop the control-ring glow, keep the status pill Remove the green glow entirely — the live view now shows only the status pill ("AI is controlling" / "Your turn"). Also stop the composer's action buttons from being dragged to the bottom when the test panel grows: keep them under the form instead of pinning them to the column's foot. * feat(app): add the Browser Connections settings page Org-level management for the vendor logins that power browser automations — a dense table (design 2a) with a health summary, per-connection reconnect, change-login, rename, and remove, wired to the existing org-scoped profile API. Enables the previously-stubbed settings route and surfaces it in the settings nav (gated by the web-automations flag + integration:read). Replaces the old single-connection scaffolding. * feat: count automations per browser connection listProfiles now tallies how many BrowserAutomations in the org run on each connection (matched by targetUrl hostname, no FK) and returns it as automationCount. Surfaced as the "Automations" column on the Connections page and in the remove-connection warning ("N automations stop until reconnected"). * feat(db): add browser automation steps schema Add BrowserAutomationStep (ordered, bound to a connection) and BrowserAutomationStepRun (per-step run evidence) — the schema for multi-vendor sequential automations where each step reuses its connection's saved session. Additive migration: creates the tables and backfills one step per existing automation (matched to a connection by hostname). The inline automation fields stay for now; the engine, API, and UI move onto steps next, and the columns are dropped once nothing reads them. * feat(api): store browser automations as ordered steps Create/update now accept a steps[] array (each step = connection + targetUrl + instruction + optional check) and persist BrowserAutomationStep rows. The legacy single fields are mirrored from the first step so the current execution path keeps working until it reads steps directly; a bare inline instruction is stored as a one-step automation. Reads include the ordered steps. * feat(api): run browser automations step by step Execution now runs an automation's ordered steps in sequence, each on its own connection's saved session (no re-login), recording per-step evidence as BrowserAutomationStepRun and rolling the results into one run verdict (fails if any step fails; the check fails if any step's check fails). A single-step or legacy automation behaves exactly as before. Split the step orchestration into a step-runner service and a pure results module to keep each file focused and under the size limit. * feat(app): carry automation steps through the app types and hooks Add BrowserAutomationStep / BrowserAutomationStepInput and a steps field on the BrowserAutomation type, and let the create/update hook forward steps[] to the API. Foundation for the multi-step composer UI (no behavior change yet — the single-step composer keeps sending the inline instruction). * feat(app): multi-step (multi-vendor) automation composer Replace the single-instruction composer with a stacked step-cards editor (design: Stacked step-cards). Each step picks its own connection and has its own instruction + optional pass/fail check; steps can be added, removed, and reordered, with "session reused — no sign-in" connectors between them. A step on a needs-reconnect connection blocks saving until it's fixed. Test runs the active step; Save sends the ordered steps[] to the API. Drops the old single-step form. * fix(app): guard the composer against a missing connections prop Default connections to [] and fall back to the primary connection so the composer never crashes on connectionOf(...).find and the picker is never empty, even if a caller (or a stale HMR bundle) renders it without the list. * fix(app): tidy the step card — hostname label, single-step chrome, spacing - Connection picker showed the raw profile id: resolve the label to the hostname explicitly, and always include the step's own connection in the options so it matches. - Hide the reorder ↑/↓ (and remove) controls on a single-step automation — they only make sense with more than one step. - Keep "Runs on" on one line and let the picker flex, so the header no longer wraps in the narrow column. * feat(db): add BrowserAutomationDraft schema Server-side store for in-progress (unsaved) automations so a draft survives refreshes and follows the user across devices. A draft holds the composer state (steps as JSON) per task; it becomes a BrowserAutomation on save and is deleted then. Additive migration — new table only. * feat(api): browser automation draft endpoints CRUD for in-progress automation drafts (list per task, create, autosave via PATCH, delete), org-scoped through the task and gated by the same task:* perms as automations. Steps are stored as plain JSON so a half-written draft persists; the frontend deletes the draft once the automation is saved for real. * feat(app): autosave + resume browser automation drafts The composer autosaves an in-progress automation as you edit (debounced, create mode) to a server-side draft, so a refresh — or another device — resumes exactly where you left off. Drafts appear pinned in a dashed strip above the section with Continue / Delete. Saving finalizes the automation and discards its draft; canceling keeps it for later. Adds the drafts hook + DraftsStrip (+ tests). * feat(app): automation-centric evidence list with vendor chains (design 4a) Rework the list from connection-grouped to automation-centric: each row is one automation — which can span several vendors — showing its ordered vendor chain (GH → AWS → OK), and flags inline when one of its connections needs reconnect. Header shows the count + New evidence + Connect another; long lists get Load more. Connection health/management now lives on the Connections page, so the per-connection grouping and manage menu are dropped here. * fix(app): make the connect flow resume from anywhere on refresh Persist the URL from the moment it's entered (resume at the URL step) and the analysis once we have it (resume at the method chooser), and never store a live analysis-run token or sign-in session — both die on reload. A live sign-in now falls back to the method chooser (analysis kept) or the URL step instead of dropping to the empty "Connect a vendor" screen. So a refresh or navigation never loses your URL/progress; only the live sign-in itself must be re-entered (the remote browser session can't survive a reload). * fix(app): tighten the step-card type scale to fit the compact composer The DS Label/Textarea defaults (~14px) read oversized in the dense step card next to the 10px "Runs on" / 12.5px rows. Drop to a 13px label and shrink the textareas (and their placeholders) to 13px so everything sits on one scale. (DS Label/Textarea omit className, so this uses a plain label + an arbitrary-variant wrapper on the textarea.) * fix(app): shrink the step-card connection picker to match Bring the "Runs on" connection Select down to the step card's 13px scale (and a tighter 32px height) via a wrapper arbitrary-variant, since the DS Select omits className. * feat: capture a focused close-up beside the full-page evidence (option D) Evidence runs now also capture the agent's final viewport as a focused close-up, so a reviewer gets the proof readable (close-up) plus full context (full page) — robust, no flaky coordinate-cropping. The pass/fail reason also names the value and where it appears. Shown in the composer's test result; persisting it for scheduled-run history (schema + run detail) is the next step. * fix(app): draft vendor identity, no duplicate evidence shot, capped previews - Drafts now show the vendor monogram(s) + host (resolved from the step's connection) so a draft is identifiable, not a bare line. Monogram, not a fetched favicon — a favicon CDN would leak which vendors the org uses. - Only capture the close-up when the page is actually taller than one screen; when the page fits, the close-up equals the full page, so we skip the dup. - Cap the test-result screenshots at 360px (scroll within the box) so a tall full-page image no longer pushes the step timeline off-screen. * fix(app): compact the automation row Collapse the stacked last-run / big schedule picker / summary into one muted meta line, and move the cadence picker into the actions row (compact). The row is now two tidy lines instead of a tall five-line stack. * feat(app): show real vendor logos across browser automations Replace the two-letter monogram placeholders with each vendor's actual favicon in the Connections table, the drafts strip, and the automation list rows. Logos are served through a first-party /api/vendor-logo proxy so the browser never calls a third-party icon CDN directly (that would leak which vendors an org uses and trip the app CSP); the proxy caches hard and validates the host is a bare hostname. Falls back to a colored monogram while loading, on error, or when a site has no favicon. * fix(app): shrink the automation row's schedule and action controls The Daily schedule dropdown (legacy 36px trigger) and the 32px icon buttons made the right actions cluster read chunkier than the compact left content. Bring the whole cluster to a uniform 24px: narrower/shorter Daily picker and icon-sm buttons, matching the Run button. * feat(app): compact schedule menu in the automation row (designer option C) Replace the wide inline "Daily" schedule dropdown with a compact Calendar-icon menu placed next to Edit, per the designer's option C. The frequency (Daily/Weekly/Monthly/Quarterly/Yearly) is picked from a small radio dropdown wired to the existing onChangeSchedule; the schedule still reads in the meta line ("Runs Daily · next …"). This narrows the actions cluster by ~70px, fixing the crowding, while keeping the per-automation schedule editable — it's a distinct field from the task's own frequency and has no other UI to change it. Adds AutomationItem tests covering the schedule control. * feat(app): fold schedule into a Run split button; neutral action icons Merge the standalone schedule (calendar) button into the Run button as a split control — Run on the left, the cadence menu on the calendar half — so the actions row carries one fewer button. Also neutralize the pause icon (was primary-green) so the whole cluster reads as uniform muted icons; run state is still conveyed by the status dot, Paused pill, and dimmed row. * feat(app): match designer's C mockup for the automation row - Run becomes a solid-green split button: Run on the left, the schedule (calendar) menu on the right, split by a hairline — one entity. - Pause/Edit/Delete become outlined icon buttons with uniform muted glyphs (matches the mockup's bordered targets). - Mobile: the actions bar wraps onto its own full-width line below the content, icons left and the Run entity pushed right; desktop stays on one row. Status dot top-aligns to the name on mobile. * fix(app): neutral schedule half; clearer "first run" label - The calendar (schedule) half of the Run split is no longer green: Run stays solid primary, the calendar becomes a neutral outlined segment with a muted icon, joined as one control. - ScheduleSummary labels a never-run automation "first run" instead of "next". A never-run automation is due on the next tick for any cadence (server isDueToday treats null lastRunAt as always due), so the date is today for daily OR weekly; the date only reflects the cadence after the first run. This removes the "Runs Weekly · next: today" confusion. * feat(app): implement designer option A — unified Run split + meta grammar Run control (option A): a single unified split button — Run and a ▾ caret, both solid primary, split by a hairline; the caret opens the cadence list. Running state no longer shifts the layout: the play glyph swaps for a same-size (12px) spinner and the "Run" label stays, so the button keeps its exact width instead of widening to "Running…". Meta line: new AutomationMetaLine with a per-state grammar (one fact per clause, no restating): • never run → "First run today · then daily" (kills the old "Never run · … · first run" redundancy) • ran ok → "Last ran … · daily · next Jul 24" • failed → red "Last run failed …" · weekly · next Jul 28 • paused → "Paused · monthly schedule on hold" Next-run dates are formatted in UTC (formatMonthDayUtc) to avoid off-by-one in negative-UTC timezones. Shared schedule math extracted to schedule-utils and reused by ScheduleSummary. Tests cover all four meta states and the no-shift running label. * fix(app): remove the white seam in the Run split button The 25%-white hairline between Run and the ▾ caret read as a white gap. Drop it so the two halves are one seamless green pill; hover still darkens each half independently, and the caret keeps signalling the cadence menu. * feat(app): darker-green caret half to distinguish the schedule zone Give the ▾ cadence half of the Run split a slightly darker shade of the same primary (brightness-90, darker on hover) so it reads as a distinct dropdown zone — the standard split-button treatment — instead of a white seam. The two halves share the green; only the shade differs. * fix(app): match Run split icon sizes to the rest of the row The play icon, running spinner, and cadence caret were 12px while every other row icon (pause/edit/delete/expand) is 14px, so the Run split read as slightly undersized. Bump them to 14px for a consistent icon scale. * fix(browser-automations): show a screenshot per step for multi-step runs A multi-step (multi-vendor) automation captures one screenshot per step (stored in BrowserAutomationStepRun), but the run response only ever returned the single rolled-up run.screenshotUrl and the detail view only rendered that — so a GitHub→AWS automation showed just one screenshot. - API: include stepRuns (with their step's targetUrl) on run fetches and presign each step's screenshot via a shared presignRun helper. - App: BrowserAutomationRun gains stepRuns; RunDetailOverlay renders one labelled screenshot + verdict per step for multi-step runs, and a step with no screenshot now surfaces why (its error/reason) — which is how a silently-blocked later step becomes visible. Single-step keeps its layout. * feat(browser-automations): task-wide schedule in the section header Browser evidence runs daily across the board and there's no real need to schedule each automation separately, so move the cadence to one control in the "Browser evidence" header (calendar icon → Daily/Weekly/…). It sets the schedule for every automation on the task at once, and new automations inherit the task's current cadence. - API: PATCH automations/task/:taskId/schedule (SetTaskScheduleDto, task:update) bulk-updates every automation's scheduleFrequency; create inherits the task's existing cadence when none is given. Tests added. - App: useBrowserAutomations.setTaskSchedule; the header dropdown shows and sets the shared cadence. This removes the per-row schedule caret, so the Run split button is gone — Run is a plain primary button again, and the only remaining row arrow is the run-history expand chevron (no more duplicate down-arrows). Running state keeps its no-shift spinner. * feat(browser-automations): show the AI's live step list during Run Run now streams the AI's steps the same way the setup/Test screen does, instead of running quietly. The step list appears next to the live browser so you can see what the AI is doing ("signed in", "opened repo", "captured…"). - Run executes as a background task (execute-automation-live) so it can stream a step timeline via realtime metadata (runSteps) — mirroring the test-vendor-instruction flow. New POST automations/:id/execute-live returns the run handle; the live view subscribes with useRealtimeRun. - Extract the shared step-timeline builder (createEvidenceTimeline) reused by the Test service and the run execute path; executeAutomationOnSession gains an onSteps callback. - BrowserLiveView renders the StepList beside the browser during a run. Note: the realtime streaming only runs with the app + `trigger dev` up, so it can't be verified in CI — the logic mirrors the proven Test flow. * fix(app): cap the run screenshot preview height in the expanded run Full-page screenshots render very tall at full width. Cap the preview to max-h-80 with scroll; "Open full size" still opens the real image. * fix(browser-automations): interactive Run executes every vendor, not just the first Clicking Run only ran the first step (executeAutomationOnSession runs the single inline instruction), so a GitHub→AWS automation stopped after GitHub. Route the interactive Run through the full multi-step engine instead. - runSteps gains firstSessionId + onSteps: step 0 runs on the pre-opened live session (watchable), later vendors each get their own session; a combined step timeline streams to the Run view (with a "Step N · host" boundary per vendor). runStep picks executeEvidenceOnSession (live) vs runEvidence (fresh). - New executeAutomationLive runs the whole sequence on the existing run; the execute-automation-live task now calls it. Scheduled/unattended runs are unchanged (no firstSessionId/onSteps). Each vendor now produces its own step-run + screenshot (shown per step in the detail). Note: the live iframe shows the first vendor's session; later vendors run in their own sessions (watchable-live multi-vendor is a separate piece). * feat(browser-automations): step-ledger run detail + persist close-up screenshot Implements the designer's C3 layout for the run detail: each step is a compact ledger row (number, vendor, host, reason, verdict, "N screenshots"); expanding a row shows the close-up "proof" beside the scrollable full-page "context". Single-step runs auto-expand; multi-vendor runs list a row per vendor; failed steps show the reason in red with no screenshots. Shared RunStepLedger is used by both the inline run (RunItem) and the run-detail dialog (RunDetailOverlay), replacing their single-screenshot layouts. Also persist the close-up: the runner already captured and uploaded a focused viewport, but it was dropped on save. Add focusScreenshotUrl to BrowserAutomationRun + BrowserAutomationStepRun (migration), store it in finishRun/finishStepRun, and presign it alongside the full page — so the ledger shows both screenshots per step. * fix(app): align the two run-detail screenshot panes to one height The close-up and full-page panes rendered at different heights and looked uneven. Give both a fixed height: the close-up is contained/centered, the full page scrolls inside its box, so they line up side by side. * feat(browser-automations): follow each vendor's live view during a multi-step run The Run iframe only showed the first vendor while the steps panel advanced to the next — because each vendor is a separate browser session and only the first was wired to the live view. Each session already has a watchable live-view URL, so stream the current step's URL and swap the iframe to it as the run moves between vendors. - runEvidence exposes its session via an onSession callback; the step runner threads onLiveView through to it (fresh-session steps only — step 0 already runs on the visible live session). - execute-automation-live publishes the current live view to `liveViewUrl` realtime metadata; useBrowserExecution swaps the iframe when it changes. * feat(browser-automations): run-history ledger, truncated names, on/off switch Implements the designer's Run History and Row Fixes options 1A + 2A + 3A. 1A — run history is now a compact ledger: each run is a text row (date · vendor chain · verdict · N steps · M screenshots · duration) that expands into the per-step ledger. Removed the redundant thumbnail strip (and the run-detail dialog it opened) — screenshots live in exactly one place. RunItem rewritten as the run row; RunHistory is a flat bordered list. 2A — the automation name truncates to one line with the full text on hover (title), so long names/instructions no longer break the row. 3A — the enable/pause control is a real switch (teal = runs on, gray = paused) instead of a static power icon, so the paused state is visible. Deletes now-unused RunHistoryStrip + RunDetailOverlay (+ tests). * feat(app): tuck Edit + Delete into a ⋯ menu on the automation row The row was a wall of icons (switch, edit, delete, Run, chevron). Move the low-frequency Edit and Delete into an overflow (⋯) menu, leaving the row clean: pause switch · Run · ⋯ · expand chevron. Delete still confirms inline (replaces the actions with "Delete this automation? Delete / Cancel"). * feat(app): calmer automation row and deeper run history Row (1B + 2B): move the on/off switch to the left beside the name it governs and drop the redundant status dot; list one compact line per step (number + vendor logo + instruction, truncated with a tooltip) so what runs, in order, reads without expanding; fold the schedule meta inline with the name. Run + a ⋯ overflow menu (Edit steps / Delete) sit on the right, and the whole row is the expand target — the separate chevron is gone. Run history (3B): flatten the latest run — its step ledger with every screenshot shown inline, nothing to open — and collapse older runs into an "Earlier" chip strip; selecting a chip shows that run in place. Also add a flat mode to the step ledger (always-expanded, non-clickable rows) and an inline variant to the meta line, and drop the now-unused RunItem row. * fix(app): cover the live view between vendors so runs never look crashed A multi-vendor run opens a separate browser session per vendor and closes each one when its step finishes. When a session closes, Browserbase's live-view iframe briefly shows its own "WebSocket disconnected" notice — which, arriving right as the run wraps up, reads to the user like a crash, even though the evidence was already captured and saved server-side. Stream a live-view phase over the run's realtime metadata: the runner signals right before it tears a session down ('switching' when another vendor follows, 'finishing' on the last), and clears back to 'running' when the next vendor's live view opens. The Run view uses this to cover the iframe with a calm "Switching to the next vendor…/Saving evidence…" overlay during the teardown window, so the disconnect flash never reaches the user. Single-vendor and scheduled runs are unaffected. * feat(app): auto-expand a manual run's row when it finishes After clicking Run, the automation's row stayed collapsed on completion, so the user had to click it again to see the screenshots and verdict. Remember which automation the user ran, and once its fresh run has been refetched, expand that row automatically so the results show at once. The signal is a new object per completion, so re-running the same automation re-expands it. Scheduled runs don't go through this path and are unaffected. * fix(app): align the composer close button with the title The header close button sat at the top of the two-line title/subtitle block (items-start) with a tiny 12px icon, so it read as small and slightly off. Put it on the same row as the title (items-center), bump the icon to 16px to match the title text, and give it a slightly larger body with a subtle hover surface. * feat(app): richer "Browser evidence" header with a status strip The header carried only controls and no signal. Implement the designer's option A: title + subtitle stay left, a quiet status strip sits centered in the free space — aggregate run health (All passing / N failing / Not run yet), an overlapping stack of the distinct vendor logos, and the task-wide next run — and the actions stay right. On mobile the strip drops to its own line and the schedule + connect fold into a ⋯ menu; read-only keeps the strip and drops the actions. Extracted into BrowserEvidenceHeader to keep the list under the file-size limit. * fix(app): pin the Manage connection sheet's close to the top-right The DS SheetContent's built-in close button positions itself with a className on a DS Button, but DS Button hardcodes its own className and drops any passed in — so the close rendered unpositioned and fell to the bottom-left of the flex-col panel. Hide the built-in close (showCloseButton={false}) and render our own SheetClose pinned top-right. * feat(app): redesign the Manage connection panel (grouped sections) Implement the designer's option A: a favicon-anchored identity header with the vendor logo, name, host, and a status pill (plus a blocked reason when relevant); a bordered facts block; SESSION and DETAILS groups; and a fenced danger zone pinned to the bottom (SheetFooter). Reconnect goes solid teal only when the session needs attention, and degrades to a "view access" note for read-only users. SSO connections hide "Change login" (no stored password) and the credentials row. * refactor(app): reuse the shared connect flow on the Connections page The org-level Connections page had its own bespoke connect/reconnect: a raw interactive-browser iframe that required signing in entirely by hand, which was unreliable (clicks often didn't register). Replace it with the same ConnectVendorLoginFlow the evidence task uses — method detection, automated credential entry, and a working live takeover only when needed — for both connecting a new vendor and reconnecting an existing one (seed derived from the connection's method). Delete the bespoke BrowserConnectionLiveView and its session/navigate/verify machinery. The task flow is untouched; the flow is only cosmetically task-scoped (a localStorage resume key), so the page passes a stable "org-connections" key. Management (rename / change login / remove) is unchanged. * feat(app): detected-field sign-in form + passkey-aware method chooser Part 1 (designer 1A): the capture form is now built from what we detect on the vendor's login page — the actual fields, in order, with the vendor's labels — instead of a fixed email+password form. A detected field that IS the identifier (e.g. AWS "IAM username") is promoted to the identifier so it's no longer duplicated by a generic "Username or email", and email isn't shown when the site has no email login. Detection failures fall back to the generic form + "add a field". A pure deriveCaptureFields() drives it; the form maps back to the unchanged {username, password, totpSeed, extraFields} runtime contract on submit, so the working sign-in is untouched. Part 2 (designer 1E): the method chooser becomes radio cards (password recommended, SSO neutral) + Continue, with a clear footnote when the site also offers passkey (device-bound — can't work in the cloud browser), plus dedicated passkey-only and nothing-detected states. Softened the SSO copy (dropped the "can't run unattended / we'll email you" warning — reconnect is universal and already surfaced when it happens). Multi-step logins (email → Continue → password) already work at runtime and are unchanged. * fix(app): honest method-card label + "Comp AI" branding in connect copy Name the credentials method after what the vendor actually asks for — "Username & password" when the identifier is a username (e.g. AWS IAM), "Email & password" otherwise — instead of always saying "Email" when the site has no email login. Also correct the product name in the browser-automations / connections copy: standalone "Comp" → "Comp AI" (method chooser, live sign-in, empty states, connections page). * fix(api): use Stagehand mode:'cua' instead of deprecated cua:true Silences the "[DEPRECATED] The 'cua: true' option is deprecated. Use 'mode: cua' instead" warning from Stagehand's agent. Cosmetic — behavior is unchanged. * fix(api): don't mark a connection verified while still on the sign-in page The sign-in flow short-circuits to "already signed in → verified" when a classifier reads the page as logged-in. On vendors that bounce through sign-in hosts (e.g. AWS redirecting via signin.aws.amazon.com), the classifier misread a mid-redirect / sign-in page as logged-in, so the connection was marked verified instantly — without ever entering credentials or reaching the real form (the reported false success). Guard both the pre-fill "already signed in?" check and the post-fill verification with looksLikeSignInUrl(): a genuinely signed-in user is redirected OFF the sign-in host, so a "logged in" verdict while still on a signin/login/auth/sso URL is treated as a false positive (proceed to sign in / report failure instead of verifying). Also strengthen the classifier prompt to require positive evidence of being signed in and to never answer logged_in for a blank, loading, or redirecting page. * refactor(api): let the classifier judge login by URL + content, not a regex Replace the hardcoded sign-in URL patterns (looksLikeSignInUrl) with a URL-aware classifier: pass the browser's current URL into the login classifier so the model itself decides whether we're on the real app or still on a sign-in / identity-provider page. It generalizes to any vendor (it already knows hosts like signin.aws.amazon.com or login.microsoftonline.com) without us enumerating patterns, and still fixes the AWS false "signed in" — the classifier no longer calls a mid-redirect / sign-in page "logged in". The URL is a hint layered on the page content; it degrades to content-only when unavailable. * fix(api): wait for sign-in redirects to settle before reading/filling navigateToSignIn opened the "Sign in" link then waited a flat 1.5s — too short for vendors that bounce through a redirect chain (e.g. AWS from the marketing homepage → signin.aws.amazon.com), so classification and credential fill ran against the stale homepage (no fields), producing the misleading "Entering username/password" steps on a page that has none. Now wait until the URL stops changing (bounded ~5.6s) before proceeding, so we act on the real sign-in form. Also nudge the instruction to include "Sign in to Console"-style links. (For AWS, pasting the direct sign-in URL still avoids the homepage hunt entirely.) * feat(app): show the vendor's real identifier-field label in sign-in steps The live sign-in steps showed a generic "Entering username" even when the vendor's field is something else (e.g. AWS "IAM username") — extra fields already stream their detected labels, but the identifier didn't, which read as hardcoded. Thread the detected identifier label from the capture form through the sign-in call to the step log, so the streamed step names the real field. Kept out-of-band from credential storage/resolution (no change to how secrets are stored) and the fill stays semantic, so nothing about the working sign-in changes; scheduled runs fall back to the generic label. Password label is universal, left as-is. * fix(app): don't trap the connect flow on a stale resumed step Cancelling the connect flow left its resume-state in localStorage, so re-opening "Connect a vendor" reloaded the last in-progress step (e.g. "choose sign-in method" for the previous vendor) with no way back — the user couldn't start a new vendor or escape. handleCancel now clears the resume-state (fixes both the Connections page and the evidence task, since the flow is shared). The Connections page additionally clears on open, so "Connect a vendor" there always starts fresh at URL entry (a settings page has no "resume where I left off"). * fix(api): bring the sign-in page to front so the live view follows it When a vendor opens its sign-in in a new tab (e.g. AWS from its marketing homepage), the AI signed in on the real form — reaching the 2FA prompt — but the session live view kept showing the front tab (the homepage), so the user couldn't see the sign-in page or complete the 2FA take-over. Evidence runs already handle this via resolveEvidencePage + bringEvidencePageToFront; the connect sign-in never did. Reuse resolveEvidencePage after navigating to the sign-in form to bring the right page to front (best-effort). For AWS, pasting the direct sign-in URL also reduces the redirect/tab hopping. * revert(api): drop the sign-in bring-to-front (picked the wrong tab) resolveEvidencePage prefers the initial page when the target hostname doesn't match (AWS's entered URL redirects to a different host), so it brought the marketing homepage to front instead of the sign-in tab — making the live-view mismatch worse. Revert to reassess with a correct multi-tab approach. * feat(app): live view follows the AI's active tab during sign-in When a vendor opens its sign-in in a new tab (e.g. AWS from its homepage), the AI worked on that tab but the live view stayed pinned to the original (the marketing homepage), so the user couldn't watch — or complete a 2FA take-over on — the real form. Follow the active tab (designer option B): after navigating to the sign-in (and after the SSO click), resolve the live-view URL of the tab the AI is actually on via Browserbase `sessions.debug()` per-tab URLs, and stream it over the run's metadata as `signinLiveViewUrl`; the connect flow re-points its iframe to it. Tab selection (match by URL, then hostname, then newest) is a pure `selectLiveViewUrl` with unit tests. Best-effort — the live view keeps its current tab if lookup fails. * fix(api): screenshot the tab the agent ended on, not the stale start page An automation's screenshot and its pass/fail check disagreed: the check read the real page (via stagehand) — e.g. the AWS IAM users list — but the screenshot captured the marketing homepage. Cause: selectEvidencePage matched the ENTERED URL's host (the sign-in host), and when the agent navigated to a different host (console.aws.amazon.com) in a new tab, no tab matched, so it fell back to the initial (homepage) tab. Prefer the newest non-initial tab (where the agent actually ended up) over the stale initial page, so the screenshot matches what the check judged. Same-tab navigations are unchanged (only the initial page is open). * feat(browser-automations): per-vendor 2FA setup-key guidance Users must give Comp AI the authenticator setup key (TOTP seed), not the 6-digit code, for unattended scheduled sign-ins — but they rarely know where to find it. Add AI-generated, per-vendor instructions (no hardcode): - BrowserMfaInstructionsService: generateObject (Sonnet) + strict schema with a confidence flag; confidence-gated to a single universal fallback so we never show shaky invented steps. In-memory per-hostname TTL cache (1 day) — public, cheap-to-regenerate guidance, so no DB migration; a shared DB cache is a drop-in swap behind getInstructions. Web-search grounding is the next step. - GET /v1/browserbase/mfa-instructions?host=... (integration:read). - MfaSetupHelp: lazy 'How do I find this key?' collapsible under the setup-key field, shown in both the task and settings connect flows. - Manage panel 'add 2FA later': 2FA checkbox + setup-key field + helper in Change login, reusing the credentials endpoint that already stores the TOTP seed. Tests: api 4, app 6 (+ existing browser suites green: api 140, app 97). * feat(browser-automations): ground 2FA setup steps in the vendor's live docs Before generating the per-vendor authenticator instructions, pull the vendor's current help docs via Firecrawl web search (v2/search) and feed them to the model, so steps track the live UI instead of the model's training snapshot. - Best-effort: no FIRECRAWL_API_KEY, a failed/timed-out request, or empty results all fall back to ungrounded generation — grounding never blocks, and the confidence gate still protects the output. - Grounding runs inside generate(), so the per-hostname cache means Firecrawl is hit at most once per vendor per day. - New 'grounded' flag flows to the UI: MfaSetupHelp shows a subtle 'Checked against this vendor's current help docs' trust line when true. Tests: api +2 (grounded path, Firecrawl-failure path), app +1 (trust line). Suites green: api 142, app 98. * feat(browser-automations): implement the Unattended 2FA design (2a + 2d) Surface 1 (2a — quiet disclosure): the 'How do I find this key?' helper is now a muted link-row that opens a soft panel with numbered per-vendor steps and a single footer line — a green 'Checked against 's current help docs · ' trust line when grounded, or an honest tinted generic-fallback note. Loading and error (with retry) states included. Adds a checkedAt timestamp to the API payload. Surface 2 (2d — Automatic 2FA row): 'add 2FA later' moves out of Change login into its own row in the Manage panel's DETAILS group with an ON / NOT SET UP pill. 'Add authenticator key' stores ONLY the TOTP seed — no username/password re-entry: - New credential-storage methods read/attach/remove just the vault item's TOTP field (1Password items.get + put); status is read live from the vault, so no DB flag can drift and no migration is needed. - Endpoints: GET/POST/DELETE /v1/browserbase/profiles/:id/totp (integration read/update). useTotpStatus hook drives the pill; Replace / Turn off when on. Tests: api +7 (storage TOTP status/set/clear), app 2FA-row + 2a states green. Suites: api 149, app 100. * fix(browser-automations): strip markdown from 2FA steps + prefetch guidance - The model sometimes returns markdown bold (**Settings**); we render raw text, so the asterisks showed literally. Strip **/__/backticks in the service and tell the model to write plain text — the 2a design shows unstyled steps. - Prefetch the per-vendor guidance while the user fills in the connect form, so the 'How do I find this key?' helper opens instantly. Bounded by the per-host cache, so this just front-loads the once-per-vendor-per-day generation and warms it for the next person connecting the same vendor. Tests: api +1 (markdown strip). Suites: api 150, app 100. * fix(browser-automations): pointer cursor on buttons + spacing in 2FA hint - Native ), - Card: ({ children }: HTMLAttributes) =>
{children}
, - CardContent: ({ children }: HTMLAttributes) =>
{children}
, - CardDescription: ({ children }: HTMLAttributes) =>

{children}

, - CardHeader: ({ children }: HTMLAttributes) =>
{children}
, - CardTitle: ({ children }: HTMLAttributes) =>

{children}

, Input: (props: InputHTMLAttributes) => , - Label: ({ children, ...props }: LabelHTMLAttributes) => ( - + Spinner: () => , + Section: ({ + title, + description, + actions, + children, + }: { + title?: ReactNode; + description?: ReactNode; + actions?: ReactNode; + children?: ReactNode; + }) => ( +
+ {title &&

{title}

} + {description &&

{description}

} + {actions} + {children} +
), - Spinner: () => , })); vi.mock('@trycompai/design-system/icons', () => ({ - Globe: () => , - Renew: () => , - Screen: () => , + Add: () => , })); +// Sub-components are covered on their own; stub them to focus on the client. +vi.mock('./ConnectionsTable', () => ({ + ConnectionsTable: ({ connections }: { connections: Connection[] }) => ( +
{connections.length} rows
+ ), +})); +vi.mock('./ManageConnectionSheet', () => ({ ManageConnectionSheet: () => null })); +// The shared connect flow is covered by the task suite; stub it so we only test +// that the client renders it (and doesn't pull the whole task subsystem). +vi.mock( + '@/app/(app)/[orgId]/tasks/[taskId]/components/browser-automations/ConnectVendorLoginFlow', + () => ({ ConnectVendorLoginFlow: () =>
}), +); + +import { apiClient } from '@/lib/api-client'; import { BrowserConnectionClient } from './BrowserConnectionClient'; +const profile: Connection = { + id: 'bap_1', + hostname: 'github.com', + loginIdentity: 'ci-bot@acme.com', + displayName: 'GitHub', + status: 'verified', + vaultExternalItemRef: 'op://vault/item', +}; + describe('BrowserConnectionClient permission gating', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + beforeEach(() => vi.clearAllMocks()); - it('shows Connect Browser button when user has integration:create permission', () => { + it('shows "Connect a vendor" and the table when the user can create integrations', () => { setMockPermissions(ADMIN_PERMISSIONS); - render(); + render(); - const connectButton = screen.getByRole('button', { name: /connect browser/i }); - expect(connectButton).toBeInTheDocument(); - expect(connectButton).not.toBeDisabled(); + expect(screen.getByRole('button', { name: /connect a vendor/i })).toBeInTheDocument(); + expect(screen.getByTestId('table')).toHaveTextContent('1 rows'); }); - it('hides Connect Browser button when user lacks integration:create permission', () => { + it('hides "Connect a vendor" for a read-only user but still lists connections', () => { setMockPermissions(AUDITOR_PERMISSIONS); - render(); + render(); - expect(screen.queryByRole('button', { name: /connect browser/i })).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: /open browser/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /connect a vendor/i })).not.toBeInTheDocument(); + expect(screen.getByTestId('table')).toBeInTheDocument(); }); - it('hides Connect Browser button when user has no permissions', () => { - setMockPermissions({}); - render(); + it('shows an empty state when there are no connections', () => { + setMockPermissions(ADMIN_PERMISSIONS); + render(); - expect(screen.queryByRole('button', { name: /connect browser/i })).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: /open browser/i })).not.toBeInTheDocument(); + expect(screen.getByText(/no connections yet/i)).toBeInTheDocument(); + expect(screen.queryByTestId('table')).not.toBeInTheDocument(); }); - it('still shows the URL input regardless of permissions', () => { - setMockPermissions({}); - render(); + it('keeps existing connections visible when the profiles refresh fails', async () => { + setMockPermissions(ADMIN_PERMISSIONS); + // A failed refresh (apiClient resolves with an error, it doesn't throw) must + // NOT blank the list to the "no connections" empty state. + vi.mocked(apiClient.get).mockResolvedValueOnce({ error: 'boom' } as never); + render(); + + expect(await screen.findByTestId('table')).toHaveTextContent('1 rows'); + expect(screen.queryByText(/no connections yet/i)).not.toBeInTheDocument(); + }); + + it('opens the shared connect flow when "Connect a vendor" is clicked', () => { + setMockPermissions(ADMIN_PERMISSIONS); + render(); - expect(screen.getByLabelText('Website URL')).toBeInTheDocument(); + expect(screen.queryByTestId('connect-flow')).not.toBeInTheDocument(); + fireEvent.click(screen.getAllByRole('button', { name: /connect a vendor/i })[0]); + expect(screen.getByTestId('connect-flow')).toBeInTheDocument(); }); }); diff --git a/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionClient.tsx b/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionClient.tsx index 5064647673..848d3da829 100644 --- a/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionClient.tsx @@ -1,284 +1,312 @@ 'use client'; +// Reuse the task flow's proven connect/reconnect experience here so org-level +// connections use the same reliable path (method detection + automated +// credential entry + a working live takeover) instead of a bespoke, flaky one. +import { ConnectVendorLoginFlow } from '@/app/(app)/[orgId]/tasks/[taskId]/components/browser-automations/ConnectVendorLoginFlow'; +import { clearConnectState } from '@/app/(app)/[orgId]/tasks/[taskId]/components/browser-automations/connect-flow-storage'; import { usePermissions } from '@/hooks/use-permissions'; import { apiClient } from '@/lib/api-client'; -import { - Badge, - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Input, - Label, - Spinner, -} from '@trycompai/design-system'; -import { Globe, Screen } from '@trycompai/design-system/icons'; +import { Button, Section } from '@trycompai/design-system'; +import { Add } from '@trycompai/design-system/icons'; import { useCallback, useEffect, useState } from 'react'; -import { BrowserConnectionInstructions } from './BrowserConnectionInstructions'; -import { BrowserConnectionLiveView } from './BrowserConnectionLiveView'; -import { - BrowserConnectionProfileList, - type BrowserConnectionProfile, -} from './BrowserConnectionProfileList'; +import { toast } from 'sonner'; +import { methodOf, type Connection } from './connection-format'; +import { ConnectionsTable } from './ConnectionsTable'; +import { ManageConnectionSheet } from './ManageConnectionSheet'; -interface ResolveProfileResponse { - profile: BrowserConnectionProfile & { contextId: string }; - isNew: boolean; +interface BrowserConnectionClientProps { + organizationId: string; + initialProfiles?: Connection[]; } -interface SessionResponse { - sessionId: string; - liveViewUrl: string; -} +/** The connect/reconnect flow the page is currently showing (full-screen). */ +type ActiveFlow = { kind: 'connect' } | { kind: 'reconnect'; connection: Connection }; -interface AuthStatusResponse { - isLoggedIn: boolean; - username?: string; -} +// Stable resume-state key for the shared connect flow on this page. Cleared each +// time the flow opens so "Connect a vendor" always starts fresh (a settings page +// has no "resume where I left off" — that would trap the user on a stale step). +const CONNECT_FLOW_KEY = 'org-connections'; -interface VerifyProfileResponse { - profile: BrowserConnectionProfile; - auth: AuthStatusResponse; -} - -type Status = 'idle' | 'loading' | 'session-active' | 'checking'; +/** + * Org-level browser connections manager. Lists every vendor login the org has, + * with health, and wires the shared connect flow for connect / reconnect plus + * the profile API for rename / change-login / remove. + */ +export function BrowserConnectionClient({ + organizationId: _organizationId, + initialProfiles = [], +}: BrowserConnectionClientProps) { + const { hasPermission } = usePermissions(); + const canConnect = hasPermission('integration', 'create'); + const canUpdate = hasPermission('integration', 'update'); + const canDelete = hasPermission('integration', 'delete'); -interface BrowserConnectionClientProps { - organizationId: string; -} + const [profiles, setProfiles] = useState(initialProfiles); + const [flow, setFlow] = useState(null); + const [busy, setBusy] = useState(false); -export function BrowserConnectionClient({ organizationId }: BrowserConnectionClientProps) { - const { hasPermission } = usePermissions(); - const canManageBrowser = hasPermission('integration', 'create'); - const [status, setStatus] = useState('idle'); - const [profileId, setProfileId] = useState(null); - const [profiles, setProfiles] = useState([]); - const [sessionId, setSessionId] = useState(null); - const [liveViewUrl, setLiveViewUrl] = useState(null); - const [urlToCheck, setUrlToCheck] = useState('https://github.com'); - const [authStatus, setAuthStatus] = useState(null); - const [error, setError] = useState(null); - const hasContext = profiles.some((profile) => profile.status === 'verified'); + const [manageConnection, setManageConnection] = useState(null); + const [manageOpen, setManageOpen] = useState(false); - const checkContextStatus = useCallback(async () => { - try { - const res = await apiClient.get('/v1/browserbase/profiles'); - if (res.data) { - setProfiles(res.data); - const verifiedProfile = res.data.find((profile) => profile.status === 'verified'); - const firstProfile = verifiedProfile ?? res.data[0]; - setProfileId(firstProfile?.id ?? null); - } - } catch { - // Ignore + const fetchProfiles = useCallback(async () => { + const res = await apiClient.get('/v1/browserbase/profiles'); + // apiClient resolves (doesn't throw) on an HTTP error — surface it and keep + // the connections we already have instead of blanking the list to "none". + if (res.error) { + toast.error('Could not load connections. Please refresh.'); + return; } + setProfiles(Array.isArray(res.data) ? res.data : []); }, []); useEffect(() => { - checkContextStatus(); - }, [checkContextStatus]); - - const handleStartSession = async () => { - let startedSessionId: string | null = null; - try { - setError(null); - setStatus('loading'); + void fetchProfiles(); + }, [fetchProfiles]); - const profileRes = await apiClient.post( - '/v1/browserbase/profiles/resolve', - { url: urlToCheck }, - ); - if (profileRes.error || !profileRes.data) { - throw new Error(profileRes.error || 'Failed to create auth profile'); - } - setProfileId(profileRes.data.profile.id); - setProfiles((currentProfiles) => { - const rest = currentProfiles.filter( - (profile) => profile.id !== profileRes.data?.profile.id, - ); - return profileRes.data ? [profileRes.data.profile, ...rest] : currentProfiles; - }); + // After the shared flow connects or reconnects, refresh the list and return. + const handleFlowDone = useCallback( + (message: string) => { + void fetchProfiles(); + setFlow(null); + toast.success(message); + }, + [fetchProfiles], + ); - const sessionRes = await apiClient.post( - `/v1/browserbase/profiles/${profileRes.data.profile.id}/session`, - {}, - ); - if (sessionRes.error || !sessionRes.data) { - throw new Error(sessionRes.error || 'Failed to create session'); - } - startedSessionId = sessionRes.data.sessionId; - setSessionId(startedSessionId); - setLiveViewUrl(sessionRes.data.liveViewUrl); + // Always start the shared flow from a clean slate on this page. + const openConnect = useCallback(() => { + clearConnectState(CONNECT_FLOW_KEY); + setFlow({ kind: 'connect' }); + }, []); - // Navigate to the URL - await apiClient.post('/v1/browserbase/navigate', { - sessionId: startedSessionId, - url: urlToCheck, - }); + const handleReconnect = useCallback((connection: Connection) => { + setManageOpen(false); + clearConnectState(CONNECT_FLOW_KEY); + setFlow({ kind: 'reconnect', connection }); + }, []); - setStatus('session-active'); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to start session'); - setSessionId(null); - setLiveViewUrl(null); - setStatus('idle'); + const handleManage = useCallback((connection: Connection) => { + setManageConnection(connection); + setManageOpen(true); + }, []); - // If we created a session but navigation failed, close it to avoid orphaned sessions - if (startedSessionId) { - try { - await apiClient.post('/v1/browserbase/session/close', { sessionId: startedSessionId }); - } catch { - // Ignore cleanup errors (don't mask original error) + const handleRename = useCallback( + async (connection: Connection, name: string) => { + setBusy(true); + try { + const res = await apiClient.patch( + `/v1/browserbase/profiles/${connection.id}`, + { displayName: name }, + ); + if (res.error) { + toast.error(res.error || 'Could not rename.'); + return; } + await fetchProfiles(); + setManageOpen(false); + toast.success('Connection renamed.'); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Could not rename.'); + } finally { + setBusy(false); } - } - }; + }, + [fetchProfiles], + ); - const handleCheckAuth = async () => { - if (!sessionId || !profileId) return; + const handleChangeLogin = useCallback( + async (connection: Connection, creds: { username: string; password: string }) => { + setBusy(true); + try { + const res = await apiClient.post( + `/v1/browserbase/profiles/${connection.id}/credentials`, + creds, + ); + if (res.error) { + toast.error(res.error || 'Could not update the login.'); + return; + } + await fetchProfiles(); + setManageOpen(false); + toast.success('Login updated. Reconnect to verify it works.'); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Could not update the login.'); + } finally { + setBusy(false); + } + }, + [fetchProfiles], + ); + const handleSetTotp = useCallback(async (connection: Connection, totpSeed: string) => { + setBusy(true); try { - setError(null); - setStatus('checking'); - - const res = await apiClient.post( - `/v1/browserbase/profiles/${profileId}/verify`, - { sessionId, url: urlToCheck }, + const res = await apiClient.post( + `/v1/browserbase/profiles/${connection.id}/totp`, + { totpSeed }, ); - if (res.error || !res.data) { - throw new Error(res.error || 'Failed to check auth'); + if (res.error) { + toast.error(res.error || 'Could not save the authenticator key.'); + return; } + toast.success('Automatic 2FA is on. Scheduled runs generate the code for you.'); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Could not save the authenticator key.'); + } finally { + setBusy(false); + } + }, []); - setAuthStatus(res.data.auth); - setProfiles((currentProfiles) => { - const rest = currentProfiles.filter((profile) => profile.id !== res.data?.profile.id); - return res.data ? [res.data.profile, ...rest] : currentProfiles; - }); - - // Close the session after checking - await apiClient.post('/v1/browserbase/session/close', { sessionId }); - setSessionId(null); - setLiveViewUrl(null); - setProfileId(null); - setStatus('idle'); + const handleClearTotp = useCallback(async (connection: Connection) => { + setBusy(true); + try { + const res = await apiClient.delete( + `/v1/browserbase/profiles/${connection.id}/totp`, + ); + if (res.error) { + toast.error(res.error || 'Could not turn off automatic 2FA.'); + return; + } + toast.success('Automatic 2FA turned off.'); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to check auth'); - setStatus('session-active'); + toast.error(err instanceof Error ? err.message : 'Could not turn off automatic 2FA.'); + } finally { + setBusy(false); } - }; + }, []); - const handleCloseSession = async () => { - if (sessionId) { + const handleRemove = useCallback( + async (connection: Connection) => { + setBusy(true); try { - await apiClient.post('/v1/browserbase/session/close', { sessionId }); - } catch { - // Ignore + const res = await apiClient.delete( + `/v1/browserbase/profiles/${connection.id}`, + ); + if (res.error) { + toast.error(res.error || 'Could not remove.'); + return; + } + await fetchProfiles(); + setManageOpen(false); + toast.success('Connection removed.'); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Could not remove.'); + } finally { + setBusy(false); } - } - setSessionId(null); - setLiveViewUrl(null); - setProfileId(null); - setStatus('idle'); - }; + }, + [fetchProfiles], + ); + + // Connect / reconnect take over the whole panel via the shared flow — the same + // one the evidence task uses, so behaviour is identical and reliable. + if (flow) { + const reconnect = + flow.kind === 'reconnect' + ? { + url: flow.connection.lastAuthCheckUrl || `https://${flow.connection.hostname}`, + mode: methodOf(flow.connection), + } + : undefined; + return ( + handleFlowDone('Connection added.')} + onReconnected={() => handleFlowDone('Connection reconnected.')} + onCancel={() => setFlow(null)} + /> + ); + } return ( -
- {/* Status Card */} - - -
-
-
- -
-
- Browser Session - - {hasContext - ? 'At least one browser auth profile is verified' - : 'No verified browser auth profile yet'} - -
-
- - {hasContext ? 'Connected' : 'Not Connected'} - +
}> + Connect a vendor + + ) : undefined + } + > +
+ {/* Orient anyone who lands here from Settings without knowing the feature: + what it does, that it's unattended, and where the automations live. */} +
+
+ How it works
- - - {error &&

{error}

} +
    +
  1. + 1. + + Connect a vendor login here — Comp AI signs in for you, including any + two-factor codes. + +
  2. +
  3. + 2. + + On a schedule it signs in, screenshots the required page as audit evidence, + and re-signs in on its own when a session expires. + +
  4. +
  5. + 3. + + You add the automations that use these logins inside an{' '} + evidence task, in its + “Browser evidence” section. + +
  6. +
+
- {status === 'idle' && ( -
-
- -
-
- setUrlToCheck(e.target.value)} - /> -
- {canManageBrowser && ( - - )} -
-

- Open a browser session to authenticate with websites. Your login session will be - saved and used for browser automations. -

+ {profiles.length === 0 ? ( +
+
+
No connections yet
+

+ Connect a vendor login so Comp AI can sign in and capture evidence for your + browser automations. +

+ {canConnect && ( +
+
- - {authStatus && ( -
-
-
- - {authStatus.isLoggedIn - ? `Logged in${authStatus.username ? ` as ${authStatus.username}` : ''}` - : 'Not logged in'} - -
-
- )} -
- )} - - {status === 'loading' && ( -
- - Starting browser session... -
- )} - - - - {(status === 'session-active' || status === 'checking') && liveViewUrl && ( - +
+ ) : ( + )} - - -
+ +
+
); } diff --git a/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionInstructions.tsx b/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionInstructions.tsx deleted file mode 100644 index fa9e4fbed4..0000000000 --- a/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionInstructions.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/design-system'; - -export function BrowserConnectionInstructions() { - return ( - - - How it works - - -
    -
  1. - Create a service account - Use a - dedicated account for browser automations when possible. -
  2. -
  3. - Authenticate per site - Each hostname gets - its own auth profile and Browserbase context. -
  4. -
  5. - Complete 2FA manually - Live View supports - manual approvals; Comp does not store passwords or TOTP secrets. -
  6. -
  7. - Reconnect when needed - Expired or blocked - profiles are shown above and skipped by scheduled runs. -
  8. -
-
-
- ); -} diff --git a/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionLiveView.tsx b/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionLiveView.tsx deleted file mode 100644 index 43ffa900d4..0000000000 --- a/apps/app/src/app/(app)/[orgId]/settings/browser-connection/components/BrowserConnectionLiveView.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import { - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@trycompai/design-system'; -import { Renew } from '@trycompai/design-system/icons'; - -export function BrowserConnectionLiveView({ - liveViewUrl, - isChecking, - canManageBrowser, - onCheckAuth, - onClose, -}: { - liveViewUrl: string; - isChecking: boolean; - canManageBrowser: boolean; - onCheckAuth: () => void; - onClose: () => void; -}) { - return ( - - -
-
- Browser Session - - Log in to websites below. Your session will be saved for automations. - -
-
- - -
-
-
- -
-