feat(webview): add dedicated Cline Kanban launch modal

This commit is contained in:
Saoud Rizwan
2026-03-25 04:20:10 -07:00
parent 7627a382aa
commit beb54a4c07
6 changed files with 183 additions and 14 deletions
+1
View File
@@ -1,4 +1,5 @@
demo.gif filter=lfs diff=lfs merge=lfs -text
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
webview-ui/src/assets/cline_kanban_demo.mp4 filter=lfs diff=lfs merge=lfs -text
* text=auto eol=lf
+2
View File
@@ -115,6 +115,7 @@ export abstract class WebviewProvider {
font-src ${this.getCspSource()} data:;
style-src ${this.getCspSource()} 'unsafe-inline';
img-src ${this.getCspSource()} https: data:;
media-src ${this.getCspSource()} https: data: blob:;
script-src 'nonce-${nonce}' 'unsafe-eval';">
<title>Cline</title>
</head>
@@ -202,6 +203,7 @@ export abstract class WebviewProvider {
`font-src ${this.getCspSource()}`,
`style-src ${this.getCspSource()} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${this.getCspSource()} https: data:`,
`media-src ${this.getCspSource()} https: data: blob: http://${localServerUrl} http://0.0.0.0:${localPort}`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
+55 -14
View File
@@ -1,7 +1,8 @@
import type { Boolean, EmptyRequest } from "@shared/proto/cline/common"
import { useEffect } from "react"
import { useCallback, useEffect, useState } from "react"
import AccountView from "./components/account/AccountView"
import ChatView from "./components/chat/ChatView"
import ClineKanbanLaunchModal, { CLINE_KANBAN_MODAL_DISMISS_ID } from "./components/common/ClineKanbanLaunchModal"
import HistoryView from "./components/history/HistoryView"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import OnboardingView from "./components/onboarding/OnboardingView"
@@ -11,13 +12,14 @@ import WorktreesView from "./components/worktrees/WorktreesView"
import { useClineAuth } from "./context/ClineAuthContext"
import { useExtensionState } from "./context/ExtensionStateContext"
import { Providers } from "./Providers"
import { UiServiceClient } from "./services/grpc-client"
import { StateServiceClient, UiServiceClient } from "./services/grpc-client"
const AppContent = () => {
const {
didHydrateState,
showWelcome,
shouldShowAnnouncement,
dismissedBanners,
showMcp,
mcpTab,
showSettings,
@@ -37,23 +39,61 @@ const AppContent = () => {
hideWorktrees,
hideAnnouncement,
} = useExtensionState()
const [showKanbanModal, setShowKanbanModal] = useState(false)
const [hasShownKanbanModal, setHasShownKanbanModal] = useState(false)
const { clineUser, organizations, activeOrganization } = useClineAuth()
useEffect(() => {
if (shouldShowAnnouncement) {
setShowAnnouncement(true)
const showUpdateAnnouncementModal = useCallback(() => {
setShowAnnouncement(true)
UiServiceClient.onDidShowAnnouncement({} as EmptyRequest)
.then((response: Boolean) => {
setShouldShowAnnouncement(response.value)
})
.catch((error) => {
console.error("Failed to acknowledge announcement:", error)
})
}, [setShouldShowAnnouncement, setShowAnnouncement])
// Use the gRPC client instead of direct WebviewMessage
UiServiceClient.onDidShowAnnouncement({} as EmptyRequest)
.then((response: Boolean) => {
setShouldShowAnnouncement(response.value)
})
.catch((error) => {
console.error("Failed to acknowledge announcement:", error)
})
useEffect(() => {
if (!didHydrateState || showWelcome || hasShownKanbanModal) {
return
}
}, [shouldShowAnnouncement, setShouldShowAnnouncement, setShowAnnouncement])
const hasDismissedKanbanModal = dismissedBanners?.some((banner) => banner.bannerId === CLINE_KANBAN_MODAL_DISMISS_ID)
if (!hasDismissedKanbanModal) {
setShowKanbanModal(true)
}
setHasShownKanbanModal(true)
}, [didHydrateState, dismissedBanners, hasShownKanbanModal, showWelcome])
// Keep update announcements queued until the Kanban modal has either shown and closed or been skipped.
useEffect(() => {
if (!didHydrateState || showWelcome || !shouldShowAnnouncement || showAnnouncement) {
return
}
const isKanbanModalBlocking = showKanbanModal || !hasShownKanbanModal
if (isKanbanModalBlocking) {
return
}
showUpdateAnnouncementModal()
}, [
didHydrateState,
showWelcome,
shouldShowAnnouncement,
showAnnouncement,
showKanbanModal,
hasShownKanbanModal,
showUpdateAnnouncementModal,
])
const handleCloseKanbanModal = useCallback((doNotShowAgain: boolean) => {
setShowKanbanModal(false)
if (doNotShowAgain) {
StateServiceClient.dismissBanner({ value: CLINE_KANBAN_MODAL_DISMISS_ID }).catch((error) =>
console.error("Failed to persist Cline Kanban modal dismissal:", error),
)
}
}, [])
if (!didHydrateState) {
return null
@@ -65,6 +105,7 @@ const AppContent = () => {
return (
<div className="flex h-screen w-full flex-col">
<ClineKanbanLaunchModal onClose={handleCloseKanbanModal} open={showKanbanModal} />
{showSettings && <SettingsView onDone={hideSettings} targetSection={settingsTargetSection} />}
{showHistory && <HistoryView onDone={hideHistory} />}
{showMcp && <McpView initialTab={mcpTab} onDone={closeMcpView} />}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fa7c90bf657f317e63dee745e010898ca0cb13ee82e9dbe341ac16199170399a
size 3738635
@@ -0,0 +1,121 @@
import { StringRequest } from "@shared/proto/cline/common"
import { VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useMemo, useState } from "react"
import kanbanDemoVideo from "@/assets/cline_kanban_demo.mp4"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
import { FileServiceClient, StateServiceClient } from "@/services/grpc-client"
const INSTALL_COMMAND = "npm install -g cline"
const COPIED_TIMEOUT = 1500
const kanbanDemoVideoSrc = kanbanDemoVideo.startsWith("/src/")
? new URL(kanbanDemoVideo, import.meta.url).toString()
: kanbanDemoVideo
export const CLINE_KANBAN_MODAL_DISMISS_ID = "cline-kanban-launch-modal-v1"
interface ClineKanbanLaunchModalProps {
open: boolean
onClose: (doNotShowAgain: boolean) => void
}
export const ClineKanbanLaunchModal: React.FC<ClineKanbanLaunchModalProps> = ({ open, onClose }) => {
const [doNotShowAgain, setDoNotShowAgain] = useState(false)
const [isInstalling, setIsInstalling] = useState(false)
const [copied, setCopied] = useState(false)
const isVsCode = useMemo(() => PLATFORM_CONFIG.type === PlatformType.VSCODE, [])
useEffect(() => {
if (open) {
setCopied(false)
setIsInstalling(false)
}
}, [open])
const handleAction = async () => {
if (isVsCode) {
setIsInstalling(true)
try {
await StateServiceClient.installClineCli({})
} catch (error) {
console.error("Failed to run CLI install command:", error)
} finally {
setIsInstalling(false)
}
return
}
try {
await FileServiceClient.copyToClipboard(StringRequest.create({ value: INSTALL_COMMAND }))
setCopied(true)
setTimeout(() => setCopied(false), COPIED_TIMEOUT)
} catch (error) {
console.error("Failed to copy CLI install command:", error)
}
}
return (
<Dialog onOpenChange={(isOpen) => !isOpen && onClose(doNotShowAgain)} open={open}>
<DialogContent
aria-describedby="cline-kanban-description"
aria-labelledby="cline-kanban-title"
className="pt-4 px-5 pb-4 gap-0 max-w-2xl">
<div className="space-y-3" id="cline-kanban-description">
<div className="pr-6 min-h-6 flex items-center">
<h2
className="m-0 text-lg font-semibold"
id="cline-kanban-title"
style={{ color: "var(--vscode-editor-foreground)" }}>
Introducing Cline Kanban
</h2>
</div>
<video
autoPlay
className="w-full rounded-md border border-[var(--vscode-editorGroup-border)]"
loop
muted
playsInline
src={kanbanDemoVideoSrc}
/>
<p className="text-sm" style={{ color: "var(--vscode-descriptionForeground)" }}>
A replacement for your IDE better suited for running many agents in parallel and reviewing diffs. Enable
auto-commit and link cards together to create dependency chains that complete large amounts of work
autonomously.
</p>
<div className="p-1">
<code className="block rounded-sm px-2 py-1 bg-[var(--vscode-textCodeBlock-background)] text-sm">
{INSTALL_COMMAND}
</code>
<div className="mt-3">
<VSCodeButton disabled={isInstalling} onClick={handleAction}>
{isVsCode
? isInstalling
? "Running install command..."
: "Run in terminal"
: copied
? "Copied"
: "Copy command"}
</VSCodeButton>
</div>
</div>
<div className="pt-2">
<VSCodeCheckbox
checked={doNotShowAgain}
onChange={(e: any) => {
setDoNotShowAgain(e.target.checked === true)
}}>
Do not show again
</VSCodeCheckbox>
</div>
</div>
</DialogContent>
</Dialog>
)
}
export default ClineKanbanLaunchModal
+1
View File
@@ -38,6 +38,7 @@ if (!VALID_PLATFORMS.includes(platform)) {
console.log("Building webview for", platform)
export default defineConfig({
base: "./",
optimizeDeps: {
force: true, // Forces re-optimization
},