Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 .changeset/loud-jokes-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Add new welcome screen for improved onboarding
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export const globalSettingsSchema = z.object({
enterBehavior: z.enum(["send", "newline"]).optional(),
profileThresholds: z.record(z.string(), z.number()).optional(),
hasOpenedModeSelector: z.boolean().optional(),
hasCompletedOnboarding: z.boolean().optional(), // kilocode_change: Track if user has completed onboarding flow
lastModeExportPath: z.string().optional(),
lastModeImportPath: z.string().optional(),
appendSystemPrompt: z.string().optional(), // kilocode_change: Custom text to append to system prompt (CLI only)
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ export type ExtensionState = Pick<
marketplaceInstalledMetadata?: { project: Record<string, any>; global: Record<string, any> }
profileThresholds: Record<string, number>
hasOpenedModeSelector: boolean
hasCompletedOnboarding?: boolean // kilocode_change: Track if user has completed onboarding flow
openRouterImageApiKey?: string
kiloCodeImageApiKey?: string
openRouterUseMiddleOutTransform?: boolean
Expand Down Expand Up @@ -843,6 +844,7 @@ export interface WebviewMessage {
| "searchFiles"
| "toggleApiConfigPin"
| "hasOpenedModeSelector"
| "hasCompletedOnboarding" // kilocode_change: Mark onboarding as completed
| "clearCloudAuthSkipModel"
| "cloudButtonClicked"
| "rooCloudSignIn"
Expand Down
2 changes: 2 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2521,6 +2521,7 @@ export class ClineProvider
profileThresholds: profileThresholds ?? {},
cloudApiUrl: getRooCodeApiUrl(),
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
hasCompletedOnboarding: this.getGlobalState("hasCompletedOnboarding"), // kilocode_change: Track onboarding completion - undefined means new user
systemNotificationsEnabled: systemNotificationsEnabled ?? false, // kilocode_change
dismissedNotificationIds: dismissedNotificationIds ?? [], // kilocode_change
morphApiKey, // kilocode_change
Expand Down Expand Up @@ -2589,6 +2590,7 @@ export class ClineProvider
| "clineMessages"
| "renderContext"
| "hasOpenedModeSelector"
| "hasCompletedOnboarding" // kilocode_change
| "version"
| "shouldShowAnnouncement"
| "hasSystemPromptOverride"
Expand Down
6 changes: 6 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,12 @@ export const webviewMessageHandler = async (
await updateGlobalState("hasOpenedModeSelector", message.bool ?? true)
await provider.postStateToWebview()
break
// kilocode_change start: Handle onboarding completion
case "hasCompletedOnboarding":
await updateGlobalState("hasCompletedOnboarding", message.bool ?? true)
await provider.postStateToWebview()
break
// kilocode_change end
// kilocode_change start
case "kiloCodeImageApiKey":
await provider.contextProxy.setValue("kiloCodeImageApiKey", message.text)
Expand Down
49 changes: 44 additions & 5 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ExtensionStateContextProvider, useExtensionState } from "./context/Exte
import ChatView, { ChatViewRef } from "./components/chat/ChatView"
import HistoryView from "./components/history/HistoryView"
import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView"
import WelcomeView from "./components/kilocode/welcome/WelcomeView" // kilocode_change
import OnboardingView from "./components/kilocode/welcome/OnboardingView" // kilocode_change
import ProfileView from "./components/kilocode/profile/ProfileView" // kilocode_change
import McpView from "./components/mcp/McpView" // kilocode_change
import AuthView from "./components/kilocode/auth/AuthView" // kilocode_change
Expand Down Expand Up @@ -80,7 +80,6 @@ const defaultSectionByAction: Partial<Record<NonNullable<ExtensionMessage["actio
const App = () => {
const {
didHydrateState,
showWelcome,
shouldShowAnnouncement,
telemetrySetting,
telemetryKey,
Expand All @@ -94,6 +93,8 @@ const App = () => {
renderContext,
mdmCompliant,
apiConfiguration, // kilocode_change
hasCompletedOnboarding, // kilocode_change: Track onboarding state
taskHistoryFullLength, // kilocode_change: Used to detect existing users
} = useExtensionState()

// Create a persistent state manager
Expand Down Expand Up @@ -314,16 +315,54 @@ const App = () => {
}
}, [tab])

// kilocode_change start: Onboarding handlers
const handleSelectFreeModels = useCallback(() => {
// Mark onboarding as complete - the default profile is already set up with a free model
vscode.postMessage({ type: "hasCompletedOnboarding", bool: true })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: "Free models" selection only sets onboarding complete; it may still show the Welcome screen

handleSelectFreeModels posts hasCompletedOnboarding but does not navigate to chat or otherwise affect showWelcome. On the next render, this falls through to showWelcome ? <WelcomeView /> : …, so users who still have showWelcome === true may immediately land on WelcomeView instead of "Start coding immediately".

}, [])

const handleSelectPremiumModels = useCallback(() => {
// Mark onboarding as complete
vscode.postMessage({ type: "hasCompletedOnboarding", bool: true })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Onboarding is marked complete before premium sign-in succeeds

handleSelectPremiumModels posts hasCompletedOnboarding: true immediately, before the device auth flow has actually completed. If the user cancels / fails auth, they'll be treated as "onboarded" and may no longer see the onboarding entry point.

Consider setting completion only after successful authentication (or using a separate "started onboarding" flag).

// Navigate to auth view which will show the device code and handle the OAuth flow
// The AuthView auto-starts device auth on mount
switchTab("auth")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: switchTab("auth") won't take effect while onboarding is still rendered

handleSelectPremiumModels sets the tab to auth, but the render short-circuits to <OnboardingView /> while showOnboarding is true. Since hasCompletedOnboarding only flips after the extension posts updated state back, users can momentarily remain on the onboarding screen; and if showWelcome is still true when onboarding completes, they may land on <WelcomeView /> instead of the auth flow.

Consider hiding onboarding immediately (e.g., local state) or making the render conditional prioritize tab === "auth" / tab === "settings" after a selection.

setAuthReturnTo("chat")
}, [switchTab])

const handleSelectBYOK = useCallback(() => {
// Mark onboarding as complete
vscode.postMessage({ type: "hasCompletedOnboarding", bool: true })
// Navigate to settings with providers section
switchTab("settings")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Navigation won’t be visible until onboarding is dismissed

Because App short-circuits to OnboardingView while hasCompletedOnboarding !== true, this switchTab("settings") won’t render until the extension posts updated state. Consider also updating local UI state (optimistically hide onboarding) or deferring navigation until after the state refresh.

setCurrentSection("providers")
}, [switchTab])

// One-time migration: mark existing users as having completed onboarding
useEffect(() => {
if (hasCompletedOnboarding !== true && (taskHistoryFullLength ?? 0) > 0) {
vscode.postMessage({ type: "hasCompletedOnboarding", bool: true })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Onboarding migration may repeatedly post state updates

The migration useEffect posts hasCompletedOnboarding whenever hasCompletedOnboarding !== true && (taskHistoryFullLength ?? 0) > 0. If other state changes cause re-renders before the extension posts updated state back, this can send multiple duplicate hasCompletedOnboarding messages (and write global state multiple times).

Consider guarding with a useRef (send-once) or deriving the render condition directly from taskHistoryFullLength so existing users never render onboarding without needing to post a migration message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again don't think this is a concern and have not seen it in my testing.

}
}, [hasCompletedOnboarding, taskHistoryFullLength])
// kilocode_change end

if (!didHydrateState) {
return null
}

// kilocode_change start: Show OnboardingView for new users who haven't completed onboarding
const showOnboarding = hasCompletedOnboarding !== true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Onboarding may briefly flash for existing users

showOnboarding is derived solely from hasCompletedOnboarding, but existing users upgrading may initially have this unset/false. That means the UI renders <OnboardingView /> until the migration useEffect posts hasCompletedOnboarding and the extension sends updated state back.

If the goal is to never show onboarding to existing users, consider including taskHistoryFullLength (or another persisted signal) directly in the render condition so existing users skip onboarding on the first post-hydration render.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is an actual concern, and I don't see it in my testing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test it by completing the onboarding, doing zero tasks, and restarting the extensions (cmd-shift-p, reload webviews) with the extensions open? If the bot is right you'll probably see this screen flash. It sounds at least 70% probable to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markijbema - I've tested. It does not flash, at least for me. AI says the reason why is:

The Flow

  1. Initial state (line 297): hasCompletedOnboarding: undefined

  2. Before hydration (App.tsx line 349-350):
    if (!didHydrateState) {
    return null // Nothing renders at all
    }

  3. Hydration occurs (ExtensionStateContext.tsx lines 415-419):
    case "state": {
    const newState = message.state!
    setState((prevState) => mergeExtensionState(prevState, newState))
    // ...
    setDidHydrateState(true) // Only AFTER state is merged
    }

  4. The extension sends state (ClineProvider.ts line 2524):
    hasCompletedOnboarding: this.getGlobalState("hasCompletedOnboarding")

  5. State merge (line 242): { ...prevRest, ...newRest } - the extension's value overwrites the default undefined.

The key is that setDidHydrateState(true) is called AFTER setState merges the new state. This happens in the same event handler, so React batches these updates.

By the time the component renders for the first time (when didHydrateState becomes true), hasCompletedOnboarding already has the value from the extension's global state.


// Do not conditionally load ChatView, it's expensive and there's state we
// don't want to lose (user input, disableInput, askResponse promise, etc.)
// kilocode_change: no WelcomeViewProvider toggle
return showWelcome ? (
<WelcomeView />
return showOnboarding ? (
<OnboardingView
onSelectFreeModels={handleSelectFreeModels}
onSelectPremiumModels={handleSelectPremiumModels}
onSelectBYOK={handleSelectBYOK}
/>
) : (
// kilocode_change end
<>
{/* kilocode_change start */}
<MemoryWarningBanner />
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/__tests__/App.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ describe("App", () => {
didHydrateState: true,
showWelcome: false,
shouldShowAnnouncement: false,
// kilocode_change start: avoid rendering onboarding screen in App tests
hasCompletedOnboarding: true,
taskHistoryFullLength: 1,
// kilocode_change end
experiments: {},
language: "en",
telemetrySetting: "enabled",
Expand Down
63 changes: 63 additions & 0 deletions webview-ui/src/components/kilocode/welcome/OnboardingView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// kilocode_change - new file
import React from "react"
import Logo from "../common/Logo"
import { useAppTranslation } from "@/i18n/TranslationContext"

interface OnboardingOptionProps {
title: string
description: string
onClick: () => void
}

const OnboardingOption: React.FC<OnboardingOptionProps> = ({ title, description, onClick }) => {
return (
<button

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Add an explicit type="button"

Buttons default to type="submit", which can trigger unintended form submissions if this component is ever used within a <form>.

Suggested change
<button
<button type="button"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be a risk, since this is not in a form and unlikely to be.

className="w-full p-5 rounded-lg border border-vscode-panel-border bg-vscode-editor-background hover:bg-vscode-list-hoverBackground cursor-pointer text-left transition-colors"
onClick={onClick}>
<h3 className="text-lg font-semibold text-vscode-foreground m-0 mb-2">{title}</h3>
<p className="text-sm text-vscode-descriptionForeground m-0">{description}</p>
</button>
)
}

interface OnboardingViewProps {
onSelectFreeModels: () => void
onSelectPremiumModels: () => void
onSelectBYOK: () => void
}

const OnboardingView: React.FC<OnboardingViewProps> = ({ onSelectFreeModels, onSelectPremiumModels, onSelectBYOK }) => {
const { t } = useAppTranslation()

return (
<div className="flex flex-col items-center justify-center min-h-screen p-6 bg-vscode-sideBar-background">
<Logo width={80} height={80} />

<h1 className="text-2xl font-bold text-vscode-foreground text-center mt-4 mb-10">
{t("kilocode:onboarding.title")}
</h1>

<div className="w-full max-w-md flex flex-col gap-4">
<OnboardingOption
title={t("kilocode:onboarding.freeModels.title")}
description={t("kilocode:onboarding.freeModels.description")}
onClick={onSelectFreeModels}
/>

<OnboardingOption
title={t("kilocode:onboarding.premiumModels.title")}
description={t("kilocode:onboarding.premiumModels.description")}
onClick={onSelectPremiumModels}
/>

<OnboardingOption
title={t("kilocode:onboarding.byok.title")}
description={t("kilocode:onboarding.byok.description")}
onClick={onSelectBYOK}
/>
</div>
</div>
)
}

export default OnboardingView
96 changes: 0 additions & 96 deletions webview-ui/src/components/kilocode/welcome/WelcomeView.tsx

This file was deleted.

Loading
Loading