diff --git a/.github/workflows/12-check-unit-tests.yml b/.github/workflows/12-check-unit-tests.yml index 157d81e470..a2a5f3e103 100644 --- a/.github/workflows/12-check-unit-tests.yml +++ b/.github/workflows/12-check-unit-tests.yml @@ -88,7 +88,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-browsers + key: ${{ runner.os }}-playwright-browsers-${{ hashFiles('web/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright-browsers- # System deps for chromium only (not the full webkit/firefox apt set, # which pulls in the gstreamer/audio/video stack and takes ~14m). Runs @@ -98,10 +100,23 @@ jobs: working-directory: web/tests run: pnpm exec playwright install-deps chromium - - name: Install Playwright if not cached - if: (github.event_name != 'workflow_dispatch' || contains(fromJSON('["all","web-only"]'), inputs.packages)) && steps.restore-playwright-cache.outputs.cache-hit != 'true' + - name: Install Playwright browser + if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all","web-only"]'), inputs.packages) working-directory: web/tests - run: pnpm exec playwright install chromium + run: | + for attempt in 1 2 3; do + echo "::group::playwright install chromium (attempt ${attempt}/3)" + if timeout 180 pnpm exec playwright install chromium; then + echo "::endgroup::" + echo "browser install succeeded on attempt ${attempt}" + exit 0 + fi + echo "::endgroup::" + echo "attempt ${attempt} stalled or failed; retrying after 5s..." + sleep 5 + done + echo "playwright browser install failed after 3 attempts" >&2 + exit 1 - name: Run web unit tests if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all","web-only"]'), inputs.packages) diff --git a/api/oss/tests/pytest/unit/services/test_db_manager.py b/api/oss/tests/pytest/unit/services/test_db_manager.py index 0e380438ef..8b93b80261 100644 --- a/api/oss/tests/pytest/unit/services/test_db_manager.py +++ b/api/oss/tests/pytest/unit/services/test_db_manager.py @@ -55,7 +55,9 @@ def _patch_core_session(monkeypatch, memberships): @pytest.mark.asyncio -async def test_get_default_workspace_id_prefers_owner_membership(monkeypatch): +async def test_get_default_workspace_id_ignores_owner_role(monkeypatch): + # Owner-role is NOT preferred: under multi-org an invitee owns their own + # empty personal workspace, so the oldest membership wins regardless of role. owner_workspace_id = uuid4() editor_workspace_id = uuid4() @@ -77,7 +79,7 @@ async def test_get_default_workspace_id_prefers_owner_membership(monkeypatch): workspace_id = await db_manager.get_default_workspace_id(str(uuid4())) - assert workspace_id == str(owner_workspace_id) + assert workspace_id == str(editor_workspace_id) @pytest.mark.asyncio diff --git a/api/pyproject.toml b/api/pyproject.toml index d35b5004b1..49b67d707e 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "api" -version = "0.104.0" +version = "0.104.1" description = "Agenta API" requires-python = ">=3.11,<3.14" authors = [ diff --git a/api/uv.lock b/api/uv.lock index caf5b6e239..46e0565926 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agenta" -version = "0.104.0" +version = "0.104.1" source = { editable = "../sdks/python" } dependencies = [ { name = "agenta-client" }, @@ -70,7 +70,7 @@ dev = [ [[package]] name = "agenta-client" -version = "0.104.0" +version = "0.104.1" source = { editable = "../clients/python" } dependencies = [ { name = "httpx" }, @@ -259,7 +259,7 @@ wheels = [ [[package]] name = "api" -version = "0.104.0" +version = "0.104.1" source = { virtual = "." } dependencies = [ { name = "agenta" }, diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index 3a2a8eb324..8c6f2dd026 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agenta-client" -version = "0.104.0" +version = "0.104.1" description = "Fern-generated Python client for the Agenta API." requires-python = ">=3.11,<3.14" authors = [ diff --git a/clients/python/uv.lock b/clients/python/uv.lock index e1a385a8fb..bc64e2f822 100644 --- a/clients/python/uv.lock +++ b/clients/python/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11, <3.14" [[package]] name = "agenta-client" -version = "0.104.0" +version = "0.104.1" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/hosting/docker-compose/ee/docker-compose.gh.local.yml b/hosting/docker-compose/ee/docker-compose.gh.local.yml index 7e72548082..20465efb50 100644 --- a/hosting/docker-compose/ee/docker-compose.gh.local.yml +++ b/hosting/docker-compose/ee/docker-compose.gh.local.yml @@ -409,6 +409,7 @@ services: - --api.dashboard=true - --api.insecure=true - --providers.docker + - --providers.docker.constraints=Label(`com.docker.compose.project`,`${COMPOSE_PROJECT_NAME:-agenta-ee-gh-local}`) - --entrypoints.web.address=:80 # === STORAGE ============================================== # volumes: diff --git a/hosting/docker-compose/ee/docker-compose.gh.yml b/hosting/docker-compose/ee/docker-compose.gh.yml index a74e799626..83adfeda76 100644 --- a/hosting/docker-compose/ee/docker-compose.gh.yml +++ b/hosting/docker-compose/ee/docker-compose.gh.yml @@ -400,6 +400,7 @@ services: command: - --api.dashboard=true - --providers.docker + - --providers.docker.constraints=Label(`com.docker.compose.project`,`${COMPOSE_PROJECT_NAME:-agenta-ee-gh}`) - --entrypoints.web.address=:80 # === STORAGE ============================================== # volumes: diff --git a/hosting/docker-compose/oss/docker-compose.gh.local.yml b/hosting/docker-compose/oss/docker-compose.gh.local.yml index 4bc21b5293..98efae76df 100644 --- a/hosting/docker-compose/oss/docker-compose.gh.local.yml +++ b/hosting/docker-compose/oss/docker-compose.gh.local.yml @@ -409,6 +409,7 @@ services: command: - --api.dashboard=true - --providers.docker + - --providers.docker.constraints=Label(`com.docker.compose.project`,`${COMPOSE_PROJECT_NAME:-agenta-oss-gh-local}`) - --entrypoints.web.address=:80 # === STORAGE ============================================== # volumes: diff --git a/hosting/docker-compose/oss/docker-compose.gh.yml b/hosting/docker-compose/oss/docker-compose.gh.yml index d39ffb8643..b217180177 100644 --- a/hosting/docker-compose/oss/docker-compose.gh.yml +++ b/hosting/docker-compose/oss/docker-compose.gh.yml @@ -430,6 +430,7 @@ services: command: - --api.dashboard=true - --providers.docker + - --providers.docker.constraints=Label(`com.docker.compose.project`,`${COMPOSE_PROJECT_NAME:-agenta-oss-gh}`) - --entrypoints.web.address=:80 # === STORAGE ============================================== # volumes: diff --git a/hosting/docker-compose/oss/ssl/traefik.yml b/hosting/docker-compose/oss/ssl/traefik.yml index e479f9af83..acbe3fa6f7 100644 --- a/hosting/docker-compose/oss/ssl/traefik.yml +++ b/hosting/docker-compose/oss/ssl/traefik.yml @@ -25,4 +25,5 @@ certificatesResolvers: entryPoint: "web" # Validates domain ownership via port 80 providers: - docker: {} + docker: + constraints: "Label(`com.docker.compose.project`,`agenta-gh-ssl`)" diff --git a/hosting/kubernetes/helm/Chart.yaml b/hosting/kubernetes/helm/Chart.yaml index d38313fb77..1905796e59 100644 --- a/hosting/kubernetes/helm/Chart.yaml +++ b/hosting/kubernetes/helm/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: agenta description: A Helm chart for deploying Agenta (OSS or EE) on Kubernetes type: application -version: 0.104.0 -appVersion: "v0.104.0" +version: 0.104.1 +appVersion: "v0.104.1" keywords: - agenta - llm diff --git a/sdks/python/agenta/sdk/engines/running/handlers.py b/sdks/python/agenta/sdk/engines/running/handlers.py index fbc515674a..ac1174c9ad 100644 --- a/sdks/python/agenta/sdk/engines/running/handlers.py +++ b/sdks/python/agenta/sdk/engines/running/handlers.py @@ -2220,7 +2220,10 @@ async def chat_v0( message = response.choices[0].message # type: ignore - return message.model_dump(exclude_none=True) # type: ignore + # Normalize to the canonical Message shape (drops provider-specific fields). + return Message.model_validate(message.model_dump(exclude_none=True)).model_dump( + exclude_none=True + ) @instrument(ignore_inputs=["parameters"]) diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index e5ef492fb9..49333aad01 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agenta" -version = "0.104.0" +version = "0.104.1" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" requires-python = ">=3.11,<3.14" diff --git a/sdks/python/uv.lock b/sdks/python/uv.lock index aa2895ade4..37f42a9323 100644 --- a/sdks/python/uv.lock +++ b/sdks/python/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11, <3.14" [[package]] name = "agenta" -version = "0.104.0" +version = "0.104.1" source = { editable = "." } dependencies = [ { name = "agenta-client" }, @@ -83,7 +83,7 @@ dev = [ [[package]] name = "agenta-client" -version = "0.104.0" +version = "0.104.1" source = { editable = "../../clients/python" } dependencies = [ { name = "httpx" }, diff --git a/services/pyproject.toml b/services/pyproject.toml index 20a4d1c799..8572ba45e7 100644 --- a/services/pyproject.toml +++ b/services/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "services" -version = "0.104.0" +version = "0.104.1" description = "Agenta Services (Chat & Completion)" requires-python = ">=3.11,<3.14" authors = [ diff --git a/services/uv.lock b/services/uv.lock index 4aa66882b9..e0b40277f3 100644 --- a/services/uv.lock +++ b/services/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agenta" -version = "0.104.0" +version = "0.104.1" source = { editable = "../sdks/python" } dependencies = [ { name = "agenta-client" }, @@ -70,7 +70,7 @@ dev = [ [[package]] name = "agenta-client" -version = "0.104.0" +version = "0.104.1" source = { editable = "../clients/python" } dependencies = [ { name = "httpx" }, @@ -2378,7 +2378,7 @@ wheels = [ [[package]] name = "services" -version = "0.104.0" +version = "0.104.1" source = { virtual = "." } dependencies = [ { name = "agenta" }, diff --git a/web/ee/package.json b/web/ee/package.json index cfd9748b6d..c75047b2d3 100644 --- a/web/ee/package.json +++ b/web/ee/package.json @@ -1,6 +1,6 @@ { "name": "@agenta/ee", - "version": "0.104.0", + "version": "0.104.1", "private": true, "engines": { "node": "24.x" diff --git a/web/oss/package.json b/web/oss/package.json index ad5f9e31ed..118c6b6dc7 100644 --- a/web/oss/package.json +++ b/web/oss/package.json @@ -1,6 +1,6 @@ { "name": "@agenta/oss", - "version": "0.104.0", + "version": "0.104.1", "private": true, "engines": { "node": "24.x" diff --git a/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/index.tsx b/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/index.tsx index 90e2bf80ee..0d943a1480 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/index.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/index.tsx @@ -217,6 +217,10 @@ const PlaygroundVariantConfig: React.FC< revisionId={variantId} onRefinePrompt={handleRefinePrompt} viewMode={viewMode} + // Embedded (drawer) renders the variant config + // header non-sticky, so the section headers have + // nothing to clear — pin them at the scroll top. + stickyHeaderTop={embedded ? 0 : 48} /> diff --git a/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionDrawerContent.tsx b/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionDrawerContent.tsx index 2bb7320975..606c3c05b5 100644 --- a/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionDrawerContent.tsx +++ b/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionDrawerContent.tsx @@ -23,7 +23,10 @@ interface TraceDrawerContentProps { } const SessionDrawerContent = ({onClose, onToggleWidth, isExpanded}: TraceDrawerContentProps) => { - const [selected, setSelected] = useState("") + // Default-select the "Session" root node so the tree opens with a selection + // instead of nothing highlighted. The root node's key is always "root", and + // selecting it is a no-op in handleSelect (it early-returns for "root"). + const [selected, setSelected] = useState("root") const {isLoading} = useSessionDrawer() if (isLoading) { return ( diff --git a/web/oss/src/components/SidebarBanners/state/atoms.ts b/web/oss/src/components/SidebarBanners/state/atoms.ts index 6724400836..8697bd6e11 100644 --- a/web/oss/src/components/SidebarBanners/state/atoms.ts +++ b/web/oss/src/components/SidebarBanners/state/atoms.ts @@ -19,6 +19,13 @@ export const PRIORITY_ORDER: Record = { trial: 3, // Lowest priority - show after other banners are dismissed } +/** + * Maximum number of dismissible sidebar banners a user should have to clear. + * Apply this before dismissal filtering so older changelog entries do not + * backfill the sidebar after each close. + */ +export const MAX_DISMISSIBLE_SIDEBAR_BANNERS = 2 + /** * Persisted atom for dismissed banner IDs. * Uses localStorage to remember which banners the user has dismissed. @@ -107,8 +114,17 @@ export const activeBannersAtom = atom((get) => { export const visibleBannersAtom = atom((get) => { const allBanners = get(activeBannersAtom) const dismissedIds = get(dismissedBannerIdsAtom) + const sortedBanners = [...allBanners].sort( + (a, b) => PRIORITY_ORDER[a.type] - PRIORITY_ORDER[b.type], + ) + + const cappedDismissibleBanners = sortedBanners + .filter((banner) => banner.dismissible) + .slice(0, MAX_DISMISSIBLE_SIDEBAR_BANNERS) + + const nonDismissibleBanners = sortedBanners.filter((banner) => !banner.dismissible) - return allBanners + return [...cappedDismissibleBanners, ...nonDismissibleBanners] .filter((banner) => !dismissedIds.includes(banner.id)) .sort((a, b) => PRIORITY_ORDER[a.type] - PRIORITY_ORDER[b.type]) }) diff --git a/web/oss/src/components/pages/observability/components/SessionsTable/components/Cells/SessionIdCell.tsx b/web/oss/src/components/pages/observability/components/SessionsTable/components/Cells/SessionIdCell.tsx index 0fcd143913..be8dc956e3 100644 --- a/web/oss/src/components/pages/observability/components/SessionsTable/components/Cells/SessionIdCell.tsx +++ b/web/oss/src/components/pages/observability/components/SessionsTable/components/Cells/SessionIdCell.tsx @@ -4,7 +4,10 @@ import {Tag} from "antd" export const SessionIdCell = ({sessionId}: {sessionId: string}) => { return ( - + # {sessionId} diff --git a/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx b/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx index 0d15650646..188b362b27 100644 --- a/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx +++ b/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx @@ -2,7 +2,7 @@ import {useCallback, useEffect, useMemo, useState} from "react" import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table" import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table" -import {useAtomValue, useSetAtom} from "jotai" +import {useAtomValue, useSetAtom, useStore} from "jotai" import dynamic from "next/dynamic" import {SessionDrawer} from "@/oss/components/SharedDrawers/SessionDrawer" @@ -49,6 +49,14 @@ const SessionsTable: React.FC = () => { resetSessionPages, } = useSessions() + // The per-session cells (Traces count, First input, metrics, …) read their + // data from page-level atoms keyed by session id (e.g. `sessionsSpansAtom`). + // Without this, the table mounts its rows inside an isolated Jotai store + // (`useIsolatedStore` when no `store` is passed), where those atoms are empty + // — so every cell renders 0/"-" even though the data is loaded in the page + // store. Sharing the page store lets the cells resolve the real data. + const store = useStore() + const isNewUser = useAtomValue(isNewUserAtom) const onboardingStorageUserId = useAtomValue(onboardingStorageUserIdAtom) const openDrawer = useSetAtom(openSessionDrawerWithUrlAtom) @@ -111,7 +119,7 @@ const SessionsTable: React.FC = () => { const isEmptyState = sessionIds.length === 0 && !isLoading return ( -
+
{ ) : ( + store={store} tableScope={tableScope} columns={columns} rowKey="session_id" pagination={pagination} - autoHeight={false} resizableColumns enableExport={false} useSettingsDropdown={false} diff --git a/web/oss/src/components/pages/settings/APIKeys/APIKeys.tsx b/web/oss/src/components/pages/settings/APIKeys/APIKeys.tsx index 37425bc426..0684f398f9 100644 --- a/web/oss/src/components/pages/settings/APIKeys/APIKeys.tsx +++ b/web/oss/src/components/pages/settings/APIKeys/APIKeys.tsx @@ -100,8 +100,9 @@ const APIKeys: React.FC = () => { Make sure to copy your API Key now. You won’t be able to see it again!
-
+
{ > {data} - + copyToClipboard(data)} diff --git a/web/oss/src/state/newObservability/atoms/queries.ts b/web/oss/src/state/newObservability/atoms/queries.ts index 29b74ee98c..c63e3c1d85 100644 --- a/web/oss/src/state/newObservability/atoms/queries.ts +++ b/web/oss/src/state/newObservability/atoms/queries.ts @@ -509,15 +509,18 @@ export const sessionTraceCountAtomFamily = atomFamily((sessionId: string) => }), ) -// Sorted traces are required for time-based metrics (Start/End/Duration) -// We memoize this to avoid re-sorting for every time-related cell +// Sorted traces are required for time-based metrics (Start/End/Duration) and for +// the First input / Last output cells. Sort by `start_time` (falling back to +// `created_at`) to match the Session drawer's tree ordering +// (SessionTree sorts its "Trace N" nodes by start_time). Sorting by a different +// key here made the table's first/last trace diverge from the drawer's. const sessionSortedTracesAtomFamily = atomFamily((sessionId: string) => atom((get) => { const traces = get(sessionTracesAtomFamily(sessionId)) if (!traces.length) return [] - return [...traces].sort( - (a, b) => new Date(a.created_at || 0).getTime() - new Date(b.created_at || 0).getTime(), - ) + const sortKey = (t: (typeof traces)[number]) => + new Date(t.start_time || t.created_at || 0).getTime() + return [...traces].sort((a, b) => sortKey(a) - sortKey(b)) }), ) diff --git a/web/package.json b/web/package.json index 92c533d843..2053606d57 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "agenta-web", - "version": "0.104.0", + "version": "0.104.1", "workspaces": [ "ee", "oss", diff --git a/web/packages/agenta-api-client/package.json b/web/packages/agenta-api-client/package.json index 88e8c6a0dc..e4566dcda4 100644 --- a/web/packages/agenta-api-client/package.json +++ b/web/packages/agenta-api-client/package.json @@ -1,6 +1,6 @@ { "name": "@agentaai/api-client", - "version": "0.104.0", + "version": "0.104.1", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/web/packages/agenta-entity-ui/src/DrillInView/components/PlaygroundConfigSection.tsx b/web/packages/agenta-entity-ui/src/DrillInView/components/PlaygroundConfigSection.tsx index 0618b14bd8..a77bcad86e 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/components/PlaygroundConfigSection.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/components/PlaygroundConfigSection.tsx @@ -451,6 +451,15 @@ export interface PlaygroundConfigSectionProps { onRefinePrompt?: (promptKey: string) => void /** View mode controlled from parent (form/json/yaml) */ viewMode?: ConfigViewMode + /** + * Top offset (px) for the sticky section headers. Defaults to 48 to clear + * the sticky `PlaygroundVariantConfigHeader` (h-[48px], `sticky top-0`) that + * sits above the config in the full playground. In the embedded drawer that + * header is rendered non-sticky (`grow`), so there is nothing to clear — + * pass 0 there to keep the section headers flush with the scroll top instead + * of floating 48px down into the editor content. + */ + stickyHeaderTop?: number } function PlaygroundConfigSection({ @@ -461,6 +470,7 @@ function PlaygroundConfigSection({ moleculeAdapter, onRefinePrompt, viewMode: externalViewMode, + stickyHeaderTop = 48, }: PlaygroundConfigSectionProps) { const {llmProviderConfig} = useDrillInUI() @@ -1362,7 +1372,8 @@ function PlaygroundConfigSection({ return (
toggleSection(fieldKey)} >
@@ -1578,7 +1589,10 @@ function PlaygroundConfigSection({ ) : ( <> {!hasTopLevelObjectSection && ( -
+
Config
)} diff --git a/web/packages/agenta-ui/src/Editor/plugins/code/utils/pasteUtils.ts b/web/packages/agenta-ui/src/Editor/plugins/code/utils/pasteUtils.ts index 39c12219d0..10f0055617 100644 --- a/web/packages/agenta-ui/src/Editor/plugins/code/utils/pasteUtils.ts +++ b/web/packages/agenta-ui/src/Editor/plugins/code/utils/pasteUtils.ts @@ -117,8 +117,13 @@ export function $insertLinesWithSelectionAndIndent({ } } - // Clone trailing lines before removal - const clonedTrailingLines = linesAfter.map((l) => $copyNode(l)) + // Capture trailing-line TEXT before removal, then rebuild fresh nodes after + // insertion (see re-insertion below). We can't element-clone these lines: + // `$copyNode` assigns the clone a fresh key, and Lexical's ElementNode only + // carries child pointers when the key is unchanged — so a cloned CodeLineNode + // comes back EMPTY, wiping every line below the paste point. Rebuilding from + // text mirrors the large-paste fast path and preserves the content faithfully. + const trailingLineTexts = linesAfter.map((l) => l.getTextContent()) linesAfter.forEach((l) => l.remove()) // Remove current line currentLine.remove() @@ -174,11 +179,12 @@ export function $insertLinesWithSelectionAndIndent({ insertIdx++ } - // Add trailing lines (use clones) - clonedTrailingLines.forEach((l, i) => { + // Add trailing lines (rebuilt from captured text — see note above) + const trailingLanguage = parentBlock.getLanguage() + trailingLineTexts.forEach((text, i) => { const prevChild = $getLineAtIndex(parentBlock, insertIdx - 1 + i) if (prevChild) { - prevChild.insertAfter(l) + prevChild.insertAfter($createNodeForLineWithTabs(text, trailingLanguage)) } }) diff --git a/web/packages/agenta-ui/src/SharedEditor/SharedEditorImpl.tsx b/web/packages/agenta-ui/src/SharedEditor/SharedEditorImpl.tsx index 8c6a2e027f..ab93140354 100644 --- a/web/packages/agenta-ui/src/SharedEditor/SharedEditorImpl.tsx +++ b/web/packages/agenta-ui/src/SharedEditor/SharedEditorImpl.tsx @@ -453,12 +453,24 @@ const SharedEditor = ({ style={ { ...props.style, - interpolateSize: "allow-keywords", height: "var(--editor-h, auto)", overflow: "hidden", - transitionProperty: "height", - transitionDuration: "300ms", - transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)", + // The height transition exists only to animate the collapse + // toggle (which sets `--editor-h` to a fixed px ↔ auto). Code + // editors never use that toggle, so `--editor-h` stays `auto` + // and `interpolate-size: allow-keywords` would instead animate + // EVERY content-height change while typing/pasting — making the + // editor grow over 300ms and lurching the surrounding drawer's + // scroll. Disable the animation for codeOnly so growth is + // instant; keep it for the collapsible rich-text/prompt editors. + ...(editorProps?.codeOnly + ? {} + : { + interpolateSize: "allow-keywords", + transitionProperty: "height", + transitionDuration: "300ms", + transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)", + }), } as React.CSSProperties } onPasteCapture={handlePasteCapture}