mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 03:30:10 +00:00
feat: persist save_browser_session_intent checkbox state (SKY-8329) (#5203)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cdf1dfe16c
commit
750fb22a3e
7 changed files with 80 additions and 6 deletions
|
|
@ -0,0 +1,30 @@
|
|||
"""add save_browser_session_intent to credentials
|
||||
|
||||
Revision ID: 6e966003a58e
|
||||
Revises: 786acdf95243
|
||||
Create Date: 2026-03-23 19:04:17.856273+00:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "6e966003a58e"
|
||||
down_revision: Union[str, None] = "786acdf95243"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"credentials",
|
||||
sa.Column("save_browser_session_intent", sa.Boolean(), nullable=True, server_default=sa.text("false")),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("credentials", "save_browser_session_intent")
|
||||
|
|
@ -582,6 +582,7 @@ export type CredentialApiResponse = {
|
|||
browser_profile_id?: string | null;
|
||||
tested_url?: string | null;
|
||||
user_context?: string | null;
|
||||
save_browser_session_intent?: boolean | null;
|
||||
};
|
||||
|
||||
export function isPasswordCredential(
|
||||
|
|
|
|||
|
|
@ -195,10 +195,17 @@ function CredentialsModal({
|
|||
// reset by the time onSuccess runs, so we snapshot them here.
|
||||
const saveIntentRef = useRef<{
|
||||
shouldTestAfterSave: boolean;
|
||||
saveBrowserSessionIntent: boolean;
|
||||
testUrl: string;
|
||||
userContext: string;
|
||||
name: string;
|
||||
}>({ shouldTestAfterSave: false, testUrl: "", userContext: "", name: "" });
|
||||
}>({
|
||||
shouldTestAfterSave: false,
|
||||
saveBrowserSessionIntent: false,
|
||||
testUrl: "",
|
||||
userContext: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
// Cleanup polling on unmount
|
||||
useEffect(() => {
|
||||
|
|
@ -251,7 +258,10 @@ function CredentialsModal({
|
|||
if (editingCredential.tested_url) {
|
||||
setTestUrl(editingCredential.tested_url);
|
||||
}
|
||||
if (editingCredential.browser_profile_id) {
|
||||
if (
|
||||
editingCredential.save_browser_session_intent ||
|
||||
!!editingCredential.browser_profile_id
|
||||
) {
|
||||
setTestAndSave(true);
|
||||
}
|
||||
if (editingCredential.user_context) {
|
||||
|
|
@ -324,6 +334,7 @@ function CredentialsModal({
|
|||
setUserContext("");
|
||||
saveIntentRef.current = {
|
||||
shouldTestAfterSave: false,
|
||||
saveBrowserSessionIntent: false,
|
||||
testUrl: "",
|
||||
userContext: "",
|
||||
name: "",
|
||||
|
|
@ -532,18 +543,20 @@ function CredentialsModal({
|
|||
onSuccess: async (data) => {
|
||||
const {
|
||||
shouldTestAfterSave,
|
||||
saveBrowserSessionIntent,
|
||||
testUrl: capturedTestUrl,
|
||||
userContext: capturedUserContext,
|
||||
} = saveIntentRef.current;
|
||||
|
||||
// Save metadata (tested_url, user_context) on the credential via PATCH
|
||||
if (capturedTestUrl || capturedUserContext) {
|
||||
// Save metadata (tested_url, user_context, save_browser_session_intent) on the credential via PATCH
|
||||
if (capturedTestUrl || capturedUserContext || saveBrowserSessionIntent) {
|
||||
try {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
await client.patch(`/credentials/${data.credential_id}`, {
|
||||
name: data.name,
|
||||
...(capturedTestUrl && { tested_url: capturedTestUrl }),
|
||||
user_context: capturedUserContext?.trim() || null,
|
||||
save_browser_session_intent: saveBrowserSessionIntent,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort — credential was created, URL is just metadata
|
||||
|
|
@ -606,12 +619,13 @@ function CredentialsModal({
|
|||
onSuccess: async () => {
|
||||
const {
|
||||
shouldTestAfterSave,
|
||||
saveBrowserSessionIntent,
|
||||
testUrl: capturedTestUrl,
|
||||
userContext: capturedUserContext,
|
||||
name: capturedName,
|
||||
} = saveIntentRef.current;
|
||||
|
||||
// Persist metadata (tested_url, user_context) via PATCH
|
||||
// Persist metadata (tested_url, user_context, save_browser_session_intent) via PATCH
|
||||
if (editingCredential?.credential_id) {
|
||||
try {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
|
|
@ -621,6 +635,7 @@ function CredentialsModal({
|
|||
name: capturedName || editingCredential.name,
|
||||
...(capturedTestUrl && { tested_url: capturedTestUrl }),
|
||||
user_context: capturedUserContext?.trim() || null,
|
||||
save_browser_session_intent: saveBrowserSessionIntent,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
|
|
@ -680,20 +695,25 @@ function CredentialsModal({
|
|||
name,
|
||||
tested_url,
|
||||
user_context,
|
||||
save_browser_session_intent,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
tested_url?: string;
|
||||
user_context?: string | null;
|
||||
save_browser_session_intent?: boolean;
|
||||
}) => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
const body: Record<string, string | null> = { name };
|
||||
const body: Record<string, string | boolean | null> = { name };
|
||||
if (tested_url) {
|
||||
body.tested_url = tested_url;
|
||||
}
|
||||
if (user_context !== undefined) {
|
||||
body.user_context = user_context;
|
||||
}
|
||||
if (save_browser_session_intent !== undefined) {
|
||||
body.save_browser_session_intent = save_browser_session_intent;
|
||||
}
|
||||
const response = await client.patch<CredentialApiResponse>(
|
||||
`/credentials/${id}`,
|
||||
body,
|
||||
|
|
@ -795,6 +815,7 @@ function CredentialsModal({
|
|||
name,
|
||||
tested_url: url || undefined,
|
||||
user_context: ctx || null,
|
||||
save_browser_session_intent: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -813,6 +834,7 @@ function CredentialsModal({
|
|||
testStatus !== "completed" &&
|
||||
testUrl.trim() !== "" &&
|
||||
hasEditModeChanges,
|
||||
saveBrowserSessionIntent: testAndSave,
|
||||
testUrl: testUrl.trim(),
|
||||
userContext: userContext.trim(),
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -6299,6 +6299,7 @@ class AgentDB(BaseAlchemyDB):
|
|||
browser_profile_id: str | None | object = _UNSET,
|
||||
tested_url: str | None | object = _UNSET,
|
||||
user_context: str | None | object = _UNSET,
|
||||
save_browser_session_intent: bool | None | object = _UNSET,
|
||||
) -> Credential:
|
||||
async with self.Session() as session:
|
||||
credential = (
|
||||
|
|
@ -6319,6 +6320,8 @@ class AgentDB(BaseAlchemyDB):
|
|||
credential.tested_url = tested_url
|
||||
if user_context is not _UNSET:
|
||||
credential.user_context = user_context
|
||||
if save_browser_session_intent is not _UNSET:
|
||||
credential.save_browser_session_intent = save_browser_session_intent
|
||||
await session.commit()
|
||||
await session.refresh(credential)
|
||||
return Credential.model_validate(credential)
|
||||
|
|
|
|||
|
|
@ -1014,6 +1014,7 @@ class CredentialModel(Base):
|
|||
browser_profile_id = Column(String, nullable=True)
|
||||
tested_url = Column(String, nullable=True)
|
||||
user_context = Column(String(1000), nullable=True)
|
||||
save_browser_session_intent = Column(Boolean, nullable=True, default=False)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
|
|
|
|||
|
|
@ -429,6 +429,8 @@ async def rename_credential(
|
|||
update_kwargs["tested_url"] = data.tested_url
|
||||
if data.user_context is not None:
|
||||
update_kwargs["user_context"] = data.user_context
|
||||
if data.save_browser_session_intent is not None:
|
||||
update_kwargs["save_browser_session_intent"] = data.save_browser_session_intent
|
||||
updated = await app.DATABASE.update_credential(**update_kwargs)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=500, detail="Failed to update credential")
|
||||
|
|
@ -1867,6 +1869,7 @@ def _convert_to_response(credential: Credential) -> CredentialResponse:
|
|||
browser_profile_id=credential.browser_profile_id,
|
||||
tested_url=credential.tested_url,
|
||||
user_context=credential.user_context,
|
||||
save_browser_session_intent=credential.save_browser_session_intent,
|
||||
)
|
||||
elif credential.credential_type == CredentialType.CREDIT_CARD:
|
||||
credential_response = CreditCardCredentialResponse(
|
||||
|
|
@ -1881,6 +1884,7 @@ def _convert_to_response(credential: Credential) -> CredentialResponse:
|
|||
browser_profile_id=credential.browser_profile_id,
|
||||
tested_url=credential.tested_url,
|
||||
user_context=credential.user_context,
|
||||
save_browser_session_intent=credential.save_browser_session_intent,
|
||||
)
|
||||
elif credential.credential_type == CredentialType.SECRET:
|
||||
credential_response = SecretCredentialResponse(secret_label=credential.secret_label)
|
||||
|
|
@ -1892,6 +1896,7 @@ def _convert_to_response(credential: Credential) -> CredentialResponse:
|
|||
browser_profile_id=credential.browser_profile_id,
|
||||
tested_url=credential.tested_url,
|
||||
user_context=credential.user_context,
|
||||
save_browser_session_intent=credential.save_browser_session_intent,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Credential type not supported")
|
||||
|
|
|
|||
|
|
@ -181,6 +181,10 @@ class CredentialResponse(BaseModel):
|
|||
default=None,
|
||||
description="User-provided context describing the login sequence (e.g., 'click SSO button first')",
|
||||
)
|
||||
save_browser_session_intent: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether the user intends to save a browser session, regardless of test outcome",
|
||||
)
|
||||
|
||||
|
||||
class Credential(BaseModel):
|
||||
|
|
@ -216,6 +220,10 @@ class Credential(BaseModel):
|
|||
default=None,
|
||||
description="User-provided context describing the login sequence (e.g., 'click SSO button first')",
|
||||
)
|
||||
save_browser_session_intent: bool | None = Field(
|
||||
default=False,
|
||||
description="Whether the user intends to save a browser session, regardless of test outcome",
|
||||
)
|
||||
|
||||
created_at: datetime = Field(..., description="Timestamp when the credential was created")
|
||||
modified_at: datetime = Field(..., description="Timestamp when the credential was last modified")
|
||||
|
|
@ -241,6 +249,10 @@ class UpdateCredentialRequest(BaseModel):
|
|||
max_length=1000,
|
||||
description="Optional user-provided context describing the login sequence (e.g., 'click SSO button first')",
|
||||
)
|
||||
save_browser_session_intent: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether the user intends to save a browser session, regardless of test outcome",
|
||||
)
|
||||
|
||||
@field_validator("user_context", mode="before")
|
||||
@classmethod
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue