Merge branch 'main' into kirillk/model-picker

This commit is contained in:
Kirill Kalishev
2026-03-17 11:59:30 -04:00
committed by GitHub
129 changed files with 1539 additions and 977 deletions
+16 -16
View File
@@ -27,7 +27,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/kilo-i18n": "workspace:*",
"@kilocode/kilo-ui": "workspace:*",
@@ -79,7 +79,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -112,7 +112,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -142,7 +142,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -171,7 +171,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@kilocode/plugin": "workspace:*",
@@ -203,7 +203,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -213,7 +213,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"@opentelemetry/api": "1.9.0",
@@ -233,7 +233,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kobalte/core": "0.13.11",
"@opencode-ai/util": "workspace:*",
@@ -262,7 +262,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-i18n": "workspace:*",
@@ -314,7 +314,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.0.48",
"version": "7.0.49",
"bin": {
"opencode": "./bin/opencode",
},
@@ -434,7 +434,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"zod": "catalog:",
@@ -448,14 +448,14 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@types/bun": "catalog:",
},
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -466,7 +466,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.0.48",
"version": "7.0.49",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -489,7 +489,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -536,7 +536,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "7.0.48",
"version": "7.0.49",
"dependencies": {
"zod": "catalog:",
},
+1 -1
View File
@@ -111,6 +111,6 @@
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
},
"version": "7.0.48",
"version": "7.0.49",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "7.0.48",
"version": "7.0.49",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.0.48"
version = "7.0.49"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.48/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.48/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.48/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.48/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.48/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.49/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.0.48",
"version": "7.0.49",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.0.48",
"version": "7.0.49",
"type": "module",
"license": "MIT",
"exports": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.0.48",
"version": "7.0.49",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
+34
View File
@@ -365,6 +365,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "abort":
await this.handleAbort(message.sessionID)
break
case "revertSession":
this.handleRevertSession(message.sessionID, message.messageID).catch((e) =>
console.error("[Kilo New] handleRevertSession failed:", e),
)
break
case "unrevertSession":
this.handleUnrevertSession(message.sessionID).catch((e) =>
console.error("[Kilo New] handleUnrevertSession failed:", e),
)
break
case "permissionResponse":
await this.handlePermissionResponse(
message.permissionId,
@@ -1697,6 +1707,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
private async handleRevertSession(sessionID: string, messageID: string): Promise<void> {
if (!this.client) return
const dir = this.getWorkspaceDirectory(sessionID)
const { data, error } = await this.client.session.revert({ sessionID, messageID, directory: dir })
if (error) {
console.error("[Kilo New] KiloProvider: Failed to revert session:", error)
this.postMessage({ type: "error", message: "Failed to revert session", sessionID })
return
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
private async handleUnrevertSession(sessionID: string): Promise<void> {
if (!this.client) return
const dir = this.getWorkspaceDirectory(sessionID)
const { data, error } = await this.client.session.unrevert({ sessionID, directory: dir })
if (error) {
console.error("[Kilo New] KiloProvider: Failed to unrevert session:", error)
this.postMessage({ type: "error", message: "Failed to redo session", sessionID })
return
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
/**
* Handle compact (context summarization) request from the webview.
*/
@@ -44,6 +44,11 @@ export function sessionToWebview(session: Session) {
title: session.title,
createdAt: new Date(session.time.created).toISOString(),
updatedAt: new Date(session.time.updated).toISOString(),
// Use null (not undefined) so the value survives postMessage JSON serialization.
// Without this, unrevert responses lose the revert key entirely and the
// SolidJS store merge never clears the existing revert state.
revert: session.revert ?? null,
summary: session.summary ?? null,
}
}
@@ -174,6 +179,7 @@ export type WebviewMessage =
| { type: "permissionError"; permissionID: string }
| { type: "sessionCreated"; session: ReturnType<typeof sessionToWebview> }
| { type: "sessionUpdated"; session: ReturnType<typeof sessionToWebview> }
| { type: "messageRemoved"; sessionID: string; messageID: string }
| null
export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | undefined): WebviewMessage {
@@ -209,6 +215,14 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
},
}
}
case "message.removed": {
const props = event.properties as { sessionID: string; messageID: string }
return {
type: "messageRemoved",
sessionID: props.sessionID,
messageID: props.messageID,
}
}
case "session.status": {
const info = event.properties.status
return {
@@ -20,13 +20,10 @@ export class AutocompleteModel {
private connectionService: KiloConnectionService | null = null
public profileName: string | null = null
public profileType: string | null = null
public loaded = false
public hasKilocodeProfileWithNoBalance = false
constructor(connectionService?: KiloConnectionService) {
if (connectionService) {
this.connectionService = connectionService
this.loaded = true
}
}
@@ -37,21 +34,6 @@ export class AutocompleteModel {
this.connectionService = service
}
/**
* Load model configuration.
* Returns true if the connection service is available.
*/
public async reload(): Promise<boolean> {
this.loaded = true
if (this.connectionService) {
const state = this.connectionService.getConnectionState()
return state === "connected"
}
return false
}
public supportsFim(): boolean {
return true
}
@@ -18,7 +18,6 @@ export interface AutocompleteServiceSettings {
provider?: string
model?: string
snoozeUntil?: number
hasKilocodeProfileWithNoBalance?: boolean
}
function readSettings(): AutocompleteServiceSettings {
@@ -59,6 +58,7 @@ export class AutocompleteServiceManager {
public readonly codeActionProvider: AutocompleteCodeActionProvider
public readonly inlineCompletionProvider: AutocompleteInlineCompletionProvider
private inlineCompletionProviderDisposable: vscode.Disposable | null = null
private unsubscribeState: (() => void) | null = null
constructor(context: vscode.ExtensionContext, connectionService: KiloConnectionService) {
if (AutocompleteServiceManager._instance) {
@@ -86,6 +86,12 @@ export class AutocompleteServiceManager {
new AutocompleteTelemetry(),
)
// Reload when CLI backend connection state changes so autocomplete
// picks up the connected state even if it wasn't ready at startup.
this.unsubscribeState = connectionService.onStateChange(() => {
void this.load()
})
void this.load()
}
@@ -97,7 +103,6 @@ export class AutocompleteServiceManager {
}
public async load() {
await this.model.reload()
this.settings = readSettings()
await this.updateGlobalContext()
@@ -119,12 +124,12 @@ export class AutocompleteServiceManager {
return
}
// Register classic provider
// Register classic provider (tracked via this.inlineCompletionProviderDisposable,
// not context.subscriptions, so re-registration on reconnect doesn't leak)
this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider(
{ scheme: "file" },
this.inlineCompletionProvider,
)
this.context.subscriptions.push(this.inlineCompletionProviderDisposable)
}
public async disable() {
@@ -237,11 +242,6 @@ export class AutocompleteServiceManager {
const document = editor.document
// Ensure model is loaded
if (!this.model.loaded) {
await this.load()
}
// Call the inline completion provider directly with manual trigger context
const position = editor.selection.active
const context: vscode.InlineCompletionContext = {
@@ -293,22 +293,16 @@ export class AutocompleteServiceManager {
})
}
private getCurrentModelName(): string | undefined {
if (!this.model.loaded) {
return
}
private getCurrentModelName(): string {
return this.model.getModelName()
}
private getCurrentProviderName(): string | undefined {
if (!this.model.loaded) {
return
}
private getCurrentProviderName(): string {
return this.model.getProviderDisplayName()
}
private hasNoUsableProvider(): boolean {
return this.model.loaded && !this.model.hasValidCredentials() && !this.model.hasKilocodeProfileWithNoBalance
return !this.model.hasValidCredentials()
}
private updateCostTracking(cost: number, _inputTokens: number, _outputTokens: number): void {
@@ -328,7 +322,6 @@ export class AutocompleteServiceManager {
model: this.getCurrentModelName(),
provider: this.getCurrentProviderName(),
profileName: this.model.profileName,
hasKilocodeProfileWithNoBalance: this.model.hasKilocodeProfileWithNoBalance,
hasNoUsableProvider: this.hasNoUsableProvider(),
totalSessionCost: this.sessionCost,
completionCount: this.completionCount,
@@ -360,6 +353,10 @@ export class AutocompleteServiceManager {
this.snoozeTimer = null
}
// Unsubscribe from connection state changes
this.unsubscribeState?.()
this.unsubscribeState = null
// Dispose inline completion provider registration
if (this.inlineCompletionProviderDisposable) {
this.inlineCompletionProviderDisposable.dispose()
@@ -86,22 +86,12 @@ export class AutocompleteStatusBar {
}
public render() {
if (this.props.hasKilocodeProfileWithNoBalance) {
return this.renderNoCreditsError()
}
if (this.props.hasNoUsableProvider) {
return this.renderNoUsableProviderError()
}
return this.renderDefault()
}
private renderNoCreditsError() {
this.statusBar.text = t("kilocode:autocomplete.statusBar.warning")
this.statusBar.tooltip = this.createMarkdownTooltip(
t("kilocode:autocomplete.statusBar.tooltip.noCredits", { command: SETTINGS_COMMAND }),
)
}
private renderNoUsableProviderError() {
this.statusBar.text = t("kilocode:autocomplete.statusBar.warning")
this.statusBar.tooltip = this.createMarkdownTooltip(
@@ -1,732 +1,169 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { describe, it, expect, vi } from "vitest"
import { AutocompleteModel } from "../AutocompleteModel"
import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager"
import { AUTOCOMPLETE_PROVIDER_MODELS } from "../utils/kilocode-utils"
import * as apiIndex from "../../../api"
import { TelemetryService } from "../../../../packages/telemetry/src/TelemetryService"
import type { KiloConnectionService } from "../../cli-backend"
function createMockConnectionService(state: "connecting" | "connected" | "disconnected" | "error" = "connected") {
return {
getConnectionState: vi.fn().mockReturnValue(state),
getClient: vi.fn().mockReturnValue({
kilo: {
fim: vi.fn(),
},
}),
onStateChange: vi.fn().mockReturnValue(() => {}),
} as unknown as KiloConnectionService
}
describe("AutocompleteModel", () => {
let mockProviderSettingsManager: ProviderSettingsManager
beforeEach(() => {
mockProviderSettingsManager = {
listConfig: vi.fn(),
getProfile: vi.fn(),
} as any
describe("constructor", () => {
it("defaults profileName and profileType to null", () => {
const model = new AutocompleteModel()
expect(model.profileName).toBeNull()
expect(model.profileType).toBeNull()
})
})
describe("reload", () => {
it("sorts profiles by supportedProviders index order", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "3", name: "profile3", apiProvider: supportedProviders[2] },
{ id: "1", name: "profile1", apiProvider: supportedProviders[0] },
{ id: "2", name: "profile2", apiProvider: supportedProviders[1] },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "profile1",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
describe("setConnectionService", () => {
it("sets the connection service after construction", () => {
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "1" })
})
it("filters out profiles without apiProvider", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "profile1", apiProvider: undefined },
{ id: "2", name: "profile2", apiProvider: supportedProviders[0] },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "2",
name: "profile2",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" })
})
it("filters out profiles with unsupported apiProvider", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "profile1", apiProvider: "unsupported" },
{ id: "2", name: "profile2", apiProvider: supportedProviders[0] },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "2",
name: "profile2",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" })
})
it("handles empty profile list", async () => {
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue([])
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
expect(mockProviderSettingsManager.getProfile).not.toHaveBeenCalled()
expect(model.hasValidCredentials()).toBe(false)
expect(result).toBe(false)
})
it("returns true when profile found", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [{ id: "1", name: "mistral-profile", apiProvider: "mistral" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "mistral-profile",
apiProvider: "mistral",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
expect(result).toBe(true)
expect(model.loaded).toBe(true)
const connection = createMockConnectionService("connected")
model.setConnectionService(connection)
expect(model.hasValidCredentials()).toBe(true)
})
})
describe("provider usability", () => {
beforeEach(() => {
// Mock fetch globally for these tests
vi.stubGlobal("fetch", vi.fn())
// Mock TelemetryService
const mockTelemetryService = {
captureEvent: vi.fn(),
captureException: vi.fn(),
isTelemetryEnabled: vi.fn().mockReturnValue(false),
shutdown: vi.fn(),
}
vi.spyOn(TelemetryService, "hasInstance").mockReturnValue(true)
vi.spyOn(TelemetryService, "instance", "get").mockReturnValue(mockTelemetryService as any)
describe("hasValidCredentials", () => {
it("returns true when connected", () => {
const connection = createMockConnectionService("connected")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(true)
})
afterEach(() => {
// Restore fetch
vi.unstubAllGlobals()
// Restore TelemetryService mocks
vi.restoreAllMocks()
it("returns false when disconnected", () => {
const connection = createMockConnectionService("disconnected")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(false)
})
it("should skip kilocode provider when balance is zero and use openrouter instead", async () => {
const profiles = [
{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" },
{ id: "2", name: "openrouter-profile", apiProvider: "openrouter" },
] as any
it("returns false when connecting", () => {
const connection = createMockConnectionService("connecting")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(false)
})
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profiles with tokens
vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => {
if (args.id === "1") {
return {
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any
} else if (args.id === "2") {
return {
id: "2",
name: "openrouter-profile",
apiProvider: "openrouter",
openRouterApiKey: "test-key",
} as any
}
return null as any
})
// Mock fetch to return zero balance for kilocode
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ data: { balance: 0 } }),
} as any
}
// For OpenRouter models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({ data: [] }),
} as any
}
// For other URLs, return a basic response
return {
ok: true,
json: async () => ({}),
} as any
})
it("returns false when in error state", () => {
const connection = createMockConnectionService("error")
const model = new AutocompleteModel(connection)
expect(model.hasValidCredentials()).toBe(false)
})
it("returns false without connection service", () => {
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should have tried both providers but used openrouter (since kilocode balance is 0)
expect(result).toBe(true)
expect(model.loaded).toBe(true)
expect(model.hasValidCredentials()).toBe(false)
})
})
it("should use kilocode provider when balance is greater than zero", async () => {
const profiles = [
{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" },
{ id: "2", name: "openrouter-profile", apiProvider: "openrouter" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profiles with tokens
vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => {
if (args.id === "1") {
return {
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any
} else if (args.id === "2") {
return {
id: "2",
name: "openrouter-profile",
apiProvider: "openrouter",
openRouterApiKey: "test-key",
} as any
}
return null as any
})
// Mock fetch to return positive balance for kilocode
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ data: { balance: 10.5 } }),
} as any
}
// For OpenRouter models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({ data: [] }),
} as any
}
// For other URLs, return a basic response
return {
ok: true,
json: async () => ({}),
} as any
})
describe("supportsFim", () => {
it("always returns true", () => {
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should have used kilocode provider (first one with positive balance)
expect(result).toBe(true)
expect(model.loaded).toBe(true)
expect(model.supportsFim()).toBe(true)
})
})
it("should handle kilocode provider with no token", async () => {
const profiles = [
{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" },
{ id: "2", name: "openrouter-profile", apiProvider: "openrouter" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profiles - kilocode without token
vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => {
if (args.id === "1") {
return {
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "", // No token
} as any
} else if (args.id === "2") {
return {
id: "2",
name: "openrouter-profile",
apiProvider: "openrouter",
openRouterApiKey: "test-key",
} as any
}
return null as any
})
// Mock fetch to handle the no-token case
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
// This should not be called since there's no token
return {
ok: false,
status: 401,
} as any
}
// For OpenRouter models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({ data: [] }),
} as any
}
// For other URLs, return a basic response
return {
ok: true,
json: async () => ({}),
} as any
})
describe("getModelName", () => {
it("returns the default model", () => {
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should skip kilocode (no token) and use openrouter
expect(result).toBe(true)
expect(model.loaded).toBe(true)
})
it("should set hasKilocodeProfileWithNoBalance when kilocode profile exists but has no balance", async () => {
const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profile with token
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any)
// Mock fetch to return zero balance for kilocode
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 0 }),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should not find a usable provider
expect(result).toBe(false)
expect(model.loaded).toBe(true)
// Should have set the flag indicating kilocode profile exists but has no balance
expect(model.hasKilocodeProfileWithNoBalance).toBe(true)
})
it("should not set hasKilocodeProfileWithNoBalance when kilocode profile has balance", async () => {
const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profile with token
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any)
// Mock fetch to return positive balance for kilocode and valid models response
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 10.5 }),
} as any
}
// For OpenRouter/Kilocode models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({
data: [
{
id: "mistralai/codestral-2508",
name: "Codestral",
context_length: 32000,
pricing: { prompt: "0.0001", completion: "0.0003" },
},
],
}),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
const model = new AutocompleteModel()
const result = await model.reload(mockProviderSettingsManager)
// Should find a usable provider
expect(result).toBe(true)
expect(model.loaded).toBe(true)
// Should not have set the flag
expect(model.hasKilocodeProfileWithNoBalance).toBe(false)
})
it("should clear hasKilocodeProfileWithNoBalance on reload", async () => {
const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
// Mock profile with token
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "kilocode-profile",
apiProvider: "kilocode",
kilocodeToken: "test-token",
} as any)
// First reload: zero balance
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 0 }),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.hasKilocodeProfileWithNoBalance).toBe(true)
// Second reload: positive balance with valid models response
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.includes("/api/profile/balance")) {
return {
ok: true,
json: async () => ({ balance: 10.5 }),
} as any
}
// For OpenRouter/Kilocode models endpoint
if (url.includes("/models")) {
return {
ok: true,
json: async () => ({
data: [
{
id: "mistralai/codestral-2508",
name: "Codestral",
context_length: 32000,
pricing: { prompt: "0.0001", completion: "0.0003" },
},
],
}),
} as any
}
return {
ok: true,
json: async () => ({}),
} as any
})
await model.reload(mockProviderSettingsManager)
// Flag should be cleared after reload with positive balance
expect(model.hasKilocodeProfileWithNoBalance).toBe(false)
expect(model.getModelName()).toBe("mistralai/codestral-2508")
})
})
describe("getProviderDisplayName", () => {
it("returns undefined when no provider is loaded", () => {
it("returns Kilo Gateway", () => {
const model = new AutocompleteModel()
expect(model.getProviderDisplayName()).toBeUndefined()
})
it("returns provider name from API handler when provider is loaded", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [{ id: "1", name: "profile1", apiProvider: supportedProviders[0] }] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "profile1",
apiProvider: supportedProviders[0],
mistralApiKey: "test-key",
} as any)
// Mock buildApiHandler to return a handler with providerName
const mockApiHandler = {
providerName: "Mistral",
getModel: vi.fn().mockReturnValue({ id: "mistral-model", info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
const providerName = model.getProviderDisplayName()
expect(providerName).toBeTruthy()
expect(typeof providerName).toBe("string")
expect(providerName).toBe("Mistral")
// Restore the spy
vi.restoreAllMocks()
})
describe("profile information", () => {
it("returns null for profile name when no profile is loaded", () => {
const model = new AutocompleteModel()
expect(model.profileName).toBeNull()
})
it("returns null for profile type when no profile is loaded", () => {
const model = new AutocompleteModel()
expect(model.profileType).toBeNull()
})
it("stores and returns profile name after loading", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{
id: "1",
name: "My Autocomplete Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
},
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Autocomplete Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.profileName).toBe("My Autocomplete Profile")
})
it("stores and returns profile type after loading", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "My Profile", apiProvider: supportedProviders[0], profileType: "autocomplete" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.profileType).toBe("autocomplete")
})
it("clears profile information on cleanup", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const profiles = [
{ id: "1", name: "My Profile", apiProvider: supportedProviders[0], profileType: "autocomplete" },
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Profile",
apiProvider: supportedProviders[0],
profileType: "autocomplete",
mistralApiKey: "test-key",
} as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
expect(model.profileName).toBe("My Profile")
expect(model.profileType).toBe("autocomplete")
// Reload with empty profiles to trigger cleanup
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue([])
await model.reload(mockProviderSettingsManager)
expect(model.profileName).toBeNull()
expect(model.profileType).toBeNull()
})
expect(model.getProviderDisplayName()).toBe("Kilo Gateway")
})
})
describe("reload model override behavior", () => {
beforeEach(() => {
// Mock buildApiHandler to return a handler with getModel
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: "test-model", info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
})
afterEach(() => {
vi.restoreAllMocks()
})
it("should use custom model for explicit autocomplete profiles", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const provider = supportedProviders[0]
const customModelId = "custom-autocomplete-model"
const profiles = [
{
id: "1",
name: "My Autocomplete Profile",
apiProvider: provider,
profileType: "autocomplete",
},
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Autocomplete Profile",
apiProvider: provider,
profileType: "autocomplete",
mistralApiKey: "test-key",
apiModelId: customModelId, // Custom model set by user
} as any)
// Mock buildApiHandler to return the custom model
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: customModelId, info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
describe("generateFimResponse", () => {
it("throws when connection service is not available", async () => {
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
// The model should use the custom model from the profile
const modelName = model.getModelName()
expect(modelName).toBe(customModelId)
await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow(
"Connection service is not available",
)
})
it("should override model for non-autocomplete profiles", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const provider = supportedProviders[0]
const defaultAutocompleteModel = AUTOCOMPLETE_PROVIDER_MODELS.get(provider)
const profiles = [
{
id: "1",
name: "My Chat Profile",
apiProvider: provider,
profileType: "chat", // Not an autocomplete profile
},
] as any
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Chat Profile",
apiProvider: provider,
profileType: "chat",
mistralApiKey: "test-key",
apiModelId: "custom-chat-model", // This should be overridden
} as any)
// Mock buildApiHandler to return the overridden model
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: defaultAutocompleteModel, info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
// The model should be overridden with the default autocomplete model
const modelName = model.getModelName()
expect(modelName).toBe(defaultAutocompleteModel)
it("throws when not connected", async () => {
const connection = createMockConnectionService("disconnected")
const model = new AutocompleteModel(connection)
await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow(
"CLI backend is not connected (state: disconnected)",
)
})
it("should override model for profiles without profileType", async () => {
const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()]
const provider = supportedProviders[0]
const defaultAutocompleteModel = AUTOCOMPLETE_PROVIDER_MODELS.get(provider)
const profiles = [
it("streams chunks and returns metadata", async () => {
const chunks = [
{ choices: [{ delta: { content: "hello" } }] },
{
id: "1",
name: "My Generic Profile",
apiProvider: provider,
// No profileType specified
choices: [{ delta: { content: " world" } }],
usage: { prompt_tokens: 10, completion_tokens: 5 },
cost: 0.001,
},
] as any
]
vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles)
vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({
id: "1",
name: "My Generic Profile",
apiProvider: provider,
mistralApiKey: "test-key",
apiModelId: "custom-model", // This should be overridden
} as any)
const connection = createMockConnectionService("connected")
const client = (connection as any).getClient()
client.kilo.fim.mockResolvedValue({
stream: (async function* () {
for (const chunk of chunks) yield chunk
})(),
})
// Mock buildApiHandler to return the overridden model
const mockApiHandler = {
getModel: vi.fn().mockReturnValue({ id: defaultAutocompleteModel, info: {} }),
createMessage: vi.fn(),
countTokens: vi.fn(),
}
vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any)
const model = new AutocompleteModel(connection)
const received: string[] = []
const result = await model.generateFimResponse("prefix", "suffix", (text) => received.push(text))
expect(received).toEqual(["hello", " world"])
expect(result).toEqual({
cost: 0.001,
inputTokens: 10,
outputTokens: 5,
cacheWriteTokens: 0,
cacheReadTokens: 0,
})
})
it("passes model parameters to fim call", async () => {
const connection = createMockConnectionService("connected")
const client = (connection as any).getClient()
client.kilo.fim.mockResolvedValue({
stream: (async function* () {})(),
})
const model = new AutocompleteModel(connection)
const signal = new AbortController().signal
await model.generateFimResponse("pre", "suf", vi.fn(), signal)
expect(client.kilo.fim).toHaveBeenCalledWith(
{
prefix: "pre",
suffix: "suf",
model: "mistralai/codestral-2508",
maxTokens: 256,
temperature: 0.2,
},
{ signal },
)
})
})
describe("generateResponse", () => {
it("throws because FIM is the primary strategy", async () => {
const model = new AutocompleteModel()
await model.reload(mockProviderSettingsManager)
// The model should be overridden with the default autocomplete model
const modelName = model.getModelName()
expect(modelName).toBe(defaultAutocompleteModel)
await expect(model.generateResponse("system", "user", vi.fn())).rejects.toThrow(
"Chat-based completions are not supported via CLI backend",
)
})
})
})
@@ -63,13 +63,8 @@ vi.mock("vscode", () => {
vi.mock("../AutocompleteModel", () => {
class AutocompleteModel {
public loaded = false
public profileName = "test-profile"
public async reload(): Promise<void> {
this.loaded = true
}
public getModelName(): string {
return "test-model"
}
@@ -169,7 +164,9 @@ async function createManager(): Promise<AutocompleteServiceManager> {
postStateToWebview: vi.fn().mockResolvedValue(undefined),
}
const manager = new AutocompleteServiceManager(context, cline as any)
const connection = { onStateChange: vi.fn().mockReturnValue(() => {}), ...cline }
const manager = new AutocompleteServiceManager(context, connection as any)
await manager.load()
@@ -15,10 +15,6 @@ export class ChatTextAreaAutocomplete {
this.telemetry = new AutocompleteTelemetry("chat-textarea")
}
async initialize(): Promise<boolean> {
return this.model.reload()
}
async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> {
const startTime = Date.now()
@@ -29,13 +25,6 @@ export class ChatTextAreaAutocomplete {
provider: this.model.getProviderDisplayName(),
}
if (!this.model.loaded) {
const loaded = await this.initialize()
if (!loaded) {
return { suggestion: "" }
}
}
// Check if model has valid credentials (but don't require FIM)
if (!this.model.hasValidCredentials()) {
return { suggestion: "" }
@@ -16,7 +16,6 @@ describe("ChatTextAreaAutocomplete", () => {
it("should work with non-FIM models using chat-based completion", async () => {
// Setup: Model without FIM support (like Mistral)
const mockModel = new AutocompleteModel()
mockModel.loaded = true
vi.spyOn(mockModel, "hasValidCredentials").mockReturnValue(true)
vi.spyOn(mockModel, "supportsFim").mockReturnValue(false)
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (معطل)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**لا يوجد رصيد في حسابك**\n\nحساب Kilo Code الخاص بك لا يحتوي على رصيد. لاستخدام الإكمال التلقائي، يرجى إضافة رصيد إلى حسابك.\n\n[فتح الإعدادات]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**لم يتم تكوين نموذج الإكمال التلقائي**\n\nلتمكين الإكمال التلقائي، أضف ملفًا شخصيًا مع أحد هذه المزودين المدعومين: {{providers}}.\n\n[فتح الإعدادات]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "إجمالي تكلفة الجلسة:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (desactivat)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**No tens crèdits al teu compte**\n\nEl teu compte de Kilo Code no té crèdits. Per utilitzar l'autocompletat, si us plau afegeix crèdits al teu compte.\n\n[Obrir Configuració]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No s'ha configurat cap model d'autocompletat**\n\nPer habilitar l'autocompletat, afegeix un perfil amb un d'aquests proveïdors compatibles: {{providers}}.\n\n[Obrir Configuració]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Cost total de la sessió:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (zakázáno)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Na tvém účtu nejsou žádné kredity**\n\nTvůj účet Kilo Code nemá žádné kredity. Pro použití automatického doplňování prosím přidej kredity na svůj účet.\n\n[Otevřít Nastavení]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Není nakonfigurován žádný model automatického doplňování**\n\nPro povolení automatického doplňování přidej profil s jedním z těchto podporovaných poskytovatelů: {{providers}}.\n\n[Otevřít Nastavení]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Celkové náklady relace:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deaktiviert)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Kein Guthaben auf deinem Konto**\n\nDein Kilo Code Konto hat kein Guthaben. Um Autocomplete zu nutzen, füge bitte Guthaben zu deinem Konto hinzu.\n\n[Einstellungen öffnen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Kein Autocomplete-Modell konfiguriert**\n\nUm Autocomplete zu aktivieren, füge ein Profil mit einem dieser unterstützten Anbieter hinzu: {{providers}}.\n\n[Einstellungen öffnen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Sitzungsgesamtkosten:",
@@ -7,8 +7,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (disabled)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**No credits in your account**\n\nYour Kilo Code account has no credits. To use autocomplete, please add credits to your account.\n\n[Open Settings]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No autocomplete model configured**\n\nTo enable autocomplete, add a profile with one of these supported providers: {{providers}}.\n\n[Open Settings]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Session total cost:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deshabilitado)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**No hay créditos en tu cuenta**\n\nTu cuenta de Kilo Code no tiene créditos. Para usar el autocompletado, por favor añade créditos a tu cuenta.\n\n[Abrir Configuración]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No hay modelo de autocompletado configurado**\n\nPara habilitar el autocompletado, añade un perfil con uno de estos proveedores compatibles: {{providers}}.\n\n[Abrir Configuración]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo total de la sesión:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (désactivé)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Pas de crédits sur ton compte**\n\nTon compte Kilo Code n'a pas de crédits. Pour utiliser l'autocomplétion, ajoute des crédits à ton compte.\n\n[Ouvrir les Paramètres]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Aucun modèle d'autocomplétion configuré**\n\nPour activer l'autocomplétion, ajoute un profil avec l'un de ces fournisseurs pris en charge : {{providers}}.\n\n[Ouvrir les Paramètres]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Coût total de la session :",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (अक्षम)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**आपके खाते में कोई क्रेडिट नहीं है**\n\nआपके Kilo Code खाते में कोई क्रेडिट नहीं है। ऑटोकम्प्लीट का उपयोग करने के लिए, कृपया अपने खाते में क्रेडिट जोड़ें।\n\n[सेटिंग्स खोलें]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**कोई ऑटोकम्प्लीट मॉडल कॉन्फ़िगर नहीं किया गया**\n\nऑटोकम्प्लीट सक्षम करने के लिए, इन समर्थित प्रदाताओं में से एक के साथ एक प्रोफ़ाइल जोड़ें: {{providers}}।\n\n[सेटिंग्स खोलें]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "सत्र की कुल लागत:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (dinonaktifkan)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Tidak ada kredit di akun kamu**\n\nAkun Kilo Code kamu tidak memiliki kredit. Untuk menggunakan autocomplete, silakan tambahkan kredit ke akun kamu.\n\n[Buka Pengaturan]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Tidak ada model autocomplete yang dikonfigurasi**\n\nUntuk mengaktifkan autocomplete, tambahkan profil dengan salah satu penyedia yang didukung ini: {{providers}}.\n\n[Buka Pengaturan]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Total biaya sesi:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (disabilitato)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Nessun credito nel tuo account**\n\nIl tuo account Kilo Code non ha crediti. Per usare l'autocompletamento, aggiungi crediti al tuo account.\n\n[Apri Impostazioni]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nessun modello di autocompletamento configurato**\n\nPer abilitare l'autocompletamento, aggiungi un profilo con uno di questi provider supportati: {{providers}}.\n\n[Apri Impostazioni]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo totale della sessione:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(無効)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**アカウントにクレジットがありません**\n\nKilo Code アカウントにクレジットがありません。オートコンプリートを使用するには、アカウントにクレジットを追加してください。\n\n[設定を開く]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**オートコンプリートモデルが設定されていません**\n\nオートコンプリートを有効にするには、これらのサポートされているプロバイダーのいずれかでプロファイルを追加してください: {{providers}}。\n\n[設定を開く]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "セッション合計コスト:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (비활성화됨)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**계정에 크레딧이 없습니다**\n\nKilo Code 계정에 크레딧이 없습니다. 자동완성을 사용하려면 계정에 크레딧을 추가해 주세요.\n\n[설정 열기]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**자동완성 모델이 구성되지 않았습니다**\n\n자동완성을 활성화하려면 다음 지원되는 제공업체 중 하나로 프로필을 추가하세요: {{providers}}.\n\n[설정 열기]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "세션 총 비용:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (uitgeschakeld)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Geen tegoed op je account**\n\nJe Kilo Code account heeft geen tegoed. Om autocomplete te gebruiken, voeg tegoed toe aan je account.\n\n[Instellingen openen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Geen autocomplete model geconfigureerd**\n\nOm autocomplete in te schakelen, voeg een profiel toe met een van deze ondersteunde providers: {{providers}}.\n\n[Instellingen openen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Totale sessiekosten:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (wyłączone)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Brak środków na koncie**\n\nTwoje konto Kilo Code nie ma środków. Aby korzystać z autouzupełniania, dodaj środki do swojego konta.\n\n[Otwórz Ustawienia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nie skonfigurowano modelu autouzupełniania**\n\nAby włączyć autouzupełnianie, dodaj profil z jednym z tych obsługiwanych dostawców: {{providers}}.\n\n[Otwórz Ustawienia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Całkowity koszt sesji:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (desabilitado)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Sem créditos na sua conta**\n\nSua conta Kilo Code não tem créditos. Para usar o autocompletar, adicione créditos à sua conta.\n\n[Abrir Configurações]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nenhum modelo de autocompletar configurado**\n\nPara habilitar o autocompletar, adicione um perfil com um destes provedores suportados: {{providers}}.\n\n[Abrir Configurações]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Custo total da sessão:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (отключено)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**На вашем счёте нет кредитов**\n\nНа вашем счёте Kilo Code нет кредитов. Чтобы использовать автодополнение, пожалуйста, пополните счёт.\n\n[Открыть Настройки]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Модель автодополнения не настроена**\n\nЧтобы включить автодополнение, добавьте профиль с одним из поддерживаемых провайдеров: {{providers}}.\n\n[Открыть Настройки]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Общая стоимость сессии:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (zakázané)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Na tvojom účte nie sú žiadne kredity**\n\nTvoj účet Kilo Code nemá žiadne kredity. Pre použitie automatického doplňovania prosím pridaj kredity na svoj účet.\n\n[Otvoriť Nastavenia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nie je nakonfigurovaný žiadny model automatického doplňovania**\n\nPre povolenie automatického doplňovania pridaj profil s jedným z týchto podporovaných poskytovateľov: {{providers}}.\n\n[Otvoriť Nastavenia]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Celkové náklady relácie:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (ปิดใช้งาน)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**ไม่มีเครดิตในบัญชีของคุณ**\n\nบัญชี Kilo Code ของคุณไม่มีเครดิต หากต้องการใช้การเติมข้อความอัตโนมัติ กรุณาเพิ่มเครดิตในบัญชีของคุณ\n\n[เปิดการตั้งค่า]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**ไม่ได้กำหนดค่าโมเดลการเติมข้อความอัตโนมัติ**\n\nหากต้องการเปิดใช้งานการเติมข้อความอัตโนมัติ ให้เพิ่มโปรไฟล์กับผู้ให้บริการที่รองรับเหล่านี้: {{providers}}\n\n[เปิดการตั้งค่า]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "ค่าใช้จ่ายรวมของเซสชัน:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (devre dışı)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Hesabınızda kredi yok**\n\nKilo Code hesabınızda kredi bulunmuyor. Otomatik tamamlamayı kullanmak için lütfen hesabınıza kredi ekleyin.\n\n[Ayarları Aç]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Otomatik tamamlama modeli yapılandırılmadı**\n\nOtomatik tamamlamayı etkinleştirmek için desteklenen sağlayıcılardan biriyle bir profil ekleyin: {{providers}}.\n\n[Ayarları Aç]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Oturum toplam maliyeti:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (вимкнено)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**На вашому рахунку немає кредитів**\n\nНа вашому рахунку Kilo Code немає кредитів. Щоб використовувати автодоповнення, будь ласка, поповніть рахунок.\n\n[Відкрити Налаштування]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Модель автодоповнення не налаштована**\n\nЩоб увімкнути автодоповнення, додайте профіль з одним із підтримуваних провайдерів: {{providers}}.\n\n[Відкрити Налаштування]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Загальна вартість сесії:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (đã tắt)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**Không có tín dụng trong tài khoản của bạn**\n\nTài khoản Kilo Code của bạn không có tín dụng. Để sử dụng tự động hoàn thành, vui lòng thêm tín dụng vào tài khoản của bạn.\n\n[Mở Cài đặt]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Chưa cấu hình mô hình tự động hoàn thành**\n\nĐể bật tự động hoàn thành, hãy thêm hồ sơ với một trong các nhà cung cấp được hỗ trợ sau: {{providers}}.\n\n[Mở Cài đặt]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Tổng chi phí phiên:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(已禁用)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**账户余额不足**\n\n你的 Kilo Code 账户没有余额。要使用自动补全功能,请为账户充值。\n\n[打开设置]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**未配置自动补全模型**\n\n要启用自动补全,请添加一个使用以下支持的提供商的配置文件:{{providers}}。\n\n[打开设置]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "会话总费用:",
@@ -6,8 +6,6 @@ export const dict = {
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(已停用)",
"kilocode:autocomplete.statusBar.tooltip.noCredits":
"**帳戶中沒有額度**\n\nKilo Code 帳戶沒有額度。若要使用 Autocomplete,請為帳戶儲值。\n\n[開啟設定]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**未設定 Autocomplete 模型**\n\n若要啟用 Autocomplete,請新增一個使用以下支援供應商的設定檔:{{providers}}。\n\n[開啟設定]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "工作階段總費用:",
@@ -137,7 +137,6 @@ export interface AutocompleteStatusBarStateProps {
model?: string
provider?: string
profileName?: string | null
hasKilocodeProfileWithNoBalance?: boolean
hasNoUsableProvider?: boolean
totalSessionCost: number
completionCount: number
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ba76e2d5b993e999914fd0538650728adf78f7ad70978d8ee9982525f6ab5e8
size 17626
oid sha256:321afc41e5908e73656b1a8d5e3c32f23f54d10dee12655506b4b58348230305
size 13718
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29872fa36a6a6ba9e20765f92380b86f9a91beb9702b3cdb95914ea46a2cf40c
size 17833
oid sha256:666d73c69b19e8c79815129b9d17747dbf755c316f3e6da0cba6977966b9cccc
size 13906
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce915e449cdec411c00df613ffc1017f95dba69737e0474fc8c483ea9f284026
size 17872
oid sha256:b0608142ccb5bab0468ae44060bed17c5ba98b8ee2d5a477627644d1c91e760a
size 13952
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b39675360da34fc6cb9c1ff4e51afeff79417ca4baa88816967d2b0a4ece858f
size 18043
oid sha256:7dd67617fdfc9ea6c4b0f54895d930d76c5c712b158e4be93967a3a3239a53b2
size 14143
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c3b7d96f123e880b1395d160d17a48f385ef59344326c44d7cd13b342d2ca49e
size 24193
oid sha256:43b16bc81164f310ae40f7ebe6459281f374be323dfc07e3bef6b2d1608b6637
size 20835
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:739b8eb94e5cde39e4127a6ed3c499392174a380812e2e3497965643cac3dee9
size 22265
oid sha256:ee48a78cd2fe43c34d0014262ac794e48e5405d2116140d08219fc8e1621be0c
size 18516
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:62ef30b05bf2ec47a7ddd02a03f5f1cded5ced9cf9332dd3bd50d6809bcce96d
size 18447
oid sha256:f69b6545bd98003edf8c7208eb655066a24847e055cf0a5492c99ff67bbbab38
size 14621
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f9864861136b578c4e46ece7dae5a139ada7e7be3d219462ef3a549b94239ec
size 18648
oid sha256:5a7db3c77ca7dee3a74fe3fd08b45e8d32a00193d1fb13ec7b4e2b1671dac7a3
size 14831
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:514803fa46f43ff58d8d403e630afc5c01a8841ac420d406755515bceb38beae
size 23132
oid sha256:c4aa38c3ade48132b3f0083438ef71c0e34f180db3bbd4d70455d6bb34a1225c
size 19202
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:578f6daa54a25fba38b049d623e2b12069b0cbe1c5e094031d8a0b0af0a56f52
size 23966
oid sha256:e46b79adada26121c948ce33b78ba78c8b0b13dc153708b60a56f1cfc6034503
size 20095
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91ba11c0a81ced5dfe824a4c9e8fdcbaebaaddc660ef2b245df56903e180e4b7
size 18949
oid sha256:699b49cbb768c3a9cea64878a01b5507a1f304a14f12363f2329ad07c8cc35cb
size 15082
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f8bc26191620881314eff9ac921abba4e201c5af7ec8d222e405fdee9ddae9a
size 17909
oid sha256:60e6d4b1d0e860195c26752e50a97e6d22d7911d029e7d08d24f46b327b21a01
size 14075
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce112e9762168271fd8b7b4316b740008269e9d248a668887adce3dde13478f0
size 18362
oid sha256:4ff80ac6cd76dbb278e6483c2e5c352095fc893ae5f5d5cb2eb70ded640bfc0e
size 16913
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d499d73467c37052d1277deefdd419e370a8239d7e117806d8442b2b9cc851a7
size 18886
oid sha256:6531e4d0e975a29d538bc7b609a987071afc0997b396fd06e52cbbf2b7ef2e26
size 17467
@@ -0,0 +1,407 @@
/**
* Translation key validation
*
* 1. Ensures every string-literal translation key passed to a t() function
* actually exists in the corresponding English dictionary.
* 2. Ensures every English key has a translation in all other locale files.
*
* Three independent key pools are checked:
* - Webview (sidebar + agent manager): merged from app, ui, kilo-i18n, agent-manager dicts
* - CLI backend (extension-side server-manager): cli-backend/i18n dict
* - Autocomplete (extension-side): autocomplete/i18n dict
*
* Dynamic keys (template literals, variables) are intentionally skipped —
* only string literals are validated.
*/
import { describe, it, expect } from "bun:test"
import { Glob } from "bun"
import path from "node:path"
// ── Webview dictionaries ────────────────────────────────────────────────────
// Layer 1: app-local (sidebar)
import { dict as appEn } from "../../webview-ui/src/i18n/en"
import { dict as appZh } from "../../webview-ui/src/i18n/zh"
import { dict as appZht } from "../../webview-ui/src/i18n/zht"
import { dict as appKo } from "../../webview-ui/src/i18n/ko"
import { dict as appDe } from "../../webview-ui/src/i18n/de"
import { dict as appEs } from "../../webview-ui/src/i18n/es"
import { dict as appFr } from "../../webview-ui/src/i18n/fr"
import { dict as appDa } from "../../webview-ui/src/i18n/da"
import { dict as appJa } from "../../webview-ui/src/i18n/ja"
import { dict as appPl } from "../../webview-ui/src/i18n/pl"
import { dict as appRu } from "../../webview-ui/src/i18n/ru"
import { dict as appAr } from "../../webview-ui/src/i18n/ar"
import { dict as appNo } from "../../webview-ui/src/i18n/no"
import { dict as appBr } from "../../webview-ui/src/i18n/br"
import { dict as appTh } from "../../webview-ui/src/i18n/th"
import { dict as appBs } from "../../webview-ui/src/i18n/bs"
// Layer 2: upstream UI (@opencode-ai/ui re-exported via @kilocode/kilo-ui)
import { dict as uiEn } from "../../../ui/src/i18n/en"
import { dict as uiZh } from "../../../ui/src/i18n/zh"
import { dict as uiZht } from "../../../ui/src/i18n/zht"
import { dict as uiKo } from "../../../ui/src/i18n/ko"
import { dict as uiDe } from "../../../ui/src/i18n/de"
import { dict as uiEs } from "../../../ui/src/i18n/es"
import { dict as uiFr } from "../../../ui/src/i18n/fr"
import { dict as uiDa } from "../../../ui/src/i18n/da"
import { dict as uiJa } from "../../../ui/src/i18n/ja"
import { dict as uiPl } from "../../../ui/src/i18n/pl"
import { dict as uiRu } from "../../../ui/src/i18n/ru"
import { dict as uiAr } from "../../../ui/src/i18n/ar"
import { dict as uiNo } from "../../../ui/src/i18n/no"
import { dict as uiBr } from "../../../ui/src/i18n/br"
import { dict as uiTh } from "../../../ui/src/i18n/th"
import { dict as uiBs } from "../../../ui/src/i18n/bs"
// Layer 3: kilo-i18n overrides
import { dict as kiloEn } from "../../../kilo-i18n/src/en"
import { dict as kiloZh } from "../../../kilo-i18n/src/zh"
import { dict as kiloZht } from "../../../kilo-i18n/src/zht"
import { dict as kiloKo } from "../../../kilo-i18n/src/ko"
import { dict as kiloDe } from "../../../kilo-i18n/src/de"
import { dict as kiloEs } from "../../../kilo-i18n/src/es"
import { dict as kiloFr } from "../../../kilo-i18n/src/fr"
import { dict as kiloDa } from "../../../kilo-i18n/src/da"
import { dict as kiloJa } from "../../../kilo-i18n/src/ja"
import { dict as kiloPl } from "../../../kilo-i18n/src/pl"
import { dict as kiloRu } from "../../../kilo-i18n/src/ru"
import { dict as kiloAr } from "../../../kilo-i18n/src/ar"
import { dict as kiloNo } from "../../../kilo-i18n/src/no"
import { dict as kiloBr } from "../../../kilo-i18n/src/br"
import { dict as kiloTh } from "../../../kilo-i18n/src/th"
import { dict as kiloBs } from "../../../kilo-i18n/src/bs"
// Layer 4: agent manager (locale alignment already tested in agent-manager-i18n-split.test.ts)
import { dict as amEn } from "../../webview-ui/agent-manager/i18n/en"
// ── Extension-side dictionaries ─────────────────────────────────────────────
import { dict as cliEn } from "../../src/services/cli-backend/i18n/en"
import { dict as cliZh } from "../../src/services/cli-backend/i18n/zh"
import { dict as cliZht } from "../../src/services/cli-backend/i18n/zht"
import { dict as cliDe } from "../../src/services/cli-backend/i18n/de"
import { dict as cliEs } from "../../src/services/cli-backend/i18n/es"
import { dict as cliFr } from "../../src/services/cli-backend/i18n/fr"
import { dict as cliDa } from "../../src/services/cli-backend/i18n/da"
import { dict as cliJa } from "../../src/services/cli-backend/i18n/ja"
import { dict as cliKo } from "../../src/services/cli-backend/i18n/ko"
import { dict as cliPl } from "../../src/services/cli-backend/i18n/pl"
import { dict as cliRu } from "../../src/services/cli-backend/i18n/ru"
import { dict as cliAr } from "../../src/services/cli-backend/i18n/ar"
import { dict as cliNo } from "../../src/services/cli-backend/i18n/no"
import { dict as cliBr } from "../../src/services/cli-backend/i18n/br"
import { dict as cliTh } from "../../src/services/cli-backend/i18n/th"
import { dict as cliBs } from "../../src/services/cli-backend/i18n/bs"
import { dict as acEn } from "../../src/services/autocomplete/i18n/en"
// ── Locale maps ─────────────────────────────────────────────────────────────
const ROOT = path.resolve(import.meta.dir, "../..")
const appLocales: Record<string, Record<string, string>> = {
en: appEn,
zh: appZh,
zht: appZht,
ko: appKo,
de: appDe,
es: appEs,
fr: appFr,
da: appDa,
ja: appJa,
pl: appPl,
ru: appRu,
ar: appAr,
no: appNo,
br: appBr,
th: appTh,
bs: appBs,
}
const kiloLocales: Record<string, Record<string, string>> = {
en: kiloEn,
zh: kiloZh,
zht: kiloZht,
ko: kiloKo,
de: kiloDe,
es: kiloEs,
fr: kiloFr,
da: kiloDa,
ja: kiloJa,
pl: kiloPl,
ru: kiloRu,
ar: kiloAr,
no: kiloNo,
br: kiloBr,
th: kiloTh,
bs: kiloBs,
}
const uiLocales: Record<string, Record<string, string>> = {
en: uiEn,
zh: uiZh,
zht: uiZht,
ko: uiKo,
de: uiDe,
es: uiEs,
fr: uiFr,
da: uiDa,
ja: uiJa,
pl: uiPl,
ru: uiRu,
ar: uiAr,
no: uiNo,
br: uiBr,
th: uiTh,
bs: uiBs,
}
const cliLocales: Record<string, Record<string, string>> = {
en: cliEn,
zh: cliZh,
zht: cliZht,
de: cliDe,
es: cliEs,
fr: cliFr,
da: cliDa,
ja: cliJa,
ko: cliKo,
pl: cliPl,
ru: cliRu,
ar: cliAr,
no: cliNo,
br: cliBr,
th: cliTh,
bs: cliBs,
}
// Merge webview dictionaries in the same priority order as language.tsx
const webviewKeys = new Set(Object.keys({ ...appEn, ...uiEn, ...kiloEn, ...amEn }))
const cliKeys = new Set(Object.keys(cliEn))
const acKeys = new Set(Object.keys(acEn))
// ── File scanning ───────────────────────────────────────────────────────────
interface Missing {
file: string
line: number
key: string
}
/**
* Regex to match t("key") / t('key') calls, including multiline.
*
* The `s` (dotAll) flag lets `\s*` span newlines so that calls like:
* t(
* "key"
* )
* are captured. Runs against the entire file content, not line-by-line.
*/
const T_CALL = /(?:^|[^a-zA-Z_$])t\(\s*(?:["'])([^"']+)["']/gs
function offsetToLine(content: string, offset: number): number {
let line = 1
for (let i = 0; i < offset && i < content.length; i++) {
if (content[i] === "\n") line++
}
return line
}
function extractKeys(content: string): Array<{ line: number; key: string }> {
const results: Array<{ line: number; key: string }> = []
let match: RegExpExecArray | null
T_CALL.lastIndex = 0
while ((match = T_CALL.exec(content)) !== null) {
const key = match[1]
if (key) results.push({ line: offsetToLine(content, match.index), key })
}
return results
}
async function collectFiles(glob: Glob, dir: string): Promise<string[]> {
const files: string[] = []
for await (const entry of glob.scan({ cwd: dir, absolute: true })) {
files.push(entry)
}
return files
}
// ── Webview files ───────────────────────────────────────────────────────────
async function findWebviewMissing(): Promise<Missing[]> {
const glob = new Glob("**/*.{ts,tsx}")
const dirs = [path.join(ROOT, "webview-ui/src"), path.join(ROOT, "webview-ui/agent-manager")]
const files: string[] = []
for (const dir of dirs) {
files.push(...(await collectFiles(glob, dir)))
}
// Exclude i18n dictionary files themselves and storybook files
const filtered = files.filter((f) => !f.includes("/i18n/") && !f.includes(".stories.") && !f.includes("/stories/"))
const missing: Missing[] = []
for (const file of filtered) {
const content = await Bun.file(file).text()
for (const { line, key } of extractKeys(content)) {
if (!webviewKeys.has(key)) {
missing.push({ file: path.relative(ROOT, file), line, key })
}
}
}
return missing
}
// ── CLI backend files ───────────────────────────────────────────────────────
async function findCliBackendMissing(): Promise<Missing[]> {
const glob = new Glob("**/*.ts")
const dir = path.join(ROOT, "src/services/cli-backend")
const files = (await collectFiles(glob, dir)).filter((f) => !f.includes("/i18n/"))
const missing: Missing[] = []
for (const file of files) {
const content = await Bun.file(file).text()
for (const { line, key } of extractKeys(content)) {
if (!cliKeys.has(key)) {
missing.push({ file: path.relative(ROOT, file), line, key })
}
}
}
return missing
}
// ── Autocomplete files ──────────────────────────────────────────────────────
async function findAutocompleteMissing(): Promise<Missing[]> {
const glob = new Glob("**/*.ts")
const dir = path.join(ROOT, "src/services/autocomplete")
const files = (await collectFiles(glob, dir)).filter((f) => !f.includes("/i18n/") && !f.includes("/shims/"))
const missing: Missing[] = []
for (const file of files) {
const content = await Bun.file(file).text()
for (const { line, key } of extractKeys(content)) {
if (!acKeys.has(key)) {
missing.push({ file: path.relative(ROOT, file), line, key })
}
}
}
return missing
}
// ── Locale completeness helpers ─────────────────────────────────────────────
function findMissingLocaleKeys(
en: Record<string, string>,
locales: Record<string, Record<string, string>>,
): Array<{ locale: string; key: string }> {
const base = Object.keys(en)
const results: Array<{ locale: string; key: string }> = []
for (const [locale, dict] of Object.entries(locales)) {
if (locale === "en") continue
const keys = new Set(Object.keys(dict))
for (const key of base) {
if (!keys.has(key)) {
results.push({ locale, key })
}
}
}
return results
}
function formatLocaleReport(items: Array<{ locale: string; key: string }>): string {
return items.map((m) => ` [${m.locale}] "${m.key}"`).join("\n")
}
// ── Tests ───────────────────────────────────────────────────────────────────
function formatReport(missing: Missing[]): string {
return missing.map((m) => ` ${m.file}:${m.line} — "${m.key}"`).join("\n")
}
describe("i18n key validation — no missing translation keys", () => {
it("webview: all t() string literal keys exist in merged dictionaries", async () => {
const missing = await findWebviewMissing()
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} translation key(s) not present in any dictionary:\n${formatReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("cli-backend: all t() string literal keys exist in cli-backend dictionary", async () => {
const missing = await findCliBackendMissing()
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} translation key(s) not present in cli-backend dictionary:\n${formatReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("autocomplete: all t() string literal keys exist in autocomplete dictionary", async () => {
const missing = await findAutocompleteMissing()
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} translation key(s) not present in autocomplete dictionary:\n${formatReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
})
describe("i18n locale completeness — every English key exists in all locales", () => {
it("shared UI: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(uiEn, uiLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing shared UI translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("sidebar app: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(appEn, appLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing sidebar translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("kilo-i18n: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(kiloEn, kiloLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing kilo-i18n translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
it("cli-backend: every English key has a translation in all locales", () => {
const missing = findMissingLocaleKeys(cliEn, cliLocales)
if (missing.length > 0) {
expect(
missing,
`Found ${missing.length} missing cli-backend translation(s):\n${formatLocaleReport(missing)}`,
).toEqual([])
}
expect(missing).toEqual([])
})
})
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3656d23c630a2d7f412d2fc3eee231f45cbe47e6d1e4e7ca44ae4cca3c3160b
size 15323
oid sha256:cb8dd7f0ecbbeed00e1321c050964b54f832a571c8acbc37269de205bd64a483
size 13788
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c95ebadbd707aab03b0b14858cd9e25c4e7b06e8976abf76e377b9eaa5bd2144
size 16900
oid sha256:484ec50824ececf29d8b1464fb94ef48637d4f97b9a3c68dcf3ae5d8f157dcf6
size 15502
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1c43a0a2f6a351d53da542aba6b68113e8231c1ee9cf7276bf6a16263b65d811
size 18775
oid sha256:ab9edfc18d0d7094f7c5410d7b08e0012c2128f1638b7090aad9301f127473ed
size 17425
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d23b2a1621e33e92e65c7ead23b42f05decd443b370ef7122b840e6f5cf043e1
size 21177
oid sha256:05783f1a5b4c8631dee2582b243a724eb4fb811cde894f86ce3575e2da1a997e
size 17416
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63b8651967a042ef6e79f721fc4bf99eae875296dbd991566d3c3b7e90f04669
size 18542
oid sha256:24364efcbb54ed842a9af0c630e6b2a4f5df37d8cabb651e18dcbebda37a26c5
size 17221
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94ef29b3eca3b0e9e407daf9c17388dcf5735c134a74bc13500559bcf9707c9b
size 16338
oid sha256:e8e117613ac4378e82739c266c2e2f818e9ed79bc0e84e0ebe7b8f61c02b88c0
size 14836
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:168eacdc6523223587bad5f72f3828e03211733b4dc1fa39f152409089e03ff3
size 16468
oid sha256:2259e821d911a5455a4bdd72716d5ae1de1a5fd5b6aaad666d41846edac716cc
size 15079
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c2402fb61b05c355cedea5a26c39f4a5b4d4a1e04c0fb8ac1ac50edbdc746b3
size 16426
oid sha256:3fc2414e502c0f064fc8d15e5f14ccdc5c250ded619b8362472d2a41fb522de9
size 15023
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38712f08584902b1ccb267fed0271c6c1a9b5f549b47bd743ca344604df2095a
size 17433
oid sha256:acb0efba94927a8ada6a16a66bd82160a1f41d320a2f512114da54aaa2cc1574
size 16143
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84e3f1c43bd36828f6548f3a3a85827693b370f6f3fc319408829eb742146bb5
size 19287
oid sha256:c21917a9f4f80354ac18a7c4ced1f204a1b28bed4ac5fb0cf34e339dbdc7d5da
size 15929
@@ -134,7 +134,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
</div>
</Show>
<Show when={!props.readonly}>
<PromptInput />
<PromptInput blocked={blocked} />
</Show>
</div>
</Show>
@@ -20,6 +20,7 @@ import { formatRelativeDate } from "../../utils/date"
import { CloudImportDialog } from "./CloudImportDialog"
import { FeedbackDialog } from "./FeedbackDialog"
import { VscodeSessionTurn } from "./VscodeSessionTurn"
import { RevertBanner } from "./RevertBanner"
import { WorkingIndicator } from "../shared/WorkingIndicator"
import { activeUserMessageID as getActiveUserMessageID } from "../../context/session-queue"
@@ -64,8 +65,14 @@ export const MessageList: Component<MessageListProps> = (props) => {
}
})
const userMessages = () => session.userMessages()
const isEmpty = () => userMessages().length === 0 && !session.loading()
const allUserMessages = () => session.userMessages()
const boundary = () => session.revert()?.messageID
const userMessages = createMemo(() => {
const b = boundary()
if (!b) return allUserMessages()
return allUserMessages().filter((m) => m.id < b)
})
const isEmpty = () => userMessages().length === 0 && !session.loading() && !boundary()
const recent = createMemo(() =>
[...session.sessions()]
@@ -153,6 +160,9 @@ export const MessageList: Component<MessageListProps> = (props) => {
)
}}
</For>
<Show when={boundary()}>
<RevertBanner />
</Show>
<WorkingIndicator />
</Show>
</div>
@@ -179,7 +179,6 @@ export const PermissionDock: Component<{
<div data-slot="permission-hint">{toolDescription()}</div>
</Show>
<p data-slot="permission-session-hint">{language.t("ui.permission.sessionHint")}</p>
<div data-slot="permission-actions">
<Button
variant="primary"
@@ -41,7 +41,11 @@ function mergeReviewComments(current: ReviewComment[], incoming: ReviewComment[]
return [...map.values()]
}
export const PromptInput: Component = () => {
interface PromptInputProps {
blocked?: () => boolean
}
export const PromptInput: Component<PromptInputProps> = (props) => {
const session = useSession()
const server = useServer()
const language = useLanguage()
@@ -172,7 +176,7 @@ export const PromptInput: Component = () => {
const isBusy = () => session.status() === "busy"
const isDisabled = () => !server.isConnected()
const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0
const canSend = () => hasInput() && !isDisabled()
const canSend = () => hasInput() && !isDisabled() && !props.blocked?.()
const showStop = () => isBusy() && !hasInput()
const placeholder = () => {
switch (server.connectionState()) {
@@ -450,7 +454,7 @@ export const PromptInput: Component = () => {
const pending = reviewComments()
const review = pending.length > 0 ? formatReviewCommentsMarkdown(pending) : ""
const message = draft && review ? `${review}\n\n${draft}` : draft || review
if ((!message && imgs.length === 0) || isDisabled()) return
if ((!message && imgs.length === 0) || isDisabled() || props.blocked?.()) return
const mentionFiles = mention.parseFileAttachments(draft)
const imgFiles = imgs.map((img) => ({ mime: img.mime, url: img.dataUrl, filename: img.filename }))
@@ -639,7 +643,10 @@ export const PromptInput: Component = () => {
<Show
when={showStop()}
fallback={
<Tooltip value={language.t("prompt.action.send")} placement="top">
<Tooltip
value={props.blocked?.() ? language.t("prompt.action.send.blocked") : language.t("prompt.action.send")}
placement="top"
>
<Button
variant="ghost"
size="small"
@@ -0,0 +1,74 @@
/**
* RevertBanner component
* Shows when the session is in a reverted state, displaying the number of
* reverted messages, per-file diff stats, and Redo / Redo All actions.
*/
import { Component, For, Show } from "solid-js"
import { Button } from "@kilocode/kilo-ui/button"
import { Icon } from "@kilocode/kilo-ui/icon"
import { DiffChanges } from "@kilocode/kilo-ui/diff-changes"
import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"
export const RevertBanner: Component = () => {
const session = useSession()
const language = useLanguage()
const info = () => session.revert()
const count = () => session.revertedCount()
const diffs = () => session.summary()?.diffs
const users = () => session.userMessages()
const handleRedo = () => {
const boundary = info()?.messageID
if (!boundary) return
const next = users().find((m) => m.id > boundary)
if (!next) {
session.unrevertSession()
return
}
session.revertSession(next.id)
}
return (
<Show when={info()}>
<div class="revert-banner">
<div class="revert-banner-header">
<div class="revert-banner-info">
<Icon name="arrow-left" size="small" />
<span class="revert-banner-count">
{count() === 1
? language.t("revert.banner.count_one", { count: count() })
: language.t("revert.banner.count_other", { count: count() })}
</span>
</div>
<div class="revert-banner-actions">
<Button variant="ghost" size="small" onClick={handleRedo}>
{language.t("revert.banner.redo")}
</Button>
<Show when={count() > 1}>
<Button variant="ghost" size="small" onClick={() => session.unrevertSession()}>
{language.t("revert.banner.redo.all")}
</Button>
</Show>
</div>
</div>
<Show when={diffs()?.length}>
<div class="revert-banner-files">
<For each={diffs()!}>
{(file) => (
<div class="revert-banner-file">
<span class="revert-banner-filename">{file.file}</span>
<DiffChanges changes={file} />
</div>
)}
</For>
</div>
</Show>
<div class="revert-banner-hint">{language.t("revert.banner.hint")}</div>
</div>
</Show>
)
}
@@ -29,6 +29,7 @@ import type {
} from "@kilocode/sdk/v2"
import { ErrorDisplay } from "./ErrorDisplay"
import { useServer } from "../../context/server"
import { useSession } from "../../context/session"
function getDirectory(path: string): string {
const sep = path.includes("/") ? "/" : "\\"
@@ -53,6 +54,7 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
const i18n = useI18n()
const diffComponent = useDiffComponent()
const server = useServer()
const session = useSession()
const emptyMessages: SDKMessage[] = []
const emptyParts: SDKPart[] = []
@@ -168,6 +170,11 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
parts={parts() as unknown as Parameters<typeof UserMessageDisplay>[0]["parts"]}
interrupted={interrupted()}
queued={props.queued}
onRevert={
assistantMessages().length > 0 && session.status() === "idle" && !session.revert()
? () => session.revertSession(props.messageID)
: undefined
}
/>
</div>
@@ -135,7 +135,14 @@ interface SessionContextValue {
currentVariant: () => string | undefined
selectVariant: (value: string) => void
// Revert/undo state for the current session
revert: Accessor<SessionInfo["revert"]>
revertedCount: Accessor<number>
summary: Accessor<SessionInfo["summary"]>
// Actions
revertSession: (messageID: string) => void
unrevertSession: () => void
sendMessage: (text: string, providerID?: string, modelID?: string, files?: FileAttachment[]) => void
abort: () => void
compact: () => void
@@ -493,6 +500,10 @@ export const SessionProvider: ParentComponent = (props) => {
handleSessionDeleted(message.sessionID)
break
case "messageRemoved":
handleMessageRemoved(message.sessionID, message.messageID)
break
case "error":
// Only clear loading if the error is for the current session
// (or has no sessionID for backwards compatibility)
@@ -903,6 +914,18 @@ export const SessionProvider: ParentComponent = (props) => {
})
}
// Matches desktop app's event-reducer.ts: message.removed handler.
// Splices the message from the store and deletes its parts.
function handleMessageRemoved(sessionID: string, messageID: string) {
setStore("messages", sessionID, (msgs = []) => msgs.filter((m) => m.id !== messageID))
setStore(
"parts",
produce((parts) => {
delete parts[messageID]
}),
)
}
function handleCloudSessionDataLoaded(cloudSessionId: string, title: string, messages: Message[]) {
if (cloudPreviewId() !== cloudSessionId) return
const key = `cloud:${cloudSessionId}`
@@ -1262,6 +1285,47 @@ export const SessionProvider: ParentComponent = (props) => {
const userMessages = createMemo(() => messages().filter((m) => m.role === "user"))
const revert = createMemo(() => {
const id = currentSessionID()
// revert can be null (cleared by unrevert) or undefined (never set) — treat both as "no revert"
return id ? (store.sessions[id]?.revert ?? undefined) : undefined
})
const revertedCount = createMemo(() => {
const boundary = revert()?.messageID
if (!boundary) return 0
return userMessages().filter((m) => m.id >= boundary).length
})
const summary = createMemo(() => {
const id = currentSessionID()
return id ? (store.sessions[id]?.summary ?? undefined) : undefined
})
function revertSession(messageID: string) {
const id = currentSessionID()
if (!id) return
// Restore the reverted user message's prompt text into the input.
// Dispatch as a window message so PromptInput picks it up via onMessage.
const parts = store.parts[messageID]
if (parts) {
const text = parts
.filter((p) => p.type === "text" && !(p as { synthetic?: boolean }).synthetic)
.map((p) => (p as { text: string }).text ?? "")
.join("")
if (text) window.postMessage({ type: "setChatBoxMessage", text }, "*")
}
vscode.postMessage({ type: "revertSession", sessionID: id, messageID })
}
function unrevertSession() {
const id = currentSessionID()
if (!id) return
// Clear the prompt input on full redo (matching TUI/desktop behavior)
window.postMessage({ type: "setChatBoxMessage", text: "" }, "*")
vscode.postMessage({ type: "unrevertSession", sessionID: id })
}
function syncSession(sessionID: string) {
vscode.postMessage({ type: "syncSession", sessionID })
}
@@ -1360,6 +1424,11 @@ export const SessionProvider: ParentComponent = (props) => {
variantList,
currentVariant,
selectVariant,
revert,
revertedCount,
summary,
revertSession,
unrevertSession,
sendMessage,
abort,
compact,
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "فتح الإعدادات",
"command.session.previous": "الجلسة السابقة",
"command.session.next": "الجلسة التالية",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "الجلسة غير المقروءة السابقة",
"command.session.next.unseen": "الجلسة غير المقروءة التالية",
"command.session.archive": "أرشفة الجلسة",
"command.palette": "لوحة الأوامر",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "تراجع عن الرسالة الأخيرة",
"command.session.redo": "إعادة",
"command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "ضغط الجلسة",
"command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق",
"command.session.fork": "تشعب من الرسالة",
@@ -230,6 +238,7 @@ export const dict = {
"prompt.action.attachFile": "إرفاق ملف",
"prompt.attachment.remove": "إزالة المرفق",
"prompt.action.send": "إرسال",
"prompt.action.send.blocked": "أجب عن السؤال المعلق أو تجاهله أولاً",
"prompt.action.stop": "توقف",
"prompt.action.enhance": "تحسين النص",
"prompt.action.resetModel": "إعادة تعيين النموذج إلى الافتراضي",
@@ -681,6 +690,9 @@ export const dict = {
"settings.providers.tag.config": "التكوين",
"settings.providers.tag.custom": "مخصص",
"settings.providers.tag.other": "أخرى",
"settings.providers.modeModels": "نموذج لكل وضع",
"settings.providers.modeModels.description":
"تجاوز النموذج الافتراضي لأوضاع محددة. إذا لم يتم التعيين، يتم استخدام النموذج الافتراضي العام.",
"settings.models.title": "النماذج",
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
"settings.agents.title": "الوكلاء",
@@ -828,12 +840,14 @@ export const dict = {
"Kilo Code هو مساعد ترميز بالذكاء الاصطناعي. اطلب منه بناء ميزات أو إصلاح أخطاء أو شرح قاعدة الشيفرة.",
"session.messages.scrollToBottom": "التمرير إلى الأسفل",
"session.status.writingResponse": "...جارٍ كتابة الرد",
"session.status.retry": "جارٍ إعادة المحاولة…",
"session.status.retrying": "...إعادة المحاولة (المحاولة {{ attempt }})… {{ message }}",
"session.status.working": "...جارٍ العمل",
"dialog.model.noProviders": "لا يوجد موفرون",
"prompt.placeholder.connecting": "جارٍ الاتصال بالخادم...",
"prompt.placeholder.error": "فشل الاتصال. تحقق من لوحة الإخراج أو أعد تشغيل الإضافة.",
"prompt.placeholder.default": "اكتب رسالة... (Enter للإرسال، Shift+Enter لسطر جديد)",
"context.usage.sessionCost": "تكلفة الجلسة",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "Abrir configurações",
"command.session.previous": "Sessão anterior",
"command.session.next": "Próxima sessão",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Sessão não lida anterior",
"command.session.next.unseen": "Próxima sessão não lida",
"command.session.archive": "Arquivar sessão",
"command.palette": "Paleta de comandos",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "Desfazer a última mensagem",
"command.session.redo": "Refazer",
"command.session.redo.description": "Refazer a última mensagem desfeita",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Compactar sessão",
"command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto",
"command.session.fork": "Bifurcar da mensagem",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "Anexar arquivo",
"prompt.attachment.remove": "Remover anexo",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda ou feche a pergunta pendente primeiro",
"prompt.action.stop": "Parar",
"prompt.action.enhance": "Melhorar prompt",
"prompt.action.resetModel": "Redefinir modelo para o padrão",
@@ -688,6 +697,9 @@ export const dict = {
"settings.providers.tag.config": "Configuração",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Outro",
"settings.providers.modeModels": "Modelo por Modo",
"settings.providers.modeModels.description":
"Substitua o modelo padrão para modos específicos. Se não definido, o modelo padrão global é usado.",
"settings.models.title": "Modelos",
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
"settings.agents.title": "Agentes",
@@ -836,12 +848,14 @@ export const dict = {
"Kilo Code é um assistente de programação com IA. Peça para criar funcionalidades, corrigir bugs ou explicar sua base de código.",
"session.messages.scrollToBottom": "Rolar para o final",
"session.status.writingResponse": "Escrevendo resposta…",
"session.status.retry": "Tentando novamente…",
"session.status.retrying": "Tentando novamente (tentativa {{ attempt }})… {{ message }}",
"session.status.working": "Trabalhando…",
"dialog.model.noProviders": "Nenhum provedor",
"prompt.placeholder.connecting": "Conectando ao servidor...",
"prompt.placeholder.error": "Conexão falhou. Verifique o painel de saída ou reinicie a extensão.",
"prompt.placeholder.default": "Digite uma mensagem... (Enter para enviar, Shift+Enter para nova linha)",
"context.usage.sessionCost": "Custo da sessão",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "Poništi posljednju poruku",
"command.session.redo": "Vrati",
"command.session.redo.description": "Vrati posljednju poništenu poruku",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Sažmi sesiju",
"command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst",
"command.session.fork": "Fork iz poruke",
@@ -232,6 +240,7 @@ export const dict = {
"prompt.action.attachFile": "Priloži datoteku",
"prompt.attachment.remove": "Ukloni prilog",
"prompt.action.send": "Pošalji",
"prompt.action.send.blocked": "Prvo odgovorite ili odbacite pitanje na čekanju",
"prompt.action.stop": "Zaustavi",
"prompt.action.enhance": "Poboljšaj prompt",
"prompt.action.resetModel": "Resetuj model na zadani",
@@ -692,6 +701,9 @@ export const dict = {
"settings.providers.tag.config": "Konfiguracija",
"settings.providers.tag.custom": "Prilagođeno",
"settings.providers.tag.other": "Ostalo",
"settings.providers.modeModels": "Model po režimu",
"settings.providers.modeModels.description":
"Zamijenite podrazumijevani model za određene režime. Ako nije postavljeno, koristi se globalni podrazumijevani model.",
"settings.models.title": "Modeli",
"settings.models.description": "Postavke modela će se ovdje moći podešavati.",
"settings.agents.title": "Agenti",
@@ -839,12 +851,14 @@ export const dict = {
"Kilo Code je AI asistent za programiranje. Zatražite da gradi funkcionalnosti, ispravlja greške ili objasni vašu bazu koda.",
"session.messages.scrollToBottom": "Pomakni se na dno",
"session.status.writingResponse": "Pisanje odgovora…",
"session.status.retry": "Ponovni pokušaj…",
"session.status.retrying": "Ponovni pokušaj (pokušaj {{ attempt }})… {{ message }}",
"session.status.working": "Radim…",
"dialog.model.noProviders": "Nema pružatelja",
"prompt.placeholder.connecting": "Povezivanje na server...",
"prompt.placeholder.error": "Povezivanje nije uspjelo. Provjerite panel za izlaz ili ponovo pokrenite ekstenziju.",
"prompt.placeholder.default": "Unesite poruku... (Enter za slanje, Shift+Enter za novi red)",
"context.usage.sessionCost": "Cijena sesije",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "Åbn indstillinger",
"command.session.previous": "Forrige session",
"command.session.next": "Næste session",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Forrige ulæste session",
"command.session.next.unseen": "Næste ulæste session",
"command.session.archive": "Arkivér session",
"command.palette": "Kommandopalette",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "Fortryd den sidste besked",
"command.session.redo": "Omgør",
"command.session.redo.description": "Omgør den sidste fortrudte besked",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Komprimér session",
"command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen",
"command.session.fork": "Forgren fra besked",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "Vedhæft fil",
"prompt.attachment.remove": "Fjern vedhæftning",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Besvar eller afvis det afventende spørgsmål først",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Forbedr prompt",
"prompt.action.resetModel": "Nulstil model til standard",
@@ -687,6 +696,9 @@ export const dict = {
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.custom": "Brugerdefineret",
"settings.providers.tag.other": "Andet",
"settings.providers.modeModels": "Model pr. tilstand",
"settings.providers.modeModels.description":
"Tilsidesæt standardmodellen for bestemte tilstande. Hvis ikke angivet, bruges den globale standardmodel.",
"settings.models.title": "Modeller",
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
@@ -834,12 +846,14 @@ export const dict = {
"Kilo Code er en AI-kodningsassistent. Bed den om at bygge funktioner, rette fejl eller forklare din kodebase.",
"session.messages.scrollToBottom": "Rul til bunden",
"session.status.writingResponse": "Skriver svar…",
"session.status.retry": "Prøver igen…",
"session.status.retrying": "Prøver igen (forsøg {{ attempt }})… {{ message }}",
"session.status.working": "Arbejder…",
"dialog.model.noProviders": "Ingen udbydere",
"prompt.placeholder.connecting": "Opretter forbindelse til server...",
"prompt.placeholder.error": "Forbindelse mislykkedes. Tjek outputpanelet eller genstart udvidelsen.",
"prompt.placeholder.default": "Skriv en besked... (Enter for at sende, Shift+Enter for ny linje)",
"context.usage.sessionCost": "Sessionsomkostning",
+17 -2
View File
@@ -32,8 +32,8 @@ export const dict = {
"command.settings.open": "Einstellungen öffnen",
"command.session.previous": "Vorherige Sitzung",
"command.session.next": "Nächste Sitzung",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Vorherige ungelesene Sitzung",
"command.session.next.unseen": "Nächste ungelesene Sitzung",
"command.session.archive": "Sitzung archivieren",
"command.palette": "Befehlspalette",
@@ -82,6 +82,14 @@ export const dict = {
"command.session.undo.description": "Letzte Nachricht rückgängig machen",
"command.session.redo": "Wiederherstellen",
"command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Sitzung komprimieren",
"command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren",
"command.session.fork": "Von Nachricht abzweigen",
@@ -235,6 +243,7 @@ export const dict = {
"prompt.action.attachFile": "Datei anhängen",
"prompt.attachment.remove": "Anhang entfernen",
"prompt.action.send": "Senden",
"prompt.action.send.blocked": "Beantworten oder verwerfen Sie zuerst die ausstehende Frage",
"prompt.action.stop": "Stopp",
"prompt.action.enhance": "Prompt verbessern",
"prompt.action.resetModel": "Modell auf Standard zurücksetzen",
@@ -697,6 +706,9 @@ export const dict = {
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.custom": "Benutzerdefiniert",
"settings.providers.tag.other": "Andere",
"settings.providers.modeModels": "Modell pro Modus",
"settings.providers.modeModels.description":
"Überschreiben Sie das Standardmodell für bestimmte Modi. Wenn nicht festgelegt, wird das globale Standardmodell verwendet.",
"settings.models.title": "Modelle",
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
"settings.agents.title": "Agenten",
@@ -844,12 +856,15 @@ export const dict = {
"Kilo Code ist ein KI-Programmierassistent. Bitten Sie ihn, Funktionen zu erstellen, Fehler zu beheben oder Ihre Codebasis zu erklären.",
"session.messages.scrollToBottom": "Nach unten scrollen",
"session.status.writingResponse": "Antwort wird geschrieben…",
"session.status.retry": "Erneuter Versuch…",
"session.status.retrying": "Erneuter Versuch ({{ attempt }})… {{ message }}",
"session.status.working": "Wird bearbeitet…",
"dialog.model.noProviders": "Keine Anbieter",
"prompt.placeholder.connecting": "Verbindung zum Server wird hergestellt...",
"prompt.placeholder.error":
"Verbindung fehlgeschlagen. Überprüfen Sie das Ausgabepanel oder starten Sie die Erweiterung neu.",
"prompt.placeholder.default": "Nachricht eingeben... (Enter zum Senden, Shift+Enter für neue Zeile)",
"context.usage.sessionCost": "Sitzungskosten",
+11 -1
View File
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "Undo the last message",
"command.session.redo": "Redo",
"command.session.redo.description": "Redo the last undone message",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Compact session",
"command.session.compact.description": "Summarize the session to reduce context size",
"command.session.fork": "Fork from message",
@@ -232,6 +240,7 @@ export const dict = {
"prompt.action.attachFile": "Attach file",
"prompt.attachment.remove": "Remove attachment",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Answer or dismiss the pending question first",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Enhance prompt",
"prompt.action.resetModel": "Reset model to default",
@@ -839,6 +848,7 @@ export const dict = {
"Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase.",
"session.messages.scrollToBottom": "Scroll to bottom",
"session.status.writingResponse": "Writing response...",
"session.status.retry": "Retrying…",
"session.status.retrying": "Retrying (attempt {{ attempt }})… {{ message }}",
"session.status.working": "Working...",
@@ -846,7 +856,7 @@ export const dict = {
"prompt.placeholder.connecting": "Connecting to server...",
"prompt.placeholder.default": "Type a message... (Enter to send, Shift+Enter for new line)",
"prompt.placeholder.error": "",
"prompt.placeholder.error": "Connection failed. Check the output panel or restart the extension.",
"context.usage.sessionCost": "Session cost",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "Abrir ajustes",
"command.session.previous": "Sesión anterior",
"command.session.next": "Siguiente sesión",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Sesión no leída anterior",
"command.session.next.unseen": "Siguiente sesión no leída",
"command.session.archive": "Archivar sesión",
"command.palette": "Paleta de comandos",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "Deshacer el último mensaje",
"command.session.redo": "Rehacer",
"command.session.redo.description": "Rehacer el último mensaje deshecho",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Compactar sesión",
"command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto",
"command.session.fork": "Bifurcar desde mensaje",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "Adjuntar archivo",
"prompt.attachment.remove": "Eliminar adjunto",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda o descarte la pregunta pendiente primero",
"prompt.action.stop": "Detener",
"prompt.action.enhance": "Mejorar prompt",
"prompt.action.resetModel": "Restablecer modelo al predeterminado",
@@ -693,6 +702,9 @@ export const dict = {
"settings.providers.tag.config": "Configuración",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Otro",
"settings.providers.modeModels": "Modelo por modo",
"settings.providers.modeModels.description":
"Anula el modelo predeterminado para modos específicos. Si no se establece, se usa el modelo predeterminado global.",
"settings.models.title": "Modelos",
"settings.models.description": "La configuración de modelos estará disponible aquí.",
"settings.agents.title": "Agentes",
@@ -841,12 +853,14 @@ export const dict = {
"Kilo Code es un asistente de programación con IA. Pídele que cree funcionalidades, corrija errores o explique tu código.",
"session.messages.scrollToBottom": "Desplazar al final",
"session.status.writingResponse": "Escribiendo respuesta…",
"session.status.retry": "Reintentando…",
"session.status.retrying": "Reintentando (intento {{ attempt }})… {{ message }}",
"session.status.working": "Trabajando…",
"dialog.model.noProviders": "Sin proveedores",
"prompt.placeholder.connecting": "Conectando al servidor...",
"prompt.placeholder.error": "Conexión fallida. Revisa el panel de salida o reinicia la extensión.",
"prompt.placeholder.default": "Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva línea)",
"context.usage.sessionCost": "Coste de la sesión",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "Ouvrir les paramètres",
"command.session.previous": "Session précédente",
"command.session.next": "Session suivante",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Session non lue précédente",
"command.session.next.unseen": "Session non lue suivante",
"command.session.archive": "Archiver la session",
"command.palette": "Palette de commandes",
@@ -79,6 +79,14 @@ export const dict = {
"command.session.undo.description": "Annuler le dernier message",
"command.session.redo": "Rétablir",
"command.session.redo.description": "Rétablir le dernier message annulé",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Compacter la session",
"command.session.compact.description": "Résumer la session pour réduire la taille du contexte",
"command.session.fork": "Bifurquer à partir du message",
@@ -232,6 +240,7 @@ export const dict = {
"prompt.action.attachFile": "Joindre un fichier",
"prompt.attachment.remove": "Supprimer la pièce jointe",
"prompt.action.send": "Envoyer",
"prompt.action.send.blocked": "Répondez ou rejetez d'abord la question en attente",
"prompt.action.stop": "Arrêter",
"prompt.action.enhance": "Améliorer le prompt",
"prompt.action.resetModel": "Réinitialiser le modèle par défaut",
@@ -697,6 +706,9 @@ export const dict = {
"settings.providers.tag.config": "Configuration",
"settings.providers.tag.custom": "Personnalisé",
"settings.providers.tag.other": "Autre",
"settings.providers.modeModels": "Modèle par mode",
"settings.providers.modeModels.description":
"Remplacez le modèle par défaut pour des modes spécifiques. Si non défini, le modèle par défaut global est utilisé.",
"settings.models.title": "Modèles",
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
"settings.agents.title": "Agents",
@@ -846,12 +858,14 @@ export const dict = {
"Kilo Code est un assistant de programmation IA. Demandez-lui de créer des fonctionnalités, corriger des bugs ou expliquer votre code.",
"session.messages.scrollToBottom": "Défiler vers le bas",
"session.status.writingResponse": "Rédaction de la réponse…",
"session.status.retry": "Nouvelle tentative…",
"session.status.retrying": "Nouvelle tentative (essai {{ attempt }})… {{ message }}",
"session.status.working": "En cours…",
"dialog.model.noProviders": "Aucun fournisseur",
"prompt.placeholder.connecting": "Connexion au serveur...",
"prompt.placeholder.error": "Échec de la connexion. Vérifiez le panneau de sortie ou redémarrez l'extension.",
"prompt.placeholder.default": "Tapez un message... (Entrée pour envoyer, Maj+Entrée pour un saut de ligne)",
"context.usage.sessionCost": "Coût de la session",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "設定を開く",
"command.session.previous": "前のセッション",
"command.session.next": "次のセッション",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "前の未読セッション",
"command.session.next.unseen": "次の未読セッション",
"command.session.archive": "セッションをアーカイブ",
"command.palette": "コマンドパレット",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "最後のメッセージを元に戻す",
"command.session.redo": "やり直す",
"command.session.redo.description": "元に戻したメッセージをやり直す",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "セッションを圧縮",
"command.session.compact.description": "セッションを要約してコンテキストサイズを削減",
"command.session.fork": "メッセージからフォーク",
@@ -230,6 +238,7 @@ export const dict = {
"prompt.action.attachFile": "ファイルを添付",
"prompt.attachment.remove": "添付ファイルを削除",
"prompt.action.send": "送信",
"prompt.action.send.blocked": "最初に保留中の質問に答えるか、閉じてください",
"prompt.action.stop": "停止",
"prompt.action.enhance": "プロンプトを改善",
"prompt.action.resetModel": "モデルをデフォルトにリセット",
@@ -686,6 +695,9 @@ export const dict = {
"settings.providers.tag.config": "設定",
"settings.providers.tag.custom": "カスタム",
"settings.providers.tag.other": "その他",
"settings.providers.modeModels": "モードごとのモデル",
"settings.providers.modeModels.description":
"特定のモードのデフォルトモデルを上書きします。設定されていない場合、グローバルデフォルトモデルが使用されます。",
"settings.models.title": "モデル",
"settings.models.description": "モデル設定はここで構成できます。",
"settings.agents.title": "エージェント",
@@ -833,12 +845,14 @@ export const dict = {
"Kilo CodeはAIコーディングアシスタントです。機能の構築、バグの修正、コードベースの説明を依頼できます。",
"session.messages.scrollToBottom": "下にスクロール",
"session.status.writingResponse": "応答を作成中…",
"session.status.retry": "再試行中…",
"session.status.retrying": "再試行中({{ attempt }}回目)… {{ message }}",
"session.status.working": "作業中…",
"dialog.model.noProviders": "プロバイダーなし",
"prompt.placeholder.connecting": "サーバーに接続中...",
"prompt.placeholder.error": "接続に失敗しました。出力パネルを確認するか、拡張機能を再起動してください。",
"prompt.placeholder.default": "メッセージを入力... (Enterで送信、Shift+Enterで改行)",
"context.usage.sessionCost": "セッションコスト",
+16 -2
View File
@@ -32,8 +32,8 @@ export const dict = {
"command.settings.open": "설정 열기",
"command.session.previous": "이전 세션",
"command.session.next": "다음 세션",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "이전 읽지 않은 세션",
"command.session.next.unseen": "다음 읽지 않은 세션",
"command.session.archive": "세션 보관",
"command.palette": "명령 팔레트",
@@ -82,6 +82,14 @@ export const dict = {
"command.session.undo.description": "마지막 메시지 실행 취소",
"command.session.redo": "다시 실행",
"command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "세션 압축",
"command.session.compact.description": "컨텍스트 크기를 줄이기 위해 세션 요약",
"command.session.fork": "메시지에서 분기",
@@ -234,6 +242,7 @@ export const dict = {
"prompt.action.attachFile": "파일 첨부",
"prompt.attachment.remove": "첨부 파일 제거",
"prompt.action.send": "전송",
"prompt.action.send.blocked": "먼저 대기 중인 질문에 답하거나 닫아주세요",
"prompt.action.stop": "중지",
"prompt.action.enhance": "프롬프트 개선",
"prompt.action.resetModel": "모델을 기본값으로 재설정",
@@ -687,6 +696,9 @@ export const dict = {
"settings.providers.tag.config": "구성",
"settings.providers.tag.custom": "사용자 지정",
"settings.providers.tag.other": "기타",
"settings.providers.modeModels": "모드별 모델",
"settings.providers.modeModels.description":
"특정 모드의 기본 모델을 재정의합니다. 설정하지 않으면 전역 기본 모델이 사용됩니다.",
"settings.models.title": "모델",
"settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.",
"settings.agents.title": "에이전트",
@@ -833,12 +845,14 @@ export const dict = {
"Kilo Code는 AI 코딩 어시스턴트입니다. 기능 구축, 버그 수정 또는 코드베이스 설명을 요청하세요.",
"session.messages.scrollToBottom": "하단으로 스크롤",
"session.status.writingResponse": "응답 작성 중...",
"session.status.retry": "재시도 중…",
"session.status.retrying": "재시도 중 ({{ attempt }}번째 시도)… {{ message }}",
"session.status.working": "작업 중...",
"dialog.model.noProviders": "공급자 없음",
"prompt.placeholder.connecting": "서버에 연결 중...",
"prompt.placeholder.error": "연결에 실패했습니다. 출력 패널을 확인하거나 확장 프로그램을 다시 시작하세요.",
"prompt.placeholder.default": "메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄 바꿈)",
"context.usage.sessionCost": "세션 비용",
+17 -2
View File
@@ -31,8 +31,8 @@ export const dict = {
"command.settings.open": "Åpne innstillinger",
"command.session.previous": "Forrige sesjon",
"command.session.next": "Neste sesjon",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Forrige uleste økt",
"command.session.next.unseen": "Neste uleste økt",
"command.session.archive": "Arkiver sesjon",
"command.palette": "Kommandopalett",
@@ -81,6 +81,14 @@ export const dict = {
"command.session.undo.description": "Angre siste melding",
"command.session.redo": "Gjør om",
"command.session.redo.description": "Gjør om siste angrede melding",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Komprimer sesjon",
"command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen",
"command.session.fork": "Forgren fra melding",
@@ -234,6 +242,7 @@ export const dict = {
"prompt.action.attachFile": "Legg ved fil",
"prompt.attachment.remove": "Fjern vedlegg",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Svar på eller avvis det ventende spørsmålet først",
"prompt.action.stop": "Stopp",
"prompt.action.enhance": "Forbedre prompt",
"prompt.action.resetModel": "Tilbakestill modell til standard",
@@ -399,6 +408,7 @@ export const dict = {
"error.page.title": "Noe gikk galt",
"error.page.description": "Det oppstod en feil under lasting av applikasjonen.",
"error.page.details.label": "Feildetaljer",
"error.details.show": "Detaljer",
"error.page.action.restart": "Start på nytt",
"error.page.action.checking": "Sjekker...",
"error.page.action.checkUpdates": "Se etter oppdateringer",
@@ -690,6 +700,9 @@ export const dict = {
"settings.providers.tag.config": "Konfigurasjon",
"settings.providers.tag.custom": "Tilpasset",
"settings.providers.tag.other": "Annet",
"settings.providers.modeModels": "Modell per modus",
"settings.providers.modeModels.description":
"Overstyr standardmodellen for bestemte moduser. Hvis ikke angitt, brukes den globale standardmodellen.",
"settings.models.title": "Modeller",
"settings.models.description": "Modellinnstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
@@ -837,12 +850,14 @@ export const dict = {
"Kilo Code er en AI-kodingsassistent. Be den om å bygge funksjoner, fikse feil eller forklare kodebasen din.",
"session.messages.scrollToBottom": "Rull til bunnen",
"session.status.writingResponse": "Skriver svar…",
"session.status.retry": "Prøver på nytt…",
"session.status.retrying": "Prøver på nytt (forsøk {{ attempt }})… {{ message }}",
"session.status.working": "Arbeider…",
"dialog.model.noProviders": "Ingen leverandører",
"prompt.placeholder.connecting": "Kobler til server...",
"prompt.placeholder.error": "Tilkobling mislyktes. Sjekk utdatapanelet eller start utvidelsen på nytt.",
"prompt.placeholder.default": "Skriv en melding... (Enter for å sende, Shift+Enter for ny linje)",
"context.usage.sessionCost": "Sesjonskostnad",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "Otwórz ustawienia",
"command.session.previous": "Poprzednia sesja",
"command.session.next": "Następna sesja",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Poprzednia nieprzeczytana sesja",
"command.session.next.unseen": "Następna nieprzeczytana sesja",
"command.session.archive": "Zarchiwizuj sesję",
"command.palette": "Paleta poleceń",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "Cofnij ostatnią wiadomość",
"command.session.redo": "Ponów",
"command.session.redo.description": "Ponów ostatnią cofniętą wiadomość",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Kompaktuj sesję",
"command.session.compact.description": "Podsumuj sesję, aby zmniejszyć rozmiar kontekstu",
"command.session.fork": "Rozwidlij od wiadomości",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "Załącz plik",
"prompt.attachment.remove": "Usuń załącznik",
"prompt.action.send": "Wyślij",
"prompt.action.send.blocked": "Najpierw odpowiedz na oczekujące pytanie lub je odrzuć",
"prompt.action.stop": "Zatrzymaj",
"prompt.action.enhance": "Ulepsz prompt",
"prompt.action.resetModel": "Zresetuj model do domyślnego",
@@ -690,6 +699,9 @@ export const dict = {
"settings.providers.tag.config": "Konfiguracja",
"settings.providers.tag.custom": "Niestandardowe",
"settings.providers.tag.other": "Inne",
"settings.providers.modeModels": "Model na tryb",
"settings.providers.modeModels.description":
"Zastąp domyślny model dla określonych trybów. Jeśli nie ustawiono, używany jest globalny domyślny model.",
"settings.models.title": "Modele",
"settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.",
"settings.agents.title": "Agenci",
@@ -836,12 +848,14 @@ export const dict = {
"Kilo Code to asystent programowania AI. Poproś go o tworzenie funkcji, naprawę błędów lub wyjaśnienie bazy kodu.",
"session.messages.scrollToBottom": "Przewiń na dół",
"session.status.writingResponse": "Pisanie odpowiedzi…",
"session.status.retry": "Ponawianie…",
"session.status.retrying": "Ponawiam próbę ({{ attempt }})… {{ message }}",
"session.status.working": "Pracuję…",
"dialog.model.noProviders": "Brak dostawców",
"prompt.placeholder.connecting": "Łączenie z serwerem...",
"prompt.placeholder.error": "Połączenie nie powiodło się. Sprawdź panel wyjściowy lub uruchom ponownie rozszerzenie.",
"prompt.placeholder.default": "Wpisz wiadomość... (Enter, aby wysłać, Shift+Enter dla nowej linii)",
"context.usage.sessionCost": "Koszt sesji",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "Открыть настройки",
"command.session.previous": "Предыдущая сессия",
"command.session.next": "Следующая сессия",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "Предыдущая непрочитанная сессия",
"command.session.next.unseen": "Следующая непрочитанная сессия",
"command.session.archive": "Архивировать сессию",
"command.palette": "Палитра команд",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "Отменить последнее сообщение",
"command.session.redo": "Повторить",
"command.session.redo.description": "Повторить отменённое сообщение",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "Сжать сессию",
"command.session.compact.description": "Сократить сессию для уменьшения размера контекста",
"command.session.fork": "Создать ответвление",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "Прикрепить файл",
"prompt.attachment.remove": "Удалить вложение",
"prompt.action.send": "Отправить",
"prompt.action.send.blocked": "Сначала ответьте на ожидающий вопрос или отклоните его",
"prompt.action.stop": "Остановить",
"prompt.action.enhance": "Улучшить промпт",
"prompt.action.resetModel": "Сбросить модель на значение по умолчанию",
@@ -693,6 +702,9 @@ export const dict = {
"settings.providers.tag.config": "Конфигурация",
"settings.providers.tag.custom": "Пользовательский",
"settings.providers.tag.other": "Другое",
"settings.providers.modeModels": "Модель для режима",
"settings.providers.modeModels.description":
"Переопределите модель по умолчанию для определённых режимов. Если не задано, используется глобальная модель по умолчанию.",
"settings.models.title": "Модели",
"settings.models.description": "Настройки моделей будут доступны здесь.",
"settings.agents.title": "Агенты",
@@ -840,12 +852,14 @@ export const dict = {
"Kilo Code — это AI-помощник для программирования. Попросите его создать функции, исправить ошибки или объяснить вашу кодовую базу.",
"session.messages.scrollToBottom": "Прокрутить вниз",
"session.status.writingResponse": "Пишу ответ…",
"session.status.retry": "Повторная попытка…",
"session.status.retrying": "Повторная попытка ({{ attempt }})… {{ message }}",
"session.status.working": "Работаю…",
"dialog.model.noProviders": "Нет провайдеров",
"prompt.placeholder.connecting": "Подключение к серверу...",
"prompt.placeholder.error": "Не удалось подключиться. Проверьте панель вывода или перезапустите расширение.",
"prompt.placeholder.default": "Введите сообщение... (Enter для отправки, Shift+Enter для новой строки)",
"context.usage.sessionCost": "Стоимость сессии",
+16 -2
View File
@@ -28,8 +28,8 @@ export const dict = {
"command.settings.open": "เปิดการตั้งค่า",
"command.session.previous": "เซสชันก่อนหน้า",
"command.session.next": "เซสชันถัดไป",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "เซสชันที่ยังไม่ได้อ่านก่อนหน้า",
"command.session.next.unseen": "เซสชันที่ยังไม่ได้อ่านถัดไป",
"command.session.archive": "จัดเก็บเซสชัน",
"command.palette": "คำสั่งค้นหา",
@@ -78,6 +78,14 @@ export const dict = {
"command.session.undo.description": "ยกเลิกข้อความล่าสุด",
"command.session.redo": "ทำซ้ำ",
"command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "บีบอัดเซสชัน",
"command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท",
"command.session.fork": "แตกแขนงจากข้อความ",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "แนบไฟล์",
"prompt.attachment.remove": "เอาไฟล์แนบออก",
"prompt.action.send": "ส่ง",
"prompt.action.send.blocked": "โปรดตอบหรือข้ามคำถามที่รอดำเนินการก่อน",
"prompt.action.stop": "หยุด",
"prompt.action.enhance": "ปรับปรุงพรอมต์",
"prompt.action.resetModel": "รีเซ็ตโมเดลเป็นค่าเริ่มต้น",
@@ -684,6 +693,9 @@ export const dict = {
"settings.providers.tag.config": "กำหนดค่า",
"settings.providers.tag.custom": "กำหนดเอง",
"settings.providers.tag.other": "อื่น ๆ",
"settings.providers.modeModels": "โมเดลต่อโหมด",
"settings.providers.modeModels.description":
"แทนที่โมเดลเริ่มต้นสำหรับโหมดที่กำหนด หากไม่ได้ตั้งค่า จะใช้โมเดลเริ่มต้นทั่วไป",
"settings.models.title": "โมเดล",
"settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่",
"settings.agents.title": "เอเจนต์",
@@ -829,12 +841,14 @@ export const dict = {
"session.messages.welcome": "Kilo Code เป็นผู้ช่วยเขียนโค้ด AI ขอให้สร้างฟีเจอร์ แก้ไขบัก หรืออธิบายโค้ดเบสของคุณ",
"session.messages.scrollToBottom": "เลื่อนไปด้านล่าง",
"session.status.writingResponse": "กำลังเขียนคำตอบ...",
"session.status.retry": "กำลังลองใหม่…",
"session.status.retrying": "กำลังลองใหม่ (ครั้งที่ {{ attempt }})… {{ message }}",
"session.status.working": "กำลังทำงาน...",
"dialog.model.noProviders": "ไม่มีผู้ให้บริการ",
"prompt.placeholder.connecting": "กำลังเชื่อมต่อกับเซิร์ฟเวอร์...",
"prompt.placeholder.error": "การเชื่อมต่อล้มเหลว ตรวจสอบแผงเอาต์พุตหรือรีสตาร์ทส่วนขยาย",
"prompt.placeholder.default": "พิมพ์ข้อความ... (Enter เพื่อส่ง, Shift+Enter เพื่อขึ้นบรรทัดใหม่)",
"context.usage.sessionCost": "ค่าใช้จ่ายเซสชัน",
+15 -2
View File
@@ -32,8 +32,8 @@ export const dict = {
"command.settings.open": "打开设置",
"command.session.previous": "上一个会话",
"command.session.next": "下一个会话",
"command.session.previous.unseen": "Previous unread session",
"command.session.next.unseen": "Next unread session",
"command.session.previous.unseen": "上一个未读会话",
"command.session.next.unseen": "下一个未读会话",
"command.session.archive": "归档会话",
"command.palette": "命令面板",
@@ -82,6 +82,14 @@ export const dict = {
"command.session.undo.description": "撤销上一条消息",
"command.session.redo": "重做",
"command.session.redo.description": "重做上一条撤销的消息",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "精简会话",
"command.session.compact.description": "总结会话以减少上下文大小",
"command.session.fork": "从消息分叉",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "附加文件",
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "发送",
"prompt.action.send.blocked": "请先回答或忽略待处理的问题",
"prompt.action.stop": "停止",
"prompt.action.enhance": "优化提示词",
"prompt.action.resetModel": "重置模型为默认值",
@@ -677,6 +686,8 @@ export const dict = {
"settings.providers.tag.config": "配置",
"settings.providers.tag.custom": "自定义",
"settings.providers.tag.other": "其他",
"settings.providers.modeModels": "按模式选择模型",
"settings.providers.modeModels.description": "为特定模式覆盖默认模型。如果未设置,将使用全局默认模型。",
"settings.models.title": "模型",
"settings.models.description": "模型设置将在此处可配置。",
"settings.agents.title": "智能体",
@@ -822,12 +833,14 @@ export const dict = {
"session.messages.welcome": "Kilo Code 是一个 AI 编程助手。让它帮你构建功能、修复 bug 或解释代码库。",
"session.messages.scrollToBottom": "滚动到底部",
"session.status.writingResponse": "正在撰写回复…",
"session.status.retry": "正在重试…",
"session.status.retrying": "正在重试(第 {{ attempt }} 次)… {{ message }}",
"session.status.working": "处理中…",
"dialog.model.noProviders": "无供应商",
"prompt.placeholder.connecting": "正在连接服务器...",
"prompt.placeholder.error": "连接失败。请检查输出面板或重启扩展。",
"prompt.placeholder.default": "输入消息... (Enter 发送,Shift+Enter 换行)",
"context.usage.sessionCost": "会话费用",
@@ -82,6 +82,14 @@ export const dict = {
"command.session.undo.description": "復原上一則訊息",
"command.session.redo": "重做",
"command.session.redo.description": "重做上一則復原的訊息",
"revert.banner.count_one": "{{count}} message reverted",
"revert.banner.count_other": "{{count}} messages reverted",
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "Send a new message to make this permanent",
"revert.turn.revert": "Revert to here",
"revert.turn.copy": "Copy message",
"revert.bottom.undo": "Undo Changes",
"command.session.compact": "精簡工作階段",
"command.session.compact.description": "總結工作階段以減少上下文大小",
"command.session.fork": "從訊息分支",
@@ -231,6 +239,7 @@ export const dict = {
"prompt.action.attachFile": "附加檔案",
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "傳送",
"prompt.action.send.blocked": "請先回答或忽略待處理的問題",
"prompt.action.stop": "停止",
"prompt.action.enhance": "改善提示詞",
"prompt.action.resetModel": "重置模型為預設值",
@@ -679,6 +688,8 @@ export const dict = {
"settings.providers.tag.config": "設定",
"settings.providers.tag.custom": "自訂",
"settings.providers.tag.other": "其他",
"settings.providers.modeModels": "按模式選擇模型",
"settings.providers.modeModels.description": "為特定模式覆寫預設模型。如果未設定,將使用全域預設模型。",
"settings.models.title": "模型",
"settings.models.description": "可在此調整模型設定。",
"settings.agents.title": "Agents",
@@ -824,12 +835,14 @@ export const dict = {
"session.messages.welcome": "Kilo Code 是一個 AI 程式設計助手。讓它幫你建構功能、修復 bug 或解釋程式碼庫。",
"session.messages.scrollToBottom": "捲動至底部",
"session.status.writingResponse": "正在撰寫回覆…",
"session.status.retry": "正在重試…",
"session.status.retrying": "正在重試(第 {{ attempt }} 次)… {{ message }}",
"session.status.working": "處理中…",
"dialog.model.noProviders": "沒有供應商",
"prompt.placeholder.connecting": "正在連線至伺服器...",
"prompt.placeholder.error": "連線失敗。請檢查輸出面板或重新啟動擴充功能。",
"prompt.placeholder.default": "輸入訊息... (Enter 送出,Shift+Enter 換行)",
"context.usage.sessionCost": "工作階段費用",
@@ -152,6 +152,11 @@ export function mockSessionValue(overrides?: {
getSessionModel: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }),
setSessionModel: noop,
setSessionAgent: noop,
revert: () => undefined,
revertedCount: () => 0,
summary: () => undefined,
revertSession: noop,
unrevertSession: noop,
variantList: () => [],
currentVariant: () => undefined,
selectVariant: noop,
@@ -446,6 +446,75 @@
background: var(--vscode-button-hoverBackground);
}
/* ============================================
Revert Banner
============================================ */
.revert-banner {
margin: 8px 12px;
padding: 8px 12px;
border-radius: 4px;
background: var(--surface-secondary, var(--vscode-editorWidget-background, #252526));
border: 1px solid var(--border-base, var(--vscode-editorWidget-border, #454545));
font-size: 12px;
}
.revert-banner-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.revert-banner-info {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted, var(--vscode-descriptionForeground));
}
.revert-banner-count {
font-weight: 500;
}
.revert-banner-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.revert-banner-files {
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 2px;
}
.revert-banner-file {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 1px 0;
color: var(--text-base, var(--vscode-foreground));
}
.revert-banner-filename {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
direction: rtl;
text-align: left;
}
.revert-banner-hint {
margin-top: 6px;
color: var(--text-muted, var(--vscode-descriptionForeground));
font-style: italic;
font-size: 11px;
}
/* ============================================
Prompt Input
============================================ */
@@ -1383,14 +1452,6 @@
margin-top: 0;
}
[data-slot="permission-session-hint"] {
font-size: 11px;
color: var(--text-weak, var(--vscode-descriptionForeground));
margin: 0;
padding: 4px 0 0;
text-align: left;
}
[data-slot="permission-actions"] {
display: flex;
align-items: center;
@@ -113,12 +113,34 @@ export interface Message {
tokens?: TokenUsage
}
// File diff info (matches Snapshot.FileDiff from CLI backend)
export interface SessionFileDiff {
file: string
before: string
after: string
additions: number
deletions: number
status?: "added" | "deleted" | "modified"
}
// Session info (simplified for webview)
export interface SessionInfo {
id: string
title?: string
createdAt: string
updatedAt: string
revert?: {
messageID: string
partID?: string
snapshot?: string
diff?: string
} | null
summary?: {
additions: number
deletions: number
files: number
diffs?: SessionFileDiff[]
} | null
}
// Cloud session info (from Kilo cloud API)
@@ -451,6 +473,12 @@ export interface SessionDeletedMessage {
sessionID: string
}
export interface MessageRemovedMessage {
type: "messageRemoved"
sessionID: string
messageID: string
}
export interface MessagesLoadedMessage {
type: "messagesLoaded"
sessionID: string
@@ -1105,6 +1133,7 @@ export type ExtensionMessage =
| SessionCreatedMessage
| SessionUpdatedMessage
| SessionDeletedMessage
| MessageRemovedMessage
| MessagesLoadedMessage
| MessageCreatedMessage
| SessionsLoadedMessage
@@ -1201,6 +1230,17 @@ export interface AbortRequest {
sessionID: string
}
export interface RevertSessionRequest {
type: "revertSession"
sessionID: string
messageID: string
}
export interface UnrevertSessionRequest {
type: "unrevertSession"
sessionID: string
}
export interface PermissionResponseRequest {
type: "permissionResponse"
permissionId: string
@@ -1682,6 +1722,8 @@ export interface SetDefaultBaseBranchRequest {
export type WebviewMessage =
| SendMessageRequest
| AbortRequest
| RevertSessionRequest
| UnrevertSessionRequest
| PermissionResponseRequest
| CreateSessionRequest
| ClearSessionRequest
+7 -7
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.0.48",
"version": "7.0.49",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -81,14 +81,16 @@
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@kilocode/kilo-gateway": "workspace:*",
"@kilocode/kilo-telemetry": "workspace:*",
"@kilocode/plugin": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@modelcontextprotocol/sdk": "1.25.2",
"@morphllm/morphsdk": "0.2.148",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@kilocode/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.86",
@@ -122,6 +124,7 @@
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"simple-git": "3.31.1",
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",
@@ -133,10 +136,7 @@
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
"zod-to-json-schema": "3.24.5",
"@kilocode/kilo-gateway": "workspace:*",
"@kilocode/kilo-telemetry": "workspace:*",
"simple-git": "3.31.1"
"zod-to-json-schema": "3.24.5"
},
"overrides": {
"drizzle-orm": "1.0.0-beta.12-a5629fb"
@@ -158,12 +158,12 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
<Switch>
<Match when={props.request.always.length === 1 && props.request.always[0] === "*"}>
{/* kilocode_change */}
<TextBody title={"This will allow " + props.request.permission + " until Kilo is restarted."} />
<TextBody title={"This will allow " + props.request.permission + " permanently."} />
</Match>
<Match when={true}>
<box paddingLeft={1} gap={1}>
{/* kilocode_change */}
<text fg={theme.textMuted}>This will allow the following patterns until Kilo is restarted</text>
<text fg={theme.textMuted}>This will allow the following patterns permanently</text>
<box>
<For each={props.request.always}>
{(pattern) => (
+36 -9
View File
@@ -1426,7 +1426,7 @@ export namespace Config {
export async function update(config: Info) {
const filepath = path.join(Instance.directory, "config.json")
const existing = await loadFile(filepath)
await Filesystem.writeJson(filepath, stripNulls(mergeDeep(existing, config) as Record<string, unknown>)) // kilocode_change - strip null delete sentinels
await Filesystem.writeJson(filepath, mergeConfig(existing, config)) // kilocode_change
await Instance.dispose()
}
@@ -1462,6 +1462,35 @@ export namespace Config {
}
// kilocode_change end
// kilocode_change start — merge config with normalization pipeline
/**
* Merge a patch into an existing config:
* 1. Normalize permission scalars objects when the patch has an object
* (e.g. existing `"bash": "ask"` + patch `"bash": { "npm *": "allow" }`
* promotes existing to `"bash": { "*": "ask" }` so mergeDeep works)
* 2. Deep-merge
* 3. Strip null delete sentinels
*/
function mergeConfig(existing: Info, patch: Info): Info {
const e = { ...existing } as Record<string, unknown>
const p = patch as Record<string, unknown>
// Normalize permission scalars before merge (clone to avoid mutating the input)
const existingPerm = e.permission
const patchPerm = p.permission
if (isRecord(existingPerm) && isRecord(patchPerm)) {
const cloned = { ...existingPerm }
for (const [key, patchValue] of Object.entries(patchPerm)) {
const existingValue = cloned[key]
if (typeof existingValue === "string" && isRecord(patchValue)) {
cloned[key] = { "*": existingValue }
}
}
e.permission = cloned
}
return stripNulls(mergeDeep(e, p) as Record<string, unknown>) as Info
}
// kilocode_change end
function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
if (!isRecord(patch)) {
// kilocode_change - null means "delete this key" — pass undefined to jsonc-parser's modify()
@@ -1478,11 +1507,15 @@ export namespace Config {
// scalar (e.g. permission.bash is "ask" as a string), jsonc-parser cannot
// add child keys to it. Detect this case and replace the whole node with
// the patch object in a single modify() call instead of recursing.
// For permission keys, promote the scalar to { "*": scalarValue } so the
// wildcard default is preserved. For other keys, replace directly.
if (path.length > 0) {
const tree = parseTree(input)
const node = tree && findNodeAtLocation(tree, path)
if (node && node.type !== "object") {
const edits = modify(input, path, patch, {
const isPermissionKey = path[0] === "permission" && path.length === 2
const replacement = isPermissionKey ? { "*": node.value, ...patch } : patch
const edits = modify(input, path, replacement, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
})
return applyEdits(input, edits)
@@ -1540,7 +1573,7 @@ export namespace Config {
const next = await (async () => {
if (!filepath.endsWith(".jsonc")) {
const existing = parseConfig(before, filepath)
const merged = stripNulls(mergeDeep(existing, config) as Record<string, unknown>) as Info // kilocode_change - strip null delete sentinels
const merged = mergeConfig(existing, config) // kilocode_change
await Filesystem.writeJson(filepath, merged)
return merged
}
@@ -1553,11 +1586,6 @@ export namespace Config {
global.reset()
// kilocode_change start - only reset config cache, don't dispose all instances.
// Instance.disposeAll() was destroying all session state, MCP connections, and
// in-flight operations across every project whenever any global config changed
// (e.g. removing a mode). The cache reset above is sufficient — consumers will
// pick up the new config on their next read.
GlobalBus.emit("event", {
directory: "global",
payload: {
@@ -1565,7 +1593,6 @@ export namespace Config {
properties: {},
},
})
// kilocode_change end
return next
}
+44 -4
View File
@@ -65,6 +65,28 @@ export namespace PermissionNext {
return rulesets.flat()
}
// kilocode_change start — inverse of fromConfig: convert rules back to config format
export function toConfig(rules: Ruleset): Config.Permission {
const result: Config.Permission = {}
for (const rule of rules) {
const existing = result[rule.permission]
if (existing === undefined) {
// Always use object format to avoid replacing existing granular rules
// when merged via updateGlobal (e.g. { read: "allow" } would wipe
// { read: { "*": "ask", "src/*": "allow" } })
result[rule.permission] = { [rule.pattern]: rule.action }
continue
}
if (typeof existing === "string") {
result[rule.permission] = { "*": existing, [rule.pattern]: rule.action }
continue
}
existing[rule.pattern] = rule.action
}
return result
}
// kilocode_change end
export const Request = z
.object({
id: Identifier.schema("permission"),
@@ -175,11 +197,19 @@ export namespace PermissionNext {
const validRules = new Set(existing.info.metadata?.rules ?? [])
const permission = existing.info.permission
for (const pattern of input.approvedAlways ?? []) {
if (validRules.has(pattern)) s.approved.push({ permission, pattern, action: "allow" })
// Build rules in metadata.rules order so broader patterns come before
// specific ones, preserving intended precedence for evaluate(findLast).
const approvedSet = new Set(input.approvedAlways ?? [])
const deniedSet = new Set(input.deniedAlways ?? [])
const newRules: Ruleset = []
for (const pattern of existing.info.metadata?.rules ?? []) {
if (approvedSet.has(pattern)) newRules.push({ permission, pattern, action: "allow" })
if (deniedSet.has(pattern)) newRules.push({ permission, pattern, action: "deny" })
}
for (const pattern of input.deniedAlways ?? []) {
if (validRules.has(pattern)) s.approved.push({ permission, pattern, action: "deny" })
s.approved.push(...newRules)
if (newRules.length > 0) {
await Config.updateGlobal({ permission: toConfig(newRules) })
}
},
)
@@ -254,6 +284,16 @@ export namespace PermissionNext {
// UI to manage it
// db().insert(PermissionTable).values({ projectID: Instance.project.id, data: s.approved })
// .onConflictDoUpdate({ target: PermissionTable.projectID, set: { data: s.approved } }).run()
// kilocode_change start - persist always rules to global config
const alwaysRules: Ruleset = existing.info.always.map((pattern) => ({
permission: existing.info.permission,
pattern,
action: "allow" as const,
}))
if (alwaysRules.length > 0) {
await Config.updateGlobal({ permission: toConfig(alwaysRules) })
}
// kilocode_change end
return
}
},

Some files were not shown because too many files have changed in this diff Show More