Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.
Open
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
2 changes: 1 addition & 1 deletion vscode/webviews/CodyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const CodyPanel: FunctionComponent<CodyPanelProps> = ({
orientation="vertical"
className={styles.outerContainer}
>
<Notices user={user} instanceNotices={instanceNotices} />
<Notices instanceNotices={instanceNotices} />
{/* Hide tab bar in editor chat panels. */}
{config.webviewType !== 'editor' && (
<TabsBar
Expand Down
36 changes: 0 additions & 36 deletions vscode/webviews/components/Notices.story.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { CodyIDE } from '@sourcegraph/cody-shared'
import { ExtensionAPIProviderForTestsOnly, MOCK_API } from '@sourcegraph/prompt-editor'
import type { Meta, StoryObj } from '@storybook/react'
import { TelemetryRecorderContext } from '../utils/telemetry'
Expand All @@ -22,55 +21,20 @@ export default meta

type Story = StoryObj<typeof Notices>

// Mock user data
const baseUser = {
isDotComUser: true,
isCodyProUser: false,
user: {
id: 'user-1',
username: 'test-user',
displayName: 'Test User',
avatarURL: '',
organizations: [],
endpoint: 'https://example.com',
},
IDE: CodyIDE.VSCode,
}

export const SgTeammateNotice: Story = {
args: {
user: {
...baseUser,
user: {
...baseUser.user,
organizations: [
{
name: 'sourcegraph',
id: 'sourcegraph-01',
},
],
},
},
instanceNotices: [],
},
}

export const NoNotices: Story = {
args: {
user: {
...baseUser,
isDotComUser: false,
},
instanceNotices: [],
},
}

export const WebUserNoNotices: Story = {
args: {
user: {
...baseUser,
IDE: CodyIDE.Web,
},
instanceNotices: [],
},
}
255 changes: 7 additions & 248 deletions vscode/webviews/components/Notices.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import { CodyIDE, type CodyNotice, isWorkspaceInstance } from '@sourcegraph/cody-shared'
import { S2_URL } from '@sourcegraph/cody-shared/src/sourcegraph-api/environments'
import {
ArrowLeftRightIcon,
ArrowRightIcon,
BuildingIcon,
EyeIcon,
HeartIcon,
XIcon,
} from 'lucide-react'
import { type FunctionComponent, type ReactNode, useCallback, useMemo, useState } from 'react'
import SourcegraphIcon from '../../resources/sourcegraph-mark.svg'
import type { UserAccountInfo } from '../Chat'
import { CodyLogo } from '../icons/CodyLogo'
import { getVSCodeAPI } from '../utils/VSCodeApi'
import type { CodyNotice } from '@sourcegraph/cody-shared'
import { XIcon } from 'lucide-react'
import { type FunctionComponent, useCallback, useMemo, useState } from 'react'
import { useTelemetryRecorder } from '../utils/telemetry'
import { MarkdownFromCody } from './MarkdownFromCody'
import { useLocalStorage } from './hooks'
Expand All @@ -24,42 +12,19 @@ interface Notice {
content: JSX.Element
}

type NoticeVariants = 'default' | 'warning'
type NoticeIDs =
| 'DogfoodS2'
| 'TeamsUpgrade'
| 'DeepCodyDotCom'
| 'DeepCodyEnterprise'
| 'CodyDeprecation'

interface NoticesProps {
user: UserAccountInfo
instanceNotices: CodyNotice[]
}

const storageKey = 'DismissedWelcomeNotices'

// Cody deprecation message constants
const CODY_DEPRECATION_MESSAGES = {
ENTERPRISE_STARTER_WITH_CODY:
"Cody in Enterprise Starter is being sunset. You can continue using Cody until July 23rd. Your code indexing and code search capabilities will not be affected. We encourage you to try Amp, Sourcegraph's new agentic coding tool.",
ENTERPRISE_STARTER_WITHOUT_CODY:
"Cody in Enterprise Starter is being sunset. Because you signed up after 6/25, your account doesn't have Cody, we encourage you to try Amp, Sourcegraph's new agentic coding tool.",
PRO_USER:
"Cody Pro is being sunset. You can continue using Cody until July 23rd, and your subscription will not renew. We encourage you to try Amp, Sourcegraph's new agentic coding tool.",
FREE_USER_WITH_CODY:
"Cody Free is being sunset. You can continue using Cody until July 23rd. We encourage you to try Amp, Sourcegraph's new agentic coding tool.",
FREE_USER_WITHOUT_CODY:
"Cody Free is being sunset. Because you signed up after 6/25, your account doesn't have Cody. We encourage you to try Amp, Sourcegraph's new agentic coding tool.",
} as const

export const Notices: React.FC<NoticesProps> = ({ user, instanceNotices }) => {
export const Notices: React.FC<NoticesProps> = ({ instanceNotices }) => {
const telemetryRecorder = useTelemetryRecorder()

// dismissed notices from local storage
const [dismissedNotices, setDismissedNotices] = useLocalStorage(storageKey, '')
// session-only dismissal - for notices we want to show if the user logs out and logs back in.
const [sessionDismissedNotices, setSessionDismissedNotices] = useState<string[]>([])
const [_, setSessionDismissedNotices] = useState<string[]>([])

const dismissNotice = useCallback(
(noticeId: string, type: 'sessional' | 'permanent' = 'permanent') => {
Expand All @@ -77,28 +42,6 @@ export const Notices: React.FC<NoticesProps> = ({ user, instanceNotices }) => {
[telemetryRecorder, setDismissedNotices]
)

// Helper function to get the CodyDeprecation message based on user type and site Cody enablement
const getCodyDeprecationMessage = useCallback(() => {
if (isWorkspaceInstance(user.user.endpoint)) {
// For workspace instances, check if the site has Cody enabled
const hasCodyEnabled = user.siteHasCodyEnabled === true
if (hasCodyEnabled) {
return CODY_DEPRECATION_MESSAGES.ENTERPRISE_STARTER_WITH_CODY
}
return CODY_DEPRECATION_MESSAGES.ENTERPRISE_STARTER_WITHOUT_CODY
}
if (user.isCodyProUser) {
return CODY_DEPRECATION_MESSAGES.PRO_USER
}
// For free users, check if they have a subscription
const hasSubscription =
user.currentUserCodySubscription !== null && user.currentUserCodySubscription !== undefined
if (hasSubscription && user.currentUserCodySubscription?.status !== 'CANCELED') {
return CODY_DEPRECATION_MESSAGES.FREE_USER_WITH_CODY
}
return CODY_DEPRECATION_MESSAGES.FREE_USER_WITHOUT_CODY
}, [user])

const notices: Notice[] = useMemo(
() => [
...instanceNotices.map(notice => ({
Expand All @@ -112,86 +55,16 @@ export const Notices: React.FC<NoticesProps> = ({ user, instanceNotices }) => {
/>
),
})),
/**
* Cody Deprecation Notice
*/
{
id: 'CodyDeprecation',
isVisible: user.isDotComUser || isWorkspaceInstance(user.user.endpoint),
content: (
<NoticeContent
id="CodyDeprecation"
variant="default"
title="Important Notice"
message={getCodyDeprecationMessage()}
onDismiss={() => dismissNotice('CodyDeprecation', 'sessional')}
actions={[
{
label: 'Try Amp',
variant: 'default',
href: 'https://ampcode.com',
},
{
label: 'Learn more',
variant: 'ghost',
href: 'https://sourcegraph.com/blog/changes-to-cody-free-pro-and-enterprise-starter-plans',
},
]}
/>
),
},
/**
* For Sourcegraph team members who are using Sourcegraph.com to remind them that we want to be dogfooding S2.
*/
{
id: 'DogfoodS2',
isVisible:
user.isDotComUser &&
user.user.organizations?.some(org => org.name === 'sourcegraph') &&
user.IDE !== CodyIDE.Web,
content: (
<NoticeContent
id="DogfoodS2"
variant="warning"
title=""
message="Sourcegraph team members should use S2 not dotcom (except when testing dotcom-specific behavior) so that we dogfood our enterprise customer experience."
onDismiss={() => dismissNotice('DogfoodS2', 'sessional')}
actions={[
{
label: 'Switch to S2',
onClick: () =>
getVSCodeAPI().postMessage({
command: 'auth',
authKind: 'switch',
endpoint: S2_URL.href,
}),
variant: 'default',
icon: <ArrowLeftRightIcon size={14} />,
iconPosition: 'end',
},
{
label: 'Dismiss',
onClick: () => dismissNotice('DogfoodS2', 'sessional'),
variant: 'secondary',
},
]}
/>
),
},
],
[user, dismissNotice, instanceNotices, getCodyDeprecationMessage]
[dismissNotice, instanceNotices]
)

// First, modify the activeNotice useMemo to add conditional logic for DogfoodS2
const activeNotice = useMemo(
() =>
notices.find(notice => {
if (notice.id === 'DogfoodS2' || notice.id === 'CodyDeprecation') {
return notice.isVisible && !sessionDismissedNotices.includes(notice.id)
}
return notice.isVisible && !dismissedNotices?.includes(notice.id)
}),
[dismissedNotices, sessionDismissedNotices, notices]
[dismissedNotices, notices]
)

if (!activeNotice) {
Expand All @@ -203,120 +76,6 @@ export const Notices: React.FC<NoticesProps> = ({ user, instanceNotices }) => {
)
}

interface NoticeContentProps {
variant: NoticeVariants
id: NoticeIDs
title: string
message: string
actions: Array<{
label: string
onClick?: () => void
href?: string
variant: 'default' | 'ghost' | 'secondary'
icon?: ReactNode
iconPosition?: 'start' | 'end'
}>
onDismiss: () => void
info?: string
footer?: string
}

const NoticeContent: FunctionComponent<NoticeContentProps> = ({
variant,
title,
message,
actions,
id,
info,
footer,
onDismiss,
}) => {
const telemetryRecorder = useTelemetryRecorder()

const bgColor = {
default: 'tw-bg-accent tw-bg-opacity-50 tw-text-accent-foreground',
warning: 'tw-bg-red-700 tw-text-white',
}[variant]

const header = {
DeepCodyDotCom: (
<>
<CodyLogo size={16} />
</>
),
DeepCodyEnterprise: (
<>
<CodyLogo size={16} />
</>
),
DogfoodS2: (
<>
<EyeIcon />
<HeartIcon />
<BuildingIcon />
</>
),
TeamsUpgrade: (
<>
<CodyLogo size={16} />
<ArrowRightIcon size={16} />
<img src={SourcegraphIcon} alt="Sourcegraph Logo" className="tw-h-[16px]" />
</>
),
CodyDeprecation: null,
}[id]

return (
<aside
className={`tw-w-full tw-relative tw-rounded-md tw-flex tw-flex-col tw-gap-2 tw-p-4 ${bgColor}`}
>
<div className="tw-flex tw-gap-3 tw-mb-2">{header}</div>
{title && <h1 className="tw-text-lg tw-font-semibold">{title}</h1>}
{info && <p className="tw-mb-2">ⓘ {info}</p>}
<p>{message}</p>
<div className="tw-mt-3 tw-flex tw-gap-3">
{actions.map((action, _index) => (
<Button
key={action.label + '-button'}
variant={action.variant}
size="sm"
onClick={() => {
action.onClick?.()
telemetryRecorder.recordEvent('cody.notice.cta', 'clicked', {
privateMetadata: {
noticeId: id,
title: action.label,
},
})
}}
className="tw-flex tw-gap-1"
>
{action.iconPosition === 'start' && action.icon}
{action.href ? (
<a
href={action.href}
target="_blank"
rel="noreferrer"
className="tw-text-inherit hover:!tw-text-inherit"
>
{action.label}
</a>
) : (
<span>{action.label}</span>
)}
{action.iconPosition === 'end' && action.icon}
</Button>
))}
</div>
{footer && <p className="tw-mt-2">{footer}</p>}
{/* Dismiss button. */}
<Button variant="ghost" onClick={onDismiss} className="tw-absolute tw-top-2 tw-right-2">
<XIcon size="14" />
</Button>
</aside>
)
}

interface MarkdownNotice {
title: string
content: string
Expand Down
Loading