fix(vscode): generalize model deep links

This commit is contained in:
Josh Lambert
2026-06-03 23:31:59 -04:00
parent d1b29009dd
commit 235e769fb3
12 changed files with 94 additions and 58 deletions
@@ -1,5 +0,0 @@
---
"kilo-code": minor
---
Support selecting promoted Kilo models from VS Code deep links.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Support selecting Kilo Gateway catalog models from VS Code deep links.
+4 -5
View File
@@ -21,12 +21,12 @@ import { registerToggleAutoApprove } from "./commands/toggle-auto-approve"
import { registerHeapSnapshot } from "./commands/heap-snapshot"
import { RemoteStatusService } from "./services/RemoteStatusService"
import { markWorkspace } from "./util/spotlight"
import { kiloModelFromURI } from "./kilo-provider/model-uri"
let agentManager: AgentManagerProvider | undefined
let shuttingDown = false
const RESTORE_KEY = "kilo.workbench.restore"
const PROMOTED_KILO_MODEL_IDS = new Set(["stealth/claude-opus-4.8"])
type RestoreState = {
sidebar?: boolean
@@ -485,10 +485,9 @@ export function activate(context: vscode.ExtensionContext) {
return
}
if (uri.path !== "/kilocode/model") return
const modelID = new URLSearchParams(uri.query).get("model")
if (!modelID || !PROMOTED_KILO_MODEL_IDS.has(modelID)) return
console.log("[Kilo New] URI handler: selecting promoted model:", modelID)
const modelID = kiloModelFromURI(uri)
if (!modelID) return
console.log("[Kilo New] URI handler: selecting linked Kilo model:", modelID)
await vscode.commands.executeCommand(`${KiloProvider.viewType}.focus`)
provider.selectKiloModel(modelID)
},
@@ -0,0 +1,9 @@
type URI = {
path: string
query: string
}
export function kiloModelFromURI(uri: URI): string | undefined {
if (uri.path !== "/kilocode/model") return undefined
return new URLSearchParams(uri.query).get("model") || undefined
}
@@ -22,7 +22,7 @@ function connection() {
}
}
describe("KiloProvider promoted model selection", () => {
describe("KiloProvider model URI selection", () => {
it("flushes a queued model before ancillary webview sync can fail", async () => {
const service = connection()
const provider = new KiloProvider({} as never, service as never)
@@ -39,11 +39,11 @@ describe("KiloProvider promoted model selection", () => {
throw new Error("profile unavailable")
}
provider.selectKiloModel("stealth/claude-opus-4.8")
provider.selectKiloModel("vendor/new-live-model")
expect(sent).toEqual([])
await expect(internal.handleWebviewReady()).rejects.toThrow("profile unavailable")
expect(sent).toEqual([{ type: "selectKiloModel", modelID: "stealth/claude-opus-4.8" }])
expect(sent).toEqual([{ type: "selectKiloModel", modelID: "vendor/new-live-model" }])
})
it("keeps a queued model until the backend client is available", () => {
@@ -61,11 +61,11 @@ describe("KiloProvider promoted model selection", () => {
}
internal.isWebviewReady = true
provider.selectKiloModel("stealth/claude-opus-4.8")
provider.selectKiloModel("vendor/new-live-model")
expect(sent).toEqual([])
service.state.connected = true
internal.flushPendingKiloModel()
expect(sent).toEqual([{ type: "selectKiloModel", modelID: "stealth/claude-opus-4.8" }])
expect(sent).toEqual([{ type: "selectKiloModel", modelID: "vendor/new-live-model" }])
})
})
@@ -1,7 +1,7 @@
import { describe, expect, it } from "bun:test"
import {
isCurrentModelSelections,
promotion,
kiloCatalogModelStatus,
resolveModelSelection,
} from "../../webview-ui/src/context/model-selection"
import { KILO_AUTO, parseModelString } from "../../src/shared/provider-model"
@@ -16,7 +16,7 @@ function makeProvider(id: string, name: string, modelIds: string[]): Provider {
}
const providers = {
kilo: makeProvider("kilo", "Kilo Gateway", ["kilo-auto/free"]),
kilo: makeProvider("kilo", "Kilo Gateway", ["kilo-auto/free", "vendor/new-live-model"]),
anthropic: makeProvider("anthropic", "Anthropic", ["claude-sonnet-4"]),
openai: makeProvider("openai", "OpenAI", ["gpt-4.1"]),
}
@@ -42,26 +42,30 @@ describe("parseModelString", () => {
})
})
describe("promotion", () => {
describe("Kilo catalog model URI validation", () => {
it("waits for agents and provider metadata", () => {
expect(promotion({ agents: false, loaded: true, providers, connected: [], selection: KILO_AUTO })).toBe("pending")
expect(promotion({ agents: true, loaded: false, providers, connected: [], selection: KILO_AUTO })).toBe("pending")
expect(kiloCatalogModelStatus({ agents: false, loaded: true, providers, modelID: "vendor/new-live-model" })).toBe(
"pending",
)
expect(kiloCatalogModelStatus({ agents: true, loaded: false, providers, modelID: "vendor/new-live-model" })).toBe(
"pending",
)
})
it("rejects models missing from the loaded Kilo catalog", () => {
expect(
promotion({
agents: true,
loaded: true,
providers,
connected: [],
selection: { providerID: "kilo", modelID: "stealth/claude-opus-4.8" },
}),
).toBe("invalid")
expect(kiloCatalogModelStatus({ agents: true, loaded: true, providers, modelID: "vendor/missing-model" })).toBe(
"invalid",
)
})
it("applies models exposed by the loaded Kilo catalog", () => {
expect(promotion({ agents: true, loaded: true, providers, connected: [], selection: KILO_AUTO })).toBe("apply")
it("applies arbitrary models exposed by the loaded Kilo catalog", () => {
expect(kiloCatalogModelStatus({ agents: true, loaded: true, providers, modelID: "vendor/new-live-model" })).toBe(
"apply",
)
})
it("does not accept models exposed only by non-Kilo providers", () => {
expect(kiloCatalogModelStatus({ agents: true, loaded: true, providers, modelID: "gpt-4.1" })).toBe("invalid")
})
})
@@ -26,7 +26,7 @@ describe("model state", () => {
await Promise.all([
handleMessage(
"persistModelSelection",
{ agent: "code", providerID: "kilo", modelID: "stealth/claude-opus-4.8" },
{ agent: "code", providerID: "kilo", modelID: "vendor/new-live-model" },
client,
() => {},
),
@@ -40,7 +40,7 @@ describe("model state", () => {
const data = JSON.parse(fs.readFileSync(file, "utf-8")) as { model: Record<string, unknown> }
expect(data.model).toEqual({
code: { providerID: "kilo", modelID: "stealth/claude-opus-4.8" },
code: { providerID: "kilo", modelID: "vendor/new-live-model" },
plan: { providerID: "kilo", modelID: "kilo-auto/free" },
})
})
@@ -63,7 +63,7 @@ describe("model state", () => {
try {
const pending = handleMessage(
"persistModelSelection",
{ agent: "code", providerID: "kilo", modelID: "stealth/claude-opus-4.8" },
{ agent: "code", providerID: "kilo", modelID: "vendor/new-live-model" },
client,
() => {},
)
@@ -85,7 +85,7 @@ describe("model state", () => {
expect(sent).toEqual([
{
type: "modelSelectionsLoaded",
selections: { code: { providerID: "kilo", modelID: "stealth/claude-opus-4.8" } },
selections: { code: { providerID: "kilo", modelID: "vendor/new-live-model" } },
revision: 3,
},
])
@@ -0,0 +1,25 @@
import { describe, expect, it } from "bun:test"
import { kiloModelFromURI } from "../../src/kilo-provider/model-uri"
describe("kiloModelFromURI", () => {
it("accepts arbitrary Kilo catalog model ids", () => {
expect(kiloModelFromURI({ path: "/kilocode/model", query: "model=vendor%2Fnew-live-model" })).toBe(
"vendor/new-live-model",
)
})
it("preserves additional slashes inside model ids", () => {
expect(kiloModelFromURI({ path: "/kilocode/model", query: "model=vendor%2Ffamily%2Fmodel" })).toBe(
"vendor/family/model",
)
})
it("rejects missing and empty model ids", () => {
expect(kiloModelFromURI({ path: "/kilocode/model", query: "" })).toBeUndefined()
expect(kiloModelFromURI({ path: "/kilocode/model", query: "model=" })).toBeUndefined()
})
it("rejects unrelated URI paths", () => {
expect(kiloModelFromURI({ path: "/kilocode/s/session-id", query: "model=vendor%2Fmodel" })).toBeUndefined()
})
})
+2 -3
View File
@@ -10,7 +10,6 @@ import { Diff } from "@kilocode/kilo-ui/diff"
import { File } from "@kilocode/kilo-ui/file"
import { DataProvider } from "@kilocode/kilo-ui/context/data"
import { Toast } from "@kilocode/kilo-ui/toast"
import { KILO_PROVIDER_ID } from "../../src/shared/provider-model"
import Settings from "./components/settings/Settings"
import ProfileView from "./components/profile/ProfileView"
import { VSCodeProvider, useVSCode } from "./context/vscode"
@@ -271,8 +270,8 @@ const AppContent: Component = () => {
const handleSelectKiloModel = (message: unknown) => {
if (!isSelectKiloModelMessage(message)) return
console.log("[Kilo New] App: selecting promoted Kilo model:", message.modelID)
session.selectPersistedModel(KILO_PROVIDER_ID, message.modelID)
console.log("[Kilo New] App: selecting linked Kilo model:", message.modelID)
session.selectKiloModel(message.modelID)
setCurrentView("newTask")
}
@@ -1,4 +1,5 @@
import type { ModelSelection, Provider } from "../types/messages"
import { KILO_PROVIDER_ID } from "../../../src/shared/provider-model"
import { isModelValid } from "./provider-utils"
function validate(
@@ -42,15 +43,14 @@ export function resolveModelSelection(input: {
)
}
export function promotion(input: {
export function kiloCatalogModelStatus(input: {
agents: boolean
loaded: boolean
providers: Record<string, Provider>
connected: string[]
selection: ModelSelection
modelID: string
}): "pending" | "invalid" | "apply" {
if (!input.agents || !input.loaded) return "pending"
return isModelValid(input.providers, input.connected, input.selection) ? "apply" : "invalid"
return input.providers[KILO_PROVIDER_ID]?.models[input.modelID] ? "apply" : "invalid"
}
export function isCurrentModelSelections(revision: number | undefined, current: number): boolean {
@@ -1,3 +1,4 @@
import { KILO_PROVIDER_ID } from "../../../src/shared/provider-model"
import type { Provider, ProviderModel, ModelSelection } from "../types/messages"
export type EnrichedModel = ProviderModel & { providerID: string; providerName: string }
@@ -41,6 +42,6 @@ export function isModelValid(
if (!selection) return false
const provider = providers[selection.providerID]
if (!provider) return false
if (selection.providerID !== "kilo" && !connected.includes(selection.providerID)) return false
if (selection.providerID !== KILO_PROVIDER_ID && !connected.includes(selection.providerID)) return false
return !!provider.models[selection.modelID]
}
@@ -51,14 +51,14 @@ import {
upsertSessionToolPart,
} from "./session-utils"
import { Identifier } from "../utils/id"
import { isCurrentModelSelections, promotion, resolveModelSelection } from "./model-selection"
import { isCurrentModelSelections, kiloCatalogModelStatus, resolveModelSelection } from "./model-selection"
import { resolveMessagePrefs } from "./session-preferences"
import { errorIDs } from "./session-errors"
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
@@ -172,7 +172,7 @@ interface SessionContextValue {
// Model selection (global, extension-lifetime)
selected: (sessionID?: string) => ModelSelection | null
selectModel: (providerID: string, modelID: string, sessionID?: string) => void
selectPersistedModel: (providerID: string, modelID: string) => void
selectKiloModel: (modelID: string) => void
hasModelOverride: (sessionID?: string) => boolean
clearModelOverride: (sessionID?: string) => void
@@ -346,7 +346,7 @@ export const SessionProvider: ParentComponent = (props) => {
const [agents, setAgents] = createSignal<AgentInfo[]>([])
const [allAgents, setAllAgents] = createSignal<AgentInfo[]>([])
const [defaultAgent, setDefaultAgent] = createSignal("code")
const [pendingPersistedModel, setPendingPersistedModel] = createSignal<ModelSelection | null>(null)
const [pendingKiloModelID, setPendingKiloModelID] = createSignal<string | null>(null)
let revision = 0
// Skills loaded from the CLI backend
@@ -555,27 +555,26 @@ export const SessionProvider: ParentComponent = (props) => {
}
}
function selectPersistedModel(providerID: string, modelID: string) {
setPendingPersistedModel({ providerID, modelID })
function selectKiloModel(modelID: string) {
setPendingKiloModelID(modelID)
}
createEffect(() => {
const selection = pendingPersistedModel()
if (!selection) return
const status = promotion({
const modelID = pendingKiloModelID()
if (!modelID) return
const status = kiloCatalogModelStatus({
agents: agents().length > 0,
loaded: provider.loaded(),
providers: provider.providers(),
connected: provider.connected(),
selection,
modelID,
})
if (status === "pending") return
setPendingPersistedModel(null)
setPendingKiloModelID(null)
if (status === "invalid") {
console.warn("[Kilo New] Ignoring unavailable promoted Kilo model:", selection.modelID)
console.warn("[Kilo New] Ignoring unavailable Kilo catalog model:", modelID)
return
}
applyPersistedModel(selection)
applyPersistedModel({ providerID: KILO_PROVIDER_ID, modelID })
})
function promptAgent(sessionID?: string) {
@@ -2527,7 +2526,7 @@ export const SessionProvider: ParentComponent = (props) => {
scopedSuggestions,
selected,
selectModel,
selectPersistedModel,
selectKiloModel,
hasModelOverride,
clearModelOverride,
costBreakdown,