Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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
48 changes: 47 additions & 1 deletion webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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

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.

we should now never show this right? so why have two? Any reason not to ditch the welcomeview?

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 - can you take another look? I removed this in 0e8481f

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 @@ -94,6 +95,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 +317,59 @@ const App = () => {
}
}, [tab])

// kilocode_change start: Onboarding handlers
const handleSelectFreeModels = 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: "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".

// The default profile is already set up with a free model, so just close welcome
// This will trigger a state update that sets showWelcome to false

This comment was marked as outdated.

}, [])

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])
// kilocode_change end

if (!didHydrateState) {
return null
}

// kilocode_change start: Show OnboardingView for new users who haven't completed onboarding
// Show onboarding only if:
// 1. hasCompletedOnboarding is not true (undefined or false)
// 2. AND user has no task history (meaning they're truly new, not an existing user upgrading)
//
// This ensures existing users who upgrade don't see the onboarding screen,
// while new users who have never used the extension will see it.
const isExistingUser = (taskHistoryFullLength ?? 0) > 0

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.

maybe we can also set 'hasCompletedOnboarding' if this is true so we can choose this logic at some point in the future, that way this choice doesn't tie in to specific architecture choices

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 - can you review the new code? Hopefully this is better integrated now.

const showOnboarding = hasCompletedOnboarding !== true && !isExistingUser

