Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions packages/cloud/src/CloudShareService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
76 changes: 76 additions & 0 deletions packages/types/src/__tests__/cloud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -209,6 +210,7 @@ export const ORGANIZATION_DEFAULT: OrganizationSettings = {
cloudSettings: {
recordTaskMessages: true,
enableTaskSharing: true,
allowPublicTaskSharing: true,
taskShareExpirationDays: 30,
allowMembersViewAllTasks: true,
},
Expand Down
13 changes: 13 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,7 @@ export class ClineProvider
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
publicSharingEnabled,
organizationAllowList,
organizationSettingsVersion,
maxConcurrentFileReads,
Expand Down Expand Up @@ -2025,6 +2026,7 @@ export class ClineProvider
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
cloudOrganizations,
sharingEnabled: sharingEnabled ?? false,
publicSharingEnabled: publicSharingEnabled ?? false,
organizationAllowList,
organizationSettingsVersion,
condensingApiConfigId,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -2251,6 +2263,7 @@ export class ClineProvider
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
publicSharingEnabled,
organizationAllowList,
organizationSettingsVersion,
condensingApiConfigId: stateValues.condensingApiConfigId,
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ describe("ClineProvider", () => {
autoCondenseContextPercent: 100,
cloudIsAuthenticated: false,
sharingEnabled: false,
publicSharingEnabled: false,
profileThresholds: {},
hasOpenedModeSelector: false,
diagnosticsEnabled: true,
Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ export type ExtensionState = Pick<
cloudApiUrl?: string
cloudOrganizations?: CloudOrganizationMembership[]
sharingEnabled: boolean
publicSharingEnabled: boolean
organizationAllowList: OrganizationAllowList
organizationSettingsVersion?: number

Expand Down
25 changes: 15 additions & 10 deletions webview-ui/src/components/chat/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe("TaskActions", () => {
vi.clearAllMocks()
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
publicSharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: {
organizationName: "Test Organization",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ describe("mergeExtensionState", () => {
autoCondenseContextPercent: 100,
cloudIsAuthenticated: false,
sharingEnabled: false,
publicSharingEnabled: false,
profileThresholds: {},
hasOpenedModeSelector: false, // Add the new required property
maxImageFileSize: 5,
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/hooks/useCloudUpsell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -67,5 +67,6 @@ export const useCloudUpsell = (options: UseCloudUpsellOptions = {}) => {
handleConnect,
isAuthenticated: cloudIsAuthenticated,
sharingEnabled,
publicSharingEnabled,
}
}
Loading