diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 41781d09bf4..43f52d4b18a 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -337,6 +337,11 @@ export class CloudService extends EventEmitter implements Di return this.shareService!.canShareTask() } + public async canSharePublicly(): Promise { + this.ensureInitialized() + return this.shareService!.canSharePublicly() + } + // Lifecycle public dispose(): void { diff --git a/packages/cloud/src/CloudShareService.ts b/packages/cloud/src/CloudShareService.ts index dfed068f240..3942456f597 100644 --- a/packages/cloud/src/CloudShareService.ts +++ b/packages/cloud/src/CloudShareService.ts @@ -47,4 +47,15 @@ export class CloudShareService { return false } } + + async canSharePublicly(): Promise { + try { + const cloudSettings = this.settingsService.getSettings()?.cloudSettings + // Public sharing requires both enableTaskSharing AND allowPublicTaskSharing to be true + return !!cloudSettings?.enableTaskSharing && cloudSettings?.allowPublicTaskSharing !== false + } catch (error) { + this.log("[ShareService] Error checking if task can be shared publicly:", error) + return false + } + } } diff --git a/packages/types/src/__tests__/cloud.test.ts b/packages/types/src/__tests__/cloud.test.ts index c1fdd02a66e..366916171e1 100644 --- a/packages/types/src/__tests__/cloud.test.ts +++ b/packages/types/src/__tests__/cloud.test.ts @@ -175,6 +175,82 @@ describe("organizationSettingsSchema with features", () => { }) }) +describe("organizationCloudSettingsSchema with allowPublicTaskSharing", () => { + it("should validate without allowPublicTaskSharing property", () => { + const input = { + recordTaskMessages: true, + enableTaskSharing: true, + } + const result = organizationCloudSettingsSchema.safeParse(input) + expect(result.success).toBe(true) + expect(result.data?.allowPublicTaskSharing).toBeUndefined() + }) + + it("should validate with allowPublicTaskSharing as true", () => { + const input = { + recordTaskMessages: true, + enableTaskSharing: true, + allowPublicTaskSharing: true, + } + const result = organizationCloudSettingsSchema.safeParse(input) + expect(result.success).toBe(true) + expect(result.data?.allowPublicTaskSharing).toBe(true) + }) + + it("should validate with allowPublicTaskSharing as false", () => { + const input = { + recordTaskMessages: true, + enableTaskSharing: true, + allowPublicTaskSharing: false, + } + const result = organizationCloudSettingsSchema.safeParse(input) + expect(result.success).toBe(true) + expect(result.data?.allowPublicTaskSharing).toBe(false) + }) + + it("should reject non-boolean allowPublicTaskSharing", () => { + const input = { + allowPublicTaskSharing: "true", + } + const result = organizationCloudSettingsSchema.safeParse(input) + expect(result.success).toBe(false) + }) + + it("should have correct TypeScript type", () => { + // Type-only test to ensure TypeScript compilation + const settings: OrganizationCloudSettings = { + recordTaskMessages: true, + enableTaskSharing: true, + allowPublicTaskSharing: true, + } + expect(settings.allowPublicTaskSharing).toBe(true) + + const settingsWithoutPublicSharing: OrganizationCloudSettings = { + recordTaskMessages: false, + } + expect(settingsWithoutPublicSharing.allowPublicTaskSharing).toBeUndefined() + }) + + it("should validate in organizationSettingsSchema with allowPublicTaskSharing", () => { + const input = { + version: 1, + cloudSettings: { + recordTaskMessages: true, + enableTaskSharing: true, + allowPublicTaskSharing: false, + }, + defaultSettings: {}, + allowList: { + allowAll: true, + providers: {}, + }, + } + const result = organizationSettingsSchema.safeParse(input) + expect(result.success).toBe(true) + expect(result.data?.cloudSettings?.allowPublicTaskSharing).toBe(false) + }) +}) + describe("organizationCloudSettingsSchema with workspaceTaskVisibility", () => { it("should validate without workspaceTaskVisibility property", () => { const input = { diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 774bac52ef2..bac0ad0dd95 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -135,6 +135,7 @@ export type WorkspaceTaskVisibility = z.infer { autoCondenseContextPercent: 100, cloudIsAuthenticated: false, sharingEnabled: false, + publicSharingEnabled: false, profileThresholds: {}, hasOpenedModeSelector: false, diagnosticsEnabled: true, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index f5d4078e48f..93528b8d564 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -334,6 +334,7 @@ export type ExtensionState = Pick< cloudApiUrl?: string cloudOrganizations?: CloudOrganizationMembership[] sharingEnabled: boolean + publicSharingEnabled: boolean organizationAllowList: OrganizationAllowList organizationSettingsVersion?: number diff --git a/webview-ui/src/components/chat/ShareButton.tsx b/webview-ui/src/components/chat/ShareButton.tsx index 06aa46a459b..9e7aa4e8f99 100644 --- a/webview-ui/src/components/chat/ShareButton.tsx +++ b/webview-ui/src/components/chat/ShareButton.tsx @@ -41,6 +41,7 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => { handleConnect, isAuthenticated: cloudIsAuthenticated, sharingEnabled, + publicSharingEnabled, } = useCloudUpsell({ onAuthSuccess: () => { // Auto-open share dropdown after successful authentication @@ -195,17 +196,21 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => { )} - handleShare("public")} className="cursor-pointer"> -
- -
- {t("chat:task.sharePublicly")} - - {t("chat:task.sharePubliclyDescription")} - + {publicSharingEnabled && ( + handleShare("public")} + className="cursor-pointer"> +
+ +
+ {t("chat:task.sharePublicly")} + + {t("chat:task.sharePubliclyDescription")} + +
-
- + + )} diff --git a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx index f245719bbd9..7dba74501f7 100644 --- a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx @@ -20,6 +20,7 @@ vi.mock("@/context/ExtensionStateContext", () => ({ ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => children, useExtensionState: () => ({ sharingEnabled: true, + publicSharingEnabled: true, cloudIsAuthenticated: true, cloudUserInfo: { id: "test-user", diff --git a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx index 4115afaa558..d7a53ccacc9 100644 --- a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx @@ -81,6 +81,7 @@ describe("TaskActions", () => { vi.clearAllMocks() mockUseExtensionState.mockReturnValue({ sharingEnabled: true, + publicSharingEnabled: true, cloudIsAuthenticated: true, cloudUserInfo: { organizationName: "Test Organization", @@ -166,6 +167,7 @@ describe("TaskActions", () => { it("does not show organization option when user is not in an organization", () => { mockUseExtensionState.mockReturnValue({ sharingEnabled: true, + publicSharingEnabled: true, cloudIsAuthenticated: true, cloudUserInfo: { // No organizationName property @@ -264,6 +266,7 @@ describe("TaskActions", () => { // Simulate user becoming authenticated (e.g., from CloudView) mockUseExtensionState.mockReturnValue({ sharingEnabled: true, + publicSharingEnabled: true, cloudIsAuthenticated: true, cloudUserInfo: { organizationName: "Test Organization", @@ -302,6 +305,7 @@ describe("TaskActions", () => { // Simulate user becoming authenticated after clicking connect from share button mockUseExtensionState.mockReturnValue({ sharingEnabled: true, + publicSharingEnabled: true, cloudIsAuthenticated: true, cloudUserInfo: { organizationName: "Test Organization", diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 8215f0dd03b..08294f9fe8e 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -43,6 +43,7 @@ export interface ExtensionStateContextType extends ExtensionState { cloudIsAuthenticated: boolean cloudOrganizations?: CloudOrganizationMembership[] sharingEnabled: boolean + publicSharingEnabled: boolean maxConcurrentFileReads?: number mdmCompliant?: boolean hasOpenedModeSelector: boolean // New property to track if user has opened mode selector @@ -250,6 +251,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode cloudIsAuthenticated: false, cloudOrganizations: [], sharingEnabled: false, + publicSharingEnabled: false, organizationAllowList: ORGANIZATION_ALLOW_ALL, organizationSettingsVersion: -1, autoCondenseContext: true, diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 4cf104f81d5..15c87d03c4d 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -206,6 +206,7 @@ describe("mergeExtensionState", () => { autoCondenseContextPercent: 100, cloudIsAuthenticated: false, sharingEnabled: false, + publicSharingEnabled: false, profileThresholds: {}, hasOpenedModeSelector: false, // Add the new required property maxImageFileSize: 5, diff --git a/webview-ui/src/hooks/useCloudUpsell.ts b/webview-ui/src/hooks/useCloudUpsell.ts index 1476a5a83e8..3175fa57db3 100644 --- a/webview-ui/src/hooks/useCloudUpsell.ts +++ b/webview-ui/src/hooks/useCloudUpsell.ts @@ -13,7 +13,7 @@ export const useCloudUpsell = (options: UseCloudUpsellOptions = {}) => { const { onAuthSuccess, autoOpenOnAuth = false } = options const [isOpen, setIsOpen] = useState(false) const [shouldOpenOnAuth, setShouldOpenOnAuth] = useState(false) - const { cloudIsAuthenticated, sharingEnabled } = useExtensionState() + const { cloudIsAuthenticated, sharingEnabled, publicSharingEnabled } = useExtensionState() const wasUnauthenticatedRef = useRef(false) const initiatedAuthRef = useRef(false) @@ -67,5 +67,6 @@ export const useCloudUpsell = (options: UseCloudUpsellOptions = {}) => { handleConnect, isAuthenticated: cloudIsAuthenticated, sharingEnabled, + publicSharingEnabled, } }