diff --git a/.changeset/vscode-kilo-model-links.md b/.changeset/vscode-kilo-model-links.md new file mode 100644 index 00000000000..2f71e28221a --- /dev/null +++ b/.changeset/vscode-kilo-model-links.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Support selecting Kilo Gateway catalog models from VS Code deep links. diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 8bb48e6b2ff..193950adbfb 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -54,7 +54,8 @@ "open code" ], "activationEvents": [ - "onStartupFinished" + "onStartupFinished", + "onUri" ], "main": "./dist/extension.js", "contributes": { diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index b332128d12c..844fc53f651 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -273,6 +273,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private configWarningsShown = false /** Cached notificationsLoaded payload */ private cachedNotificationsMessage: unknown = null + private pendingKiloModelID: string | null = null private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = [] private readyResolvers: (() => void)[] = [] private promptRecoveryQueued = false @@ -687,6 +688,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "openCloudSession", sessionId }) } + public selectKiloModel(modelID: string): void { + this.pendingKiloModelID = modelID + this.flushPendingKiloModel() + } + public setContinueInWorktreeHandler( handler: (sessionId: string, progress: (status: string, detail?: string, error?: string) => void) => Promise, ): void { @@ -759,6 +765,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper console.log("[Kilo New] KiloProvider: ✅ webviewReady received") this.isWebviewReady = true this.visibleTaskStreams.clear() + this.flushPendingKiloModel() await this.syncWebviewState("webviewReady") this.flushPendingReviewComments() this.recoverPendingPrompts() @@ -1245,6 +1252,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Connect the shared service (no-op if already connected) await this.connectionService.connect(workspaceDir) + this.flushPendingKiloModel() // Subscribe to SSE events for this webview (filtered by tracked sessions) this.unsubscribeEvent = this.connectionService.onEventFiltered( @@ -1283,6 +1291,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postConnectionState(error) if (state === "connected") { + this.flushPendingKiloModel() // Fire config warnings independently so a failure in the // sequential await chain doesn't prevent warnings from being shown void this.checkConfigWarnings("state") @@ -3219,6 +3228,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }) } + private flushPendingKiloModel(): void { + if (!this.webview || !this.isWebviewReady || !this.client || !this.pendingKiloModelID) return + + const modelID = this.pendingKiloModelID + this.pendingKiloModelID = null + this.postMessage({ type: "selectKiloModel", modelID }) + } + public async appendReviewComments(comments: unknown[], autoSend = false): Promise { this.pendingReviewComments.push({ comments, autoSend }) diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 2d956ed825e..9d19f9aab36 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -36,9 +36,9 @@ const panelTitleHandler = (panel: vscode.WebviewPanel) => (title: string) => { panel.title = title || EXTENSION_DISPLAY_NAME } -// Activated via "onStartupFinished" (package.json) so that commands, code actions, keybindings, -// autocomplete, commit-message generation, and URI deep links all work immediately — without -// requiring the user to open a Kilo sidebar or panel first. The CLI backend is NOT spawned here; +// Activated via "onStartupFinished" and "onUri" (package.json) so that commands, code actions, +// keybindings, autocomplete, commit-message generation, and URI deep links all work immediately — +// without requiring the user to open a Kilo sidebar or panel first. The CLI backend is NOT spawned here; // it starts lazily when a webview connects or when ensureBackendForAutocomplete() triggers it. export function activate(context: vscode.ExtensionContext) { console.log("Kilo Code extension is now active") @@ -476,17 +476,25 @@ export function activate(context: vscode.ExtensionContext) { ), ) - // Register URI handler for session imports (vscode://kilocode.kilo-code/kilocode/s/{sessionId}) + // Register URI handler for extension deep links (vscode://kilocode.kilo-code/kilocode/...) context.subscriptions.push( vscode.window.registerUriHandler({ async handleUri(uri: vscode.Uri) { - const match = uri.path.match(/^\/kilocode\/s\/([a-zA-Z0-9_-]+)$/) - if (!match) return - const sessionId = match[1] - if (!sessionId) return - console.log("[Kilo New] URI handler: opening cloud session:", sessionId) + const sessionMatch = uri.path.match(/^\/kilocode\/s\/([a-zA-Z0-9_-]+)$/) + const sessionId = sessionMatch?.[1] + if (sessionId) { + console.log("[Kilo New] URI handler: opening cloud session:", sessionId) + await vscode.commands.executeCommand(`${KiloProvider.viewType}.focus`) + provider.openCloudSession(sessionId) + return + } + + if (uri.path !== "/kilocode/model") return + const modelID = new URLSearchParams(uri.query).get("model") + if (!modelID) return + console.log("[Kilo New] URI handler: selecting linked Kilo model:", modelID) await vscode.commands.executeCommand(`${KiloProvider.viewType}.focus`) - provider.openCloudSession(sessionId) + provider.selectKiloModel(modelID) }, }), ) diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index e2a3c40bac9..84efeac9899 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -244,6 +244,10 @@ const AppContent: Component = () => { setCurrentView("newTask") } + const handleKiloModel = (message: { type?: string }) => { + if (message.type === "selectKiloModel") setCurrentView("newTask") + } + onMount(() => { const handler = (event: MessageEvent) => { const message = event.data @@ -262,6 +266,7 @@ const AppContent: Component = () => { session.selectCloudSession(message.sessionId) setCurrentView("newTask") } + handleKiloModel(message) handleForked(message) if (message?.type === "viewSubAgentSession" && message.sessionID) { console.log("[Kilo New] App: 🔍 viewSubAgentSession:", message.sessionID) diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 8d296b2fac4..e589b4bde0b 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -69,7 +69,7 @@ import { PartStash } from "./part-stash" import { mergeParts, sameParts } from "./session-parts" import { state as todoState } from "./todo-revert" import { getVariant, sessionVariantKeys, transferVariants, variantKey } from "./session-variant-store" -import { KILO_AUTO, parseModelString } from "../../../src/shared/provider-model" +import { KILO_AUTO, KILO_PROVIDER_ID, parseModelString } from "../../../src/shared/provider-model" import { visibleMessages as filterVisibleMessages } from "./session-queue" const RECENT_LIMIT = 5 @@ -364,6 +364,8 @@ export const SessionProvider: ParentComponent = (props) => { const [agents, setAgents] = createSignal([]) const [allAgents, setAllAgents] = createSignal([]) const [defaultAgent, setDefaultAgent] = createSignal("code") + const [pendingKiloModel, setPendingKiloModel] = createSignal<{ modelID: string; after: number } | null>(null) + const [catalog, setCatalog] = createSignal(0) // Skills loaded from the CLI backend const [skills, setSkills] = createSignal([]) @@ -589,6 +591,31 @@ export const SessionProvider: ParentComponent = (props) => { } } + function selectKiloModel(modelID: string) { + setPendingKiloModel({ modelID, after: catalog() }) + vscode.postMessage({ type: "requestProviders" }) + } + + const unsubKiloModel = vscode.onMessage((message: ExtensionMessage) => { + if (message.type === "providersLoaded") { + setCatalog((value) => value + 1) + return + } + if (message.type === "selectKiloModel") selectKiloModel(message.modelID) + }) + onCleanup(unsubKiloModel) + + createEffect(() => { + const pending = pendingKiloModel() + if (!pending || agents().length === 0 || catalog() <= pending.after) return + setPendingKiloModel(null) + if (!provider.providers()[KILO_PROVIDER_ID]?.models[pending.modelID]) { + console.warn("[Kilo New] Ignoring unavailable Kilo catalog model:", pending.modelID) + return + } + selectModel(KILO_PROVIDER_ID, pending.modelID) + }) + function promptAgent(sessionID?: string) { const name = agentForScope(sessionID) return name !== defaultAgent() ? name : undefined diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index d0d74bd4d92..1d5d77fdd7a 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -237,6 +237,11 @@ export interface OpenCloudSessionMessage { sessionId: string } +export interface SelectKiloModelMessage { + type: "selectKiloModel" + modelID: string +} + export interface ActionMessage { type: "action" action: string @@ -1027,6 +1032,7 @@ export type ExtensionMessage = | CloudSessionImportedMessage | CloudSessionImportFailedMessage | OpenCloudSessionMessage + | SelectKiloModelMessage | AgentManagerBranchesMessage | AgentManagerExternalWorktreesMessage | AgentManagerImportResultMessage