fix(vscode): allow agent-only switch links

This commit is contained in:
Josh Lambert
2026-06-16 16:54:18 -04:00
parent a2ccf574a7
commit 214b8da6c6
7 changed files with 49 additions and 33 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
"kilo-code": minor
---
Support VS Code switch links that select a Kilo model and optional agent.
Support VS Code switch links that select a Kilo model, agent, or both.
@@ -142,19 +142,21 @@ The model selection is remembered per mode across sessions.
For details on configuring subagent models, see [Custom Subagents](/docs/customize/custom-subagents).
## Selecting a Model via a Link (VS Code)
## Selecting a Model or Agent via a Link (VS Code)
The VS Code extension supports a `vscode://` protocol handler that lets you open VS Code and automatically select a specific model — no manual picker interaction required. This is useful for sharing model recommendations, launching a specific model tier from a web page, or switching quickly to a newly announced model.
The VS Code extension supports a `vscode://` protocol handler that lets you open VS Code and automatically select a model, an agent, or both — no manual picker interaction required. This is useful for sharing model recommendations, launching a specific model tier from a web page, or switching quickly to a preferred agent.
### URL Format
Include at least one of the `model` or `agent` parameters:
```
vscode://kilocode.kilo-code/kilocode/switch?model=<modelID>[&agent=<agentName>]
vscode://kilocode.kilo-code/kilocode/switch?model=<modelID>
vscode://kilocode.kilo-code/kilocode/switch?agent=<agentName>
vscode://kilocode.kilo-code/kilocode/switch?model=<modelID>&agent=<agentName>
```
Replace `<modelID>` with the Kilo Gateway model ID you want to select (e.g. `kilo-auto/free`). Only models available in the Kilo Gateway catalog are accepted; the link is silently ignored if the model ID is not found.
The optional `agent` parameter switches to a visible primary agent before selecting the model. Use the agent's ID, such as `code` or `plan`, rather than its display name. `mode` is also accepted as an alias for `agent`.
Replace `<modelID>` with a Kilo Gateway model ID such as `kilo-auto/free`. Replace `<agentName>` with a visible primary agent ID such as `code` or `plan`, rather than its display name. `mode` is also accepted as an alias for `agent`.
### Example: Auto Free
@@ -164,7 +166,13 @@ To open Kilo Code and switch to the [Auto Free](/docs/code-with-ai/agents/auto-m
vscode://kilocode.kilo-code/kilocode/switch?model=kilo-auto%2Ffree
```
To switch to Plan at the same time, add the optional agent:
To switch only to Plan and use its normal model selection, specify the agent without a model:
```
vscode://kilocode.kilo-code/kilocode/switch?agent=plan
```
To select both at the same time, include both parameters:
```
vscode://kilocode.kilo-code/kilocode/switch?model=kilo-auto%2Ffree&agent=plan
@@ -176,11 +184,12 @@ URL-encode the `/` in model IDs as `%2F` when embedding this URL in HTML links o
### How It Works
- **VS Code open**: the Kilo sidebar is focused and the model is selected in the active session immediately.
- **VS Code closed**: VS Code launches, then applies the model selection once the extension is ready.
- The model is validated against the current Kilo Gateway model catalog before being applied. If the model ID is not found in the catalog, the deep link is silently ignored.
- **VS Code open**: the Kilo sidebar is focused and the linked selection is applied to the active session immediately.
- **VS Code closed**: VS Code launches, then applies the selection once the extension is ready.
- When `model` is provided, it must identify a model in the current Kilo Gateway catalog. Invalid or unavailable models cause the deep link to be ignored.
- When `agent` or `mode` is provided, it must identify a visible primary agent. Invalid or unavailable agents cause the deep link to be ignored.
- The agent is selected before the model so the linked model applies to that agent. The selection follows the same precedence as using the pickers: it updates the active session, or the next session when no session is active. It does **not** change your configured defaults in settings.
- An agent-only link uses the model that would normally be selected for that agent. When both parameters are present, the agent is selected first so the linked model applies to it.
- The selection follows the same precedence as using the pickers: it updates the active session, or the next session when no session is active. It does **not** change your configured defaults in settings.
### Sharing and Embedding
+4 -3
View File
@@ -301,7 +301,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private configWarningsShown = false
/** Cached notificationsLoaded payload */
private cachedNotificationsMessage: unknown = null
private pendingKiloModel: { modelID: string; agent?: string } | null = null
private pendingKiloModel: { modelID?: string; agent?: string } | null = null
private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = []
private readyResolvers: (() => void)[] = []
private promptRecoveryQueued = false
@@ -716,8 +716,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.postMessage({ type: "openCloudSession", sessionId })
}
public selectKiloModel(modelID: string, agent?: string): void {
this.pendingKiloModel = { modelID, ...(agent && { agent }) }
public selectKiloModel(modelID?: string, agent?: string): void {
if (!modelID && !agent) return
this.pendingKiloModel = { ...(modelID && { modelID }), ...(agent && { agent }) }
this.flushPendingKiloModel()
}
@@ -4,9 +4,9 @@ export function parseSwitchLink(path: string, query: string) {
if (!paths.has(path)) return
const params = new URLSearchParams(query)
const modelID = params.get("model")
if (!modelID) return
const modelID = params.get("model") || undefined
const agent = params.get("agent") || params.get("mode") || undefined
return { modelID, ...(agent && { agent }) }
if (!modelID && !agent) return
return { ...(modelID && { modelID }), ...(agent && { agent }) }
}
@@ -8,16 +8,21 @@ describe("parseSwitchLink", () => {
})
})
it("includes an optional agent", () => {
it("parses an agent without a model", () => {
expect(parseSwitchLink("/kilocode/switch", "agent=plan")).toEqual({
agent: "plan",
})
})
it("parses a model and agent together", () => {
expect(parseSwitchLink("/kilocode/switch", "model=kilo-auto%2Ffree&agent=plan")).toEqual({
modelID: "kilo-auto/free",
agent: "plan",
})
})
it("accepts mode as an agent alias", () => {
expect(parseSwitchLink("/kilocode/switch", "model=kilo-auto%2Ffree&mode=code")).toEqual({
modelID: "kilo-auto/free",
it("accepts mode as an agent alias without a model", () => {
expect(parseSwitchLink("/kilocode/switch", "mode=code")).toEqual({
agent: "code",
})
})
@@ -35,8 +40,8 @@ describe("parseSwitchLink", () => {
})
})
it("rejects unsupported routes and missing models", () => {
it("rejects unsupported routes and empty selections", () => {
expect(parseSwitchLink("/kilocode/other", "model=kilo-auto%2Ffree")).toBeUndefined()
expect(parseSwitchLink("/kilocode/switch", "agent=plan")).toBeUndefined()
expect(parseSwitchLink("/kilocode/switch", "")).toBeUndefined()
})
})
@@ -369,7 +369,7 @@ export const SessionProvider: ParentComponent = (props) => {
const [allAgents, setAllAgents] = createSignal<AgentInfo[]>([])
const [defaultAgent, setDefaultAgent] = createSignal("code")
const [pendingKiloModel, setPendingKiloModel] = createSignal<{
modelID: string
modelID?: string
agent?: string
after: number
} | null>(null)
@@ -602,9 +602,10 @@ export const SessionProvider: ParentComponent = (props) => {
}
}
function selectKiloModel(modelID: string, agent?: string) {
setPendingKiloModel({ modelID, ...(agent && { agent }), after: catalog() })
vscode.postMessage({ type: "requestProviders" })
function selectKiloModel(modelID?: string, agent?: string) {
if (!modelID && !agent) return
setPendingKiloModel({ ...(modelID && { modelID }), ...(agent && { agent }), after: catalog() })
if (modelID) vscode.postMessage({ type: "requestProviders" })
}
const unsubKiloModel = vscode.onMessage((message: ExtensionMessage) => {
@@ -618,9 +619,9 @@ export const SessionProvider: ParentComponent = (props) => {
createEffect(() => {
const pending = pendingKiloModel()
if (!pending || agents().length === 0 || catalog() <= pending.after) return
if (!pending || agents().length === 0 || (pending.modelID && catalog() <= pending.after)) return
setPendingKiloModel(null)
if (!provider.providers()[KILO_PROVIDER_ID]?.models[pending.modelID]) {
if (pending.modelID && !provider.providers()[KILO_PROVIDER_ID]?.models[pending.modelID]) {
console.warn("[Kilo New] Ignoring unavailable Kilo catalog model:", pending.modelID)
return
}
@@ -629,7 +630,7 @@ export const SessionProvider: ParentComponent = (props) => {
return
}
if (pending.agent) selectAgent(pending.agent)
selectModel(KILO_PROVIDER_ID, pending.modelID)
if (pending.modelID) selectModel(KILO_PROVIDER_ID, pending.modelID)
})
function promptAgent(sessionID?: string) {
@@ -241,7 +241,7 @@ export interface OpenCloudSessionMessage {
export interface SelectKiloModelMessage {
type: "selectKiloModel"
modelID: string
modelID?: string
agent?: string
}