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:
Celal Zamanoğlu 2026-03-23 22:10:58 +03:00 committed by GitHub
parent cdf1dfe16c
commit 750fb22a3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 80 additions and 6 deletions

View file

@ -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")

View file

@ -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(

View file

@ -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,

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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