mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge branch 'main' into savory-act
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Skip inferred background-process port scanning in VS Code sessions to avoid unnecessary Bun subprocess polling.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show a retryable connection error and preserve unsent prompts when the VS Code background CLI process exits.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Fix Claude Opus 4.8 reasoning on Amazon Bedrock by treating it as an adaptive thinking model like Opus 4.7. This resolves the "thinking.type.enabled is not supported for this model" error and exposes the full low/medium/high/xhigh/max reasoning effort range.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Preserve the Changes review scroll position while agents update files.
|
||||
@@ -102,7 +102,7 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
@@ -4520,8 +4520,6 @@
|
||||
|
||||
"@kilocode/kilo-docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"@kilocode/kilo-gateway/@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="],
|
||||
|
||||
"@kilocode/kilo-gateway/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||
|
||||
"@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||
@@ -5092,8 +5090,6 @@
|
||||
|
||||
"@hey-api/openapi-ts/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
"@kilocode/kilo-gateway/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@kilocode/kilo-gateway/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
@@ -5466,8 +5462,6 @@
|
||||
|
||||
"@kilocode/kilo-gateway/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@kilocode/kilo-gateway/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"@morphllm/morphsdk/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-KqxasDpdyLQBZC+N85z4HFXLn/15D9qqDyEqgvWTY7c=",
|
||||
"aarch64-linux": "sha256-YEBF+S2+8imYWO4PfbCY5AOA5uIqAZY80GO6HK8k/sM=",
|
||||
"aarch64-darwin": "sha256-CxE5cdMcvcAENZLCilCU6Ndx+wiZVbFxtx0UNySdNDA=",
|
||||
"x86_64-darwin": "sha256-Z9eU77dkavAGAM0AlcnKXCXrrFuRyLnYwM/F9nrkwrk="
|
||||
"x86_64-linux": "sha256-/cQ10dEr62YjcG5Fqm1lSHzhssvGPHMeaNHong+y/T4=",
|
||||
"aarch64-linux": "sha256-RjA9KOoLg0AAzkSg1ApgeW/kUlOMxJhlKuGJwh+jLtE=",
|
||||
"aarch64-darwin": "sha256-nfWpwSyC4CaU1Ad3PY+3pEzTyITAUVf12cxYsxaHjqk=",
|
||||
"x86_64-darwin": "sha256-ytU4wj6Ywoe1a6pBldRqXFFoXRaUXNTV/9gTqA3fz3o="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
|
||||
@@ -347,6 +347,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
}
|
||||
|
||||
private postConnectionState(error = this.connectionService.getConnectionError()): void {
|
||||
this.postMessage({
|
||||
type: "connectionState",
|
||||
state: this.connectionState,
|
||||
...(this.connectionState === "error" && {
|
||||
error: getErrorMessage(error) || "Connection to CLI backend lost. Retry to reconnect.",
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// Strip edit-tool metadata.filediff.before/after (multi-MB for edit-heavy
|
||||
// sessions) to keep session switches fast. Logic in kilo-provider/slim-metadata.ts.
|
||||
private slimPart<T>(part: T): T {
|
||||
@@ -386,7 +396,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
|
||||
// Always push connection state first so the UI can render appropriately.
|
||||
this.postMessage({ type: "connectionState", state: this.connectionState })
|
||||
this.postConnectionState()
|
||||
pushTelemetryState((m) => this.postMessage(m))
|
||||
|
||||
// Re-send ready so the webview can recover after refresh.
|
||||
@@ -1228,9 +1238,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
)
|
||||
|
||||
// Subscribe to connection state changes
|
||||
this.unsubscribeState = this.connectionService.onStateChange(async (state) => {
|
||||
this.unsubscribeState = this.connectionService.onStateChange(async (state, error) => {
|
||||
this.connectionState = state
|
||||
this.postMessage({ type: "connectionState", state })
|
||||
this.postConnectionState(error)
|
||||
|
||||
if (state === "connected") {
|
||||
// Fire config warnings independently so a failure in the
|
||||
@@ -1309,7 +1319,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
workspaceDirectory: this.getProjectDirectory(this.currentSession?.id),
|
||||
})
|
||||
}
|
||||
this.postMessage({ type: "connectionState", state: this.connectionState })
|
||||
this.postConnectionState()
|
||||
|
||||
// connect() can resolve after SSE reaches "connected" but before this
|
||||
// provider subscribes to onStateChange(). In that case the initial
|
||||
|
||||
@@ -7,7 +7,7 @@ import { resolveEventSessionId as resolveEventSessionIdPure } from "./connection
|
||||
|
||||
export type ConnectionState = "connecting" | "connected" | "disconnected" | "error"
|
||||
type SSEEventListener = (event: Event, directory?: string) => void
|
||||
type StateListener = (state: ConnectionState) => void
|
||||
type StateListener = (state: ConnectionState, error?: Error) => void
|
||||
type SSEEventFilter = (event: Event, directory?: string) => boolean
|
||||
type NotificationDismissListener = (notificationId: string) => void
|
||||
type LanguageChangeListener = (locale: string) => void
|
||||
@@ -65,6 +65,7 @@ export class KiloConnectionService {
|
||||
private info: { port: number } | null = null
|
||||
private config: ServerConfig | null = null
|
||||
private state: ConnectionState = "disconnected"
|
||||
private error: Error | null = null
|
||||
private connectPromise: Promise<void> | null = null
|
||||
private healthPollTimer: ReturnType<typeof setInterval> | null = null
|
||||
private remoteService: import("../RemoteStatusService").RemoteStatusService | null = null
|
||||
@@ -93,7 +94,7 @@ export class KiloConnectionService {
|
||||
private unsubRemote: (() => void) | null = null
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.serverManager = new ServerManager(context)
|
||||
this.serverManager = new ServerManager(context, (code) => this.handleServerExit(code))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +116,7 @@ export class KiloConnectionService {
|
||||
await this.connectPromise
|
||||
} catch (error) {
|
||||
// If doConnect() fails before SSE can emit a state transition, avoid leaving consumers stuck in "connecting".
|
||||
this.setState("error")
|
||||
this.setState("error", this.error ?? (error instanceof Error ? error : new Error(String(error))))
|
||||
throw error
|
||||
} finally {
|
||||
this.connectPromise = null
|
||||
@@ -126,7 +127,7 @@ export class KiloConnectionService {
|
||||
* Get the shared SDK client. Throws if not connected.
|
||||
*/
|
||||
getClient(): KiloClient {
|
||||
if (!this.client) {
|
||||
if (!this.client || this.state !== "connected") {
|
||||
throw new Error("Not connected — call connect() first")
|
||||
}
|
||||
return this.client
|
||||
@@ -139,11 +140,11 @@ export class KiloConnectionService {
|
||||
* or if the connection fails.
|
||||
*/
|
||||
async getClientAsync(dir?: string): Promise<KiloClient> {
|
||||
if (this.client) return this.client
|
||||
if (this.client && this.state === "connected") return this.client
|
||||
const root = dir ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!root) throw new Error("No workspace folder open")
|
||||
await this.connect(root)
|
||||
return this.client!
|
||||
return this.getClient()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,6 +190,13 @@ export class KiloConnectionService {
|
||||
return this.state
|
||||
}
|
||||
|
||||
/**
|
||||
* Last connection error. Cleared when a new connection attempt begins.
|
||||
*/
|
||||
getConnectionError(): Error | null {
|
||||
return this.error
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to SSE events. Returns unsubscribe function.
|
||||
*/
|
||||
@@ -503,12 +511,14 @@ export class KiloConnectionService {
|
||||
this.config = null
|
||||
this.info = null
|
||||
this.state = "disconnected"
|
||||
this.error = null
|
||||
}
|
||||
|
||||
private setState(state: ConnectionState): void {
|
||||
private setState(state: ConnectionState, error?: Error): void {
|
||||
this.state = state
|
||||
this.error = state === "error" ? (error ?? this.error) : null
|
||||
for (const listener of this.stateListeners) {
|
||||
listener(state)
|
||||
listener(state, this.error ?? undefined)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,10 +568,28 @@ export class KiloConnectionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async doConnect(workspaceDir: string): Promise<void> {
|
||||
// If we reconnect, ensure the previous SSE connection is cleaned up first.
|
||||
private resetConnection(): void {
|
||||
this.stopHealthPoll()
|
||||
this.sseClient?.dispose()
|
||||
const sse = this.sseClient
|
||||
this.sseClient = null
|
||||
sse?.disconnect()
|
||||
this.client = null
|
||||
this.config = null
|
||||
this.info = null
|
||||
}
|
||||
|
||||
private handleServerExit(code: number | null): void {
|
||||
console.warn("[Kilo New] ConnectionService: CLI background process exited:", code)
|
||||
this.resetConnection()
|
||||
this.setState(
|
||||
"error",
|
||||
new Error(`CLI background process exited with code ${code ?? "unknown"}. Retry to reconnect.`),
|
||||
)
|
||||
}
|
||||
|
||||
private async doConnect(workspaceDir: string): Promise<void> {
|
||||
// Never expose a stale SDK client while its replacement server is starting.
|
||||
this.resetConnection()
|
||||
|
||||
const server = await this.serverManager.getServer()
|
||||
this.info = { port: server.port }
|
||||
@@ -575,14 +603,15 @@ export class KiloConnectionService {
|
||||
|
||||
// Create SDK client with Basic Auth header
|
||||
const authHeader = `Basic ${Buffer.from(`kilo:${server.password}`).toString("base64")}`
|
||||
this.client = createKiloClient({
|
||||
const client = createKiloClient({
|
||||
baseUrl: config.baseUrl,
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
})
|
||||
|
||||
this.sseClient = new SdkSSEAdapter(this.client)
|
||||
const sse = new SdkSSEAdapter(client)
|
||||
this.client = client
|
||||
this.sseClient = sse
|
||||
|
||||
// Wait until SSE yields its first server event before resolving connect().
|
||||
// Initial stream failures are handled by the adapter reconnect loop.
|
||||
@@ -596,18 +625,29 @@ export class KiloConnectionService {
|
||||
let didConnect = false
|
||||
|
||||
// Wire SSE events → broadcast to all registered listeners
|
||||
this.sseClient.onEvent((event, directory) => {
|
||||
sse.onEvent((event, directory) => {
|
||||
if (this.sseClient !== sse) return
|
||||
for (const listener of this.eventListeners) {
|
||||
listener(event, directory)
|
||||
}
|
||||
})
|
||||
|
||||
this.sseClient.onError(() => {
|
||||
this.setState("error")
|
||||
sse.onError((error) => {
|
||||
if (this.sseClient !== sse) return
|
||||
this.setState("error", error)
|
||||
})
|
||||
|
||||
// Wire SSE state → broadcast to all registered state listeners
|
||||
this.sseClient.onStateChange((sseState) => {
|
||||
sse.onStateChange((sseState) => {
|
||||
if (this.sseClient !== sse) {
|
||||
if (!didConnect && sseState === "disconnected") {
|
||||
rejectConnected?.(new Error(`SSE connection ended in state: ${sseState}`))
|
||||
resolveConnected = null
|
||||
rejectConnected = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.setState(sseState)
|
||||
|
||||
if (sseState === "connected") {
|
||||
@@ -625,7 +665,7 @@ export class KiloConnectionService {
|
||||
}
|
||||
})
|
||||
|
||||
this.sseClient.connect()
|
||||
sse.connect()
|
||||
|
||||
await connectedPromise
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface ServerInstance {
|
||||
const STARTUP_TIMEOUT_SECONDS = 30
|
||||
|
||||
type WorkspaceFolderLike = { uri: { fsPath: string } }
|
||||
type ServerExitListener = (code: number | null) => void
|
||||
|
||||
export function resolveServerCwd(folders: readonly WorkspaceFolderLike[] | undefined, storage: string): string {
|
||||
return folders?.[0]?.uri.fsPath ?? storage
|
||||
@@ -31,7 +32,10 @@ export class ServerManager {
|
||||
private instance: ServerInstance | null = null
|
||||
private startupPromise: Promise<ServerInstance> | null = null
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {}
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly onExit?: ServerExitListener,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get or start the server instance
|
||||
@@ -171,6 +175,7 @@ export class ServerManager {
|
||||
console.log("[Kilo New] ServerManager: 🛑 Process exited with code:", code)
|
||||
if (this.instance?.process === serverProcess) {
|
||||
this.instance = null
|
||||
this.onExit?.(code)
|
||||
}
|
||||
if (!resolved) {
|
||||
const { userMessage, userDetails } = toErrorMessage(
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
|
||||
const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern"
|
||||
const STORY_ID = "agentmanager--full-screen-diff-agent-edit-scroll"
|
||||
|
||||
function storyUrl() {
|
||||
return `/iframe.html?id=${STORY_ID}&viewMode=story&globals=${GLOBALS}`
|
||||
}
|
||||
|
||||
async function disableAnimations(page: Page) {
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0s !important;
|
||||
animation-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
async function openStory(page: Page) {
|
||||
await page.setViewportSize({ width: 800, height: 720 })
|
||||
await page.addInitScript(() => {
|
||||
const win = window as Window & { nativeIntersectionObserver?: typeof IntersectionObserver }
|
||||
win.nativeIntersectionObserver = window.IntersectionObserver
|
||||
Object.defineProperty(window, "IntersectionObserver", { configurable: true, value: undefined, writable: true })
|
||||
})
|
||||
await page.goto(storyUrl(), { waitUntil: "load" })
|
||||
await disableAnimations(page)
|
||||
await page.waitForSelector("#storybook-root *", { state: "attached" })
|
||||
|
||||
const first = page.locator('[data-file-path="src/agent-edit.ts"] [data-component="diff"]')
|
||||
await expect.poll(async () => first.evaluate((el) => el.getBoundingClientRect().height)).toBeGreaterThan(3_000)
|
||||
return first
|
||||
}
|
||||
|
||||
test("preserves diff scroll position while an agent edit refreshes a file", async ({ page }) => {
|
||||
const first = await openStory(page)
|
||||
const scroller = page.locator(".am-review-diff")
|
||||
const target = page.locator('[data-file-path="src/target.ts"]')
|
||||
|
||||
// The initial tall diff rendered eagerly. Restore the real observer before
|
||||
// moving it offscreen so an unfixed row remount takes the deferred path.
|
||||
await page.evaluate(() => {
|
||||
const win = window as Window & { nativeIntersectionObserver?: typeof IntersectionObserver }
|
||||
Object.defineProperty(window, "IntersectionObserver", {
|
||||
configurable: true,
|
||||
value: win.nativeIntersectionObserver,
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
await scroller.evaluate((el) => {
|
||||
const target = el.querySelector('[data-file-path="src/target.ts"]')
|
||||
if (!(target instanceof HTMLElement)) throw new Error("Target diff row not found")
|
||||
el.scrollTop += target.getBoundingClientRect().top - el.getBoundingClientRect().top - 24
|
||||
})
|
||||
|
||||
const before = await scroller.evaluate((el) => el.scrollTop)
|
||||
const top = await target.evaluate((el) => el.getBoundingClientRect().top)
|
||||
expect(before).toBeGreaterThan(3_000)
|
||||
|
||||
await page.getByRole("button", { name: "Apply agent edit" }).click()
|
||||
await expect(page.getByTestId("agent-edit-version")).toHaveText("after")
|
||||
await expect.poll(async () => first.evaluate((el) => el.getBoundingClientRect().height)).toBeGreaterThan(3_000)
|
||||
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
|
||||
|
||||
const after = await scroller.evaluate((el) => el.scrollTop)
|
||||
const next = await target.evaluate((el) => el.getBoundingClientRect().top)
|
||||
expect(after).toBeCloseTo(before, 0)
|
||||
expect(next).toBeCloseTo(top, 0)
|
||||
})
|
||||
|
||||
test("remounts diff rows when the review context changes", async ({ page }) => {
|
||||
const first = await openStory(page)
|
||||
await page.evaluate(() => {
|
||||
class IdleObserver {
|
||||
readonly root = null
|
||||
readonly rootMargin = "0px"
|
||||
readonly thresholds = []
|
||||
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
takeRecords() {
|
||||
return []
|
||||
}
|
||||
unobserve() {}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, "IntersectionObserver", {
|
||||
configurable: true,
|
||||
value: IdleObserver,
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
await page.getByRole("button", { name: "Switch review context" }).click()
|
||||
await expect(page.getByTestId("review-context")).toHaveText("changed-context")
|
||||
await expect.poll(async () => first.evaluate((el) => el.getBoundingClientRect().height)).toBe(1_200)
|
||||
})
|
||||
@@ -69,6 +69,7 @@ function connection() {
|
||||
getServerInfo: () => ({ port: 12345 }),
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:12345", password: "test" }),
|
||||
getConnectionState: () => "connected" as const,
|
||||
getConnectionError: () => null,
|
||||
resolveEventSessionId: (event: Event) => (event.type === "session.created" ? event.properties.info.id : undefined),
|
||||
recordMessageSessionId: () => undefined,
|
||||
notifyNotificationDismissed: () => undefined,
|
||||
|
||||
@@ -98,6 +98,7 @@ function createConnection(client: ReturnType<typeof createClient>) {
|
||||
registerDirectoryProvider: () => () => undefined,
|
||||
getServerInfo: () => ({ port: 12345 }),
|
||||
getConnectionState: () => "connected" as const,
|
||||
getConnectionError: () => null,
|
||||
resolveEventSessionId: () => undefined,
|
||||
recordMessageSessionId: () => undefined,
|
||||
notifyNotificationDismissed: () => undefined,
|
||||
|
||||
@@ -91,6 +91,7 @@ function createConnection(client: ReturnType<typeof createClient>) {
|
||||
getServerInfo: () => ({ port: 12345 }),
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:12345", password: "test" }),
|
||||
getConnectionState: () => "connected" as const,
|
||||
getConnectionError: () => null,
|
||||
resolveEventSessionId: () => undefined,
|
||||
recordMessageSessionId: () => undefined,
|
||||
notifyNotificationDismissed: () => undefined,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
describe("PromptInput connection guard", () => {
|
||||
const path = join(__dirname, "..", "..", "webview-ui", "src", "components", "chat", "PromptInput.tsx")
|
||||
const src = readFileSync(path, "utf8")
|
||||
|
||||
it("rechecks the connection after resolving async attachments and before clearing the draft", () => {
|
||||
const attachments = src.indexOf("const gitFile = await git.resolveAttachment")
|
||||
const guard = src.indexOf("if (isDisabled()) return", attachments)
|
||||
const send = src.indexOf("session.sendMessage(message", guard)
|
||||
const clear = src.indexOf("drafts.delete(key)", send)
|
||||
|
||||
expect(attachments).toBeGreaterThan(-1)
|
||||
expect(guard).toBeGreaterThan(attachments)
|
||||
expect(send).toBeGreaterThan(guard)
|
||||
expect(clear).toBeGreaterThan(send)
|
||||
})
|
||||
})
|
||||
@@ -110,6 +110,38 @@ describe("SdkSSEAdapter", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("KiloConnectionService backend crash", () => {
|
||||
it("invalidates the stale SDK client and reports a retryable error", () => {
|
||||
const service = new KiloConnectionService({} as any)
|
||||
const states: Array<{ state: string; error?: string }> = []
|
||||
;(service as any).client = {}
|
||||
;(service as any).config = { baseUrl: "http://127.0.0.1:52512", password: "secret" }
|
||||
;(service as any).info = { port: 52512 }
|
||||
;(service as any).state = "connected"
|
||||
service.onStateChange((state, error) => states.push({ state, error: error?.message }))
|
||||
;(service as any).handleServerExit(9)
|
||||
|
||||
expect(service.getConnectionState()).toBe("error")
|
||||
expect(service.getConnectionError()?.message).toContain("CLI background process exited with code 9")
|
||||
expect(service.getServerConfig()).toBeNull()
|
||||
expect(service.getServerInfo()).toBeNull()
|
||||
expect(() => service.getClient()).toThrow("Not connected")
|
||||
expect(states).toEqual([
|
||||
{ state: "error", error: "CLI background process exited with code 9. Retry to reconnect." },
|
||||
])
|
||||
service.dispose()
|
||||
})
|
||||
|
||||
it("does not expose an SDK client while a replacement server is connecting", () => {
|
||||
const service = new KiloConnectionService({} as any)
|
||||
;(service as any).client = {}
|
||||
;(service as any).state = "connecting"
|
||||
|
||||
expect(() => service.getClient()).toThrow("Not connected")
|
||||
service.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe("KiloConnectionService SSE startup", () => {
|
||||
it("waits through an initial SSE fetch failure until the stream opens", async () => {
|
||||
const original = globalThis.fetch
|
||||
|
||||
@@ -50,6 +50,7 @@ async function disableAnimations(page: Page) {
|
||||
// Permission dock config-preloaded has non-deterministic toggle rendering.
|
||||
const SKIP = new Set<string>([
|
||||
"agentmanager--worktree-item-busy",
|
||||
"agentmanager--full-screen-diff-agent-edit-scroll",
|
||||
"composite-webview--permission-dock-config-preloaded",
|
||||
])
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ async function disableAnimations(page: Page) {
|
||||
// Permission dock config-preloaded has non-deterministic toggle rendering.
|
||||
const SKIP = new Set<string>([
|
||||
"agentmanager--worktree-item-busy",
|
||||
"agentmanager--full-screen-diff-agent-edit-scroll",
|
||||
"agentmanager--pr-badge-checks-pending",
|
||||
"composite-webview--permission-dock-config-preloaded",
|
||||
])
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
import { DiffEndMarker } from "./DiffEndMarker"
|
||||
import { treeOrder } from "./file-tree-utils"
|
||||
import { isMarkdownFile, MarkdownDiffView } from "./MarkdownDiffView"
|
||||
import { diffToken } from "./diff-state"
|
||||
import { createDiffRows, diffToken } from "./diff-state"
|
||||
|
||||
// --- Data model ---
|
||||
|
||||
@@ -120,6 +120,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
// Reorder diffs to match the file-tree's depth-first visual order so
|
||||
// scrolling through the accordion matches the tree grouping.
|
||||
const sorted = createMemo(() => treeOrder(props.diffs))
|
||||
const rows = createDiffRows(sorted, () => props.sessionKey)
|
||||
const eager = createMemo(() => eagerDiffFiles(sorted()))
|
||||
|
||||
const comments = () => props.comments
|
||||
@@ -484,7 +485,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
<Show when={props.diffs.length > 0}>
|
||||
<div class="am-diff-content" data-component="session-review" ref={scroller}>
|
||||
<Accordion multiple value={open()} onChange={setOpen}>
|
||||
<For each={sorted()}>
|
||||
<For each={rows()}>
|
||||
{(diff) => {
|
||||
const isAdded = () => diff.status === "added"
|
||||
const isDeleted = () => diff.status === "deleted"
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
} from "./diff-open-policy"
|
||||
import { DiffEndMarker } from "./DiffEndMarker"
|
||||
import { isMarkdownFile, MarkdownDiffView } from "./MarkdownDiffView"
|
||||
import { diffToken } from "./diff-state"
|
||||
import { createDiffRows, diffToken } from "./diff-state"
|
||||
|
||||
type DiffStyle = "unified" | "split"
|
||||
|
||||
@@ -136,6 +136,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
|
||||
// Reorder diffs to match the file-tree's depth-first visual order so
|
||||
// scrolling through the diff panel matches the tree on the left.
|
||||
const sorted = createMemo(() => treeOrder(props.diffs))
|
||||
const rows = createDiffRows(sorted, () => props.sessionKey)
|
||||
const eager = createMemo(() => eagerDiffFiles(sorted()))
|
||||
|
||||
const comments = () => props.comments
|
||||
@@ -581,7 +582,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
|
||||
<Show when={props.diffs.length > 0}>
|
||||
<div class="am-review-diff-content" data-component="session-review">
|
||||
<Accordion multiple value={open()} onChange={setOpen}>
|
||||
<For each={sorted()}>
|
||||
<For each={rows()}>
|
||||
{(diff) => {
|
||||
const isAdded = () => diff.status === "added"
|
||||
const isDeleted = () => diff.status === "deleted"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import type { WorktreeFileDiff } from "../src/types/messages"
|
||||
|
||||
export function sameDiffMeta(left: WorktreeFileDiff, right: WorktreeFileDiff) {
|
||||
@@ -18,6 +19,45 @@ export function diffToken(diff: WorktreeFileDiff) {
|
||||
return diff.stamp ?? parts.join(":")
|
||||
}
|
||||
|
||||
// Keep each rendered row mounted while live detail refreshes replace its data.
|
||||
// Otherwise Solid's keyed <For> remounts the row and deferred rendering swaps a
|
||||
// previously rendered diff above the viewport for a short placeholder.
|
||||
export function createDiffRows(source: () => WorktreeFileDiff[], key: () => string | undefined) {
|
||||
const cache = new Map<string, { diff: WorktreeFileDiff; set: (diff: WorktreeFileDiff) => void }>()
|
||||
let current: string | undefined
|
||||
|
||||
return createMemo(() => {
|
||||
const nextKey = key()
|
||||
if (current !== nextKey) {
|
||||
current = nextKey
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
const files = new Set<string>()
|
||||
const diffs = source().map((next) => {
|
||||
files.add(next.file)
|
||||
const cached = cache.get(next.file)
|
||||
if (cached) {
|
||||
cached.set(next)
|
||||
return cached.diff
|
||||
}
|
||||
|
||||
const [value, setValue] = createSignal(next)
|
||||
const diff = new Proxy(next, {
|
||||
get: (_, prop) => Reflect.get(value(), prop),
|
||||
})
|
||||
cache.set(next.file, { diff, set: setValue })
|
||||
return diff
|
||||
})
|
||||
|
||||
for (const file of cache.keys()) {
|
||||
if (files.has(file)) continue
|
||||
cache.delete(file)
|
||||
}
|
||||
return diffs
|
||||
})
|
||||
}
|
||||
|
||||
export interface MergeResult {
|
||||
diffs: WorktreeFileDiff[]
|
||||
/** Files whose metadata changed while we preserved cached content.
|
||||
|
||||
@@ -801,6 +801,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return undefined
|
||||
})
|
||||
if (hasGit() && hasGitChangesMention(message) && !gitFile) return
|
||||
if (isDisabled()) return
|
||||
|
||||
const allFiles = [
|
||||
...mentionFiles,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
|
||||
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
|
||||
import type { JSX } from "solid-js"
|
||||
import { createSignal, type JSX } from "solid-js"
|
||||
import type { WorktreeFileDiff, WorktreeState, WorktreeGitStats, PRStatus } from "../types/messages"
|
||||
import "../../agent-manager/agent-manager.css"
|
||||
import "../../agent-manager/agent-manager-review.css"
|
||||
@@ -63,6 +63,48 @@ const foldedDiffs: WorktreeFileDiff[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const ROWS = 140
|
||||
function edited(seed: string): WorktreeFileDiff {
|
||||
const before = Array.from({ length: ROWS }, (_, i) => `const row${i} = "${seed}-old-${i}"\n`).join("")
|
||||
const after = Array.from({ length: ROWS }, (_, i) => `const row${i} = "${seed}-new-${i}"\n`).join("")
|
||||
const patch = [
|
||||
"diff --git a/src/agent-edit.ts b/src/agent-edit.ts",
|
||||
"--- a/src/agent-edit.ts",
|
||||
"+++ b/src/agent-edit.ts",
|
||||
`@@ -1,${ROWS} +1,${ROWS} @@`,
|
||||
...before
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.map((line) => `-${line}`),
|
||||
...after
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.map((line) => `+${line}`),
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
return {
|
||||
file: "src/agent-edit.ts",
|
||||
status: "modified",
|
||||
additions: ROWS,
|
||||
deletions: ROWS,
|
||||
before,
|
||||
after,
|
||||
patch,
|
||||
}
|
||||
}
|
||||
|
||||
const tail: WorktreeFileDiff = {
|
||||
file: "src/target.ts",
|
||||
status: "modified",
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
before: "const target = 'before'\n",
|
||||
after: "const target = 'after'\n",
|
||||
patch:
|
||||
"diff --git a/src/target.ts b/src/target.ts\n--- a/src/target.ts\n+++ b/src/target.ts\n@@ -1 +1 @@\n-const target = 'before'\n+const target = 'after'\n",
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Meta
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -235,6 +277,51 @@ export const FullScreenDiffWithCollapsedContext: Story = {
|
||||
),
|
||||
}
|
||||
|
||||
export const FullScreenDiffAgentEditScroll: Story = {
|
||||
name: "FullScreenDiffView - preserve scroll during agent edit",
|
||||
render: () => {
|
||||
const [diffs, setDiffs] = createSignal([edited("before"), tail])
|
||||
const [version, setVersion] = createSignal("before")
|
||||
const [key, setKey] = createSignal("agent-edit-scroll")
|
||||
const update = () => {
|
||||
setDiffs([edited("after"), tail])
|
||||
setVersion("after")
|
||||
}
|
||||
const change = () => {
|
||||
setDiffs([edited("context"), tail])
|
||||
setKey("changed-context")
|
||||
}
|
||||
return (
|
||||
<StoryProviders noPadding>
|
||||
<div style={{ height: "700px", display: "flex", "flex-direction": "column" }}>
|
||||
<div style={{ display: "flex", gap: "8px", padding: "4px", "align-items": "center" }}>
|
||||
<Button size="small" onClick={update}>
|
||||
Apply agent edit
|
||||
</Button>
|
||||
<Button size="small" onClick={change}>
|
||||
Switch review context
|
||||
</Button>
|
||||
<span data-testid="agent-edit-version">{version()}</span>
|
||||
<span data-testid="review-context">{key()}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", "min-height": "0", flex: "1" }}>
|
||||
<FullScreenDiffView
|
||||
diffs={diffs()}
|
||||
loading={false}
|
||||
sessionKey={key()}
|
||||
diffStyle="unified"
|
||||
onDiffStyleChange={() => {}}
|
||||
comments={[]}
|
||||
onCommentsChange={() => {}}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WorktreeItem — shared mock helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SessionID } from "@/session/schema"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { NonNegativeInt, PositiveInt, optionalOmitUndefined, withStatics } from "@/util/schema"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { spawn, type ChildProcess } from "child_process"
|
||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
||||
@@ -172,7 +173,8 @@ export namespace BackgroundProcess {
|
||||
return changed
|
||||
}
|
||||
const fallback = active.info.ready && active.start.ready?.port ? [active.start.ready.port] : []
|
||||
const next = Array.from(new Set([...(await Ports.list(pid)), ...fallback])).toSorted((a, b) => a - b)
|
||||
const ports = Flag.KILO_CLIENT === "cli" ? await Ports.list(pid) : []
|
||||
const next = Array.from(new Set([...ports, ...fallback])).toSorted((a, b) => a - b)
|
||||
if (same(active.info.ports, next)) return false
|
||||
active.info.ports = next
|
||||
active.info.time.updated = Date.now()
|
||||
@@ -209,6 +211,7 @@ export namespace BackgroundProcess {
|
||||
|
||||
function poll(active: Active) {
|
||||
if (active.disposed) return
|
||||
if (Flag.KILO_CLIENT !== "cli") return
|
||||
if (terminal(active.info.status)) return
|
||||
if (active.poll) return
|
||||
active.poll = setTimeout(() => {
|
||||
|
||||
@@ -125,7 +125,11 @@ function build(key: string, remote: Item, url: string, prev?: Model): Model {
|
||||
variants[effort] = {
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
...(model.api.id.includes("opus-4.7") ? { display: "summarized" } : {}),
|
||||
// kilocode_change start - treat opus-4.8 like opus-4.7
|
||||
...(model.api.id.includes("opus-4.7") || model.api.id.includes("opus-4.8")
|
||||
? { display: "summarized" }
|
||||
: {}),
|
||||
// kilocode_change end
|
||||
},
|
||||
effort,
|
||||
}
|
||||
|
||||
@@ -542,9 +542,11 @@ function openaiReasoningEfforts(apiId: string, releaseDate: string): string[] |
|
||||
}
|
||||
|
||||
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
||||
if (["opus-4-7", "opus-4.7"].some((v) => apiId.includes(v))) {
|
||||
// kilocode_change start - treat opus-4.8 like opus-4.7
|
||||
if (["opus-4-7", "opus-4.7", "opus-4-8", "opus-4.8"].some((v) => apiId.includes(v))) {
|
||||
return ["low", "medium", "high", "xhigh", "max"]
|
||||
}
|
||||
// kilocode_change end
|
||||
if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) {
|
||||
return ["low", "medium", "high", "max"]
|
||||
}
|
||||
@@ -780,9 +782,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
if (adaptiveEfforts) {
|
||||
let efforts = [...adaptiveEfforts]
|
||||
if (model.providerID === "github-copilot") {
|
||||
if (model.api.id.includes("opus-4.7")) {
|
||||
// kilocode_change start - treat opus-4.8 like opus-4.7
|
||||
if (model.api.id.includes("opus-4.7") || model.api.id.includes("opus-4.8")) {
|
||||
efforts = ["medium"]
|
||||
}
|
||||
// kilocode_change end
|
||||
// Efforts currently supported are: low, medium, high
|
||||
efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
|
||||
}
|
||||
@@ -792,9 +796,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
{
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
|
||||
// kilocode_change start - treat opus-4.8 like opus-4.7
|
||||
...(["opus-4-7", "opus-4.7", "opus-4-8", "opus-4.8"].some((v) => model.api.id.includes(v))
|
||||
? { display: "summarized" }
|
||||
: {}),
|
||||
// kilocode_change end
|
||||
},
|
||||
effort,
|
||||
},
|
||||
@@ -827,9 +833,11 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
reasoningConfig: {
|
||||
type: "adaptive",
|
||||
maxReasoningEffort: effort,
|
||||
...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
|
||||
// kilocode_change start - treat opus-4.8 like opus-4.7
|
||||
...(["opus-4-7", "opus-4.7", "opus-4-8", "opus-4.8"].some((v) => model.api.id.includes(v))
|
||||
? { display: "summarized" }
|
||||
: {}),
|
||||
// kilocode_change end
|
||||
},
|
||||
},
|
||||
]),
|
||||
|
||||
@@ -23,6 +23,14 @@ async function script(dir: string, name: string, source: string) {
|
||||
return `${bin} ${arg}`
|
||||
}
|
||||
|
||||
function port() {
|
||||
const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() })
|
||||
const port = server.port
|
||||
server.stop(true)
|
||||
if (!port) throw new Error("Failed to reserve port")
|
||||
return port
|
||||
}
|
||||
|
||||
function update(sessionID: SessionID) {
|
||||
const state: { off?: () => void; timer?: ReturnType<typeof setTimeout> } = {}
|
||||
const promise = new Promise<BackgroundProcess.Info>((resolve, reject) => {
|
||||
@@ -92,6 +100,119 @@ setInterval(() => {}, 1_000)
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("reports explicit readiness ports for VS Code clients", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const sessionID = SessionID.descending()
|
||||
const listen = port()
|
||||
const command = yield* Effect.promise(() =>
|
||||
script(
|
||||
test.directory,
|
||||
"vscode-ready-port.mjs",
|
||||
`Bun.serve({ hostname: "127.0.0.1", port: ${listen}, fetch: () => new Response() })
|
||||
`,
|
||||
),
|
||||
)
|
||||
const client = process.env["KILO_CLIENT"]
|
||||
process.env["KILO_CLIENT"] = "vscode"
|
||||
|
||||
try {
|
||||
const info = yield* Effect.promise(() =>
|
||||
BackgroundProcess.start({
|
||||
sessionID,
|
||||
command,
|
||||
cwd: test.directory,
|
||||
ready: { port: listen, timeout: 5_000 },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(info.status).toBe("ready")
|
||||
expect(info.ports).toEqual([listen])
|
||||
} finally {
|
||||
yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID))
|
||||
if (client === undefined) delete process.env["KILO_CLIENT"]
|
||||
else process.env["KILO_CLIENT"] = client
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("infers ports for CLI clients", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const sessionID = SessionID.descending()
|
||||
const listen = port()
|
||||
const command = yield* Effect.promise(() =>
|
||||
script(
|
||||
test.directory,
|
||||
"cli-port.mjs",
|
||||
`Bun.serve({ hostname: "127.0.0.1", port: ${listen}, fetch: () => new Response() })
|
||||
`,
|
||||
),
|
||||
)
|
||||
const client = process.env["KILO_CLIENT"]
|
||||
process.env["KILO_CLIENT"] = "cli"
|
||||
|
||||
try {
|
||||
const info = yield* Effect.promise(() =>
|
||||
BackgroundProcess.start({
|
||||
sessionID,
|
||||
command,
|
||||
cwd: test.directory,
|
||||
}),
|
||||
)
|
||||
|
||||
let found = yield* Effect.promise(() => BackgroundProcess.get(info.id))
|
||||
if (process.platform !== "win32") {
|
||||
for (let attempt = 0; attempt < 40 && !found?.ports.includes(listen); attempt++) {
|
||||
yield* Effect.promise(() => Bun.sleep(250))
|
||||
found = yield* Effect.promise(() => BackgroundProcess.get(info.id))
|
||||
}
|
||||
}
|
||||
expect(found?.ports).toEqual(process.platform === "win32" ? [] : [listen])
|
||||
} finally {
|
||||
yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID))
|
||||
if (client === undefined) delete process.env["KILO_CLIENT"]
|
||||
else process.env["KILO_CLIENT"] = client
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("does not infer ports for VS Code clients", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const sessionID = SessionID.descending()
|
||||
const listen = port()
|
||||
const command = yield* Effect.promise(() =>
|
||||
script(
|
||||
test.directory,
|
||||
"vscode-port.mjs",
|
||||
`Bun.serve({ hostname: "127.0.0.1", port: ${listen}, fetch: () => new Response() })
|
||||
`,
|
||||
),
|
||||
)
|
||||
const client = process.env["KILO_CLIENT"]
|
||||
process.env["KILO_CLIENT"] = "vscode"
|
||||
|
||||
try {
|
||||
const info = yield* Effect.promise(() =>
|
||||
BackgroundProcess.start({
|
||||
sessionID,
|
||||
command,
|
||||
cwd: test.directory,
|
||||
}),
|
||||
)
|
||||
|
||||
yield* Effect.promise(() => Bun.sleep(2_500))
|
||||
const found = yield* Effect.promise(() => BackgroundProcess.get(info.id))
|
||||
expect(found?.ports).toEqual([])
|
||||
} finally {
|
||||
yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID))
|
||||
if (client === undefined) delete process.env["KILO_CLIENT"]
|
||||
else process.env["KILO_CLIENT"] = client
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("publishes output updates from process callbacks", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
|
||||
@@ -30,7 +30,7 @@ function mockModel(overrides: Partial<any> = {}): any {
|
||||
}
|
||||
}
|
||||
|
||||
describe("ProviderTransform.variants - Claude Opus 4.7", () => {
|
||||
describe("ProviderTransform.variants - Claude Opus 4.7 / 4.8", () => {
|
||||
test("opus-4-7 returns adaptive thinking variants including xhigh (native anthropic)", () => {
|
||||
const model = mockModel({
|
||||
api: {
|
||||
@@ -75,6 +75,50 @@ describe("ProviderTransform.variants - Claude Opus 4.7", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("opus-4-8 returns adaptive thinking variants including xhigh (native anthropic)", () => {
|
||||
const model = mockModel({
|
||||
api: {
|
||||
id: "claude-opus-4-8",
|
||||
url: "https://api.anthropic.com",
|
||||
npm: "@ai-sdk/anthropic",
|
||||
},
|
||||
})
|
||||
const result = ProviderTransform.variants(model)
|
||||
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||
expect(result.xhigh).toEqual({
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "xhigh",
|
||||
})
|
||||
})
|
||||
|
||||
test("opus-4.8 dot-form returns adaptive thinking variants via @ai-sdk/gateway", () => {
|
||||
const model = mockModel({
|
||||
id: "anthropic/claude-opus-4-8",
|
||||
api: {
|
||||
id: "anthropic/claude-opus-4.8",
|
||||
url: "https://gateway.ai",
|
||||
npm: "@ai-sdk/gateway",
|
||||
},
|
||||
})
|
||||
const result = ProviderTransform.variants(model)
|
||||
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||
})
|
||||
|
||||
test("opus-4-8 on bedrock returns adaptive reasoningConfig with xhigh", () => {
|
||||
const model = mockModel({
|
||||
api: {
|
||||
id: "anthropic.claude-opus-4-8",
|
||||
url: "https://bedrock.amazonaws.com",
|
||||
npm: "@ai-sdk/amazon-bedrock",
|
||||
},
|
||||
})
|
||||
const result = ProviderTransform.variants(model)
|
||||
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
|
||||
expect(result.xhigh).toEqual({
|
||||
reasoningConfig: { type: "adaptive", maxReasoningEffort: "xhigh", display: "summarized" },
|
||||
})
|
||||
})
|
||||
|
||||
test("opus-4-6 keeps original adaptive efforts without xhigh", () => {
|
||||
const model = mockModel({
|
||||
api: {
|
||||
|
||||
Reference in New Issue
Block a user