Merge remote-tracking branch 'origin/johnnyeric/kilo-opencode-v1.18.0' into johnnyeric/kilo-opencode-v1.18.13

This commit is contained in:
Johnny Eric Amancio
2026-08-10 13:36:14 +02:00
8 changed files with 151 additions and 35 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Support browsing an instance's model catalog before a session starts.
@@ -355,7 +355,10 @@ export class AgentManagerProvider implements Disposable {
public openPanel(preserveFocus?: boolean): void {
if (this.panel) {
this.log("Panel already open, revealing")
revealPanel(this.panel, preserveFocus, this.waitForPanelReady(this.panel), this.waitForPanelActive(this.panel))
const panel = this.panel
revealPanel(panel, preserveFocus, () =>
focusPanelPrompt(panel, this.waitForPanelReady(panel), this.waitForPanelActive(panel)),
)
return
}
this.log("Opening Agent Manager panel")
@@ -1695,7 +1698,9 @@ export class AgentManagerProvider implements Disposable {
public focusPanel(): void {
const panel = this.panel
if (!panel) return
revealPanel(panel, false, this.waitForPanelReady(panel), this.waitForPanelActive(panel))
revealPanel(panel, false, () =>
focusPanelPrompt(panel, this.waitForPanelReady(panel), this.waitForPanelActive(panel)),
)
}
public isActive(): boolean {
return this.panel?.active === true
@@ -7,12 +7,7 @@ export function focusPanelPrompt(panel: PanelContext, ready: Promise<boolean>, a
})
}
export function revealPanel(
panel: PanelContext,
preserve: boolean | undefined,
ready: Promise<boolean>,
active: Promise<boolean>,
): void {
export function revealPanel(panel: PanelContext, preserve: boolean | undefined, focus?: () => void): void {
panel.reveal(preserve)
if (!preserve) focusPanelPrompt(panel, ready, active)
if (!preserve) focus?.()
}
@@ -0,0 +1,52 @@
import { describe, expect, it, mock } from "bun:test"
import { focusPanelPrompt, revealPanel } from "../../src/agent-manager/focus-panel"
import type { PanelContext } from "../../src/agent-manager/host"
describe("revealPanel", () => {
it("reveals the panel without calling focus when preserve is true", () => {
const revealed: Array<boolean | undefined> = []
const panel = {
reveal: (preserve?: boolean) => revealed.push(preserve),
postMessage: mock(() => Promise.resolve(true)),
} as unknown as PanelContext
let focused = false
revealPanel(panel, true, () => {
focused = true
})
expect(revealed).toEqual([true])
expect(focused).toBe(false)
})
it("calls the focus callback when preserve is false or undefined", () => {
const revealed: Array<boolean | undefined> = []
const panel = {
reveal: (preserve?: boolean) => revealed.push(preserve),
postMessage: mock(() => Promise.resolve(true)),
} as unknown as PanelContext
let focused = false
revealPanel(panel, false, () => {
focused = true
})
expect(revealed).toEqual([false])
expect(focused).toBe(true)
})
it("sends focusInput message when panel is ready and active", async () => {
const messages: unknown[] = []
const panel = {
postMessage: (msg: unknown) => {
messages.push(msg)
return Promise.resolve(true)
},
} as unknown as PanelContext
focusPanelPrompt(panel, Promise.resolve(true), Promise.resolve(true))
await new Promise((r) => setTimeout(r, 10))
expect(messages).toEqual([{ type: "action", action: "focusInput" }])
})
})
@@ -198,7 +198,6 @@ interface SetupState {
type SidebarSelection = typeof LOCAL | string | null
type SidePanel = "diff" | "pr" | "terminal" | null
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const SIDE_RESIZE_INTERVAL_MS = 32
import { parseBindingTokens } from "./keybind-tokens"
import { defaultBindings } from "./keybind-defaults"
@@ -345,9 +345,7 @@ export namespace RemoteSender {
// bus listener count from inflating for senders that never handle
// attachments (the count would otherwise show up in unrelated tests
// that assert it stays at 0).
const attachments =
options.attachments ??
((sessionID: SessionID) => RemoteAttachments.create({ sessionID }))
const attachments = options.attachments ?? ((sessionID: SessionID) => RemoteAttachments.create({ sessionID }))
const attachmentCache = new Map<SessionID, RemoteAttachments.Result>()
const pending = new Map<SessionID, number>()
const retired = new Map<SessionID, RemoteAttachments.Result>()
@@ -886,27 +884,27 @@ export namespace RemoteSender {
return
}
// kilocode_change end
// kilocode_change start - sessionless list_models for the pre-session instance picker
if (msg.command === "list_models") {
const parsed = RemoteModelCatalog.Request.safeParse(msg.data)
const session = msg.sessionId ? decodeSessionID(msg.sessionId) : Option.none<SessionID>()
if (!parsed.success || Option.isNone(session)) {
options.conn.send({
type: "response",
id: msg.id,
error: "invalid list_models command",
})
// Accept an absent sessionId (the mobile instance-picker path asks for the
// instance's catalog before a session exists). A present but undecodable
// sessionId is still invalid.
const target = msg.sessionId ? decodeSessionID(msg.sessionId) : Option.none<SessionID>()
if (!parsed.success || (msg.sessionId && Option.isNone(target))) {
options.conn.send({ type: "response", id: msg.id, error: "invalid list_models command" })
return
}
const run = options.provide ?? provide
void (async () => {
try {
const info = await catalog.get(session.value)
const info = Option.isSome(target) ? await catalog.get(target.value) : undefined
const result = await run({
directory: info.directory,
directory: info?.directory ?? options.directory,
fn: async () => {
const [providers, messages, fallback] = await Promise.all([
catalog.providers(),
catalog.messages(info.id),
info ? catalog.messages(info.id) : Promise.resolve([]),
catalog.default().catch((err) => {
options.log.warn("default model lookup failed", { error: String(err) })
return undefined
@@ -914,7 +912,7 @@ export namespace RemoteSender {
])
return RemoteModelCatalog.build({
providers,
session: info,
session: info ?? {},
messages,
defaultModel: fallback,
})
@@ -928,6 +926,7 @@ export namespace RemoteSender {
})()
return
}
// kilocode_change end
if (msg.command === "send_message") {
const parsed = getRemotePromptInput().safeParse(msg.data)
if (!parsed.success) {
@@ -911,7 +911,9 @@ describe("config overlay routes", () => {
Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action,
).toBe("allow")
},
30_000,
// Cold Windows CI runs take ~32s (observed timeout at 30s); give the two
// instance create/dispose cycles of each iteration real headroom.
90_000,
)
}
@@ -1084,7 +1084,7 @@ describe("RemoteSender", () => {
expect(JSON.stringify(sent)).not.toContain("api-key")
})
test("list_models rejects unsupported versions and missing session IDs", () => {
test("list_models rejects unsupported versions and undecodable session IDs", () => {
const { conn, sent } = fakeConn()
const sender = RemoteSender.create({
conn,
@@ -1100,12 +1100,6 @@ describe("RemoteSender", () => {
sessionId: "ses_models",
data: { protocolVersion: 2 },
})
sender.handle({
type: "command",
id: "req_models_missing_session",
command: "list_models",
data: { protocolVersion: 1 },
})
sender.handle({
type: "command",
id: "req_models_invalid_session",
@@ -1116,11 +1110,72 @@ describe("RemoteSender", () => {
expect(sent).toEqual([
{ type: "response", id: "req_models_v2", error: "invalid list_models command" },
{ type: "response", id: "req_models_missing_session", error: "invalid list_models command" },
{ type: "response", id: "req_models_invalid_session", error: "invalid list_models command" },
])
})
test("list_models without a sessionId returns the instance catalog", async () => {
const { conn, sent } = fakeConn()
const dirs: string[] = []
const sender = RemoteSender.create({
conn,
directory: "/tmp/process-default",
log: nolog,
subscribe: fakeBus().subscribe,
provide: async <R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) => {
dirs.push(input.directory)
return input.fn()
},
catalog: {
get: async () => {
throw new Error("catalog.get must not be called without a sessionId")
},
messages: async () => {
throw new Error("catalog.messages must not be called without a sessionId")
},
providers: async () =>
({
custom: {
id: ProviderV2.ID.make("custom"),
name: "Custom Provider",
source: "config",
env: ["PRIVATE_API_KEY"],
key: "must-not-leak",
options: { apiKey: "must-not-leak" },
models: {
"deployment/model": catalogModel("custom", "deployment/model", "Deployment Model", true),
},
},
}) as any,
default: async () => ({
providerID: ProviderV2.ID.make("custom"),
modelID: ModelV2.ID.make("deployment/model"),
}),
},
})
sender.handle({
type: "command",
id: "req_models_sessionless",
command: "list_models",
data: { protocolVersion: 1 },
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(dirs).toEqual(["/tmp/process-default"])
expect(sent).toHaveLength(1)
expect(sent[0]?.type).toBe("response")
expect(sent[0]?.id).toBe("req_models_sessionless")
const result = sent[0]?.result as RemoteModelCatalog.Response
expect(result.protocolVersion).toBe(1)
expect(result.all).toHaveLength(1)
expect(result.all[0]?.id).toBe("custom")
expect(result.defaultModel).toEqual({ providerID: "custom", modelID: "deployment/model" })
expect(result).not.toHaveProperty("currentModel")
expect(JSON.stringify(result)).not.toContain("must-not-leak")
})
test("send_message with agent is accepted", async () => {
const { conn, sent } = fakeConn()
let resolveProvide: () => void
@@ -3066,7 +3121,9 @@ describe("RemoteSender slash commands", () => {
removeCalls.push(id)
},
},
attachSession: async () => { throw new Error("attach failed") },
attachSession: async () => {
throw new Error("attach failed")
},
})
const response = expectResponse(conn, sent, "req_spawn_failed")
@@ -3107,7 +3164,9 @@ describe("RemoteSender slash commands", () => {
throw new Error("cleanup secondary failure")
},
},
attachSession: async () => { throw new Error("attach failed") },
attachSession: async () => {
throw new Error("attach failed")
},
})
const response = expectResponse(conn, sent, "req_spawn_then_cleanup_fail")