feat(agent): split-screen agent workspace (conversation left, content panel right) - #6432
feat(agent): split-screen agent workspace (conversation left, content panel right)#6432tsahimatsliah wants to merge 60 commits into
Conversation
The page was a scrolling document with a floating toolbar overlay. Rebuild it as a fixed two-column app shell: conversation on the left, an openable content panel on the right. Left column owns its own header (back, agent identity, status, panel launchers, pause, settings), an internally-scrolling transcript, and a compact docked composer — auto-growing textarea, icon-only quick actions, Enter to send. Right panel behaves like Claude's: it accumulates tabs as you open things (post, feed, activity, debug), each tab focusable and closable, resizable by its left edge, and it covers the column as a full overlay below laptop. Activity and Debug moved out of the left column's tab bar and into that panel, so the conversation stays a pure transcript. Drops the hero card, the header title morph, and the pane/modal view toggle with it: the split shell replaces all three.
`?demo=1` is the design surface for this mock-up; real data lands later once the backend exists. Split the page so the demo route renders purely from the mock fixtures — no interest/posts/findings queries, no feature gate, no auth wall — which makes the preview link reviewable by anyone without the flag or an account. The live route keeps its queries, flag redirect and ProtectedPage untouched.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Design review pass over the split workspace. Header: one 24px optical band, every control the same XSmall tertiary button with a 16px glyph on tertiary ink. The agent's identity tile is now its power switch — a status dot on the tile, a popover with an on/off toggle, cadence, last run, and jumps to activity and settings. That replaces the "Hunting" chip, whose tinted background read as loud and whose wording never said what the agent was doing. Panel buttons show open state by colour rather than a filled background, and back uses MoveTo. Transcript: the shared markdown styles are tuned for reading an article (17px on 1.7 leading, 24px paragraph gaps). Scoped overrides step the chat down to 15px on a 1.6 leading with 12px gaps. Agent turns lose their avatar, name and per-message date — the right-aligned user bubbles already separate the voices, so only a scheduled run keeps a marker. Posts in the transcript get a compact card instead of the full feed card, and the pick-list rows tighten to match. Composer: quick actions moved out of the box and below it as labelled subtle chips, the keyboard-hint line is gone, a gradient fades the transcript out behind the bar, and the send button becomes a stop square while a run is in flight. Adds transcript block primitives — embed row/card, code with copy and run, and table — plus an AgentBlocks Storybook page covering every variant.
… tabs The toggle wrote through `update()`, which returns early on the demo surface and whenever there is no interest loaded — so switching the agent off changed nothing you could see. Run state now lives in the context, seeded from the interest and owned locally after that, so the control responds everywhere. Switching off also interrupts a run in flight; leaving one going would contradict the switch that was just used. Pane tabs were a row of near-identical pills. They are now a full-height strip: the active tab takes a surface fill, a brand underline and a brand icon, inactive tabs sit quiet behind dividers and reveal their close button on hover.
MainLayout holds its paint until boot resolves, so the mock-only demo page was blank on any environment without a reachable API — including a local dev server. Fall back to a standalone shell when boot errors out, and let the workspace own the viewport there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`/agent/[id]?demo=1` renders mock data but still sits inside the app shell, and BootDataProvider replaces the whole tree when boot fails — so the demo is blank on any dev server without an API behind it. `_app` already short-circuits `/dev/*` past the shell, so put the workspace there with a static provider stack that Storybook now shares. Also stops a post view from surfacing as an unhandled rejection when the request fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the tabs - Chat column and composer go from 12/16px side padding to 20/32/40px, so the transcript stops hugging the frame when the panel takes half the width. - Header controls move from 24px buttons on a 16px glyph to 32px on 20px, the run tile with them, so the row reads at arm's length. - Panel tabs return to floating chips. The active one carries a brand fill rather than being the only filled one, which is what made three open tabs look alike. - Panel gains a close button; it replaces the mobile-only back arrow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel was on an ad-hoc 20px that matched neither the conversation nor the post page. It now carries the post page's own ramp (16/24/32), so an article opened in the panel reads exactly as it does on its own page, and the feed, activity and debug tabs align with it. Both header rows share one inset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Selected tab is now the only chip that is filled and outlined; the rest are bare text. Brand-float against surface-float was two 8% washes over the same background, which is why three open tabs looked alike. - Send control moves to a float button on a 20px glyph, nudged a pixel right so the airplane sits optically centred, and stays icon-only. Stop keeps the square. - New usage panel in the composer's status slot, the way Claude Code hangs one off its bar: allowances with a warning tier as they run out, and the routes out of the ceiling — Plus, or your own API key. Numbers are placeholders until the backend meters runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
32px chip, 8px radius, 16px icon, label truncating at 13rem, close revealed on hover and pinned on the selected tab. Selected sits on an opaque raised surface with a white icon; the rest are bare text on a hover fill. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r beam A sent prompt now reads as received in three places at once: a light travels the composer's border while the run is in flight, the transcript's pending turn becomes a status strip (elapsed time and what it is doing), and the mark on that strip is daily.dev's logo animating — its three strokes hop in a wave and reassemble into the intact logo for most of every 2s loop. Also: the run-state dot on the agent tile was mostly cut-out ring at 8px, so it never registered; and a prompt sent over a pending turn left the earlier reply spinning forever, since the timer only ever resolves the newest one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first pass rotated the whole layer inside an overflow-clipped padding box, which dragged the blur around with it and needed an opaque underlay to stop the light leaking through the composer's 8% surface. Rebuilt on the technique from beam.jakubantalik.com: the ring is cut with mask-composite, and the conic gradient's angle animates rather than the element rotating, so the head sweeps the perimeter at an even rate and stays in the border whatever sits behind it. Ring and blur are tuned up from the reference's values — this bar is 15:1, which stretches the same sweep far thinner than the near-square card the original targets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Swaps my port for the published component (border-beam@1.3.0, MIT, no runtime deps) at size md / colorVariant colorful / strength 1, so the animation, the colour ramps, the hue-shift and the fade in and out are the reference's own rather than an approximation of them. The hand-rolled CSS is gone. It mounts client-side only: the package injects its stylesheet as a React `<style>` child, and SSR escapes the quotes in its selectors. `<style>` holds raw text, so those entities are never decoded and every rule silently fails to match — the beam renders as nothing at all. Also: the panel feed is list cards at every width (grid gave nine findings a cover image each), and the run-state dot comes back down a size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel was a desktop card pinned over the conversation with no transition. On mobile it is now the app's own drawer, full screen, sliding in from the right and back out again on close, portalled to the root so no transformed ancestor traps it. The settings menu was a popover anchored to a gear in the top corner, which is a desktop shape; on mobile it is a bottom sheet, where the thumb already is. Add-to-chat has to be permanently visible on a device that cannot hover, and a 130px button beside a post title in 375px left the title four words wide and five lines tall. On touch it keeps the glyph and drops the words, and the pick rows stack their counts under the title below tablet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Explore now opens with search and agents as one object: a single field split down the middle, search on the right where it always was, agents on the left carrying a count when a run came back while you were elsewhere. They are the two ways of going to look for something — one for when you know what you want, one for when you would rather be told later — so they sit together rather than as a button that happens to be near a search box. "Create agent" joins Free form, Share a link, Poll and Live in the rail's + panel and in the phone's + drawer. Unlike the other four it opens a page rather than a composer kind, so it is a link. Both are behind `interest_agent`, and the Explore pair stands down entirely without a spotlight provider rather than throwing off the app shell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 tests across ten files, where there were none. They are written against what each thing promises rather than how it is built, and every one of them was checked by breaking the source under it: the drag tests fail if the commit reads a stale width or the panel renders off the store, the link test fails without `passHref`, the flag test fails if the gate goes, and so on. Three real defects surfaced while writing them: The Review button and the workspace's back arrow rendered as anchors with no href. `legacyBehavior` only injects one into a plain `<a>` child, not into a component, so both were links you could not middle-click, open in a new tab, or hear announced as links. A queued prompt ran twice in development. The hand-off to the next prompt was scheduled inside a state updater, and React re-runs updaters under StrictMode, which Next has on by default. It drains from an effect now. The feed dock collapsed to nothing when its column measured zero — the case on the first paint, and whenever the column is hidden. It spans the window until there is a real number to use. Coverage where it was aimed: commands, chat and attachments at 100%, the monitor 100%/98% branches, context 97%, workspace 96%, composer 91%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I had both entry points on screen at every width. They are the same door, so each width gets one of them. Below 656px the Explore pair is the way in and the floating bar stays away: a phone has too little screen to give a permanent inch of it to a bar, and the software keyboard would take the rest. From tablet up the bar is the way in and the pair stands aside. Both gates read the viewport client-side, since each control is present or absent rather than restyled, and both ask for tablet rather than "is mobile" so the unresolved first paint is empty instead of flashing the phone layout on a desktop. The dock drops its footer-nav allowance with the width it was for. Measured at the boundary: the pair is there at 655px and gone at 656px. Six new tests hold both halves, each checked by removing the gate under it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list said one of three things, and two of those were "hunting" and "paused" — so a screen whose whole job is being scanned had nothing to scan. It now carries seven: waiting for review, running now, watching, first run due, run failed, paused and stopped. One hue each, the word beside the dot, and only the running one animates. Five come off the interest itself, including two that were being conflated: stopped is its own status in the API and was reading as paused, and an agent that has never run now says so instead of claiming to be watching. A run in flight and a run that failed have no field to read, so they arrive on a typed extra that only mock data sets today. Mock data covers all seven, because a design surface where every row says the same word designs nothing. Two fixes found while doing it. A recent run that kept nothing was reading as "waiting for review" over a line saying "kept nothing" — it is watching. And the home screen's summary counted everything that was not paused as working, which included the failed and stopped ones. Also fixes the Explore pair not appearing on a phone at all: it was mounted inside the v2 page header, which is laptop-only by design, so a phone-only control sat in the one branch that can never draw on one. Every unit test passed, because they all render the component directly — so this one renders the layout and asserts the pair is on the page. And the blank chat after creating an agent: the prompt you typed became the agent's name and nothing else, leaving you to type it again to get a word out of it. A real agent has no stored transcript yet, so it now opens with that prompt as your first turn and the agent's answer under it, rebuilt from the interest — cadence, last run, and its findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The blank page had one cause: `AgentProvider` reads `initialMessages` with `useState`, which runs once — and the page builds that opening transcript from a query that is still in flight when the page mounts. So `[]` was locked in for good, and the later data never reached it. Both routes in hit it: opening an agent from the list, and landing on one just created. Three things fix it. The create mutation hands the new agent straight to the query cache, so the page it opens has the interest on its first render and the conversation is there with no wait at all. The provider now adopts an opening transcript that arrives late — but only while there is nothing to lose, never over a transcript with turns in it. And the page waits for the interest and its findings before drawing, since neither the name nor the opening turn can be honest without them. What it draws while waiting is a skeleton in the shape of the conversation, the way every chat worth copying does it: where the name goes, a turn from you, a reply under it, a finding under that, and the real composer frame so nothing moves when the content lands. The agents list does the same, mirroring its own rows. One slow, shallow pulse shared by every block — the 50% pulse Tailwind ships reads as an alarm when six run at once. Two claims the loading state was making on the way past: "None watching" and "Spawn your first agent", both said about agents that had not arrived yet. Also drops the glyph-only add-to-chat on touch: same button, same place, every width. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mobile Explore has no header but its search field, so mounting the pair under it asked for a search twice. The pair takes that slot instead. It stands down to the field it replaced anywhere it does not draw — from tablet up, with the flag off, on the search page itself — so the slot is never left empty and there is one flag evaluation, not two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Waiting for review" is the honest phrase and on a phone it took the whole row, leaving three letters of the name it described. One word each now, in a fixed column so the names start on one line, with the full phrase as the accessible name. The elapsed times lose "ago" — it was on every row and said nothing any of them did not — and the row chevron drops a size and a tier, since it is the same mark eight times over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Explore's pair was one field split down the middle, which read as a search box with something bolted to its left. Two buttons of the same weight instead, the Subtle variant at Medium. The agent list takes the reading a session list gets on a phone: the name leads, at full weight and nearly full width, since it is the only thing on a row that differs from the rows around it. State and last summary drop to a second line, cards get air between them instead of dividers, and a dot in the left gutter marks an agent that came back and is waiting on you — the one thing worth finding without reading. The gutter holds its width when empty so the names still start on one line. Skeletons follow the new shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ywhere else The card with the state under the title was the reading for a handset. Given width there is no reason to spend two lines on four facts, so from tablet up the row goes back to one thin line — state, name, what it last said, when — in a bordered block, with the small faint chevron. Same DOM at both readings: the wrappers become `display: contents` and the fields reorder into the row, so nothing is rendered twice for a screen reader to read twice. Skeletons follow both shapes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tsahimatsliah
left a comment
There was a problem hiding this comment.
Summary
Rebuilds /agent/[id] as a two-column workspace (transcript left, tabbed content panel right) plus new entry points on Explore, the feed dock, the sidebar and the footer + drawer. CI is green (build, typecheck_strict_changed, shared/webapp/extension tests). Almost all of the surface is new, feature-flagged code, so the risk concentrates in the few shared/global touchpoints rather than in the agent components themselves.
One blocking item, plus a set of non-blocking maintainability questions left inline.
Blocking
packages/shared/src/lib/featureManagement.ts— experiment flag default flipped totrue(self-flaggedTEMP-REVIEW). Detail inline.
Merge order
This is stacked on #6392, which is still a draft. The base needs to land (or this needs rebasing onto main) before merge, otherwise the whole mockup PR rides in with it.
Non-blocking
Left inline: global CSS reaching into ArticleList's Tailwind classes, duplicated logo path geometry, dead startRunRef, the new border-beam dependency in shared, missing unmount cleanup on the pane resize drag, flag-evaluation inconsistency for anonymous visitors, and the findings.length memo key.
Two things not attachable to a line:
- Analytics. The PR declares no new events, but this is an entirely new product surface: agent created, prompt sent, run stopped, panel/tab opened, finding feedback. If that is deliberate for the mockup stage, worth saying so in the description so it is not forgotten when the backend lands.
- Comment density. Root
AGENTS.mdasks for why-only comments and explicitly calls out multi-line narrative preambles above new components; a lot of the new files open with several paragraphs of prose. Most of that reasoning would survive better in the PR description/commits.
Verification
- Root +
packages/shared+featuresAGENTS.mdread - Diff reviewed against the stacked base, not
main - Shared/global blast radius traced (header, sidebar, footer, feed page,
Tooltip,useTrackPostView,utilities.css) - Flag gating and evaluation targeting checked on every entry point
- CI checks inspected (all passing)
- Not verified: preview in both sidebar variants (the
100dvhoffsets inshell.ts), and mobile overlay behaviour on a real device
Verdict
Request changes — restore the flag default to false; everything else is non-blocking.
Reviewed by AI.
| "@tiptap/extension-placeholder": "^3.22.5", | ||
| "@tiptap/react": "^3.22.5", | ||
| "@tiptap/starter-kit": "^3.22.5", | ||
| "border-beam": "1.3.0", |
There was a problem hiding this comment.
Non-blocking (question): border-beam is a new runtime dependency in shared for one decorative composer border, which means it is bundled into the extension too, and an extension revert needs a store rollout. It also needs a client-only mount to work at all (see the SSR note in AgentComposer). Is a CSS-only conic/gradient border (the file already has several hand-written keyframe effects) or keeping this webapp-side an option, rather than taking a third-party dep for the effect? Version is pinned exactly, which is good.
Reviewed by AI.
There was a problem hiding this comment.
Not removed — and this one is a product call rather than a technical one, so flagging it rather than resolving it.
I did port it to CSS: a masked conic gradient on the frame, ~40 lines, no dependency. The design owner rejected the result — the package is not a rotating beam but 34 radial-gradient blobs positioned round the border, 15 conic layers, 21 keyframe blocks and a 12s hue-shift, and a hand-written approximation reads visibly different. Reproducing it faithfully means vendoring 74KB of generated CSS, which is worse than depending on it.
What did change, in a82a658: it is fetched rather than imported. The chunk now reaches only the screen that draws it — nothing in the SSR output, nothing in the initial bundle, and nothing in an extension build, which was the substance of the concern. That also retires the hand-rolled isMounted gate that existed to survive the SSR quote-escaping; the frame renders bare until the module lands, which is the same thing the gate did, without the unexplained gap between mount and module.
Happy to revisit if the effect is ever open to changing.
The default was flipped to `true` so the surfaces were reachable on the preview, where GrowthBook devtools cannot flip a flag. Merging that would have shipped every entry point to 100% of signed-in users with no rollback short of a deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…isitor Four things the review found. The resize drag held two window listeners and two properties on `document.body`, and only the pointer coming up took them back: a panel that went away mid-drag left the whole app unselectable under a col-resize cursor until the next reload. One AbortController now, released on unmount too. The opening transcript was memoised on the findings' *count*, so a refetch returning the same number of different posts kept citing the old ones. Keyed on their ids. Both agent routes evaluated the flag for anonymous visitors, enrolling a population that can never see the feature. They now match every other entry point, and only a signed-in reader without the flag is redirected — an anonymous one falls through to the sign-in wall as before. `startRunRef` was written every render and read nowhere: dead since the queue drain moved into an effect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mark's three path strings were in the icon and in the thinking indicator independently, so a logo tweak would have left the indicator drawing the old shape with nothing failing. They live in `svg/logoGeometry` now and both read from it. Data only — the logo renders on every screen and the sampling code beside it carries a canvas. A test renders the icon and compares it to the geometry stroke for stroke, so drift is loud. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tylesheet's The container query reached into ArticleList's Tailwind output by name — `.mobileXL\:flex-row`, `.mobileXL\:w-60`, `.mr-4`, `.flex-\[unset\]` — and used `display: contents` to reorder its children. A spacing tweak in the card would have broken the panel silently, with no test or typecheck able to see it. `ArticleList` takes `isNarrow` instead: the same stacked layout it already has for a phone, asked for by a caller whose column the window knows nothing about. Default off, so every existing consumer keeps its DOM to the class. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd reads inline Three things from using it. Sending while scrolled up left the answer off-screen behind a "New reply" button. The transcript only ever grows when a prompt is sent, so growth means the reader just spoke — and sending is itself an act of arriving at the bottom. It follows unconditionally now and re-pins. Staying put is still right for what the agent says back on its own, which is the case the pill exists for. The armed command was a chip with an icon and a close button, taking a column out of the field so a long prompt wrapped into what was left. It is inline coloured text over the first line now, with the text indented past it — the first word of the prompt, the way a terminal agent shows it. Backspace on an empty field still takes it back, which is where the hand already is, so the close button is gone. The send sits at the foot of the field rather than the middle of it, and the transcript has a deeper foot: the last reply's hover actions were reading through the beam's glow. The beam itself stays the upstream component — 34 radial gradients, 15 conic layers and 21 keyframe blocks is not something to reproduce by hand. It is fetched rather than imported, so its 74KB reaches only the screen that draws it and no extension build, which also retires the hand-rolled mount gate it needed to survive SSR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four surfaces, all on today's backend, all behind the flag. **Share this agent** in the workspace header. What travels is the standing prompt, not the transcript: there is nowhere to send someone for a conversation, and a question that keeps working for the recipient is worth more than a snapshot of someone else's findings. The link lands on the agents screen with the prompt in the field — not running, because spawning off someone else's link would spend an allowance the reader never agreed to spend. **Share a finding** on every post row the agent produces, and on the cards in the content panel. Straight to the app's own share modal: squads, every external provider and the tracked copy link are already there, and a second share UI in one product is a second one to keep honest. **Copying a reply carries its links.** A reply's whole value is the posts it picked out, and the copy stripped to text — someone pasting it into Slack got a paragraph about five articles with no way to reach any of them. Markdown now, since that is what the places this gets pasted render. The plain-text form stays for quoting back into the prompt. Row actions became one cluster so share and add-to-chat share a single geometry decision rather than two floats drifting apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…er has to fail softly Pressing "Share this agent" threw `Failed to construct 'URL'`. The link was built from `webappUrl`, which is a bare path in development, and the share pipeline hangs the referral params on it with `new URL(link)`. Built from the browser's own origin now, which is what the other in-app share links use for exactly this reason. Behind it was a second failure that swallowed the first. `getShortUrl` returns its query from inside its own `try`, so the rejection leaves before the `catch` can see it and the fallback it was given never runs: any share, anywhere in the app, dies as an unhandled rejection when the shortener is unreachable, with a button that appears to do nothing. Awaited inside the `try`, so an unshortened link with its referral params intact is what comes back. Both are covered, and both tests fail if the fix is reverted. Verified in the browser with the API down: the clipboard gets `…/agent?q=Cool+zig+projects&userid=…&cid=share_agent` and nothing throws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacked on #6392. Base is
feat/interest-agent-mockup, so the diff here is only the split-screen rework.Demo link: https://claude-interest-agent-pr-a6c00e.preview.app.daily.dev/agent/demo?demo=1 — no flag, no login, pure mock data.
Changes
Rebuilds
/agent/[id]from a scrolling document with a floating toolbar into a fixed two-column app shell, modelled on Claude Code's conversation + side panel.Left column — conversation only. One 48px header row (back, agent identity, live status, panel launchers, pause, settings), an internally-scrolling transcript, and a compact composer docked at the bottom of the column instead of floating over the page. The composer is a single auto-growing textarea with Enter to send, Shift+Enter for a newline, and one dense control row: the three quick actions as icon-only buttons with tooltips, and send. The "working" state is a thin pulsing status line above the box rather than its own bar.
Right panel — Claude's tab behaviour. Opening a post or a feed link opens it as a tab; opening another adds a tab rather than replacing. Tabs focus on click, close on their own ✕, and closing the active one falls through to its neighbour. The panel resizes by its left edge (persisted via
usePersistentContext), Esc closes the active tab, and below laptop it covers the column as a full overlay with a back arrow.Activity and Debug are panels, not tabs. They open in the right panel from the header buttons, the same way you'd open a terminal alongside a conversation. That is what let the Chat/Activity/Debug tab bar disappear and the left column stay a pure transcript.
Demo route is self-contained.
?demo=1renders purely from the mock fixtures: no interest/posts/findings queries, no feature gate, no auth wall. Real data stays on the live route and is the backend team's side of the work. Anyone with the preview link can review without the flag or an account.Removed with the shell that replaced them: the hero card (its stats fold into a compact transcript intro), the scroll-morphing header title, and the pane/modal view toggle plus
AgentFeedModal.Manual Testing
The page is auth-gated and its boot needs a backend, so
packages/storybook/stories/features/interests/AgentWorkspace.stories.tsxdrives the real components against the mock fixtures. Verified there: both panel types open, a post opens into a second tab, tabs focus/close/fall through, the composer sends and the transcript auto-scrolls, mobile overlay behaviour, and light + dark themes.Worth a look on the preview in both sidebar variants — the shell's
100dvhoffsets differ between the v1 global header and the v2 floating card, and that is the one thing Storybook cannot prove.Events
No new tracking events.
Experiment
No new experiments. The existing
featureInterestAgentflag still gates the live route; the demo route is deliberately outside it.🤖 Generated with Claude Code
Preview domain
https://claude-interest-agent-pr-a6c00e.preview.app.daily.dev