Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/animated-kilo-logo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Animate the Kilo logo on the welcome screen: hover the mark to see it turn and play the yellow slot-machine animation.
25 changes: 14 additions & 11 deletions bun.lock

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

Binary file not shown.
15 changes: 15 additions & 0 deletions packages/kilo-vscode/esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,23 @@ function notices() {
}
}

/**
* The DotLottie player defaults to a CDN for its WASM renderer. Ship the copy from
* `@lottiefiles/dotlottie-web` next to the webview bundles so the animated Kilo logo never
* reaches the network (the webview CSP blocks it anyway).
*/
function wasm() {
const root = path.dirname(require.resolve("@lottiefiles/dotlottie-web/package.json"))
fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true })
fs.copyFileSync(
path.join(root, "dist", "dotlottie-player.wasm"),
path.join(__dirname, "dist", "dotlottie-player.wasm"),
)
}

async function main() {
notices()
wasm()
const extensionConfig = getExtensionConfig()
const webviewsConfig = getWebviewsConfig()
const shikiWorkerConfig = getShikiWorkerConfig()
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1369,6 +1369,7 @@
"@kilocode/kilo-ui": "workspace:*",
"@kilocode/plugin": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@lottiefiles/dotlottie-web": "0.63.0",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@pierre/diffs": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { type Component, createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { DotLottie } from "@lottiefiles/dotlottie-web"

/**
* DotLottie defaults to a CDN for its WASM renderer. Point it at the copy esbuild ships next
* to the webview bundles (see `wasm()` in esbuild.js) so the logo never reaches the network.
* The shiki worker URI is the only `dist/` URI the host injects, so derive the WASM URI from it.
*/
const wasm = (window as { KILO_SHIKI_WORKER_URI?: string }).KILO_SHIKI_WORKER_URI?.replace(
/shiki-worker\.js$/,
"dotlottie-player.wasm",
)
if (wasm) DotLottie.setWasmUrl(wasm)

export const reduced = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true

/**
* Yellow slot-machine Kilo mark, mirrors Kilo Cloud's welcome logo hover animation.
*
* `onReady` reports whether the player can draw. The parent keeps the static mark visible until
* that is true, so a slow or failed load never leaves an empty square on hover.
*/
export const AnimatedKiloLogo: Component<{ playing: boolean; onReady: (ready: boolean) => void }> = (props) => {
const icons = (window as { ICONS_BASE_URI?: string }).ICONS_BASE_URI || ""
const [loaded, setLoaded] = createSignal(false)
let canvas!: HTMLCanvasElement
let player: DotLottie | undefined

// A failed load leaves the canvas blank, so log it and let the static mark stay visible.
const fail = (event: { error: Error }) => {
console.warn("[Kilo New] animated Kilo logo failed to load:", event.error)
setLoaded(false)
props.onReady(false)
}

onMount(() => {
const dl = new DotLottie({ canvas, src: `${icons}/kilo-yellow.lottie`, loop: true })
// `play()` is a no-op until the animation and WASM renderer are loaded, so wait for `load`.
dl.addEventListener("load", () => {
setLoaded(true)
props.onReady(true)
})
dl.addEventListener("loadError", fail)
dl.addEventListener("renderError", fail)
player = dl
onCleanup(() => dl.destroy())
})

// Only burn cycles while the mark is visible.
createEffect(() => {
const dl = player
if (!dl) return
if (loaded() && props.playing) return dl.play()
dl.pause()
})

return <canvas ref={canvas} class="kilo-logo-lottie" aria-hidden="true" />
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Component, type JSX, For, Show } from "solid-js"
import { type Component, type JSX, createSignal, For, Show } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import { AnimatedKiloLogo, reduced } from "../brand/AnimatedKiloLogo"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"
Expand All @@ -13,15 +14,40 @@ interface WelcomeEmptyStateProps {
footer?: JSX.Element
}

/**
* Square Kilo mark. Hover rotates the mark and crossfades to the yellow Lottie animation,
* matching Kilo Cloud's header logo. The player mounts on first hover so the WASM renderer
* never delays the empty state; the static mark stays visible until the player can draw, so a
* slow or failed load never leaves an empty square.
*/
export const KiloLogo = () => {
const icons = (window as { ICONS_BASE_URI?: string }).ICONS_BASE_URI || ""
const light =
document.body.classList.contains("vscode-light") || document.body.classList.contains("vscode-high-contrast-light")
const file = light ? "kilo-light.svg" : "kilo-dark.svg"
const [hover, setHover] = createSignal(false)
const [ready, setReady] = createSignal(false)
const [mounted, setMounted] = createSignal(false)

return (
<div class="kilo-logo">
<img src={`${icons}/${file}`} alt="Kilo Code" />
<div
class="kilo-logo"
classList={{ "kilo-logo-hover": hover(), "kilo-logo-ready": hover() && ready() }}
onMouseEnter={() => {
if (reduced()) return
setMounted(true)
setHover(true)
}}
onMouseLeave={() => setHover(false)}
>
<Show when={mounted()}>
<div class="kilo-logo-layer kilo-logo-animated">
<AnimatedKiloLogo playing={hover() && ready()} onReady={setReady} />
</div>
</Show>
<div class="kilo-logo-layer kilo-logo-static">
<img src={`${icons}/${file}`} alt="Kilo Code" />
</div>
</div>
)
}
Expand Down
47 changes: 46 additions & 1 deletion packages/kilo-vscode/webview-ui/src/styles/welcome.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,62 @@
============================================ */

.kilo-logo {
position: relative;
width: 48px;
height: 48px;
opacity: 0.85;
overflow: hidden;
transition: transform 0.3s ease-in-out;
}

.kilo-logo-hover {
transform: rotate(90deg);
}

.kilo-logo-layer {
position: absolute;
inset: 0;
transition:
opacity 0.2s,
transform 0.2s;
}

.kilo-logo-static {
opacity: 1;
}

.kilo-logo-animated {
opacity: 0;
}

.kilo-logo-hover .kilo-logo-animated {
transform: rotate(270deg);
}

/* The crossfade waits for the player, so a slow or failed load keeps the static mark. */
.kilo-logo-ready .kilo-logo-static {
opacity: 0;
}

.kilo-logo-ready .kilo-logo-animated {
opacity: 1;
}

.kilo-logo img {
.kilo-logo img,
.kilo-logo-lottie {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}

@media (prefers-reduced-motion: reduce) {
.kilo-logo,
.kilo-logo-layer {
transition: none;
}
}

.kilo-about-text {
max-width: 260px;
font-size: var(--kilo-font-size-13);
Expand Down
Loading