mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
fix(vscode): generalize model deep links
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support selecting promoted Kilo models from VS Code deep links.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support selecting Kilo Gateway catalog models from VS Code deep links.
|
||||
@@ -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
|
||||
}
|
||||
+5
-5
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user