Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "api"
version = "0.128.5"
version = "0.128.6"
description = "Agenta API"
requires-python = ">=3.11,<3.14"
authors = [
Expand Down
6 changes: 3 additions & 3 deletions api/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion clients/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "agenta-client"
version = "0.128.5"
version = "0.128.6"
description = "Fern-generated Python client for the Agenta API."
requires-python = ">=3.11,<3.14"
authors = [
Expand Down
2 changes: 1 addition & 1 deletion clients/python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions hosting/kubernetes/helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: agenta
description: A Helm chart for deploying Agenta (OSS or EE) on Kubernetes
type: application
version: 0.128.5
appVersion: "v0.128.5"
version: 0.128.6
appVersion: "v0.128.6"
keywords:
- agenta
- llm
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "agenta"
version = "0.128.5"
version = "0.128.6"
description = "Agenta is the open-source workspace for your agents. Build agents through chat, improve them with feedback, and share them with your team."
readme = "README.md"
requires-python = ">=3.11,<3.14"
Expand Down
4 changes: 2 additions & 2 deletions sdks/python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion services/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "services"
version = "0.128.5"
version = "0.128.6"
description = "Agenta Services (Chat & Completion)"
requires-python = ">=3.11,<3.14"
authors = [
Expand Down
6 changes: 3 additions & 3 deletions services/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web/ee/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agenta/ee",
"version": "0.128.5",
"version": "0.128.6",
"private": true,
"engines": {
"node": "24.x"
Expand Down
2 changes: 1 addition & 1 deletion web/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agenta/mobile",
"version": "0.128.5",
"version": "0.128.6",
"private": true,
"engines": {
"node": "24.x"
Expand Down
24 changes: 19 additions & 5 deletions web/mobile/src/features/playground/usePlaygroundSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import {useCallback, useEffect, useRef} from "react"
import {isLocalDraftId} from "@agenta/entities/shared"
import {workflowDetailQueryAtomFamily, workflowMolecule} from "@agenta/entities/workflow"
import {playgroundController, playgroundInitializedAtom} from "@agenta/playground"
import {projectIdAtom} from "@agenta/shared/state"
import {atom, useAtomValue, useStore} from "jotai"
import {atomFamily} from "jotai-family"
import {queryClientAtom} from "jotai-tanstack-query"
import {useRouter} from "next/router"

import {clearPlayground} from "./clearPlayground"
import {playgroundLatestRevisionIdAtomFamily} from "./playgroundLatestRevision"
import {
playgroundLatestRevisionIdAtomFamily,
playgroundLatestRevisionKey,
} from "./playgroundLatestRevision"
import {parseRevisionIds} from "./playgroundUrl"

/** How far the app and its selected revisions have loaded. */
Expand Down Expand Up @@ -109,10 +113,20 @@ export const usePlaygroundSelection = ({appId}: {appId: string}): PlaygroundSele
if (kept.length !== current.length) {
store.set(playgroundController.actions.setEntityIds, kept)
}
if (kept.length === 0 && latestId) select([latestId])
// `keptIds` only triggers the effect: in the commit that sets the playground up it is
// still the value from before, so the store is read instead. `appUsable` and
// `router.isReady` run it again in the commit that sets the playground up: the latest
if (kept.length === 0) {
// The latest revision comes from the query cache, not from `latestId`. The query
// library tells this screen of a new answer on a later task. Right after a delete
// the reload has answered, but `latestId` can still be the deleted revision.
const queryClient = store.get(queryClientAtom)
const latest = queryClient.getQueryState<string | null>(
playgroundLatestRevisionKey(appId, store.get(projectIdAtom)),
)
// A failed reload keeps the previous answer, which can be a revision just deleted.
if (latest && latest.status !== "error" && latest.data) select([latest.data])
}
// `keptIds` and `latestId` only trigger the effect: in the commit that sets the
// playground up `keptIds` is still the value from before, so the store is read instead.
// `appUsable` and `router.isReady` run it again in the commit that sets the playground up: the latest
// revision can answer before the app, and nothing else would run it then.
}, [appId, appUsable, keptIds, latestId, router.isReady, select, store])

Expand Down
43 changes: 40 additions & 3 deletions web/mobile/tests/unit/playgroundRevisionActions.render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@ import {playgroundController} from "@agenta/playground"
import {queryClient as hostQueryClient} from "@agenta/shared/api"
import {idbQueryStorage} from "@agenta/shared/api/persist"
import {projectIdAtom, sessionAtom} from "@agenta/shared/state"
import {QueryClientProvider, type QueryClient} from "@tanstack/react-query"
import {
defaultScheduler,
notifyManager,
QueryClientProvider,
type QueryClient,
} from "@tanstack/react-query"
import {getDefaultStore} from "jotai"
import {queryClientAtom} from "jotai-tanstack-query"
import {createRoot, type Root} from "react-dom/client"
import {afterEach, beforeEach, describe, expect, it, onTestFinished, vi} from "vitest"

import {playgroundLatestRevisionKey} from "@/features/playground/playgroundLatestRevision"
import {PlaygroundScreen} from "@/features/playground/PlaygroundScreen"

const {http, router} = vi.hoisted(() => ({
Expand Down Expand Up @@ -319,15 +325,23 @@ const find = (name: string) => {
),
].find((item) => label(item) === name || item.getAttribute("aria-label") === name)
}
/**
* Waits for a control to show. The compare view loads its code the first time two revisions
* show, which can take longer than one settle.
*/
const findSoon = async (name: string) => {
for (let i = 0; i < 40 && !find(name); i++) await settle()
return find(name)
}
const click = async (name: string) => {
const target = find(name)
const target = await findSoon(name)
expect(target, `a control named ${name}`).toBeDefined()
await act(async () => target!.click())
await settle()
}
/** Radix opens a menu on pointer down, not on click. */
const openMenu = async (name: string) => {
const trigger = find(name)
const trigger = await findSoon(name)
expect(trigger, `a menu named ${name}`).toBeDefined()
await act(async () => {
trigger!.dispatchEvent(
Expand Down Expand Up @@ -742,6 +756,29 @@ describe("Delete", () => {
expect(latestOnDisk?.id ?? null).not.toBe("rev-2")
})

it("After a delete, the selection takes the newest revision the reload answered, before the screen hears of it", async () => {
await mount()
expect(shown()).toEqual(["rev-2"])
// The server deletes rev-2. The latest-revision reload answers rev-1, but the screen
// hears of it later: the query library tells its observers on a later task. After a
// delete, the page takes the revision out in that gap.
world.revisions = world.revisions.filter((revision) => revision.id !== "rev-2")
const held: (() => void)[] = []
notifyManager.setScheduler((callback) => held.push(callback))
onTestFinished(() => notifyManager.setScheduler(defaultScheduler))
const latestKey = playgroundLatestRevisionKey("app-1", projectId)
await client.refetchQueries({queryKey: latestKey})
expect(client.getQueryData(latestKey)).toBe("rev-1")

act(() => store.set(playgroundController.actions.setEntityIds, []))

// The deleted revision must not come back.
expect(shown()).toEqual(["rev-1"])
notifyManager.setScheduler(defaultScheduler)
act(() => held.splice(0).forEach((callback) => callback()))
await expectShown(["rev-1"])
})

it("The dialog closes once the delete succeeds; a revision picked meanwhile stays with its edits", async () => {
await mount()
let release: () => void = () => undefined
Expand Down
2 changes: 1 addition & 1 deletion web/oss/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agenta/oss",
"version": "0.128.5",
"version": "0.128.6",
"private": true,
"engines": {
"node": "24.x"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {openEvaluatorDrawerAtom} from "@/oss/state/evaluator/evaluatorDrawerStor
import TabLabel from "../assets/TabLabel"
import {NewEvaluationModalContentProps} from "../types"

import {newEvaluationEvaluatorDrawerParams} from "./evaluatorDrawerParams"

const tabsContainerClass =
"h-full flex [&_.ant-tabs-content-holder]:pl-4 [&_.ant-tabs-content-holder]:flex-1 " +
"[&_.ant-tabs-content-holder]:overflow-auto [&_.ant-tabs-tab]:text-colorTextSecondary " +
Expand Down Expand Up @@ -133,13 +135,7 @@ const NewEvaluationModalContent: FC<NewEvaluationModalContentProps> = ({
return
}

openEvaluatorDrawer({
entityId: localId,
mode: "create",
onEvaluatorCreated,
// Created inline: stay in this modal instead of opening the new evaluator's page.
postCreateNavigation: "stay",
})
openEvaluatorDrawer(newEvaluationEvaluatorDrawerParams(localId, onEvaluatorCreated))
onSelectTemplate?.(evaluator)
},
[openEvaluatorDrawer, onSelectTemplate, onEvaluatorCreated],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
workflowRevisionDrawerCallbackAtom,
workflowRevisionDrawerIsolatedPlaygroundAtom,
workflowRevisionDrawerOpenAtom,
} from "@agenta/playground-ui/workflow-revision-drawer"
import {createStore} from "jotai"
import {describe, expect, it, vi} from "vitest"

import {openEvaluatorDrawerAtom} from "@/oss/state/evaluator/evaluatorDrawerStore"

import {newEvaluationEvaluatorDrawerParams} from "./evaluatorDrawerParams"

describe("New Evaluation evaluator drawer", () => {
it("opens in an isolated playground so closing it cannot delete the app's test cases", () => {
const store = createStore()
const onEvaluatorCreated = vi.fn()

store.set(
openEvaluatorDrawerAtom,
newEvaluationEvaluatorDrawerParams("local-evaluator", onEvaluatorCreated),
)

expect(store.get(workflowRevisionDrawerOpenAtom)).toBe(true)
expect(store.get(workflowRevisionDrawerIsolatedPlaygroundAtom)).toBe(true)
})

it("still hands the created evaluator to the modal", () => {
const store = createStore()
const onEvaluatorCreated = vi.fn()

store.set(
openEvaluatorDrawerAtom,
newEvaluationEvaluatorDrawerParams("local-evaluator", onEvaluatorCreated),
)
store.get(workflowRevisionDrawerCallbackAtom)?.({configId: "rev-1"} as never)

expect(onEvaluatorCreated).toHaveBeenCalledWith("rev-1")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {OpenDrawerParams} from "@/oss/state/evaluator/evaluatorDrawerStore"

/**
* Arguments for the evaluator creation drawer opened from the New Evaluation modal.
*
* `isolatedPlayground: true` runs the drawer in its own store. The modal can open on
* top of a playground that has test cases connected, and a shared store would let the
* drawer's close cleanup delete those rows.
*/
export const newEvaluationEvaluatorDrawerParams = (
entityId: string,
onEvaluatorCreated: ((configId?: string) => void) | undefined,
): OpenDrawerParams => ({
entityId,
mode: "create",
onEvaluatorCreated,
isolatedPlayground: true,
// Created inline: stay in this modal instead of opening the new evaluator's page.
postCreateNavigation: "stay",
})
2 changes: 1 addition & 1 deletion web/oss/src/state/evaluator/evaluatorDrawerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {atom} from "jotai"

type EvaluatorDrawerMode = "create" | "view"

interface OpenDrawerParams {
export interface OpenDrawerParams {
entityId: string
mode: EvaluatorDrawerMode
/** List of entity IDs for prev/next navigation */
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agenta-web",
"version": "0.128.5",
"version": "0.128.6",
"workspaces": [
"ee",
"mobile",
Expand Down
2 changes: 1 addition & 1 deletion web/packages/agenta-api-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentaai/api-client",
"version": "0.128.5",
"version": "0.128.6",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
Loading