// 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 ? (
return showOnboarding ? (
<OnboardingView
onSelectFreeModels={handleSelectFreeModels}
onSelectPremiumModels={handleSelectPremiumModels}
onSelectBYOK={handleSelectBYOK}
/>
) : showWelcome ? (
<WelcomeView />
) : (
// kilocode_change end
<>
{/* kilocode_change start */}
<MemoryWarningBanner />
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// kilocode_change - new file
// npx vitest src/components/kilocode/welcome/__tests__/OnboardingView.spec.tsx

import { render, screen, fireEvent } from "@/utils/test-utils"
import OnboardingView from "../OnboardingView"

// Mock Logo component
vi.mock("../../common/Logo", () => ({
default: () => <div data-testid="kilo-logo">Kilo Logo</div>,
}))

describe("OnboardingView", () => {
const mockOnSelectFreeModels = vi.fn()
const mockOnSelectPremiumModels = vi.fn()
const mockOnSelectBYOK = vi.fn()

beforeEach(() => {
vi.clearAllMocks()
})

it("renders the Kilo logo", () => {
render(
<OnboardingView
onSelectFreeModels={mockOnSelectFreeModels}
onSelectPremiumModels={mockOnSelectPremiumModels}
onSelectBYOK={mockOnSelectBYOK}
/>,
)

expect(screen.getByTestId("kilo-logo")).toBeInTheDocument()
})

it("renders the title", () => {
render(
<OnboardingView
onSelectFreeModels={mockOnSelectFreeModels}
onSelectPremiumModels={mockOnSelectPremiumModels}
onSelectBYOK={mockOnSelectBYOK}
/>,
)

// The translation key is returned as-is by the test-utils mock
expect(screen.getByText("kilocode:onboarding.title")).toBeInTheDocument()
})

it("renders all three options", () => {
render(
<OnboardingView
onSelectFreeModels={mockOnSelectFreeModels}
onSelectPremiumModels={mockOnSelectPremiumModels}
onSelectBYOK={mockOnSelectBYOK}
/>,
)

expect(screen.getByText("kilocode:onboarding.freeModels.title")).toBeInTheDocument()
expect(screen.getByText("kilocode:onboarding.freeModels.description")).toBeInTheDocument()

expect(screen.getByText("kilocode:onboarding.premiumModels.title")).toBeInTheDocument()
expect(screen.getByText("kilocode:onboarding.premiumModels.description")).toBeInTheDocument()

expect(screen.getByText("kilocode:onboarding.byok.title")).toBeInTheDocument()
expect(screen.getByText("kilocode:onboarding.byok.description")).toBeInTheDocument()
})

it("calls onSelectFreeModels when Free models option is clicked", () => {
render(
<OnboardingView
onSelectFreeModels={mockOnSelectFreeModels}
onSelectPremiumModels={mockOnSelectPremiumModels}
onSelectBYOK={mockOnSelectBYOK}
/>,
)

const freeModelsButton = screen.getByText("kilocode:onboarding.freeModels.title").closest("button")
expect(freeModelsButton).toBeInTheDocument()
fireEvent.click(freeModelsButton!)

expect(mockOnSelectFreeModels).toHaveBeenCalledTimes(1)
expect(mockOnSelectPremiumModels).not.toHaveBeenCalled()
expect(mockOnSelectBYOK).not.toHaveBeenCalled()
})

it("calls onSelectPremiumModels when Premium models option is clicked", () => {
render(
<OnboardingView
onSelectFreeModels={mockOnSelectFreeModels}
onSelectPremiumModels={mockOnSelectPremiumModels}
onSelectBYOK={mockOnSelectBYOK}
/>,
)

const premiumModelsButton = screen.getByText("kilocode:onboarding.premiumModels.title").closest("button")
expect(premiumModelsButton).toBeInTheDocument()
fireEvent.click(premiumModelsButton!)

expect(mockOnSelectPremiumModels).toHaveBeenCalledTimes(1)
expect(mockOnSelectFreeModels).not.toHaveBeenCalled()
expect(mockOnSelectBYOK).not.toHaveBeenCalled()
})

it("calls onSelectBYOK when BYOK option is clicked", () => {
render(
<OnboardingView
onSelectFreeModels={mockOnSelectFreeModels}
onSelectPremiumModels={mockOnSelectPremiumModels}
onSelectBYOK={mockOnSelectBYOK}
/>,
)

const byokButton = screen.getByText("kilocode:onboarding.byok.title").closest("button")
expect(byokButton).toBeInTheDocument()
fireEvent.click(byokButton!)

expect(mockOnSelectBYOK).toHaveBeenCalledTimes(1)
expect(mockOnSelectFreeModels).not.toHaveBeenCalled()
expect(mockOnSelectPremiumModels).not.toHaveBeenCalled()
})
})
6 changes: 6 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export interface ExtensionStateContextType extends ExtensionState {
mdmCompliant?: boolean
hasOpenedModeSelector: boolean // New property to track if user has opened mode selector
setHasOpenedModeSelector: (value: boolean) => void // Setter for the new property
hasCompletedOnboarding: boolean // kilocode_change: Track if user has completed onboarding flow
Comment thread
lambertjosh marked this conversation as resolved.
Outdated
setHasCompletedOnboarding: (value: boolean) => void // kilocode_change
alwaysAllowFollowupQuestions: boolean // New property for follow-up questions auto-approve
setAlwaysAllowFollowupQuestions: (value: boolean) => void // Setter for the new property
followupAutoApproveTimeoutMs: number | undefined // Timeout in ms for auto-approving follow-up questions
Expand Down Expand Up @@ -292,6 +294,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
customCondensingPrompt: "", // Default empty string for custom condensing prompt
yoloGatekeeperApiConfigId: "", // kilocode_change: Default empty string for gatekeeper API config ID
hasOpenedModeSelector: false, // Default to false (not opened yet)
hasCompletedOnboarding: false, // kilocode_change: Default to false (not completed yet)
Comment thread
lambertjosh marked this conversation as resolved.
Outdated
autoApprovalEnabled: true,
customModes: [],
maxOpenTabsContext: 20,
Expand Down Expand Up @@ -708,6 +711,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
enterBehavior: state.enterBehavior ?? "send",
setEnterBehavior: (value) => setState((prevState) => ({ ...prevState, enterBehavior: value })),
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),
hasCompletedOnboarding: state.hasCompletedOnboarding ?? false, // kilocode_change
Comment thread
lambertjosh marked this conversation as resolved.
Outdated
setHasCompletedOnboarding: (value) =>
setState((prevState) => ({ ...prevState, hasCompletedOnboarding: value })), // kilocode_change
setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })),
setAutoCondenseContextPercent: (value) =>
setState((prevState) => ({ ...prevState, autoCondenseContextPercent: value })),
Expand Down
15 changes: 15 additions & 0 deletions webview-ui/src/i18n/locales/ar/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions webview-ui/src/i18n/locales/ca/kilocode.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading