mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge remote-tracking branch 'origin/johnnyeric/kilo-opencode-v1.18.0' into johnnyeric/kilo-opencode-v1.18.13
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user