fix(vscode): restore native VS Code toolbar, scope webview bar to Cursor

This commit is contained in:
Bruno Agatao
2026-08-07 12:10:25 +02:00
parent 65682d4354
commit a9ef79b792
8 changed files with 198 additions and 18 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
"kilo-code": patch
---
Fix the sidebar navigation bar (New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, Settings) disappearing when the Kilo Code view is docked in the Secondary Side Bar. The navigation is now rendered inside the webview itself, so it stays visible regardless of where the view is docked.
Fix the sidebar navigation bar (New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, Settings) disappearing in Cursor when the Kilo Code view is docked in the Secondary Side Bar. Cursor now renders the navigation inside the webview itself so it stays visible regardless of dock location. VS Code is unaffected — it continues to use its native title bar toolbar, which already worked correctly everywhere.
+120 -1
View File
@@ -141,6 +141,41 @@
"category": "Kilo Code",
"icon": "$(settings-gear)"
},
{
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
"title": "New Task",
"icon": "$(add)"
},
{
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
"title": "Agent Manager",
"icon": "$(organization)"
},
{
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
"title": "KiloClaw",
"icon": "$(comment-discussion)"
},
{
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
"title": "Marketplace",
"icon": "$(extensions)"
},
{
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
"title": "History",
"icon": "$(history)"
},
{
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
"title": "Profile",
"icon": "$(account)"
},
{
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
"title": "Settings",
"icon": "$(settings-gear)"
},
{
"command": "kilo-code.new.openInTab",
"title": "Open in Tab",
@@ -426,12 +461,76 @@
],
"menus": {
"commandPalette": [
{
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
"when": "false"
},
{
"command": "kilo-code.new.agentManager.advancedWorktree",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
"when": "false"
},
{
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
"when": "false"
}
],
"view/title": [
{
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
"group": "navigation@0",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
"group": "navigation@1",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
"group": "navigation@2",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
"group": "navigation@3",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
"group": "navigation@4",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
"group": "navigation@5",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
"group": "navigation@6",
"when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor"
}
],
"view/title": [],
"scm/title": [
{
"command": "kilo-code.new.generateCommitMessage",
@@ -451,6 +550,26 @@
"command": "kilo-code.new.openInTab",
"group": "navigation",
"when": "true"
},
{
"command": "kilo-code.new.plusButtonClicked",
"group": "navigation@0",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.historyButtonClicked",
"group": "navigation@1",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.profileButtonClicked",
"group": "navigation@2",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
},
{
"command": "kilo-code.new.settingsButtonClicked",
"group": "navigation@3",
"when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor"
}
],
"editor/context": [
+5 -2
View File
@@ -17,7 +17,7 @@ import type { EditorContext, IndexingStatus } from "./services/cli-backend/types
import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController"
import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete"
import { notebookUri } from "./services/autocomplete/continuedev/core/autocomplete/notebook"
import { buildWebviewHtml, getWebviewFontSize } from "./utils"
import { buildWebviewHtml, getWebviewFontSize, isCursorHost } from "./utils"
import { saveImage } from "./kilo-provider/save-image"
import { handleEditorAction } from "./kilo-provider/editor-actions"
import { exportTranscript } from "./kilo-provider/export-transcript"
@@ -4905,7 +4905,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
title: "Kilo Code",
port: this.connectionService.getServerInfo()?.port,
extraStyles: `.container { height: 100vh; }`,
topBar: this.opts.hideTopBar !== true,
// Dedicated single-purpose panels (Settings, Profile, Sub-Agent Viewer)
// never show the bar. Sidebar and "Open in Tab" only need it in Cursor —
// VS Code's native toolbar (restored in package.json) works everywhere.
topBar: this.opts.hideTopBar !== true && isCursorHost(),
topBarSurface: this.opts.topBarSurface === "tab" ? "tab_title" : "sidebar_title",
})
}
+37 -1
View File
@@ -17,7 +17,7 @@ import { ensureBackendForAutocomplete } from "./services/autocomplete/ensure-bac
import { AutocompleteServiceManager } from "./services/autocomplete/AutocompleteServiceManager"
import { AttentionService } from "./services/attention"
import { BrowserAutomationService } from "./services/browser-automation"
import { TelemetryProxy } from "./services/telemetry"
import { TelemetryEventName, TelemetryProxy } from "./services/telemetry"
import { registerCommitMessageService } from "./services/commit-message"
import { registerCodeActions, registerTerminalActions, KiloCodeActionProvider } from "./services/code-actions"
import { registerToggleAutoApprove } from "./commands/toggle-auto-approve"
@@ -26,6 +26,7 @@ import { RemoteStatusService } from "./services/RemoteStatusService"
import { markWorkspace } from "./util/spotlight"
import { createNotebookBridge } from "./services/notebook"
import { createGitExecutable } from "./util/git-executable"
import { isCursorHost } from "./utils"
let agentManager: AgentManagerProvider | undefined
let shuttingDown = false
@@ -48,6 +49,10 @@ export function activate(context: vscode.ExtensionContext) {
console.log("Kilo Code extension is now active")
shuttingDown = false
// Drives the "!kilo-code.new.isCursor" guards on the native view/title and
// editor/title menu contributions — see isCursorHost() for why.
void vscode.commands.executeCommand("setContext", "kilo-code.new.isCursor", isCursorHost())
const telemetry = TelemetryProxy.getInstance()
// Create shared connection service (one server for all webviews)
@@ -334,8 +339,39 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
// Sidebar menus use wrapper commands so this event measures real title button presses,
// not programmatic opens, shortcuts, or editor title commands.
const track = (button: string, command: string) => {
TelemetryProxy.capture(TelemetryEventName.TITLE_BUTTON_CLICKED, {
button,
surface: "sidebar_title",
})
void vscode.commands.executeCommand(command)
}
// Register toolbar button command handlers
context.subscriptions.push(
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.plusButtonClicked", () => {
track("new_task", "kilo-code.new.plusButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.historyButtonClicked", () => {
track("history", "kilo-code.new.historyButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.agentManagerOpen", () => {
track("agent_manager", "kilo-code.new.agentManagerOpen")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.kiloClawOpen", () => {
track("kiloclaw", "kilo-code.new.kiloClawOpen")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.marketplaceButtonClicked", () => {
track("marketplace", "kilo-code.new.marketplaceButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.profileButtonClicked", () => {
track("profile", "kilo-code.new.profileButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.settingsButtonClicked", () => {
track("settings", "kilo-code.new.settingsButtonClicked")
}),
vscode.commands.registerCommand("kilo-code.new.plusButtonClicked", () => {
const tab = activeTabProvider()
if (tab) tab.postMessage({ type: "action", action: "plusButtonClicked" })
+21
View File
@@ -18,6 +18,27 @@ export function getWebviewFontSize(): number {
return clamp(raw)
}
/**
* True when running inside Cursor rather than real VS Code (or another
* fork). Cursor's Secondary Side Bar support is known to be unreliable for
* extension-contributed `view/title` toolbars (see
* https://github.com/anthropics/claude-code/issues/31375 for the same class
* of bug in a different extension), so Cursor falls back to an in-webview
* navigation bar instead of the native toolbar that VS Code renders fine
* everywhere.
*
* This is a per-host choice, not a per-dock-location one: `WebviewView` (and
* the rest of the public API, checked against @types/vscode) exposes no way
* to ask "is my view currently in the primary or secondary side bar", so the
* webview fallback bar is Cursor's only option in both locations. Accepted
* trade-off: Cursor's primary side bar loses the single-line native look it
* had before this existed, in exchange for the Secondary Side Bar actually
* working, with zero guessing about dock position anywhere.
*/
export function isCursorHost(): boolean {
return vscode.env.appName.toLowerCase().includes("cursor")
}
function fontStyle(): string {
const base = getWebviewFontSize()
const vars = SIZES.map((size) => `--kilo-font-size-${size}: ${(base * size) / 13}px;`).join("\n ")
+5 -2
View File
@@ -311,8 +311,11 @@ const AppContent: Component = () => {
// Set synchronously in the webview HTML by KiloProvider so it's available
// before this component ever mounts (see buildWebviewHtml/_getHtmlForWebview).
// Dedicated single-purpose panels (Settings, Profile, Sub-Agent Viewer) set
// KILO_TOP_BAR = false since navigating away from them makes no sense.
// False for dedicated single-purpose panels (Settings, Profile, Sub-Agent
// Viewer) always, and for the Sidebar/"Open in Tab" outside Cursor — real
// VS Code's native title bar toolbar already covers those. Defaults to
// true only when unset entirely (e.g. Storybook, which doesn't render the
// real page HTML).
const host = window as { KILO_TOP_BAR?: boolean; KILO_TOP_BAR_SURFACE?: string }
const showTopBar = host.KILO_TOP_BAR !== false
const topBarSurface = host.KILO_TOP_BAR_SURFACE ?? "sidebar_title"
@@ -1,8 +1,10 @@
/**
* Renders New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, and
* Settings inside the webview. VS Code's native `view/title` toolbar renders
* outside the webview DOM and disappears in the Secondary Side Bar with no way
* to detect or work around that — this bar guarantees the actions stay visible.
* Settings inside the webview, as a fallback for Cursor only (see isCursorHost()
* in src/utils.ts). Cursor's Secondary Side Bar support is unreliable for
* extension-contributed `view/title` toolbars, which render outside the webview
* DOM with no API to detect or work around the failure. Real VS Code renders the
* native toolbar fine everywhere, so it keeps using that instead of this bar.
*/
import { Component, For } from "solid-js"
@@ -45,13 +47,12 @@ export const SidebarTopBar: Component<SidebarTopBarProps> = (props) => {
type: "openAgentManager" | "openKiloClaw" | "openMarketplacePanel" | "openProfilePanel" | "openSettingsPanel",
) => vscode.postMessage({ type })
const actions: (Action | "spacer")[] = [
const actions: Action[] = [
{ key: "newTask", codicon: "add", button: "new_task", run: () => props.onNewTask() },
{ key: "history", codicon: "history", button: "history", run: () => props.onHistory() },
{ key: "agentManager", codicon: "organization", button: "agent_manager", run: () => open("openAgentManager") },
{ key: "kiloClaw", codicon: "comment-discussion", button: "kiloclaw", run: () => open("openKiloClaw") },
{ key: "marketplace", codicon: "extensions", button: "marketplace", run: () => open("openMarketplacePanel") },
"spacer",
{ key: "profile", codicon: "account", button: "profile", run: () => open("openProfilePanel") },
{ key: "settings", codicon: "settings-gear", button: "settings", run: () => open("openSettingsPanel") },
]
@@ -60,7 +61,6 @@ export const SidebarTopBar: Component<SidebarTopBarProps> = (props) => {
<div class="sidebar-top-bar" role="toolbar" aria-label={language.t("sidebar.topBar.label")}>
<For each={actions}>
{(action) => {
if (action === "spacer") return <div class="sidebar-top-bar-spacer" />
const label = language.t(`sidebar.topBar.${action.key}`)
return (
<Tooltip value={label} placement="bottom">
@@ -35,6 +35,9 @@
--top-bar-border: 1px solid var(--vscode-sideBar-border, var(--vscode-widget-border));
display: flex;
align-items: center;
/* Matches the native view/title toolbar, which clusters its icons at the
right edge of the row rather than the left. */
justify-content: flex-end;
gap: 2px;
flex-shrink: 0;
padding: 4px 6px;
@@ -48,11 +51,6 @@
display: none;
}
.sidebar-top-bar-spacer {
flex: 1;
min-width: 4px;
}
.chat-messages-wrapper {
position: relative;
flex: 1;