Add config to control public sharing (#10105)

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Matt Rubens 2025-12-15 21:58:15 -05:00 • committed by GitHub
parent bf81fa7237
commit 3502f41e75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 134 additions and 11 deletions

View file

@ -337,6 +337,11 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return this.shareService!.canShareTask()
}
public async canSharePublicly(): Promise<boolean> {
this.ensureInitialized()
return this.shareService!.canSharePublicly()
}
// Lifecycle
public dispose(): void {

View file

@ -47,4 +47,15 @@ export class CloudShareService {
return false
}
}
async canSharePublicly(): Promise<boolean> {
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
}
}
}

View file

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

View file

@ -135,6 +135,7 @@ export type WorkspaceTaskVisibility = z.infer<typeof workspaceTaskVisibilitySche
export const organizationCloudSettingsSchema = z.object({
recordTaskMessages: z.boolean().optional(),
enableTaskSharing: z.boolean().optional(),
allowPublicTaskSharing: z.boolean().optional(),
taskShareExpirationDays: z.number().int().positive().optional(),
allowMembersViewAllTasks: z.boolean().optional(),
workspaceTaskVisibility: workspaceTaskVisibilitySchema.optional(),
@ -209,6 +210,7 @@ export const ORGANIZATION_DEFAULT: OrganizationSettings = {
cloudSettings: {
recordTaskMessages: true,
enableTaskSharing: true,
allowPublicTaskSharing: true,
taskShareExpirationDays: 30,
allowMembersViewAllTasks: true,
},

View file

@ -1873,6 +1873,7 @@ export class ClineProvider
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
publicSharingEnabled,
organizationAllowList,
organizationSettingsVersion,
maxConcurrentFileReads,
@ -2025,6 +2026,7 @@ export class ClineProvider
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
cloudOrganizations,
sharingEnabled: sharingEnabled ?? false,
publicSharingEnabled: publicSharingEnabled ?? false,
organizationAllowList,
organizationSettingsVersion,
condensingApiConfigId,
@ -2140,6 +2142,16 @@ export class ClineProvider
)
}
let publicSharingEnabled: boolean = false
try {
publicSharingEnabled = await CloudService.instance.canSharePublicly()
} catch (error) {
console.error(
`[getState] failed to get public sharing enabled state: ${error instanceof Error ? error.message : String(error)}`,
)
}
let organizationSettingsVersion: number = -1
try {
@ -2251,6 +2263,7 @@ export class ClineProvider
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
publicSharingEnabled,
organizationAllowList,
organizationSettingsVersion,
condensingApiConfigId: stateValues.condensingApiConfigId,

View file

@ -576,6 +576,7 @@ describe("ClineProvider", () => {
autoCondenseContextPercent: 100,
cloudIsAuthenticated: false,
sharingEnabled: false,
publicSharingEnabled: false,
profileThresholds: {},
hasOpenedModeSelector: false,
diagnosticsEnabled: true,

View file

@ -334,6 +334,7 @@ export type ExtensionState = Pick<
cloudApiUrl?: string
cloudOrganizations?: CloudOrganizationMembership[]
sharingEnabled: boolean
publicSharingEnabled: boolean
organizationAllowList: OrganizationAllowList
organizationSettingsVersion?: number

View file

@ -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) => {
</div>
</CommandItem>
)}
<CommandItem onSelect={() => handleShare("public")} className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="codicon codicon-globe text-sm"></span>
<div className="flex flex-col">
<span className="text-sm">{t("chat:task.sharePublicly")}</span>
<span className="text-xs text-vscode-descriptionForeground">
{t("chat:task.sharePubliclyDescription")}
</span>
{publicSharingEnabled && (
<CommandItem
onSelect={() => handleShare("public")}
className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="codicon codicon-globe text-sm"></span>
<div className="flex flex-col">
<span className="text-sm">{t("chat:task.sharePublicly")}</span>
<span className="text-xs text-vscode-descriptionForeground">
{t("chat:task.sharePubliclyDescription")}
</span>
</div>
</div>
</div>
</CommandItem>
</CommandItem>
)}
</CommandGroup>
</CommandList>
</Command>

View file

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

View file

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

View file

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

View file

@ -206,6 +206,7 @@ describe("mergeExtensionState", () => {
autoCondenseContextPercent: 100,
cloudIsAuthenticated: false,
sharingEnabled: false,
publicSharingEnabled: false,
profileThresholds: {},
hasOpenedModeSelector: false, // Add the new required property
maxImageFileSize: 5,

View file

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