mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
feat: wire up controller and UI for banners
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
// Banner service for fetching and managing banners
|
||||
service BannerService {
|
||||
// Get active banners that haven't been dismissed
|
||||
rpc GetActiveBanners(google.protobuf.Empty) returns (BannersResponse);
|
||||
|
||||
// Dismiss a banner
|
||||
rpc DismissBanner(DismissBannerRequest) returns (google.protobuf.Empty);
|
||||
|
||||
// Track a banner event (seen, click, dismiss)
|
||||
rpc TrackBannerEvent(TrackBannerEventRequest) returns (google.protobuf.Empty);
|
||||
}
|
||||
|
||||
// Response containing active banners
|
||||
message BannersResponse {
|
||||
repeated Banner banners = 1;
|
||||
}
|
||||
|
||||
// Request to dismiss a banner
|
||||
message DismissBannerRequest {
|
||||
string banner_id = 1;
|
||||
}
|
||||
|
||||
// Request to track a banner event
|
||||
message TrackBannerEventRequest {
|
||||
string banner_id = 1;
|
||||
string event_type = 2; // "seen", "dismiss", or "click"
|
||||
}
|
||||
|
||||
// Banner message
|
||||
message Banner {
|
||||
string id = 1;
|
||||
string title_md = 2;
|
||||
string body_md = 3;
|
||||
string severity = 4; // "info", "warning", "error"
|
||||
string placement = 5; // "top", "inline"
|
||||
optional string cta_text = 6;
|
||||
optional string cta_url = 7;
|
||||
optional string active_from = 8;
|
||||
optional string active_to = 9;
|
||||
string rules_json = 10;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { DismissBannerRequest } from "@/shared/proto/cline/banners"
|
||||
import { Empty } from "@/shared/proto/google/protobuf/empty"
|
||||
import type { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Dismisses a banner
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the banner ID to dismiss
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function DismissBanner(controller: Controller, request: DismissBannerRequest): Promise<Empty> {
|
||||
try {
|
||||
await controller.dismissBanner(request.bannerId)
|
||||
return Empty.create({})
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss banner:", error)
|
||||
return Empty.create({})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { BannersResponse } from "@/shared/proto/cline/banners"
|
||||
import { Banner } from "@/shared/proto/cline/banners"
|
||||
import type { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Gets active banners that haven't been dismissed
|
||||
* @param controller The controller instance
|
||||
* @returns BannersResponse with active banners
|
||||
*/
|
||||
export async function GetActiveBanners(controller: Controller): Promise<BannersResponse> {
|
||||
try {
|
||||
const banners = await controller.fetchBannersForDisplay()
|
||||
|
||||
// Convert to proto Banner format
|
||||
const protoBanners = banners.map((banner) =>
|
||||
Banner.create({
|
||||
id: banner.id,
|
||||
titleMd: banner.titleMd,
|
||||
bodyMd: banner.bodyMd,
|
||||
severity: banner.severity,
|
||||
placement: banner.placement,
|
||||
ctaText: banner.ctaText,
|
||||
ctaUrl: banner.ctaUrl,
|
||||
activeFrom: banner.activeFrom,
|
||||
activeTo: banner.activeTo,
|
||||
rulesJson: banner.rulesJson,
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
banners: protoBanners,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get active banners:", error)
|
||||
return {
|
||||
banners: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { TrackBannerEventRequest } from "@/shared/proto/cline/banners"
|
||||
import { Empty } from "@/shared/proto/google/protobuf/empty"
|
||||
import type { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Tracks a banner event (seen, dismiss, click)
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing banner ID and event type
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function TrackBannerEvent(controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
|
||||
try {
|
||||
// Currently only "dismiss" is supported in the backend
|
||||
if (request.eventType === "dismiss") {
|
||||
await controller.trackBannerEvent(request.bannerId, "dismiss")
|
||||
}
|
||||
// For "seen" and "click", we'd need to extend the backend method
|
||||
return Empty.create({})
|
||||
} catch (error) {
|
||||
console.error("Failed to track banner event:", error)
|
||||
return Empty.create({})
|
||||
}
|
||||
}
|
||||
@@ -892,6 +892,9 @@ export class Controller {
|
||||
const lastDismissedCliBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
|
||||
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
|
||||
// Fetch API banners
|
||||
const apiBanners = await this.fetchBannersForDisplay()
|
||||
|
||||
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
@@ -995,6 +998,7 @@ export class Controller {
|
||||
user: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
featureFlag: featureFlagsService.getNativeToolCallEnabled(),
|
||||
},
|
||||
apiBanners,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,23 @@ export interface ExtensionState {
|
||||
remoteConfigSettings?: Partial<RemoteConfigFields>
|
||||
subagentsEnabled?: boolean
|
||||
nativeToolCallSetting?: ClineFeatureSetting
|
||||
apiBanners?: Banner[]
|
||||
}
|
||||
|
||||
/**
|
||||
* API-fetched banner from the banner service
|
||||
*/
|
||||
export interface Banner {
|
||||
id: string
|
||||
titleMd: string
|
||||
bodyMd: string
|
||||
severity: "info" | "warning" | "error"
|
||||
placement: "top" | "inline"
|
||||
ctaText?: string
|
||||
ctaUrl?: string
|
||||
activeFrom?: string
|
||||
activeTo?: string
|
||||
rulesJson: string
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react"
|
||||
import Announcement from "@/components/chat/Announcement"
|
||||
import ApiBanner from "@/components/common/ApiBanner"
|
||||
import CliInstallBanner, { CURRENT_CLI_BANNER_VERSION } from "@/components/common/CliInstallBanner"
|
||||
import InfoBanner, { CURRENT_INFO_BANNER_VERSION } from "@/components/common/InfoBanner"
|
||||
import HistoryPreview from "@/components/history/HistoryPreview"
|
||||
@@ -22,7 +23,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
taskHistory,
|
||||
shouldShowQuickWins,
|
||||
}) => {
|
||||
const { lastDismissedInfoBannerVersion, lastDismissedCliBannerVersion } = useExtensionState()
|
||||
const { lastDismissedInfoBannerVersion, lastDismissedCliBannerVersion, apiBanners } = useExtensionState()
|
||||
|
||||
const shouldShowInfoBanner = lastDismissedInfoBannerVersion < CURRENT_INFO_BANNER_VERSION
|
||||
// const shouldShowNewModelBanner = lastDismissedModelBannerVersion < CURRENT_MODEL_BANNER_VERSION
|
||||
@@ -40,6 +41,9 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
{showAnnouncement && <Announcement hideAnnouncement={hideAnnouncement} version={version} />}
|
||||
{/* {shouldShowNewModelBanner && <NewModelBanner />} */}
|
||||
{shouldShowCliBanner && <CliInstallBanner />}
|
||||
{apiBanners?.map((banner) => (
|
||||
<ApiBanner banner={banner} key={banner.id} />
|
||||
))}
|
||||
<HomeHeader shouldShowQuickWins={shouldShowQuickWins} />
|
||||
{!shouldShowQuickWins && taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { X } from "lucide-react"
|
||||
import React, { useCallback } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { BannerServiceClient } from "@/services/grpc-client"
|
||||
import type { Banner } from "../../../../src/shared/ExtensionMessage"
|
||||
import { DismissBannerRequest } from "../../../../src/shared/proto/cline/banners"
|
||||
|
||||
interface ApiBannerProps {
|
||||
banner: Banner
|
||||
}
|
||||
|
||||
export const ApiBanner: React.FC<ApiBannerProps> = ({ banner }) => {
|
||||
const { apiBanners } = useExtensionState()
|
||||
|
||||
const handleDismiss = useCallback(
|
||||
async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await BannerServiceClient.DismissBanner(
|
||||
DismissBannerRequest.create({
|
||||
bannerId: banner.id,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss banner:", error)
|
||||
}
|
||||
},
|
||||
[banner.id],
|
||||
)
|
||||
|
||||
const handleCtaClick = useCallback(() => {
|
||||
if (banner.ctaUrl) {
|
||||
window.open(banner.ctaUrl, "_blank")
|
||||
}
|
||||
}, [banner.ctaUrl])
|
||||
|
||||
// Different colors based on severity
|
||||
const severityStyles = {
|
||||
info: "bg-blue-500/10 border-blue-500/30",
|
||||
warning: "bg-yellow-500/10 border-yellow-500/30",
|
||||
error: "bg-red-500/10 border-red-500/30",
|
||||
}
|
||||
|
||||
const severityClass = severityStyles[banner.severity] || severityStyles.info
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border ${severityClass} p-4 mb-3 relative`} data-testid="api-banner">
|
||||
<button
|
||||
aria-label="Dismiss banner"
|
||||
className="absolute top-2 right-2 p-1 hover:bg-black/10 rounded"
|
||||
data-testid="dismiss-banner"
|
||||
onClick={handleDismiss}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
<div className="pr-8">
|
||||
{banner.titleMd && <div className="font-semibold mb-1">{banner.titleMd}</div>}
|
||||
|
||||
{banner.bodyMd && <div className="text-sm opacity-90">{banner.bodyMd}</div>}
|
||||
|
||||
{banner.ctaText && banner.ctaUrl && (
|
||||
<button
|
||||
className="mt-2 text-sm underline hover:no-underline"
|
||||
data-testid="banner-cta"
|
||||
onClick={handleCtaClick}>
|
||||
{banner.ctaText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiBanner
|
||||
Reference in New Issue
Block a user