diff --git a/src/ui/src/builder/BuilderApp.vue b/src/ui/src/builder/BuilderApp.vue
index 006b2f3a6..b42f7aace 100644
--- a/src/ui/src/builder/BuilderApp.vue
+++ b/src/ui/src/builder/BuilderApp.vue
@@ -10,7 +10,10 @@
/>
+
-
+
@@ -121,7 +125,7 @@
-
+
+
@@ -185,9 +192,12 @@ import BuilderAppSocketTimeoutModal from "./BuilderAppSocketTimeoutModal.vue";
import { useSocketTimeout } from "./useSocketTimeout";
import BlueprintsNavigationStack from "@/components/blueprints/BlueprintsNavigationStack.vue";
import BuilderDeprecationBanner from "./BuilderDeprecationBanner.vue";
+import BuilderEditBlockedState from "./BuilderEditBlockedState.vue";
+import BuilderRemigrationWarningDialog from "./BuilderRemigrationWarningDialog.vue";
const DEPRECATION_BANNER_DISMISSED_KEY =
"customAgentDeprecationBannerDismissed";
+const REMIGRATION_WARNING_QUERY_PARAM = "showRemigrationWarning";
provide(injectionKeys.isAutogenModalShown, ref(false));
@@ -227,18 +237,57 @@ const isPostCutoff = computed(() =>
wf.featureFlags.value.includes("afterDeprecationCutoffAbv2"),
);
const isOrganizationAdmin = computed(() => wf.isOrganizationAdmin.value);
+const canEditAgent = computed(() => wf.canEditAgent.value);
const showDeprecationBanner = computed(
() =>
wf.isWriterCloudApp.value &&
(isPostCutoff.value ||
(isPreCutoffFlagEnabled.value && !dismissed.value)),
);
+const showRemigrationWarningDialog = ref(false);
function onDismissBanner() {
localStorage.setItem(DEPRECATION_BANNER_DISMISSED_KEY, "true");
dismissed.value = true;
}
+function isPageRefresh() {
+ const navigation = performance.getEntriesByType("navigation")[0] as
+ | PerformanceNavigationTiming
+ | undefined;
+
+ return navigation?.type === "reload";
+}
+
+function consumeRemigrationWarningQueryParam() {
+ const url = new URL(window.location.href);
+ const shouldShowFromOpener =
+ url.searchParams.get(REMIGRATION_WARNING_QUERY_PARAM) === "true";
+
+ if (shouldShowFromOpener) {
+ url.searchParams.delete(REMIGRATION_WARNING_QUERY_PARAM);
+ window.history.replaceState(
+ window.history.state,
+ document.title,
+ url.toString(),
+ );
+ }
+
+ return shouldShowFromOpener;
+}
+
+onMounted(() => {
+ const shouldShowFromOpener = consumeRemigrationWarningQueryParam();
+
+ showRemigrationWarningDialog.value =
+ shouldShowFromOpener &&
+ !isPageRefresh() &&
+ canEditAgent.value &&
+ wf.isWriterCloudApp.value &&
+ isPreCutoffFlagEnabled.value &&
+ !isPostCutoff.value;
+});
+
const tracking = useWriterTracking(wf);
const toasts = useToasts();
@@ -310,6 +359,8 @@ const notes = computed(() =>
);
async function handleKeydown(ev: KeyboardEvent) {
+ if (!canEditAgent.value) return;
+
if (ev.key === "Escape") {
ssbm.setSelection(null);
ssbm.expandedEditorForComponent.value = null;
@@ -406,11 +457,13 @@ async function handleKeydown(ev: KeyboardEvent) {
}
function handleRendererDragover(ev: DragEvent) {
+ if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;
assignInsertionCandidacy(ev);
}
function handleRendererDrop(ev: DragEvent) {
+ if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;
ssbm.setSelection(null);
const dropInfo = dropComponent(ev);
@@ -425,6 +478,7 @@ function handleRendererDrop(ev: DragEvent) {
}
function handleRendererClick(ev: PointerEvent): void {
+ if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;
const unselectableEl = (ev.target as HTMLElement).closest(
@@ -471,10 +525,12 @@ function handleRendererClick(ev: PointerEvent): void {
ssbm.handleSelectionFromEvent(ev, targetId, targetInstancePath, "click");
}
function handleRendererDblClick() {
+ if (!canEditAgent.value) return;
ssbm.isSettingsBarCollapsed.value = false;
}
const handleRendererDragStart = (ev: DragEvent) => {
+ if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;
const targetEl = (ev.target as HTMLElement).closest(
@@ -499,6 +555,7 @@ const handleRendererDragStart = (ev: DragEvent) => {
};
function handleRendererDragEnd(ev: DragEvent) {
+ if (!canEditAgent.value) return;
ssbm.setSelection(null);
removeInsertionCandidacy(ev);
}
@@ -593,6 +650,12 @@ onUnmounted(() => {
display: grid;
}
+.mainGrid.blocked {
+ pointer-events: none;
+ filter: grayscale(1);
+ opacity: 0.52;
+}
+
.builderHeader {
grid-column: 1 / 3;
grid-row: 1;
diff --git a/src/ui/src/builder/BuilderEditBlockedState.vue b/src/ui/src/builder/BuilderEditBlockedState.vue
new file mode 100644
index 000000000..98e01bf20
--- /dev/null
+++ b/src/ui/src/builder/BuilderEditBlockedState.vue
@@ -0,0 +1,50 @@
+
+
+
+
You don't have permission to edit this agent
+
+ Ask an org admin or AI Studio full access user to update this
+ agent, or to grant you AI Studio builder access.
+
+
+
+
+
+
diff --git a/src/ui/src/builder/BuilderRemigrationWarningDialog.vue b/src/ui/src/builder/BuilderRemigrationWarningDialog.vue
new file mode 100644
index 000000000..8d0819b74
--- /dev/null
+++ b/src/ui/src/builder/BuilderRemigrationWarningDialog.vue
@@ -0,0 +1,42 @@
+
+
+
+ Changes made here won't sync automatically. If you already migrated
+ to Agent Builder v2, please migrate again to update your app.
+
+
+
+
+
+
+
diff --git a/src/ui/src/core/index.ts b/src/ui/src/core/index.ts
index 38f8a4a9e..33e406aef 100644
--- a/src/ui/src/core/index.ts
+++ b/src/ui/src/core/index.ts
@@ -71,6 +71,7 @@ export function generateCore() {
apiKey?: string;
baseUrl?: string;
isOrganizationAdmin?: boolean;
+ canEdit?: boolean;
}
| undefined
>();
@@ -147,6 +148,12 @@ export function generateCore() {
const isWriterCloudApp = computed(() =>
Boolean(writerAppId.value || writerOrgId.value),
);
+ const canEditAgent = computed(
+ () =>
+ !isWriterCloudApp.value ||
+ isOrganizationAdmin.value ||
+ Boolean(writerApplication.value?.canEdit),
+ );
/**
* Initialise the core.
@@ -1176,6 +1183,7 @@ export function generateCore() {
writerApiKey,
writerBaseUrl,
isOrganizationAdmin,
+ canEditAgent,
...navigationStack,
getFirstBlueprint,
getFirstPage,
diff --git a/src/ui/src/tests/mocks.ts b/src/ui/src/tests/mocks.ts
index cc892ecbd..1c6ca7bb8 100644
--- a/src/ui/src/tests/mocks.ts
+++ b/src/ui/src/tests/mocks.ts
@@ -44,7 +44,13 @@ export function buildMockCore() {
const userFunctions = shallowRef([]);
const featureFlags = shallowRef([]);
const writerApplication = shallowRef<
- { id: string; organizationId: string } | undefined
+ | {
+ id: string;
+ organizationId: string;
+ isOrganizationAdmin?: boolean;
+ canEdit?: boolean;
+ }
+ | undefined
>();
core.userFunctions = userFunctions;
@@ -67,6 +73,15 @@ export function buildMockCore() {
() => Number(writerApplication.value?.organizationId) || undefined,
);
core.writerAppId = computed(() => writerApplication.value?.id);
+ core.isOrganizationAdmin = computed(
+ () => writerApplication.value?.isOrganizationAdmin ?? false,
+ );
+ core.canEditAgent = computed(
+ () =>
+ !core.isWriterCloudApp.value ||
+ core.isOrganizationAdmin.value ||
+ Boolean(writerApplication.value?.canEdit),
+ );
vi.spyOn(core, "sendComponentUpdate").mockImplementation(async () => {});
diff --git a/src/ui/src/wds/WdsModal.vue b/src/ui/src/wds/WdsModal.vue
index 0938dd333..8e6f3cec2 100644
--- a/src/ui/src/wds/WdsModal.vue
+++ b/src/ui/src/wds/WdsModal.vue
@@ -5,6 +5,7 @@
class="WdsModal colorTransformer"
:class="{
'WdsModal--overflow': allowOverflow,
+ 'WdsModal--compact': size == 'compact',
'WdsModal--wide': size == 'wide',
}"
tabindex="-1"
@@ -78,7 +79,7 @@ export type ModalAction = {
const props = defineProps({
title: { type: String, required: false, default: null },
size: {
- type: String as PropType<"normal" | "wide">,
+ type: String as PropType<"compact" | "normal" | "wide">,
required: false,
default: "normal",
},
@@ -132,6 +133,13 @@ const { title, actions } = toRefs(props);
max-width: 240ch;
}
+.WdsModal--compact .WdsModal__main {
+ width: min(560px, calc(100vw - 32px));
+ max-width: calc(100vw - 32px);
+ gap: 20px;
+ padding: 24px;
+}
+
.WdsModal--overflow .WdsModal__main,
.WdsModal--overflow .WdsModal__main__content {
overflow: unset;
diff --git a/src/writer/app_runner.py b/src/writer/app_runner.py
index 9d717ab9e..e90b3b85e 100644
--- a/src/writer/app_runner.py
+++ b/src/writer/app_runner.py
@@ -191,12 +191,15 @@ def _handle_session_init(
writer_base_url = os.getenv("WRITER_BASE_URL", "https://api.writer.com")
is_org_admin_header = headers.get("x-is-org-admin", "").lower()
is_org_admin = is_org_admin_header == "true"
+ can_edit_header = headers.get("x-can-edit-agent", "").lower()
+ can_edit = is_org_admin or can_edit_header == "true"
if writer_app_id is not None and writer_org_id is not None:
writer_application = WriterApplicationInformation(
id=writer_app_id,
organizationId=writer_org_id,
baseUrl=writer_base_url,
isOrganizationAdmin=is_org_admin,
+ canEdit=can_edit,
)
if writer.Config.mode == "edit":
writer_application.apiKey = os.getenv("WRITER_API_KEY")
diff --git a/src/writer/ss_types.py b/src/writer/ss_types.py
index 517cacb00..2465484e8 100644
--- a/src/writer/ss_types.py
+++ b/src/writer/ss_types.py
@@ -74,6 +74,7 @@ class WriterApplicationInformation(BaseModel):
baseUrl: Optional[str] = None
apiKey: Optional[str] = None
isOrganizationAdmin: Optional[bool] = False
+ canEdit: Optional[bool] = False
class AutogenRequestBody(BaseModel):