Merge branch 'main' into jl-remove-allowed-providers

This commit is contained in:
Josh Lambert
2026-03-06 09:47:05 -05:00
94 changed files with 5012 additions and 2559 deletions
+35 -20
View File
@@ -1,6 +1,7 @@
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { isAbsolutePath } from "./path-utils"
import type {
KiloClient,
Session,
@@ -255,6 +256,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.sessionDirectories.delete(sessionId)
}
/** Return the currently active session ID, if any. */
public getCurrentSessionId(): string | undefined {
return this.currentSession?.id ?? undefined
}
/**
* Re-fetch and send the full session list to the webview.
* Called by AgentManagerProvider after worktree recovery completes.
@@ -761,13 +767,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (abort.signal.aborted) return
// Update currentSession so fallback logic in handleSendMessage/handleAbort
// references the correct session after switching to a historical session.
// references the correct session after switching. loadMessages is the
// canonical "user switched to this session" signal, so always update —
// the old guard `this.currentSession.id === sessionID` prevented updates
// when switching between different sessions.
// Non-blocking: don't let a failure here prevent messages from loading.
// 404s are expected for cross-worktree sessions — use silent to suppress HTTP error logs.
this.client.session
.get({ sessionID, directory: workspaceDir })
.then((result) => {
if (result.data && (!this.currentSession || this.currentSession.id === sessionID)) {
if (result.data && !abort.signal.aborted) {
this.currentSession = result.data
}
})
@@ -1613,9 +1622,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
console.log("[Kilo New] KiloProvider: 🔐 Login successful")
await this.client.global.dispose().catch((e: unknown) =>
console.warn("[Kilo New] KiloProvider: global.dispose() after login failed:", e),
)
await this.client.global
.dispose()
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after login failed:", e))
// Step 4: Fetch profile and push to webview
const { data: profileData } = await this.client.kilo.profile(undefined, { throwOnError: true })
@@ -1662,9 +1671,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return
}
await this.client.global.dispose().catch((e: unknown) =>
console.warn("[Kilo New] KiloProvider: global.dispose() after org switch failed:", e),
)
await this.client.global
.dispose()
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after org switch failed:", e))
// Org switch succeeded — refresh profile and providers independently (best-effort)
try {
@@ -1682,12 +1691,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
/**
* Handle openFile request from the webview — open a file in the VS Code editor.
* Resolves relative paths against the current session's directory (which may be
* a worktree path registered via setSessionDirectory), falling back to workspace root.
* Absolute paths (Unix `/…` or Windows `C:\…`) are used as-is.
*/
private handleOpenFile(filePath: string, line?: number, column?: number): void {
const absolute = /^(?:\/|[a-zA-Z]:[\\/])/.test(filePath)
const uri = absolute
const uri = isAbsolutePath(filePath)
? vscode.Uri.file(filePath)
: vscode.Uri.joinPath(vscode.Uri.file(this.getWorkspaceDirectory()), filePath)
: vscode.Uri.joinPath(vscode.Uri.file(this.getWorkspaceDirectory(this.currentSession?.id)), filePath)
vscode.workspace.openTextDocument(uri).then(
(doc) => {
const options: vscode.TextDocumentShowOptions = { preview: true }
@@ -1719,10 +1730,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
data: null,
})
await this.client.global.dispose().catch((e: unknown) =>
console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e),
)
await this.client.global
.dispose()
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e))
} catch (error) {
console.error("[Kilo New] KiloProvider: ❌ Logout failed:", error)
this.postMessage({
@@ -1730,8 +1740,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message: getErrorMessage(error) || "Failed to logout",
})
}
}
/**
@@ -1858,7 +1866,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Refresh provider and agent lists when the server signals a state disposal
if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
void this.reloadAfterAuthChange();
void this.reloadAfterAuthChange()
return
}
@@ -2118,14 +2126,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
},
this.cachedLegacyData?.settings,
)
// Dispose all instances after migration
// Reloading the data will be handled once the server replies with a global.disposed event
await this.client.global
.dispose()
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: global.dispose() after migration failed:", e))
// Only mark as completed if at least one item succeeded — if everything failed
// the user can still re-run migration via Settings → About.
const anySuccess = results.some((r) => r.status === "success")
if (anySuccess) {
await MigrationService.setMigrationStatus(this.extensionContext, "completed")
}
// Refresh providers so webview immediately sees the newly-migrated API keys
await this.fetchAndSendProviders()
this.postMessage({ type: "legacyMigrationComplete", results })
} catch (error) {
console.error("[Kilo New] KiloProvider: ❌ Migration failed", error)
@@ -4,10 +4,12 @@ import * as path from "path"
import type { KiloClient, Session, FileDiff } from "@kilocode/sdk/v2/client"
import type { KiloConnectionService } from "../services/cli-backend"
import { getErrorMessage } from "../kilo-provider-utils"
import { isAbsolutePath } from "../path-utils"
import { KiloProvider } from "../KiloProvider"
import { buildWebviewHtml } from "../utils"
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
import { WorktreeStateManager } from "./WorktreeStateManager"
import { chooseBaseBranch, normalizeBaseBranch } from "./base-branch"
import { GitStatsPoller, type WorktreePresenceResult } from "./GitStatsPoller"
import { GitOps, type ApplyConflict } from "./GitOps"
import { versionedName } from "./branch-name"
@@ -52,6 +54,10 @@ export class AgentManagerProvider implements vscode.Disposable {
private cachedWorktreeStats: Record<string, unknown> | undefined
private cachedLocalStats: Record<string, unknown> | undefined
private applyingWorktreeId: string | undefined
/** Session ID most recently loaded via a `loadMessages` message from the webview.
* Updated synchronously — unlike KiloProvider.currentSession which depends on
* an async `session.get` round-trip and can be stale during rapid tab switches. */
private activeSessionId: string | undefined
constructor(
private readonly extensionUri: vscode.Uri,
@@ -276,6 +282,12 @@ export class AgentManagerProvider implements vscode.Disposable {
return null
}
if (type === "agentManager.setDefaultBaseBranch") {
const branch = normalizeBaseBranch(msg.branch as string | undefined)
this.state?.setDefaultBaseBranch(branch)
this.pushState()
return null
}
if (type === "agentManager.requestExternalWorktrees") {
void this.onRequestExternalWorktrees()
return null
@@ -327,17 +339,37 @@ export class AgentManagerProvider implements vscode.Disposable {
if (type === "agentManager.openFile" && typeof msg.sessionId === "string" && typeof msg.filePath === "string") {
const line = typeof msg.line === "number" ? msg.line : undefined
this.openWorktreeFile(msg.sessionId, msg.filePath, line)
const column = typeof msg.column === "number" ? msg.column : undefined
this.openWorktreeFile(msg.sessionId, msg.filePath, line, column)
return null
}
// When switching sessions, show existing terminal if one is open
// Intercept generic "openFile" from DataBridge (markdown links, tool subtitle clicks)
// and route through worktree-aware resolution — but only for worktree sessions.
// Local sessions fall through to KiloProvider which resolves against workspace root.
// Uses activeSessionId (set synchronously by loadMessages) rather than
// KiloProvider.currentSession which can be stale during rapid tab switches.
if (type === "openFile" && typeof msg.filePath === "string") {
const sessionId = this.activeSessionId
const state = this.getStateManager()
if (sessionId && state?.directoryFor(sessionId)) {
const line = typeof msg.line === "number" ? msg.line : undefined
const column = typeof msg.column === "number" ? msg.column : undefined
this.openWorktreeFile(sessionId, msg.filePath, line, column)
return null
}
}
// Track the active session synchronously so worktree-aware file resolution
// uses the correct session even before KiloProvider's async session.get completes.
if (type === "loadMessages" && typeof msg.sessionID === "string") {
this.activeSessionId = msg.sessionID
this.terminalManager.syncOnSessionSwitch(msg.sessionID)
}
// After clearSession, re-register worktree sessions so SSE events keep flowing
// After clearSession, clear active tracking and re-register worktree sessions
if (type === "clearSession") {
this.activeSessionId = undefined
void Promise.resolve().then(() => {
if (!this.provider || !this.state) return
for (const id of this.state.worktreeSessionIds()) {
@@ -361,6 +393,31 @@ export class AgentManagerProvider implements vscode.Disposable {
// Shared helpers
// ---------------------------------------------------------------------------
/** Resolve the effective base branch using the configured default, explicit override, and existence check. */
private async resolveBaseBranch(
manager: WorktreeManager,
state: WorktreeStateManager,
explicit?: string,
): Promise<string | undefined> {
const configured = state.getDefaultBaseBranch()
if (!configured && !explicit) return undefined
const configuredExists = configured ? await manager.branchExists(configured) : false
const result = chooseBaseBranch({ explicit, configured, configuredExists })
if (result.stale) {
this.clearStaleDefaultBaseBranch(state, result.stale)
}
return result.branch
}
/** Reset a stale default base branch and notify the webview. */
private clearStaleDefaultBaseBranch(state: WorktreeStateManager, stale: string): void {
this.log(`Default base branch "${stale}" no longer exists, clearing`)
state.setDefaultBaseBranch(undefined)
this.pushState()
}
/** Create a git worktree on disk and register it in state. Returns null on failure. */
private async createWorktreeOnDisk(opts?: {
groupId?: string
@@ -386,11 +443,16 @@ export class AgentManagerProvider implements vscode.Disposable {
this.postToWebview({ type: "agentManager.worktreeSetup", status: "creating", message: "Creating git worktree..." })
// Resolve effective base branch using configured default
const effectiveBase = opts?.existingBranch
? undefined
: await this.resolveBaseBranch(manager, state, opts?.baseBranch)
let result: CreateWorktreeResult
try {
result = await manager.createWorktree({
prompt: opts?.name || "kilo",
baseBranch: opts?.baseBranch,
baseBranch: effectiveBase ?? opts?.baseBranch,
branchName: opts?.branchName,
existingBranch: opts?.existingBranch,
})
@@ -915,10 +977,23 @@ export class AgentManagerProvider implements vscode.Disposable {
try {
const result = await manager.listBranches()
const checkedOut = await manager.checkedOutBranches()
const filtered = result.branches.filter((b) => !checkedOut.has(b.name))
// Include isCheckedOut flag on each branch — let the webview decide how to filter
const branches = result.branches.map((b) => ({
...b,
isCheckedOut: checkedOut.has(b.name),
}))
// Validate configured default branch still exists
const state = this.getStateManager()
const configured = state?.getDefaultBaseBranch()
if (configured && !branches.some((b) => b.name === configured)) {
this.clearStaleDefaultBaseBranch(state!, configured)
}
this.postToWebview({
type: "agentManager.branches",
branches: filtered,
branches,
defaultBranch: result.defaultBranch,
})
} catch (error) {
@@ -1311,7 +1386,8 @@ export class AgentManagerProvider implements vscode.Disposable {
if (!manager) return
try {
const branch = await manager.currentBranch()
this.postToWebview({ type: "agentManager.repoInfo", branch })
const defaultBranch = await manager.defaultBranch()
this.postToWebview({ type: "agentManager.repoInfo", branch, defaultBranch })
} catch (error) {
this.log(`Failed to get current branch: ${error}`)
}
@@ -1384,6 +1460,7 @@ export class AgentManagerProvider implements vscode.Disposable {
sessionsCollapsed: state.getSessionsCollapsed(),
reviewDiffStyle: state.getReviewDiffStyle(),
isGitRepo: true,
defaultBaseBranch: state.getDefaultBaseBranch(),
})
this.statsPoller.setEnabled(worktrees.length > 0 || this.panel !== undefined)
@@ -1548,20 +1625,35 @@ export class AgentManagerProvider implements vscode.Disposable {
void vscode.commands.executeCommand("vscode.openFolder", uri, true)
}
/** Open a file from a worktree or local session in the VS Code editor. */
private openWorktreeFile(sessionId: string, relativePath: string, line?: number): void {
/** Open a file from a worktree or local session in the VS Code editor.
* Absolute paths (Unix `/…` or Windows `C:\…`) are opened directly.
* Relative paths are resolved against the session's worktree directory
* (or workspace root for local sessions) with symlink-traversal protection. */
private openWorktreeFile(sessionId: string, filePath: string, line?: number, column?: number): void {
if (isAbsolutePath(filePath)) {
const uri = vscode.Uri.file(filePath)
const options: vscode.TextDocumentShowOptions = { preview: true }
if (line !== undefined && line > 0) {
const col = column !== undefined && column > 0 ? column - 1 : 0
options.selection = new vscode.Range(new vscode.Position(line - 1, col), new vscode.Position(line - 1, col))
}
vscode.workspace.openTextDocument(uri).then(
(doc) => vscode.window.showTextDocument(doc, options),
(err) => console.error("[Kilo New] AgentManagerProvider: Failed to open file:", uri.fsPath, err),
)
return
}
const state = this.getStateManager()
if (!state) return
const session = state.getSession(sessionId)
if (!session) return
const base = session.worktreeId ? state.getWorktree(session.worktreeId)?.path : this.getWorkspaceRoot()
const base = session?.worktreeId ? state.getWorktree(session.worktreeId)?.path : this.getWorkspaceRoot()
if (!base) return
// Resolve real paths to prevent symlink traversal and normalize for
// consistent comparison on both Unix and Windows.
let resolved: string
try {
const root = fs.realpathSync(base)
resolved = fs.realpathSync(path.resolve(base, relativePath))
resolved = fs.realpathSync(path.resolve(base, filePath))
// Directory-boundary check: append path.sep so "/foo/bar" won't match "/foo/bar2/..."
if (resolved !== root && !resolved.startsWith(root + path.sep)) return
} catch (err) {
@@ -1569,11 +1661,13 @@ export class AgentManagerProvider implements vscode.Disposable {
return
}
const uri = vscode.Uri.file(resolved)
const options: vscode.TextDocumentShowOptions = { preview: true }
const target = Math.max(1, Math.floor(line ?? 1))
const pos = new vscode.Position(target - 1, 0)
const selection = new vscode.Range(pos, pos)
const col = column !== undefined && column > 0 ? column - 1 : 0
const pos = new vscode.Position(target - 1, col)
options.selection = new vscode.Range(pos, pos)
vscode.workspace.openTextDocument(uri).then(
(doc) => vscode.window.showTextDocument(doc, { preview: true, selection }),
(doc) => vscode.window.showTextDocument(doc, options),
(err) => console.error("[Kilo New] AgentManagerProvider: Failed to open file:", uri.fsPath, err),
)
}
@@ -62,6 +62,7 @@ export class GitOps {
return this.runGit(args, cwd)
}
/** Return the name of the currently checked-out branch, or `"HEAD"` if detached. */
async currentBranch(cwd: string): Promise<string> {
return this.raw(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "")
}
@@ -87,6 +88,7 @@ export class GitOps {
return "origin"
}
/** Resolve the upstream tracking ref for `branch`, or `undefined` if none is set. Note: the `@{upstream}` check uses the current HEAD, not `branch`. */
async resolveTrackingBranch(cwd: string, branch: string): Promise<string | undefined> {
const upstream = await this.raw(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd).catch(() => "")
if (upstream) return upstream
@@ -139,6 +141,7 @@ export class GitOps {
return job
}
/** Return the set of worktree paths for the repo, excluding bare entries. */
async listWorktreePaths(cwd: string): Promise<Set<string>> {
const raw = await this.raw(["worktree", "list", "--porcelain"], cwd)
const paths = new Set<string>()
@@ -151,12 +154,13 @@ export class GitOps {
/**
* Compute working-tree stats (staged + unstaged + untracked) without requiring
* a remote or base branch — mirrors the superset approach of running
* `git diff --numstat` and `git ls-files --others`.
* a remote or base branch. Combines `git diff HEAD --numstat` for tracked
* changes with `git ls-files --others` for new files.
*
* Returns aggregate file count, additions, and deletions across the working tree.
*/
async workingTreeStats(cwd: string): Promise<{ files: number; additions: number; deletions: number }> {
// Staged + unstaged changes relative to HEAD (like superset's dual
// git diff --cached --numstat + git diff --numstat, combined).
// Single diff against HEAD captures both staged and unstaged changes.
const [numstat, untracked] = await Promise.all([
this.raw(["diff", "HEAD", "--numstat"], cwd).catch(() => ""),
this.raw(["ls-files", "--others", "--exclude-standard"], cwd).catch(() => ""),
@@ -177,8 +181,8 @@ export class GitOps {
)
: { files: 0, additions: 0, deletions: 0 }
// Count lines in untracked files as additions (like superset's
// applyUntrackedLineCount). Cap at 1MB to avoid reading huge binaries.
// Count lines in untracked files as additions. Cap at 1MB to avoid
// reading large binary files into memory.
if (!untracked) return tracked
const paths = untracked.split("\n").filter((line) => line.trim())
@@ -205,9 +209,9 @@ export class GitOps {
}
/**
* Count commits ahead and behind in a single `rev-list --left-right --count`
* call (like superset's approach). Falls back through upstream → remote/branch
* → remote/parentBranch → parentBranch.
* Count commits ahead and behind using `rev-list --left-right --count`.
* Tries the best available ref in order: upstream tracking branch →
* remote/branch → remote/parentBranch → local parentBranch.
*/
async aheadBehind(cwd: string, parentBranch: string): Promise<{ ahead: number; behind: number }> {
const upstream = await this.raw(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd).catch(() => "")
@@ -241,6 +245,10 @@ export class GitOps {
return { ahead, behind }
}
/**
* Build a binary-safe patch of all working-tree changes relative to the
* merge-base with `baseBranch`. Optionally scoped to `selectedFiles`.
*/
async buildWorktreePatch(sourcePath: string, baseBranch: string, selectedFiles?: string[]): Promise<string> {
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), "kilo-apply-"))
const index = nodePath.join(tmp, "index")
@@ -8,10 +8,10 @@
import * as path from "path"
import * as fs from "fs"
import * as cp from "child_process"
import simpleGit, { type SimpleGit } from "simple-git"
import { generateBranchName, sanitizeBranchName } from "./branch-name"
import type { GitOps } from "./GitOps"
import { execWithShellEnv } from "./shell-env"
import {
parsePRUrl,
localBranchName,
@@ -37,10 +37,23 @@ export interface WorktreeInfo {
sessionId?: string
}
export type StartPointSource = "remote" | "local-tracking" | "local-branch" | "fallback"
export interface StartPointResult {
ref: string
branch: string
source: StartPointSource
warning?: string
}
export type WorktreeProgressStep = "syncing" | "verifying" | "fetching" | "creating"
export interface CreateWorktreeResult {
branch: string
path: string
parentBranch: string
startPointSource: StartPointSource
startPointWarning?: string
}
export interface ExternalWorktreeItem {
@@ -97,6 +110,7 @@ export class WorktreeManager {
existingBranch?: string
baseBranch?: string
branchName?: string
onProgress?: (step: WorktreeProgressStep, message: string, detail?: string) => void
}): Promise<CreateWorktreeResult> {
return this.withGitLock(() => this.createWorktreeImpl(params))
}
@@ -106,6 +120,7 @@ export class WorktreeManager {
existingBranch?: string
baseBranch?: string
branchName?: string
onProgress?: (step: WorktreeProgressStep, message: string, detail?: string) => void
}): Promise<CreateWorktreeResult> {
const repo = await this.git.checkIsRepo()
if (!repo)
@@ -113,20 +128,39 @@ export class WorktreeManager {
"This folder is not a git repository. Initialize a repository or open a git project to use worktrees.",
)
// Git LFS Pre-flight Check
if (await this.repoUsesLfs()) {
if (!(await this.checkLfsAvailable())) {
throw new Error(
"This repository uses Git LFS, but git-lfs was not found. Please install Git LFS to use this repository.",
)
}
}
await this.ensureDir()
await this.ensureGitExclude()
const parent = params.baseBranch || (await this.currentBranch())
// Resolve start point (parent branch)
let parent: string
let startPoint: StartPointResult | undefined
// Validate baseBranch exists if explicitly provided
if (params.baseBranch) {
const exists = await this.branchExists(params.baseBranch)
if (!exists) throw new Error(`Base branch "${params.baseBranch}" does not exist`)
// Check if the base branch is a remote-only branch and fetch it
const branches = await this.git.branch()
if (!branches.all.includes(params.baseBranch) && branches.all.includes(`remotes/origin/${params.baseBranch}`)) {
await this.git.fetch("origin", params.baseBranch)
if (params.existingBranch) {
// Existing branch provided directly
parent = params.existingBranch
startPoint = {
ref: params.existingBranch,
branch: params.existingBranch,
source: "local-branch",
}
} else {
// Resolve best start point for new branch
const requestedBase = params.baseBranch || (await this.defaultBranch())
params.onProgress?.("verifying", `Resolving start point: ${requestedBase}`)
startPoint = await this.resolveStartPoint(requestedBase, params.onProgress, {
allowFallback: !params.baseBranch, // Only fallback if user didn't explicitly request a specific base
})
parent = startPoint.branch
}
const sanitized = params.branchName ? sanitizeBranchName(params.branchName) : undefined
@@ -145,12 +179,15 @@ export class WorktreeManager {
await this.removeWorktreeImpl(worktreePath)
}
params.onProgress?.("creating", `Creating worktree for ${branch}...`)
// Dereference to commit SHA to prevent upstream tracking for new branches
const startRef = params.existingBranch ? undefined : `${startPoint.ref}^{commit}`
try {
const args = params.existingBranch
? ["worktree", "add", worktreePath, branch]
: params.baseBranch
? ["worktree", "add", "-b", branch, worktreePath, params.baseBranch]
: ["worktree", "add", "-b", branch, worktreePath]
: ["worktree", "add", "-b", branch, worktreePath, startRef!]
await this.git.raw(args)
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
@@ -167,14 +204,20 @@ export class WorktreeManager {
branch = `${branch}-${Date.now()}`
const retryDir = branch.replace(/\//g, "-")
worktreePath = path.join(this.dir, retryDir)
const retryArgs = params.baseBranch
? ["worktree", "add", "-b", branch, worktreePath, params.baseBranch]
: ["worktree", "add", "-b", branch, worktreePath]
const retryArgs = params.existingBranch
? ["worktree", "add", worktreePath, branch]
: ["worktree", "add", "-b", branch, worktreePath, startRef!]
await this.git.raw(retryArgs)
}
this.log(`Created worktree: ${worktreePath} (branch: ${branch}, base: ${parent})`)
return { branch, path: worktreePath, parentBranch: parent }
return {
branch,
path: worktreePath,
parentBranch: parent,
startPointSource: startPoint.source,
startPointWarning: startPoint.warning,
}
}
/**
@@ -385,6 +428,125 @@ export class WorktreeManager {
}
}
async resolveStartPoint(
branch: string,
onProgress?: (step: WorktreeProgressStep, message: string, detail?: string) => void,
opts?: { allowFallback?: boolean },
): Promise<StartPointResult> {
const { allowFallback = true } = opts || {}
// 1. Remote fetch
if (await this.hasOriginRemote()) {
onProgress?.("fetching", `Fetching origin/${branch}...`)
try {
await this.git.fetch("origin", branch)
if (await this.refExistsLocally(`origin/${branch}`)) {
return {
ref: `origin/${branch}`,
branch: branch,
source: "remote",
}
}
} catch (e) {
this.log(`Failed to fetch origin/${branch}: ${e}`)
}
}
// 2. Stale local tracking ref (offline fallback)
if (await this.refExistsLocally(`origin/${branch}`)) {
return {
ref: `origin/${branch}`,
branch: branch,
source: "local-tracking",
warning: "Used stale remote tracking branch (fetch failed)",
}
}
// 3. Local branch
if (await this.refExistsLocally(branch)) {
return {
ref: branch,
branch: branch,
source: "local-branch",
}
}
// 4. Derived fallback
if (allowFallback) {
const fallbacks = await this.derivedFallbackBranches(branch)
for (const fallback of fallbacks) {
if (fallback === branch) continue // already tried
try {
const res = await this.resolveStartPoint(fallback, onProgress, { allowFallback: false })
return {
...res,
source: "fallback",
warning: `Branch "${branch}" not found, falling back to "${fallback}"`,
}
} catch {
// continue
}
}
}
throw new Error(`Could not resolve start point for branch "${branch}"`)
}
async hasOriginRemote(): Promise<boolean> {
try {
const remotes = await this.git.getRemotes()
return remotes.some((r) => r.name === "origin")
} catch {
return false
}
}
async refExistsLocally(ref: string): Promise<boolean> {
try {
await this.git.raw(["rev-parse", "--verify", `${ref}^{commit}`])
return true
} catch {
return false
}
}
async derivedFallbackBranches(requested: string): Promise<string[]> {
const defaults = []
try {
defaults.push(await this.defaultBranch())
} catch {}
return defaults
}
async repoUsesLfs(): Promise<boolean> {
// Check .git/lfs/ directory
const gitDir = await this.resolveGitDir()
if (fs.existsSync(path.join(gitDir, "lfs"))) return true
// Check .gitattributes
try {
const attributes = await fs.promises.readFile(path.join(this.root, ".gitattributes"), "utf-8")
if (attributes.includes("filter=lfs")) return true
} catch {}
// Check .git/info/attributes
try {
const infoAttributes = await fs.promises.readFile(path.join(gitDir, "info", "attributes"), "utf-8")
if (infoAttributes.includes("filter=lfs")) return true
} catch {}
return false
}
async checkLfsAvailable(): Promise<boolean> {
try {
await execWithShellEnv("git", ["lfs", "version"], { cwd: this.root, timeout: 5000 })
return true
} catch {
return false
}
}
async currentBranch(): Promise<string> {
if (this.ops) {
const branch = await this.ops.currentBranch(this.root)
@@ -404,28 +566,32 @@ export class WorktreeManager {
}
async defaultBranch(): Promise<string> {
if (this.ops) {
const remote = await this.ops.resolveRemote(this.root)
const resolved = await this.ops.resolveDefaultBranch(this.root)
if (resolved) {
const prefix = `${remote}/`
return resolved.startsWith(prefix) ? resolved.slice(prefix.length) : resolved
}
} else {
try {
const head = await this.git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"])
const match = head.trim().match(/refs\/remotes\/origin\/(.+)$/)
if (match) return match[1]
} catch {}
// 1. Try symbolic-ref
try {
const head = await this.git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"])
const match = head.trim().match(/refs\/remotes\/origin\/(.+)$/)
if (match) return match[1]
} catch (e) {
this.log(`defaultBranch: symbolic-ref failed: ${e}`)
}
// 2. Try current branch (if not detached)
try {
const branches = await this.git.branch()
if (branches.all.includes("main")) return "main"
if (branches.all.includes("master")) return "master"
} catch {}
const current = await this.currentBranch()
if (current && current !== "HEAD") return current
} catch (e) {
this.log(`defaultBranch: currentBranch failed: ${e}`)
}
return "main"
// 3. Try first local branch
try {
const branches = await this.git.branchLocal()
if (branches.all.length > 0) return branches.all[0]
} catch (e) {
this.log(`defaultBranch: branchLocal failed: ${e}`)
}
throw new Error("Could not determine default branch")
}
// ---------------------------------------------------------------------------
@@ -561,13 +727,9 @@ export class WorktreeManager {
}
}
private exec(cmd: string, args: string[], timeout = 120000): Promise<string> {
return new Promise((resolve, reject) => {
cp.execFile(cmd, args, { cwd: this.root, timeout, encoding: "utf-8" }, (error, stdout) => {
if (error) reject(error)
else resolve(stdout)
})
})
private async exec(cmd: string, args: string[], timeout = 120000): Promise<string> {
const { stdout } = await execWithShellEnv(cmd, args, { cwd: this.root, timeout })
return stdout
}
private async gitExec(args: string[]): Promise<void> {
@@ -37,6 +37,7 @@ interface StateFile {
tabOrder?: Record<string, string[]>
sessionsCollapsed?: boolean
reviewDiffStyle?: "unified" | "split"
defaultBaseBranch?: string
}
const STATE_FILE = "agent-manager.json"
@@ -55,6 +56,7 @@ export class WorktreeStateManager {
private tabOrder: Record<string, string[]> = {}
private collapsed = false
private reviewDiffStyle: "unified" | "split" = "unified"
private defaultBase: string | undefined
private readonly log: (msg: string) => void
private saving: Promise<void> | undefined
private pendingSave = false
@@ -245,6 +247,19 @@ export class WorktreeStateManager {
void this.save()
}
// ---------------------------------------------------------------------------
// Default base branch
// ---------------------------------------------------------------------------
getDefaultBaseBranch(): string | undefined {
return this.defaultBase
}
setDefaultBaseBranch(value: string | undefined): void {
this.defaultBase = value
void this.save()
}
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
@@ -271,6 +286,7 @@ export class WorktreeStateManager {
if (data.reviewDiffStyle === "split") {
this.reviewDiffStyle = "split"
}
this.defaultBase = data.defaultBaseBranch
this.log(`Loaded state: ${this.worktrees.size} worktrees, ${this.sessions.size} sessions`)
} catch (error) {
const code = (error as NodeJS.ErrnoException).code
@@ -343,6 +359,9 @@ export class WorktreeStateManager {
if (this.reviewDiffStyle === "split") {
data.reviewDiffStyle = "split"
}
if (this.defaultBase) {
data.defaultBaseBranch = this.defaultBase
}
try {
const dir = path.dirname(this.file)
@@ -0,0 +1,34 @@
/**
* Pure logic for resolving the effective base branch.
*
* Priority chain:
* 1. Explicit (per-worktree override, from dialog)
* 2. Configured default (persisted in agent-manager.json), if it still exists
* 3. Auto-detect (undefined → WorktreeManager.defaultBranch() at call site)
*/
/** Trim whitespace, return undefined for empty/blank. */
export function normalizeBaseBranch(value: string | undefined): string | undefined {
if (!value) return undefined
const trimmed = value.trim()
return trimmed.length > 0 ? trimmed : undefined
}
/**
* Pick the effective base branch given an explicit override, a configured
* default, and whether that configured branch still exists in the repo.
*
* Returns `{ branch }` with the winner, and optionally `{ stale }` when the
* configured branch no longer exists (so the caller can clear it).
*/
export function chooseBaseBranch(opts: { explicit?: string; configured?: string; configuredExists?: boolean }): {
branch?: string
stale?: string
} {
if (opts.explicit) return { branch: opts.explicit }
if (opts.configured) {
if (opts.configuredExists) return { branch: opts.configured }
return { stale: opts.configured }
}
return {}
}
@@ -4,6 +4,7 @@ export interface BranchListItem {
isRemote: boolean
isDefault: boolean
lastCommitDate?: string
isCheckedOut?: boolean
}
export interface PRUrlParts {
@@ -0,0 +1,173 @@
/**
* macOS shell environment PATH resolution.
*
* When VS Code is launched from Finder, Spotlight, or the Dock, the extension
* host inherits a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) that excludes
* directories added by package managers (homebrew, nvm, pipx, etc.) and shell
* profiles (.zshrc, .bash_profile).
*
* This module lazily resolves the user's real PATH by spawning a login shell
* and caches the result. The fix is applied on first ENOENT and persisted to
* process.env.PATH so all subsequent child_process calls benefit.
*/
import { type ExecFileOptionsWithStringEncoding, execFile } from "child_process"
import * as os from "os"
import { promisify } from "util"
const run = promisify(execFile)
// Environment variable keys match: letters, digits, underscores, starting with a non-digit.
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*=/
let cached: Record<string, string> | null = null
let cacheTime = 0
let wasFallback = false
const TTL = 60_000
const FALLBACK_TTL = 10_000
/** In-flight fix promise so concurrent ENOENT callers wait on the same resolution. */
let fixing: Promise<boolean> | null = null
let fixed = false
/**
* Parse `env` output, handling multiline variable values correctly.
*
* A new entry starts when a line matches `KEY=value` (KEY is a valid
* environment variable name). Lines that don't match are continuations
* of the previous value.
*/
function parseEnvOutput(stdout: string): Record<string, string> {
const env: Record<string, string> = {}
let key: string | null = null
let value = ""
for (const line of stdout.split("\n")) {
const match = ENV_KEY_RE.exec(line)
if (match) {
if (key) env[key] = value
const idx = match[0].length - 1 // position of '='
key = line.substring(0, idx)
value = line.substring(idx + 1)
} else if (key) {
value += "\n" + line
}
}
if (key) env[key] = value
return env
}
/**
* Spawn the user's login shell to capture environment variables (primarily PATH).
* Uses `-lc` (login + command) — avoids `-i` (interactive) to skip TTY prompts.
* Results are cached for 1 minute (10 seconds when the fallback was used).
*/
export async function getShellEnvironment(): Promise<Record<string, string>> {
const now = Date.now()
const ttl = wasFallback ? FALLBACK_TTL : TTL
if (cached && now - cacheTime < ttl) return { ...cached }
const shell = process.env.SHELL || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash")
try {
const { stdout } = await run(shell, ["-lc", "env"], {
timeout: 10_000,
env: { ...process.env, HOME: os.homedir() },
})
const env = parseEnvOutput(stdout)
cached = env
cacheTime = now
wasFallback = false
return { ...env }
} catch (error) {
console.warn(`[shell-env] Failed to get shell environment: ${error}. Falling back to process.env`)
const env: Record<string, string> = {}
for (const [key, value] of Object.entries(process.env)) {
if (typeof value === "string") env[key] = value
}
cached = env
cacheTime = now
wasFallback = true
return { ...env }
}
}
/**
* Attempt to resolve the shell environment and patch process.env.PATH.
* Returns true if PATH was actually changed, false otherwise.
*/
async function resolvePath(): Promise<boolean> {
const original = process.env.PATH
const env = await getShellEnvironment()
if (env.PATH && env.PATH !== original) {
process.env.PATH = env.PATH
console.log("[shell-env] Patched process.env.PATH for GUI app")
return true
}
// Shell env was a fallback or PATH didn't change — resolution didn't help
return false
}
/**
* Execute a command, retrying once with shell environment on ENOENT.
*
* On macOS GUI launches, binaries installed by homebrew / nvm / etc. are not
* on the inherited PATH. When the first exec fails with ENOENT (command not
* found), this function resolves the user's login shell environment, patches
* process.env.PATH permanently, and retries the command.
*
* Concurrent callers that hit ENOENT share a single resolution promise so
* none are rejected prematurely.
*/
export async function execWithShellEnv(
cmd: string,
args: string[],
options?: Omit<ExecFileOptionsWithStringEncoding, "encoding">,
): Promise<{ stdout: string; stderr: string }> {
try {
return await run(cmd, args, { ...options, encoding: "utf8" })
} catch (error) {
if (
process.platform !== "darwin" ||
!(error instanceof Error) ||
!("code" in error) ||
(error as NodeJS.ErrnoException).code !== "ENOENT"
) {
throw error
}
// Already resolved and PATH was actually changed — no point retrying resolution.
// Just retry with the (already-patched) process.env.
if (fixed) {
return await run(cmd, args, { ...options, encoding: "utf8" })
}
// If another caller is already resolving, wait for it then retry.
if (fixing) {
await fixing
return await run(cmd, args, { ...options, encoding: "utf8" })
}
console.log(`[shell-env] "${cmd}" not found, resolving shell environment`)
fixing = resolvePath()
try {
fixed = await fixing
} finally {
fixing = null
}
return await run(cmd, args, { ...options, encoding: "utf8" })
}
}
/** Clear the cached environment (for tests). */
export function clearShellEnvCache(): void {
cached = null
cacheTime = 0
wasFallback = false
fixing = null
fixed = false
}
+27
View File
@@ -0,0 +1,27 @@
/**
* Check whether a file path is absolute.
*
* Handles both Unix (`/foo/bar`) and Windows (`C:\foo`, `D:/bar`) conventions.
* UNC paths (`\\server\share`) are also treated as absolute.
*
* Returns false for relative paths, bare filenames, empty strings, and
* protocol-prefixed strings like `https://…`.
*/
export function isAbsolutePath(filePath: string): boolean {
if (!filePath) return false
// Unix absolute
if (filePath.charCodeAt(0) === 47 /* / */) return true
// Windows drive letter: C:\ or C:/
if (
filePath.length >= 3 &&
filePath.charCodeAt(1) === 58 /* : */ &&
(filePath.charCodeAt(2) === 92 /* \ */ || filePath.charCodeAt(2) === 47) /* / */ &&
((filePath.charCodeAt(0) >= 65 && filePath.charCodeAt(0) <= 90) /* A-Z */ ||
(filePath.charCodeAt(0) >= 97 && filePath.charCodeAt(0) <= 122)) /* a-z */
)
return true
// Windows UNC path: \\server\share
if (filePath.length >= 2 && filePath.charCodeAt(0) === 92 /* \ */ && filePath.charCodeAt(1) === 92 /* \ */)
return true
return false
}
@@ -25,6 +25,8 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/review-annotations.ts"),
path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"),
path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/BranchSelect.tsx"),
path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"),
]
const TSX_FILE = TSX_FILES[0]
const PROVIDER_FILE = path.join(ROOT, "src/agent-manager/AgentManagerProvider.ts")
@@ -170,6 +172,7 @@ describe("Agent Manager Provider — onMessage routing", () => {
"agentManager.requestRepoInfo",
"agentManager.requestState",
"agentManager.setTabOrder",
"agentManager.setDefaultBaseBranch",
]
for (const msg of expected) {
expect(text, `onMessage should handle "${msg}"`).toContain(msg)
@@ -385,6 +388,50 @@ describe("KiloProvider — pending session refresh on reconnect", () => {
})
})
// ---------------------------------------------------------------------------
// handleChangeDefaultBaseBranch — listener leak fix
// ---------------------------------------------------------------------------
describe("Agent Manager — dialog listener cleanup", () => {
const tsx = fs.readFileSync(TSX_FILE, "utf-8")
/**
* Regression: handleChangeDefaultBaseBranch subscribes to vscode.onMessage
* for branch data. Previously unsub() was only called inside selectBranch()
* and the Escape keydown handler. If the dialog closed via backdrop click or
* external dialog.close(), the listener leaked and stacked on every reopen.
*
* The fix ties unsub() to Solid's onCleanup inside the dialog.show() render
* function so it always disposes regardless of how the dialog closes.
*/
it("handleChangeDefaultBaseBranch uses onCleanup(unsub) inside dialog.show", () => {
const fnStart = tsx.indexOf("const handleChangeDefaultBaseBranch")
expect(fnStart, "handleChangeDefaultBaseBranch must exist").toBeGreaterThan(-1)
// Grab the function body (enough to cover the dialog.show callback)
const snippet = tsx.slice(fnStart, fnStart + 2000)
// The dialog.show callback must register onCleanup(unsub)
const showIdx = snippet.indexOf("dialog.show(")
expect(showIdx, "dialog.show() call must exist").toBeGreaterThan(-1)
const afterShow = snippet.slice(showIdx)
expect(afterShow, "onCleanup(unsub) must be inside dialog.show callback").toContain("onCleanup(unsub)")
})
it("selectBranch does not manually call unsub (handled by onCleanup)", () => {
const fnStart = tsx.indexOf("const handleChangeDefaultBaseBranch")
const snippet = tsx.slice(fnStart, fnStart + 2000)
// Find the selectBranch function body
const selStart = snippet.indexOf("const selectBranch")
expect(selStart, "selectBranch must exist").toBeGreaterThan(-1)
const selEnd = snippet.indexOf("}", selStart + 50)
const selBody = snippet.slice(selStart, selEnd + 1)
expect(selBody, "selectBranch should not call unsub() directly").not.toContain("unsub()")
})
})
describe("SetupScriptRunner — task execution model", () => {
const runner = fs.readFileSync(SETUP_SCRIPT_RUNNER_FILE, "utf-8")
@@ -0,0 +1,59 @@
import { describe, it, expect } from "bun:test"
import { normalizeBaseBranch, chooseBaseBranch } from "../../src/agent-manager/base-branch"
describe("normalizeBaseBranch", () => {
it("returns undefined for undefined input", () => {
expect(normalizeBaseBranch(undefined)).toBeUndefined()
})
it("returns undefined for empty string", () => {
expect(normalizeBaseBranch("")).toBeUndefined()
})
it("returns undefined for whitespace-only string", () => {
expect(normalizeBaseBranch(" ")).toBeUndefined()
expect(normalizeBaseBranch("\t\n")).toBeUndefined()
})
it("trims whitespace from valid input", () => {
expect(normalizeBaseBranch(" main ")).toBe("main")
expect(normalizeBaseBranch(" develop\n")).toBe("develop")
})
it("returns the value as-is when already trimmed", () => {
expect(normalizeBaseBranch("main")).toBe("main")
expect(normalizeBaseBranch("feature/branch")).toBe("feature/branch")
})
})
describe("chooseBaseBranch", () => {
it("prefers explicit over configured", () => {
const result = chooseBaseBranch({ explicit: "release", configured: "develop", configuredExists: true })
expect(result).toEqual({ branch: "release" })
})
it("uses configured when it exists and no explicit is given", () => {
const result = chooseBaseBranch({ configured: "develop", configuredExists: true })
expect(result).toEqual({ branch: "develop" })
})
it("marks configured as stale when it no longer exists", () => {
const result = chooseBaseBranch({ configured: "old-branch", configuredExists: false })
expect(result).toEqual({ stale: "old-branch" })
})
it("falls back to auto-detect (undefined) when nothing is configured", () => {
const result = chooseBaseBranch({})
expect(result).toEqual({})
})
it("falls back to auto-detect when configured is undefined", () => {
const result = chooseBaseBranch({ configured: undefined, configuredExists: false })
expect(result).toEqual({})
})
it("explicit takes priority even when configured is stale", () => {
const result = chooseBaseBranch({ explicit: "main", configured: "deleted", configuredExists: false })
expect(result).toEqual({ branch: "main" })
})
})
@@ -0,0 +1,126 @@
import { describe, it, expect } from "bun:test"
import type { AssistantMessage } from "@kilocode/sdk/v2"
import {
unwrapError,
parseAssistantError,
isUnauthorizedPaidModelError,
isUnauthorizedPromotionLimitError,
} from "../../webview-ui/src/utils/errorUtils"
type AssistantError = AssistantMessage["error"]
describe("unwrapError", () => {
it("returns plain string messages unchanged", () => {
expect(unwrapError("something went wrong")).toBe("something went wrong")
})
it("extracts message from JSON error object with error.message", () => {
const input = JSON.stringify({ error: { message: "rate limit exceeded" } })
expect(unwrapError(input)).toBe("rate limit exceeded")
})
it("returns original message for malformed JSON", () => {
expect(unwrapError("{not valid json")).toBe("{not valid json")
})
it("strips leading 'Error: ' prefix before parsing", () => {
const json = JSON.stringify({ message: "connection refused" })
expect(unwrapError(`Error: ${json}`)).toBe("connection refused")
})
})
describe("parseAssistantError", () => {
it("returns null for null input", () => {
expect(parseAssistantError(null)).toBeNull()
})
it("returns null for undefined input", () => {
expect(parseAssistantError(undefined)).toBeNull()
})
it("returns null for non-APIError (e.g. MessageAbortedError)", () => {
const error: AssistantError = { name: "MessageAbortedError", data: { message: "aborted" } }
expect(parseAssistantError(error)).toBeNull()
})
it("returns null when APIError has no data", () => {
// Simulate a malformed error where data is missing at runtime
const error = { name: "APIError" } as unknown as AssistantError
expect(parseAssistantError(error)).toBeNull()
})
it("extracts statusCode and message from APIError data", () => {
const error: AssistantError = {
name: "APIError",
data: { statusCode: 401, message: "Unauthorized", isRetryable: false },
}
const result = parseAssistantError(error)
expect(result).toEqual({ statusCode: 401, code: undefined, message: "Unauthorized" })
})
it("extracts code from responseBody JSON with error.code", () => {
const responseBody = JSON.stringify({ error: { code: "PAID_MODEL_AUTH_REQUIRED" } })
const error: AssistantError = {
name: "APIError",
data: { statusCode: 401, message: "Unauthorized", isRetryable: false, responseBody },
}
const result = parseAssistantError(error)
expect(result).toEqual({ statusCode: 401, code: "PAID_MODEL_AUTH_REQUIRED", message: "Unauthorized" })
})
it("extracts code from responseBody JSON with top-level code", () => {
const responseBody = JSON.stringify({ code: "PROMOTION_MODEL_LIMIT_REACHED" })
const error: AssistantError = {
name: "APIError",
data: { statusCode: 429, message: "Too Many Requests", isRetryable: false, responseBody },
}
const result = parseAssistantError(error)
expect(result).toEqual({ statusCode: 429, code: "PROMOTION_MODEL_LIMIT_REACHED", message: "Too Many Requests" })
})
it("handles invalid responseBody JSON gracefully", () => {
const error: AssistantError = {
name: "APIError",
data: { statusCode: 500, message: "Server Error", isRetryable: false, responseBody: "not json" },
}
const result = parseAssistantError(error)
expect(result).toEqual({ statusCode: 500, code: undefined, message: "Server Error" })
})
it("handles missing responseBody", () => {
const error: AssistantError = {
name: "APIError",
data: { statusCode: 403, message: "Forbidden", isRetryable: false },
}
const result = parseAssistantError(error)
expect(result).toEqual({ statusCode: 403, code: undefined, message: "Forbidden" })
})
})
describe("isUnauthorizedPaidModelError", () => {
it("returns true for 401 + PAID_MODEL_AUTH_REQUIRED", () => {
expect(isUnauthorizedPaidModelError({ statusCode: 401, code: "PAID_MODEL_AUTH_REQUIRED" })).toBe(true)
})
it("returns false for 401 + different code", () => {
expect(isUnauthorizedPaidModelError({ statusCode: 401, code: "SOMETHING_ELSE" })).toBe(false)
})
it("returns false for null input", () => {
expect(isUnauthorizedPaidModelError(null)).toBe(false)
})
})
describe("isUnauthorizedPromotionLimitError", () => {
it("returns true for 401 + PROMOTION_MODEL_LIMIT_REACHED", () => {
expect(isUnauthorizedPromotionLimitError({ statusCode: 401, code: "PROMOTION_MODEL_LIMIT_REACHED" })).toBe(true)
})
it("returns true for 429 + PROMOTION_MODEL_LIMIT_REACHED", () => {
expect(isUnauthorizedPromotionLimitError({ statusCode: 429, code: "PROMOTION_MODEL_LIMIT_REACHED" })).toBe(true)
})
it("returns false for null input", () => {
expect(isUnauthorizedPromotionLimitError(null)).toBe(false)
})
})
@@ -0,0 +1,165 @@
import { describe, expect, it } from "bun:test"
import { isAbsolutePath } from "../../src/path-utils"
describe("isAbsolutePath", () => {
// ── Unix absolute paths ──────────────────────────────────────────────
describe("Unix absolute paths", () => {
it("detects root /", () => {
expect(isAbsolutePath("/")).toBe(true)
})
it("detects simple path", () => {
expect(isAbsolutePath("/foo/bar")).toBe(true)
})
it("detects deep path", () => {
expect(isAbsolutePath("/Users/marius/Documents/project/src/index.ts")).toBe(true)
})
it("detects path with spaces", () => {
expect(isAbsolutePath("/home/user/my project/file.ts")).toBe(true)
})
it("detects path with dots", () => {
expect(isAbsolutePath("/home/user/../other/./file.ts")).toBe(true)
})
it("detects path with special chars", () => {
expect(isAbsolutePath("/tmp/@scope/pkg/index.js")).toBe(true)
})
})
// ── Windows drive-letter paths ───────────────────────────────────────
describe("Windows drive-letter paths", () => {
it("detects uppercase drive with backslash", () => {
expect(isAbsolutePath("C:\\Users\\marius\\file.ts")).toBe(true)
})
it("detects uppercase drive with forward slash", () => {
expect(isAbsolutePath("C:/Users/marius/file.ts")).toBe(true)
})
it("detects lowercase drive letter", () => {
expect(isAbsolutePath("c:\\users\\file.ts")).toBe(true)
})
it("detects various drive letters", () => {
expect(isAbsolutePath("D:\\data")).toBe(true)
expect(isAbsolutePath("Z:/files")).toBe(true)
expect(isAbsolutePath("e:\\temp")).toBe(true)
})
it("detects drive root with backslash", () => {
expect(isAbsolutePath("C:\\")).toBe(true)
})
it("detects drive root with forward slash", () => {
expect(isAbsolutePath("C:/")).toBe(true)
})
it("detects mixed separators", () => {
expect(isAbsolutePath("C:\\Users/marius\\project/file.ts")).toBe(true)
})
})
// ── Windows UNC paths ────────────────────────────────────────────────
describe("Windows UNC paths", () => {
it("detects simple UNC path", () => {
expect(isAbsolutePath("\\\\server\\share")).toBe(true)
})
it("detects deeply nested UNC path", () => {
expect(isAbsolutePath("\\\\server\\share\\folder\\file.ts")).toBe(true)
})
it("detects minimal UNC path", () => {
expect(isAbsolutePath("\\\\ab")).toBe(true)
})
})
// ── Relative paths (should return false) ─────────────────────────────
describe("relative paths", () => {
it("rejects bare filename with extension", () => {
expect(isAbsolutePath("file.ts")).toBe(false)
})
it("rejects relative path with directory", () => {
expect(isAbsolutePath("src/index.ts")).toBe(false)
})
it("rejects dot-relative path", () => {
expect(isAbsolutePath("./foo/bar.ts")).toBe(false)
})
it("rejects parent-relative path", () => {
expect(isAbsolutePath("../foo/bar.ts")).toBe(false)
})
it("rejects bare filename without extension", () => {
expect(isAbsolutePath("Makefile")).toBe(false)
})
it("rejects package-style path", () => {
expect(isAbsolutePath("@scope/package/index.js")).toBe(false)
})
})
// ── Edge cases ───────────────────────────────────────────────────────
describe("edge cases", () => {
it("rejects empty string", () => {
expect(isAbsolutePath("")).toBe(false)
})
it("rejects URL with protocol", () => {
expect(isAbsolutePath("https://example.com/path")).toBe(false)
expect(isAbsolutePath("http://localhost:3000")).toBe(false)
})
it("rejects file:// URL", () => {
expect(isAbsolutePath("file:///foo/bar")).toBe(false)
})
it("rejects drive-relative path (C:file without separator)", () => {
// C:file is a Windows drive-relative path, not absolute
expect(isAbsolutePath("C:file.ts")).toBe(false)
})
it("rejects bare drive letter with colon only", () => {
expect(isAbsolutePath("C:")).toBe(false)
})
it("rejects single backslash (not UNC)", () => {
expect(isAbsolutePath("\\foo")).toBe(false)
})
it("rejects number-prefixed colon paths", () => {
expect(isAbsolutePath("1:\\foo")).toBe(false)
})
it("rejects symbol-prefixed colon paths", () => {
expect(isAbsolutePath("@:\\foo")).toBe(false)
})
it("rejects tilde home path", () => {
expect(isAbsolutePath("~/Documents/file.ts")).toBe(false)
})
it("treats /C:/foo as absolute (Unix-style prefix)", () => {
// Starts with / so it's Unix-absolute regardless of Windows-like suffix
expect(isAbsolutePath("/C:/Users/foo")).toBe(true)
})
it("rejects whitespace-only strings", () => {
expect(isAbsolutePath(" ")).toBe(false)
expect(isAbsolutePath(" ")).toBe(false)
})
it("rejects path starting with dot-dot alone", () => {
expect(isAbsolutePath("..")).toBe(false)
})
it("rejects path starting with single dot", () => {
expect(isAbsolutePath(".")).toBe(false)
})
})
})
@@ -0,0 +1,76 @@
import { afterEach, describe, expect, it } from "bun:test"
import { getShellEnvironment, execWithShellEnv, clearShellEnvCache } from "../../src/agent-manager/shell-env"
afterEach(() => {
clearShellEnvCache()
})
describe("getShellEnvironment", () => {
it("returns an object with PATH", async () => {
const env = await getShellEnvironment()
expect(env).toBeDefined()
expect(typeof env.PATH).toBe("string")
expect(env.PATH!.length).toBeGreaterThan(0)
})
it("returns HOME", async () => {
const env = await getShellEnvironment()
expect(typeof env.HOME).toBe("string")
})
it("caches results across calls", async () => {
const first = await getShellEnvironment()
const second = await getShellEnvironment()
expect(first.PATH).toBe(second.PATH)
})
it("returns a copy (mutations don't corrupt cache)", async () => {
const first = await getShellEnvironment()
first.PATH = "/mutated"
const second = await getShellEnvironment()
expect(second.PATH).not.toBe("/mutated")
})
it("handles multiline env values without corrupting PATH", async () => {
// PATH should never contain newlines — verify it parses correctly
// even if other env vars have multiline values (e.g. BASH_FUNC_*)
const env = await getShellEnvironment()
expect(env.PATH).toBeDefined()
expect(env.PATH).not.toContain("\n")
})
})
describe("execWithShellEnv", () => {
it("executes a simple command", async () => {
const { stdout } = await execWithShellEnv("echo", ["hello"])
expect(stdout.trim()).toBe("hello")
})
it("passes cwd option through", async () => {
const { stdout } = await execWithShellEnv("pwd", [], { cwd: "/tmp" })
// /tmp may resolve to /private/tmp on macOS
expect(stdout.trim()).toMatch(/\/tmp$/)
})
it("throws on non-ENOENT errors", async () => {
await expect(execWithShellEnv("ls", ["--nonexistent-flag-that-fails"])).rejects.toThrow()
})
it("concurrent calls don't reject prematurely", async () => {
// Both calls should succeed — neither should throw due to a race
const [a, b] = await Promise.all([execWithShellEnv("echo", ["first"]), execWithShellEnv("echo", ["second"])])
expect(a.stdout.trim()).toBe("first")
expect(b.stdout.trim()).toBe("second")
})
})
describe("clearShellEnvCache", () => {
it("forces fresh resolution on next call", async () => {
const first = await getShellEnvironment()
clearShellEnvCache()
const second = await getShellEnvironment()
// Both should succeed and contain PATH
expect(first.PATH).toBeDefined()
expect(second.PATH).toBeDefined()
})
})
@@ -281,7 +281,7 @@ describe("WorktreeManager.createWorktree", () => {
expect(result.path).toContain(path.join(".kilocode", "worktrees"))
})
it("records parentBranch as current branch", async () => {
it("records parentBranch as default branch", async () => {
const root = await createTempRepo()
const git = simpleGit(root)
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim()
@@ -465,7 +465,7 @@ describe("WorktreeManager.discoverWorktrees", () => {
const discovered = await mgr.discoverWorktrees()
expect(discovered.length).toBe(1)
expect(discovered[0].sessionId).toBeUndefined()
expect(discovered[0]?.sessionId).toBeUndefined()
})
it("recovers parentBranch from persisted metadata", async () => {
@@ -666,3 +666,150 @@ describe("WorktreeManager.checkedOutBranches", () => {
expect(checked.has(wt.branch)).toBe(false)
})
})
// ---------------------------------------------------------------------------
// WorktreeManager -- Start Point Resolution & Helpers
// ---------------------------------------------------------------------------
describe("WorktreeManager helpers", () => {
it("hasOriginRemote returns false when no remote exists", async () => {
const root = await createTempRepo()
const mgr = createManager(root)
expect(await mgr.hasOriginRemote()).toBe(false)
})
it("hasOriginRemote returns true when origin exists", async () => {
const root = await createTempRepo()
const git = simpleGit(root)
await git.addRemote("origin", "https://example.com/repo.git")
const mgr = createManager(root)
expect(await mgr.hasOriginRemote()).toBe(true)
})
it("refExistsLocally verifies refs", async () => {
const root = await createTempRepo()
const git = simpleGit(root)
const mgr = createManager(root)
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim()
expect(await mgr.refExistsLocally(head)).toBe(true)
expect(await mgr.refExistsLocally("nonexistent")).toBe(false)
expect(await mgr.refExistsLocally("origin/HEAD")).toBe(false)
})
it("repoUsesLfs detects .gitattributes", async () => {
const root = await createTempRepo()
const mgr = createManager(root)
expect(await mgr.repoUsesLfs()).toBe(false)
await fs.writeFile(path.join(root, ".gitattributes"), "*.png filter=lfs diff=lfs merge=lfs -text")
expect(await mgr.repoUsesLfs()).toBe(true)
})
it("repoUsesLfs detects .git/lfs directory", async () => {
const root = await createTempRepo()
const mgr = createManager(root)
expect(await mgr.repoUsesLfs()).toBe(false)
await fs.mkdir(path.join(root, ".git", "lfs"), { recursive: true })
expect(await mgr.repoUsesLfs()).toBe(true)
})
})
describe("WorktreeManager.resolveStartPoint", () => {
it("falls back to local branch when no remote exists", async () => {
const root = await createTempRepo()
const git = simpleGit(root)
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim()
const mgr = createManager(root)
const res = await mgr.resolveStartPoint(head)
expect(res.source).toBe("local-branch")
expect(res.ref).toBe(head)
})
it("falls back to default branch when requested does not exist", async () => {
const root = await createTempRepo()
const git = simpleGit(root)
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim()
const mgr = createManager(root)
const res = await mgr.resolveStartPoint("nonexistent-feature")
expect(res.source).toBe("fallback")
expect(res.branch).toBe(head) // fallback to default (HEAD)
expect(res.warning).toContain("falling back to")
})
it("does not fallback when allowFallback is false", async () => {
const root = await createTempRepo()
const mgr = createManager(root)
await expect(mgr.resolveStartPoint("nonexistent", undefined, { allowFallback: false })).rejects.toThrow(
"Could not resolve start point",
)
})
})
describe("WorktreeManager.createWorktree advanced", () => {
it("returns startPointSource in result", async () => {
const root = await createTempRepo()
const mgr = createManager(root)
const res = await mgr.createWorktree({ prompt: "source-test" })
expect(res.startPointSource).toBe("local-branch") // no remote in temp repo
})
it("does not set upstream tracking on new branch", async () => {
const root = await createTempRepo()
const mgr = createManager(root)
const res = await mgr.createWorktree({ prompt: "no-upstream" })
const git = simpleGit(res.path)
// Checking upstream should fail
let error
try {
await git.revparse(["--abbrev-ref", `${res.branch}@{upstream}`])
} catch (e) {
error = e
}
expect(error).toBeDefined()
})
it("fires onProgress callbacks", async () => {
const root = await createTempRepo()
const mgr = createManager(root)
const steps: string[] = []
await mgr.createWorktree({
prompt: "progress-test",
onProgress: (step) => steps.push(step),
})
expect(steps).toContain("verifying")
expect(steps).toContain("creating")
})
it("creates from an explicitly selected base branch", async () => {
const root = await createTempRepo()
const git = simpleGit(root)
const mgr = createManager(root)
// Create a new branch 'develop'
await git.checkoutLocalBranch("develop")
await fs.writeFile(path.join(root, "dev.txt"), "dev")
await git.add(".")
await git.commit("dev commit")
// Create worktree from 'develop'
const res = await mgr.createWorktree({
prompt: "feature",
baseBranch: "develop",
})
expect(res.parentBranch).toBe("develop")
const wtGit = simpleGit(res.path)
const headParams = await wtGit.log(["-1"])
const devParams = await git.log(["-1"])
expect(headParams.latest?.hash).toBe(devParams.latest?.hash)
})
})
@@ -46,7 +46,8 @@ async function disableAnimations(page: Page) {
}
// Stories to skip from visual regression (add IDs here if needed)
const SKIP = new Set<string>([])
// Spinner animation captures at an indeterminate frame, causing flaky diffs.
const SKIP = new Set<string>(["agentmanager--worktree-item-busy"])
// Generate one test() per story so Playwright's scheduler can distribute
// them freely across workers — no manual sharding needed.
@@ -46,7 +46,8 @@ async function disableAnimations(page: Page) {
}
// Stories to skip from visual regression (add IDs here if needed)
const SKIP = new Set<string>([])
// Spinner animation captures at an indeterminate frame, causing flaky diffs.
const SKIP = new Set<string>(["agentmanager--worktree-item-busy"])
// Generate one test() per story so Playwright's scheduler can distribute
// them freely across workers — no manual sharding needed.
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d8550b95fec011a61707b544af9a18af85dc51f91a3e93d99a4d0e91bbd86b0
size 20568
oid sha256:86f1c445d792a9c6353f0899ceb6a777d8e2d8a9490f4d520e79dd8081cc5314
size 17458
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a442e795a4bbf42cffde54230853b31e8b62338932d21cda82714726c2626619
size 14296
oid sha256:fd1e5b81a0b1de953d43390e694cd5a1045b444c75a5370a574412576dbbe621
size 13549
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c412052053c3591f14ad30ceb35f34bcbc41b979fc27a07445f43b1f89b55695
size 23389
oid sha256:3838b7760984c6c822078de1bdb74067839775d29ab8a0435626d0afff8cbb30
size 24826
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b10df4363232f03ff6357bae92a810b4d27d5590e6ed67e1e2a829aae3e7bffe
size 2087
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:32e7049d7c1bbf5f53d63259074304bf730e9be1139b1e55813494f53bed3bb9
size 2168
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7888a2095cea698edd1cf6a04c58d0f0dd7da5030d30e705e50d155750295b56
size 1914
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20dd35f3cf3390be1ff516d7531f4f7744d833c2be1f99d9a7d7c847c30c6727
size 5215
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d4fc23f1b7bf2940b3554623d8964f04fc95bedf0752f45a0601bce34b5d585
size 2554
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af516a890c5718f9e732faa0b9feb4fdba867a6938c2faaf22dddcebf8960345
size 2204
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21dead151263397b963065601adf4a7e4ff82d274552bd5477783cbd6c5193f1
size 2321
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:107e9937d6640579e6b9823e609858be9060e7a6d2855bea1564b1f8585e3c60
size 3880
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5fc5cb33f9536414b23c63e0bb19259862f6a227603602802e8a41c15e898a47
size 4110
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b782a2ce193e54bd4fdd2ebc1490ac9674325a806ccabd56a7d3ff20c87e423
size 39285
oid sha256:ca2d904ad6ea886fc32714fafcae38748a14411e1ee6f436a6bfaf91d0088c59
size 42199
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
// Reusable branch selector: search input + scrollable list with keyboard navigation
import { Component, For, Show } from "solid-js"
import type { BranchInfo } from "../src/types/messages"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { formatRelativeDate } from "../src/utils/date"
export interface AutoOption {
label: string
hint?: string
active: boolean
highlighted?: boolean
onSelect: () => void
}
export interface BranchSelectProps {
branches: BranchInfo[]
loading?: boolean
search: string
onSearch: (value: string) => void
onSelect: (branch: BranchInfo) => void
onSearchKeyDown?: (event: KeyboardEvent) => void
selected?: string
highlighted?: number
onHighlight?: (index: number) => void
defaultName?: string
searchPlaceholder: string
loadingLabel?: string
emptyLabel: string
defaultLabel: string
remoteLabel: string
autoOption?: AutoOption
}
export const BranchSelect: Component<BranchSelectProps> = (props) => {
const isDefault = (branch: BranchInfo) => {
if (props.defaultName) return branch.name === props.defaultName
return branch.isDefault
}
return (
<>
<div class="am-dropdown-search">
<Icon name="magnifying-glass" size="small" />
<input
class="am-dropdown-search-input"
type="text"
placeholder={props.searchPlaceholder}
value={props.search}
autofocus
onInput={(e) => props.onSearch(e.currentTarget.value)}
onKeyDown={(e) => props.onSearchKeyDown?.(e)}
/>
</div>
<div class="am-dropdown-list">
<Show when={props.autoOption}>
{(opt) => (
<button
class="am-branch-item"
classList={{
"am-branch-item-active": opt().active,
"am-branch-item-highlighted": opt().highlighted,
}}
data-index="auto"
onClick={() => opt().onSelect()}
type="button"
>
<span class="am-branch-item-left">
<Icon name="branch" size="small" />
<span class="am-branch-item-name">{opt().label}</span>
</span>
<Show when={opt().hint}>
<span class="am-branch-hint">{opt().hint}</span>
</Show>
</button>
)}
</Show>
<Show
when={props.branches.length > 0}
fallback={
<Show when={!props.autoOption}>
<div class="am-dropdown-empty">
{props.loading ? (props.loadingLabel ?? props.emptyLabel) : props.emptyLabel}
</div>
</Show>
}
>
<For each={props.branches}>
{(branch, index) => (
<button
class="am-branch-item"
classList={{
"am-branch-item-active": props.selected === branch.name,
"am-branch-item-highlighted": props.highlighted === index(),
}}
data-index={index()}
onClick={() => props.onSelect(branch)}
onMouseEnter={() => props.onHighlight?.(index())}
type="button"
>
<span class="am-branch-item-left">
<Icon name="branch" size="small" />
<span class="am-branch-item-name">{branch.name}</span>
<Show when={isDefault(branch)}>
<span class="am-branch-badge">{props.defaultLabel}</span>
</Show>
<Show when={!branch.isLocal && branch.isRemote}>
<span class="am-branch-badge am-branch-badge-remote">{props.remoteLabel}</span>
</Show>
</span>
<Show when={branch.lastCommitDate}>
<span class="am-branch-item-time">{formatRelativeDate(branch.lastCommitDate!)}</span>
</Show>
</button>
)}
</For>
</Show>
<Show when={props.loading && props.branches.length === 0 && !props.autoOption}>
<div class="am-dropdown-empty">
<Spinner class="am-setup-spinner" />
<span>{props.loadingLabel}</span>
</div>
</Show>
</div>
</>
)
}
@@ -0,0 +1,285 @@
/**
* Sidebar worktree item with inline delete confirmation, HoverCard, rename, and stats.
* Extracted from AgentManagerApp for reuse and visual-regression testing via Storybook.
*/
import { Component, Show, createSignal } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
import { HoverCard } from "@kilocode/kilo-ui/hover-card"
import { Button } from "@kilocode/kilo-ui/button"
import type { WorktreeState, WorktreeGitStats } from "../src/types/messages"
import { useLanguage } from "../src/context/language"
import { formatRelativeDate } from "../src/utils/date"
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
export interface WorktreeItemProps {
worktree: WorktreeState
/** Display label (resolved from label, first session title, or branch). */
label: string
active: boolean
pendingDelete: boolean
busy: boolean
stale: boolean
/** 1-indexed shortcut number shown as ⌘2, ⌘3, etc. Pass 0 or >9 to hide. */
shortcut: number
stats?: WorktreeGitStats
/** Navigation hint text shown in the hover card (e.g. "⌘⌥↑"). */
navHint?: string
/** Number of sessions attached to this worktree. */
sessions: number
/** Whether this worktree is part of a multi-version group. */
grouped: boolean
/** Whether this is the first item in a group. */
groupStart: boolean
/** Whether this is the last item in a group. */
groupEnd: boolean
/** Group size (only used when groupStart is true). */
groupSize: number
/** Whether renaming is active on this item. */
renaming: boolean
/** Current rename input value. */
renameValue: string
/** Keybinding string for the close/delete action. */
closeKeybind: string
onClick: () => void
onDelete: (e: MouseEvent) => void
onStartRename: (current: string) => void
onRenameInput: (value: string) => void
onCommitRename: () => void
onCancelRename: () => void
onRemoveStale: () => void
}
const MAX_SHORTCUT = 9
const hasStats = (s: WorktreeGitStats | undefined): s is WorktreeGitStats =>
!!s && (s.files > 0 || s.additions > 0 || s.deletions > 0 || s.ahead > 0 || s.behind > 0)
export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
const { t } = useLanguage()
const [hovered, setHovered] = createSignal(false)
const [overClose, setOverClose] = createSignal(false)
return (
<>
<Show when={props.groupStart}>
<div class="am-wt-group-header">
<Icon name="layers" size="small" />
<span class="am-wt-group-label">{t("agentManager.worktree.versions", { count: props.groupSize })}</span>
</div>
</Show>
<HoverCard
openDelay={100}
closeDelay={100}
placement="right-start"
gutter={8}
open={hovered() && !overClose() && !props.pendingDelete}
onOpenChange={(open) => setHovered(open)}
trigger={
<div
class="am-worktree-item"
classList={{
"am-worktree-item-active": props.active,
"am-worktree-pending-delete": props.pendingDelete,
"am-wt-grouped": props.grouped,
"am-wt-group-end": props.groupEnd,
}}
data-sidebar-id={props.worktree.id}
onClick={() => props.onClick()}
>
<Show when={!props.busy} fallback={<Spinner class="am-worktree-spinner" />}>
<Icon name="branch" size="small" />
</Show>
<Show when={props.stale}>
<Tooltip value={t("agentManager.worktree.staleTooltip")} placement="top" contentClass="am-tooltip-wrap">
<span class="am-worktree-stale-badge">
<Icon name="warning" size="small" />
</span>
</Tooltip>
</Show>
<Show
when={props.renaming}
fallback={
<span
class="am-worktree-branch"
onDblClick={(e) => {
e.stopPropagation()
props.onStartRename(props.label)
}}
title={t("agentManager.worktree.doubleClickRename")}
>
{props.label}
</span>
}
>
<input
class="am-worktree-rename-input"
value={props.renameValue}
onInput={(e) => props.onRenameInput(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
props.onCommitRename()
}
if (e.key === "Escape") {
e.preventDefault()
props.onCancelRename()
}
}}
onBlur={() => props.onCommitRename()}
onClick={(e) => e.stopPropagation()}
ref={(el) =>
requestAnimationFrame(() => {
el.focus()
el.select()
})
}
/>
</Show>
<Show when={props.shortcut >= 2 && props.shortcut <= MAX_SHORTCUT}>
<span class="am-shortcut-badge">
{isMac ? "⌘" : "Ctrl+"}
{props.shortcut}
</span>
</Show>
<Show when={hasStats(props.stats)}>
<div class="am-worktree-stats">
<Show when={props.stats!.files > 0}>
<span class="am-stat-files">{props.stats!.files}f</span>
</Show>
<Show when={props.stats!.additions > 0 || props.stats!.deletions > 0}>
<span class="am-worktree-diff-stats">
<Show when={props.stats!.additions > 0}>
<span class="am-stat-additions">+{props.stats!.additions}</span>
</Show>
<Show when={props.stats!.deletions > 0}>
<span class="am-stat-deletions">−{props.stats!.deletions}</span>
</Show>
</span>
</Show>
<Show when={props.stats!.ahead > 0}>
<span class="am-worktree-commits">↑{props.stats!.ahead}</span>
</Show>
<Show when={props.stats!.behind > 0}>
<span class="am-worktree-behind">↓{props.stats!.behind}</span>
</Show>
</div>
</Show>
<Show when={props.pendingDelete && !props.busy}>
<span class="am-worktree-delete-hint">{t("agentManager.worktree.confirmDelete")}</span>
</Show>
<Show when={!props.busy && !props.pendingDelete}>
<div
class="am-worktree-close"
onMouseEnter={() => setOverClose(true)}
onMouseLeave={() => setOverClose(false)}
>
<TooltipKeybind title={t("agentManager.worktree.delete")} keybind={props.closeKeybind} placement="top">
<IconButton
icon="trash"
size="small"
variant="ghost"
label={t("agentManager.worktree.delete")}
onClick={(e: MouseEvent) => props.onDelete(e)}
/>
</TooltipKeybind>
</div>
</Show>
</div>
}
>
<div class="am-hover-card">
<div class="am-hover-card-header">
<div>
<div class="am-hover-card-label">{t("agentManager.hoverCard.branch")}</div>
<div class="am-hover-card-branch">{props.worktree.branch}</div>
<div class="am-hover-card-meta">{formatRelativeDate(props.worktree.createdAt)}</div>
</div>
<Show when={props.navHint}>
<span class="am-hover-card-keybind">{props.navHint}</span>
</Show>
</div>
<Show when={props.worktree.parentBranch}>
<div class="am-hover-card-divider" />
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">{t("agentManager.hoverCard.base")}</span>
<span class="am-hover-card-row-value">{props.worktree.parentBranch}</span>
</div>
</Show>
<div class="am-hover-card-divider" />
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">{t("agentManager.hoverCard.sessions")}</span>
<span class="am-hover-card-row-value">{props.sessions}</span>
</div>
<Show when={props.stale}>
<div class="am-hover-card-divider" />
<div class="am-hover-card-row am-hover-card-row-stale">
<span class="am-hover-card-row-label">{t("agentManager.worktree.stale")}</span>
<span class="am-hover-card-row-value am-hover-card-stale-pill">
<Icon name="warning" size="small" />
{t("agentManager.worktree.stale")}
</span>
</div>
<div class="am-hover-card-note">{t("agentManager.worktree.staleTooltip")}</div>
<div class="am-hover-card-actions">
<Button
variant="ghost"
size="small"
onClick={(e) => {
e.stopPropagation()
props.onRemoveStale()
}}
>
{t("agentManager.worktree.removeStale")}
</Button>
</div>
</Show>
<Show when={hasStats(props.stats)}>
<div class="am-hover-card-divider" />
<Show when={props.stats!.files > 0}>
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">{t("agentManager.hoverCard.files")}</span>
<span class="am-hover-card-row-value">{props.stats!.files}</span>
</div>
</Show>
<Show when={props.stats!.additions > 0 || props.stats!.deletions > 0}>
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">{t("agentManager.hoverCard.changes")}</span>
<span class="am-hover-card-row-value am-hover-card-diff-stats">
<Show when={props.stats!.additions > 0}>
<span class="am-stat-additions">+{props.stats!.additions}</span>
</Show>
<Show when={props.stats!.deletions > 0}>
<span class="am-stat-deletions">−{props.stats!.deletions}</span>
</Show>
</span>
</div>
</Show>
<Show when={props.stats!.ahead > 0 || props.stats!.behind > 0}>
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">{t("agentManager.hoverCard.commits")}</span>
<span class="am-hover-card-row-value am-hover-card-diff-stats">
<Show when={props.stats!.ahead > 0}>
<span class="am-worktree-commits">↑{props.stats!.ahead}</span>
</Show>
<Show when={props.stats!.behind > 0}>
<span class="am-worktree-behind">↓{props.stats!.behind}</span>
</Show>
</span>
</div>
</Show>
</Show>
<div class="am-hover-card-divider" />
<div class="am-hover-card-hint">
<Icon name="edit" size="small" />
<span>{t("agentManager.worktree.doubleClickRename")}</span>
</div>
</div>
</HoverCard>
</>
)
}
@@ -190,6 +190,7 @@ button.am-section-toggle:hover .am-section-label {
color: var(--text-base);
min-width: 0;
width: 100%;
transition: background 200ms ease;
}
.am-worktree-item:hover {
@@ -298,6 +299,51 @@ button.am-section-toggle:hover .am-section-label {
pointer-events: none;
}
/* Inline delete confirmation */
.am-worktree-item.am-worktree-pending-delete,
.am-worktree-item.am-worktree-pending-delete.am-worktree-item-active {
background: color-mix(in srgb, var(--surface-critical-strong) 18%, transparent);
}
.am-worktree-item.am-worktree-pending-delete:hover,
.am-worktree-item.am-worktree-pending-delete.am-worktree-item-active:hover {
background: color-mix(in srgb, var(--surface-critical-strong) 25%, transparent);
}
.am-worktree-pending-delete .am-worktree-branch {
opacity: 0.5;
transition: opacity 200ms ease;
}
.am-worktree-pending-delete .am-worktree-stats {
display: none;
}
.am-worktree-pending-delete .am-shortcut-badge {
opacity: 0 !important;
}
.am-worktree-delete-hint {
font-size: var(--font-size-small);
color: var(--icon-critical-base);
white-space: nowrap;
flex-shrink: 0;
padding-right: 4px;
animation: am-hint-slide-in 200ms ease;
}
@keyframes am-hint-slide-in {
from {
opacity: 0;
transform: translateX(4px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.am-worktree-stale-badge {
display: inline-flex;
align-items: center;
@@ -1502,19 +1548,32 @@ button.am-section-toggle:hover .am-section-label {
.am-nv-dialog {
display: flex;
flex-direction: column;
gap: 14px;
padding: 0 20px 20px;
min-width: 460px;
flex: 1;
min-height: 0;
}
/* Allow dropdowns to escape the dialog bounds, but scroll when content is tall */
/* Allow portal-based dropdowns to escape the dialog bounds */
[data-component="dialog"]:has(.am-nv-dialog) [data-slot="dialog-content"] {
overflow: visible;
max-height: 85vh;
}
[data-component="dialog"]:has(.am-nv-dialog) [data-slot="dialog-body"] {
overflow-x: visible;
/* Scrollable form content area — keeps submit button always visible */
.am-nv-dialog-content {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
padding: 0 20px;
}
/* Fixed footer for the submit button */
.am-nv-dialog-footer {
flex-shrink: 0;
padding: 14px 20px 20px;
}
.am-nv-name-input {
@@ -1672,14 +1731,20 @@ button.am-section-toggle:hover .am-section-label {
color: var(--text-base);
}
/* Multi-model selector */
/* Multi-model selector popover */
.am-compare-popover[data-component="popover-content"] {
max-width: none;
width: var(--kb-popper-anchor-width);
}
.am-compare-popover [data-slot="popover-body"] {
padding: 0;
}
.am-mm-wrapper {
display: flex;
flex-direction: column;
border: 1px solid var(--border-base);
border-radius: var(--radius-sm);
overflow: hidden;
}
.am-mm-search {
@@ -2233,6 +2298,7 @@ button.am-section-toggle:hover .am-section-label {
.am-tab-switcher {
display: flex;
flex-shrink: 0;
gap: 2px;
padding: 0 20px;
margin-bottom: 4px;
@@ -2385,6 +2451,10 @@ button.am-section-toggle:hover .am-section-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-base);
}
.am-selector-placeholder {
color: var(--text-weaker);
}
@@ -3062,3 +3132,19 @@ button.am-section-toggle:hover .am-section-label {
.am-file-tree-summary-dels {
color: var(--syntax-diff-delete, #da3319);
}
/* ============ Default base branch picker dialog ============ */
.am-default-base-branch {
min-width: 360px;
}
.am-default-base-branch .am-dropdown-list {
max-height: 320px;
}
.am-branch-hint {
margin-left: auto;
color: var(--text-weaker);
font-size: 11px;
}
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Worktree جديد",
"agentManager.worktree.setupScript": "سكربت إعداد Worktree",
"agentManager.worktree.delete": "حذف Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "قديم",
"agentManager.worktree.staleTooltip": "مفقود على القرص أو لم يعد git worktree يتتبعه",
"agentManager.worktree.removeStale": "إزالة Worktree القديم",
"agentManager.worktree.doubleClickRename": "انقر مرتين لإعادة التسمية",
"agentManager.worktree.versions": "{{count}} نسخ",
"agentManager.worktree.advancedOptions": "خيارات Worktree متقدمة",
"agentManager.worktree.defaultBaseBranch": "الفرع الأساسي الافتراضي",
"agentManager.worktree.defaultBaseBranchAuto": "اكتشاف تلقائي",
"agentManager.hoverCard.branch": "الفرع",
"agentManager.hoverCard.base": "الأساس",
"agentManager.hoverCard.sessions": "الجلسات",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "تركيز اللوحة",
"agentManager.shortcuts.openAgentManager": "فتح Agent Manager",
"agentManager.shortcuts.showShortcuts": "إظهار اختصارات لوحة المفاتيح",
"agentManager.dialog.deleteWorktree.title": "حذف Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "حذف Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "؟ سيؤدي هذا إلى إزالة Worktree من القرص وفصل جميع الجلسات.",
"agentManager.dialog.deleteWorktree.cancel": "إلغاء",
"agentManager.dialog.deleteWorktree.confirm": "حذف",
"agentManager.dialog.removeStaleWorktree.title": "إزالة Worktree القديم",
"agentManager.dialog.removeStaleWorktree.messagePre": "إزالة Worktree القديم ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -80,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} Worktrees ستعمل بالتوازي",
"agentManager.dialog.compareModels": "مقارنة النماذج",
"agentManager.dialog.compareModels.searchModels": "البحث عن النماذج...",
"agentManager.dialog.compareModels.selectModels": "اختر النماذج...",
"agentManager.dialog.creating": "جارٍ الإنشاء...",
"agentManager.dialog.createWorkspace": "إنشاء شجرة العمل",
"agentManager.dialog.removeImage": "إزالة الصورة",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Novo Worktree",
"agentManager.worktree.setupScript": "Script de configuração do Worktree",
"agentManager.worktree.delete": "Excluir Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Obsoleto",
"agentManager.worktree.staleTooltip": "Ausente no disco ou não é mais rastreado pelo git worktree",
"agentManager.worktree.removeStale": "Remover Worktree obsoleto",
"agentManager.worktree.doubleClickRename": "Clique duplo para renomear",
"agentManager.worktree.versions": "{{count}} versões",
"agentManager.worktree.advancedOptions": "Opções avançadas de Worktree",
"agentManager.worktree.defaultBaseBranch": "Branch base padrão",
"agentManager.worktree.defaultBaseBranchAuto": "Detecção automática",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessões",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Focar painel",
"agentManager.shortcuts.openAgentManager": "Abrir Agent Manager",
"agentManager.shortcuts.showShortcuts": "Mostrar atalhos de teclado",
"agentManager.dialog.deleteWorktree.title": "Excluir Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Excluir Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "? Isso remove o Worktree do disco e desassocia todas as sessões.",
"agentManager.dialog.deleteWorktree.cancel": "Cancelar",
"agentManager.dialog.deleteWorktree.confirm": "Excluir",
"agentManager.dialog.removeStaleWorktree.title": "Remover Worktree obsoleto",
"agentManager.dialog.removeStaleWorktree.messagePre": "Remover Worktree obsoleto ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -80,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktrees serão executados em paralelo",
"agentManager.dialog.compareModels": "Comparar Modelos",
"agentManager.dialog.compareModels.searchModels": "Pesquisar modelos...",
"agentManager.dialog.compareModels.selectModels": "Selecionar modelos...",
"agentManager.dialog.creating": "Criando...",
"agentManager.dialog.createWorkspace": "Criar Worktree",
"agentManager.dialog.removeImage": "Remover imagem",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Novi Worktree",
"agentManager.worktree.setupScript": "Skripta za postavljanje Worktree-a",
"agentManager.worktree.delete": "Obriši Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Zastario",
"agentManager.worktree.staleTooltip": "Nedostaje na disku ili ga git worktree više ne prati",
"agentManager.worktree.removeStale": "Ukloni zastarjeli Worktree",
"agentManager.worktree.doubleClickRename": "Dvostruki klik za preimenovanje",
"agentManager.worktree.versions": "{{count}} verzija",
"agentManager.worktree.advancedOptions": "Napredne opcije Worktree-a",
"agentManager.worktree.defaultBaseBranch": "Zadana bazna grana",
"agentManager.worktree.defaultBaseBranchAuto": "Automatsko otkrivanje",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Baza",
"agentManager.hoverCard.sessions": "Sesije",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Fokusiraj panel",
"agentManager.shortcuts.openAgentManager": "Otvori Agent Manager",
"agentManager.shortcuts.showShortcuts": "Prikaži prečice na tastaturi",
"agentManager.dialog.deleteWorktree.title": "Obriši Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Obriši Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "? Ovo uklanja Worktree sa diska i odvezuje sve sesije.",
"agentManager.dialog.deleteWorktree.cancel": "Otkaži",
"agentManager.dialog.deleteWorktree.confirm": "Obriši",
"agentManager.dialog.removeStaleWorktree.title": "Ukloni zastarjeli Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Ukloni zastarjeli Worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -80,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktree-ova će se pokrenuti paralelno",
"agentManager.dialog.compareModels": "Uporedi modele",
"agentManager.dialog.compareModels.searchModels": "Pretraži modele...",
"agentManager.dialog.compareModels.selectModels": "Odaberi modele...",
"agentManager.dialog.creating": "Kreiranje...",
"agentManager.dialog.createWorkspace": "Kreiraj worktree",
"agentManager.dialog.removeImage": "Ukloni sliku",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Nyt Worktree",
"agentManager.worktree.setupScript": "Worktree-opsætningsscript",
"agentManager.worktree.delete": "Slet Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Forældet",
"agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke længere af git worktree",
"agentManager.worktree.removeStale": "Fjern forældet Worktree",
"agentManager.worktree.doubleClickRename": "Dobbeltklik for at omdøbe",
"agentManager.worktree.versions": "{{count}} versioner",
"agentManager.worktree.advancedOptions": "Avancerede Worktree-indstillinger",
"agentManager.worktree.defaultBaseBranch": "Standard basisgren",
"agentManager.worktree.defaultBaseBranchAuto": "Automatisk registrering",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessioner",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Fokuser panel",
"agentManager.shortcuts.openAgentManager": "Åbn Agent Manager",
"agentManager.shortcuts.showShortcuts": "Vis tastaturgenveje",
"agentManager.dialog.deleteWorktree.title": "Slet Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Slet Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "? Dette fjerner Worktree fra disken og afkobler alle sessioner.",
"agentManager.dialog.deleteWorktree.cancel": "Annuller",
"agentManager.dialog.deleteWorktree.confirm": "Slet",
"agentManager.dialog.removeStaleWorktree.title": "Fjern forældet Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Fjern forældet Worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -80,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktrees vil køre parallelt",
"agentManager.dialog.compareModels": "Sammenlign modeller",
"agentManager.dialog.compareModels.searchModels": "Søg modeller...",
"agentManager.dialog.compareModels.selectModels": "Vælg modeller...",
"agentManager.dialog.creating": "Opretter...",
"agentManager.dialog.createWorkspace": "Opret Worktree",
"agentManager.dialog.removeImage": "Fjern billede",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Neuer Worktree",
"agentManager.worktree.setupScript": "Worktree-Einrichtungsskript",
"agentManager.worktree.delete": "Worktree löschen",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Veraltet",
"agentManager.worktree.staleTooltip": "Fehlt auf der Festplatte oder wird nicht mehr von git worktree verfolgt",
"agentManager.worktree.removeStale": "Veralteten Worktree entfernen",
"agentManager.worktree.doubleClickRename": "Doppelklick zum Umbenennen",
"agentManager.worktree.versions": "{{count}} Versionen",
"agentManager.worktree.advancedOptions": "Erweiterte Worktree-Optionen",
"agentManager.worktree.defaultBaseBranch": "Standard-Basiszweig",
"agentManager.worktree.defaultBaseBranchAuto": "Automatisch erkennen",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Basis",
"agentManager.hoverCard.sessions": "Sitzungen",
@@ -50,13 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Panel fokussieren",
"agentManager.shortcuts.openAgentManager": "Agent Manager öffnen",
"agentManager.shortcuts.showShortcuts": "Tastenkürzel anzeigen",
"agentManager.dialog.deleteWorktree.title": "Worktree löschen",
"agentManager.dialog.deleteWorktree.messagePre": "Worktree löschen ",
"agentManager.dialog.deleteWorktree.messagePost":
"? Dies entfernt den Worktree von der Festplatte und trennt alle Sitzungen.",
"agentManager.dialog.deleteWorktree.cancel": "Abbrechen",
"agentManager.dialog.deleteWorktree.confirm": "Löschen",
"agentManager.dialog.removeStaleWorktree.title": "Veralteten Worktree entfernen",
"agentManager.dialog.removeStaleWorktree.messagePre": "Veralteten Worktree entfernen ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -81,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} Worktrees werden parallel ausgeführt",
"agentManager.dialog.compareModels": "Modelle vergleichen",
"agentManager.dialog.compareModels.searchModels": "Modelle suchen...",
"agentManager.dialog.compareModels.selectModels": "Modelle auswählen...",
"agentManager.dialog.creating": "Wird erstellt...",
"agentManager.dialog.createWorkspace": "Worktree erstellen",
"agentManager.dialog.removeImage": "Bild entfernen",
@@ -8,12 +8,15 @@ export const dict = {
"agentManager.worktree.new": "New Worktree",
"agentManager.worktree.setupScript": "Worktree Setup Script",
"agentManager.worktree.delete": "Delete worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Stale",
"agentManager.worktree.staleTooltip": "Missing on disk or no longer tracked by git worktree",
"agentManager.worktree.removeStale": "Remove stale worktree",
"agentManager.worktree.doubleClickRename": "Double-click to rename",
"agentManager.worktree.versions": "{{count}} versions",
"agentManager.worktree.advancedOptions": "Advanced worktree options",
"agentManager.worktree.defaultBaseBranch": "Default Base Branch",
"agentManager.worktree.defaultBaseBranchAuto": "Auto-detect",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
@@ -36,7 +39,6 @@ export const dict = {
"agentManager.setup.failed": "Workspace setup failed",
"agentManager.setup.settingUp": "Setting up workspace",
"agentManager.shortcuts.title": "Keyboard Shortcuts",
"agentManager.shortcuts.category.sidebar": "Sidebar",
"agentManager.shortcuts.category.tabs": "Tabs",
@@ -57,13 +59,6 @@ export const dict = {
"agentManager.shortcuts.openAgentManager": "Open Agent Manager",
"agentManager.shortcuts.showShortcuts": "Show keyboard shortcuts",
"agentManager.dialog.deleteWorktree.title": "Delete Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Delete worktree ",
"agentManager.dialog.deleteWorktree.messagePost":
"? This removes the worktree from disk and dissociates all sessions.",
"agentManager.dialog.deleteWorktree.cancel": "Cancel",
"agentManager.dialog.deleteWorktree.confirm": "Delete",
"agentManager.dialog.removeStaleWorktree.title": "Remove Stale Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Remove stale worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -88,6 +83,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktrees will run in parallel",
"agentManager.dialog.compareModels": "Compare Models",
"agentManager.dialog.compareModels.searchModels": "Search models...",
"agentManager.dialog.compareModels.selectModels": "Select models...",
"agentManager.dialog.creating": "Creating...",
"agentManager.dialog.createWorkspace": "Create Worktree",
"agentManager.dialog.removeImage": "Remove image",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Nuevo Worktree",
"agentManager.worktree.setupScript": "Script de configuración de Worktree",
"agentManager.worktree.delete": "Eliminar Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Obsoleto",
"agentManager.worktree.staleTooltip": "No está en disco o ya no está seguido por git worktree",
"agentManager.worktree.removeStale": "Eliminar Worktree obsoleto",
"agentManager.worktree.doubleClickRename": "Doble clic para renombrar",
"agentManager.worktree.versions": "{{count}} versiones",
"agentManager.worktree.advancedOptions": "Opciones avanzadas de Worktree",
"agentManager.worktree.defaultBaseBranch": "Rama base predeterminada",
"agentManager.worktree.defaultBaseBranchAuto": "Detección automática",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sesiones",
@@ -50,13 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Enfocar panel",
"agentManager.shortcuts.openAgentManager": "Abrir Agent Manager",
"agentManager.shortcuts.showShortcuts": "Mostrar atajos de teclado",
"agentManager.dialog.deleteWorktree.title": "Eliminar Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "¿Eliminar Worktree ",
"agentManager.dialog.deleteWorktree.messagePost":
"? Esto elimina el Worktree del disco y desasocia todas las sesiones.",
"agentManager.dialog.deleteWorktree.cancel": "Cancelar",
"agentManager.dialog.deleteWorktree.confirm": "Eliminar",
"agentManager.dialog.removeStaleWorktree.title": "Eliminar Worktree obsoleto",
"agentManager.dialog.removeStaleWorktree.messagePre": "¿Eliminar Worktree obsoleto ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -81,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktrees se ejecutarán en paralelo",
"agentManager.dialog.compareModels": "Comparar modelos",
"agentManager.dialog.compareModels.searchModels": "Buscar modelos...",
"agentManager.dialog.compareModels.selectModels": "Seleccionar modelos...",
"agentManager.dialog.creating": "Creando...",
"agentManager.dialog.createWorkspace": "Crear Worktree",
"agentManager.dialog.removeImage": "Eliminar imagen",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Nouveau Worktree",
"agentManager.worktree.setupScript": "Script de configuration du Worktree",
"agentManager.worktree.delete": "Supprimer le Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Obsolète",
"agentManager.worktree.staleTooltip": "Absent du disque ou n'est plus suivi par git worktree",
"agentManager.worktree.removeStale": "Supprimer le Worktree obsolète",
"agentManager.worktree.doubleClickRename": "Double-cliquez pour renommer",
"agentManager.worktree.versions": "{{count}} versions",
"agentManager.worktree.advancedOptions": "Options avancées du Worktree",
"agentManager.worktree.defaultBaseBranch": "Branche de base par défaut",
"agentManager.worktree.defaultBaseBranchAuto": "Détection automatique",
"agentManager.hoverCard.branch": "BRANCHE",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessions",
@@ -50,13 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Focaliser le panneau",
"agentManager.shortcuts.openAgentManager": "Ouvrir Agent Manager",
"agentManager.shortcuts.showShortcuts": "Afficher les raccourcis clavier",
"agentManager.dialog.deleteWorktree.title": "Supprimer le Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Supprimer le Worktree ",
"agentManager.dialog.deleteWorktree.messagePost":
" ? Cela supprime le Worktree du disque et dissocie toutes les sessions.",
"agentManager.dialog.deleteWorktree.cancel": "Annuler",
"agentManager.dialog.deleteWorktree.confirm": "Supprimer",
"agentManager.dialog.removeStaleWorktree.title": "Supprimer le Worktree obsolète",
"agentManager.dialog.removeStaleWorktree.messagePre": "Supprimer le Worktree obsolète ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -81,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktrees s'exécuteront en parallèle",
"agentManager.dialog.compareModels": "Comparer les modèles",
"agentManager.dialog.compareModels.searchModels": "Rechercher des modèles...",
"agentManager.dialog.compareModels.selectModels": "Sélectionner des modèles...",
"agentManager.dialog.creating": "Création...",
"agentManager.dialog.createWorkspace": "Créer un worktree",
"agentManager.dialog.removeImage": "Supprimer l'image",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "新しいWorktree",
"agentManager.worktree.setupScript": "Worktreeセットアップスクリプト",
"agentManager.worktree.delete": "Worktreeを削除",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "無効",
"agentManager.worktree.staleTooltip": "ディスク上に存在しないか、git worktree で追跡されていません",
"agentManager.worktree.removeStale": "無効な Worktree を削除",
"agentManager.worktree.doubleClickRename": "ダブルクリックで名前を変更",
"agentManager.worktree.versions": "{{count}}バージョン",
"agentManager.worktree.advancedOptions": "Worktreeの詳細オプション",
"agentManager.worktree.defaultBaseBranch": "デフォルトベースブランチ",
"agentManager.worktree.defaultBaseBranchAuto": "自動検出",
"agentManager.hoverCard.branch": "ブランチ",
"agentManager.hoverCard.base": "ベース",
"agentManager.hoverCard.sessions": "セッション",
@@ -50,13 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "パネルにフォーカス",
"agentManager.shortcuts.openAgentManager": "Agent Managerを開く",
"agentManager.shortcuts.showShortcuts": "キーボードショートカットを表示",
"agentManager.dialog.deleteWorktree.title": "Worktreeを削除",
"agentManager.dialog.deleteWorktree.messagePre": "Worktreeを削除 ",
"agentManager.dialog.deleteWorktree.messagePost":
"? ディスクからWorktreeを削除し、すべてのセッションの関連付けを解除します。",
"agentManager.dialog.deleteWorktree.cancel": "キャンセル",
"agentManager.dialog.deleteWorktree.confirm": "削除",
"agentManager.dialog.removeStaleWorktree.title": "無効な Worktree を削除",
"agentManager.dialog.removeStaleWorktree.messagePre": "無効な Worktree を削除 ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -81,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}}個のWorktreeが並行して実行されます",
"agentManager.dialog.compareModels": "モデルを比較",
"agentManager.dialog.compareModels.searchModels": "モデルを検索...",
"agentManager.dialog.compareModels.selectModels": "モデルを選択...",
"agentManager.dialog.creating": "作成中...",
"agentManager.dialog.createWorkspace": "ワークツリーを作成",
"agentManager.dialog.removeImage": "画像を削除",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "새 Worktree",
"agentManager.worktree.setupScript": "Worktree 설정 스크립트",
"agentManager.worktree.delete": "Worktree 삭제",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "오래됨",
"agentManager.worktree.staleTooltip": "디스크에 없거나 더 이상 git worktree에서 추적되지 않습니다",
"agentManager.worktree.removeStale": "오래된 Worktree 제거",
"agentManager.worktree.doubleClickRename": "더블 클릭하여 이름 변경",
"agentManager.worktree.versions": "{{count}}개 버전",
"agentManager.worktree.advancedOptions": "Worktree 고급 옵션",
"agentManager.worktree.defaultBaseBranch": "기본 베이스 브랜치",
"agentManager.worktree.defaultBaseBranchAuto": "자동 감지",
"agentManager.hoverCard.branch": "브랜치",
"agentManager.hoverCard.base": "베이스",
"agentManager.hoverCard.sessions": "세션",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "패널 포커스",
"agentManager.shortcuts.openAgentManager": "Agent Manager 열기",
"agentManager.shortcuts.showShortcuts": "키보드 단축키 표시",
"agentManager.dialog.deleteWorktree.title": "Worktree 삭제",
"agentManager.dialog.deleteWorktree.messagePre": "Worktree 삭제 ",
"agentManager.dialog.deleteWorktree.messagePost": "? 디스크에서 Worktree를 제거하고 모든 세션의 연결을 해제합니다.",
"agentManager.dialog.deleteWorktree.cancel": "취소",
"agentManager.dialog.deleteWorktree.confirm": "삭제",
"agentManager.dialog.removeStaleWorktree.title": "오래된 Worktree 제거",
"agentManager.dialog.removeStaleWorktree.messagePre": "오래된 Worktree 제거 ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -80,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}}개의 Worktree가 병렬로 실행됩니다",
"agentManager.dialog.compareModels": "모델 비교",
"agentManager.dialog.compareModels.searchModels": "모델 검색...",
"agentManager.dialog.compareModels.selectModels": "모델 선택...",
"agentManager.dialog.creating": "생성 중...",
"agentManager.dialog.createWorkspace": "워크트리 생성",
"agentManager.dialog.removeImage": "이미지 제거",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Nytt Worktree",
"agentManager.worktree.setupScript": "Worktree-oppsettskript",
"agentManager.worktree.delete": "Slett Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Utdatert",
"agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke lenger av git worktree",
"agentManager.worktree.removeStale": "Fjern utdatert Worktree",
"agentManager.worktree.doubleClickRename": "Dobbeltklikk for å gi nytt navn",
"agentManager.worktree.versions": "{{count}} versjoner",
"agentManager.worktree.advancedOptions": "Avanserte Worktree-alternativer",
"agentManager.worktree.defaultBaseBranch": "Standard basisgren",
"agentManager.worktree.defaultBaseBranchAuto": "Automatisk gjenkjenning",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Økter",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Fokuser panel",
"agentManager.shortcuts.openAgentManager": "Åpne Agent Manager",
"agentManager.shortcuts.showShortcuts": "Vis tastatursnarveier",
"agentManager.dialog.deleteWorktree.title": "Slett Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Slett Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "? Dette fjerner Worktree fra disken og kobler fra alle økter.",
"agentManager.dialog.deleteWorktree.cancel": "Avbryt",
"agentManager.dialog.deleteWorktree.confirm": "Slett",
"agentManager.dialog.removeStaleWorktree.title": "Fjern utdatert Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Fjern utdatert Worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -80,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktrees vil kjøre parallelt",
"agentManager.dialog.compareModels": "Sammenlign modeller",
"agentManager.dialog.compareModels.searchModels": "Søk modeller...",
"agentManager.dialog.compareModels.selectModels": "Velg modeller...",
"agentManager.dialog.creating": "Oppretter...",
"agentManager.dialog.createWorkspace": "Opprett worktree",
"agentManager.dialog.removeImage": "Fjern bilde",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Nowy Worktree",
"agentManager.worktree.setupScript": "Skrypt konfiguracji Worktree",
"agentManager.worktree.delete": "Usuń Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Nieaktualny",
"agentManager.worktree.staleTooltip": "Brak na dysku lub nie jest już śledzony przez git worktree",
"agentManager.worktree.removeStale": "Usuń nieaktualny Worktree",
"agentManager.worktree.doubleClickRename": "Kliknij dwukrotnie, aby zmienić nazwę",
"agentManager.worktree.versions": "{{count}} wersji",
"agentManager.worktree.advancedOptions": "Zaawansowane opcje Worktree",
"agentManager.worktree.defaultBaseBranch": "Domyślna gałąź bazowa",
"agentManager.worktree.defaultBaseBranchAuto": "Automatyczne wykrywanie",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Baza",
"agentManager.hoverCard.sessions": "Sesje",
@@ -50,13 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Fokus na panelu",
"agentManager.shortcuts.openAgentManager": "Otwórz Agent Manager",
"agentManager.shortcuts.showShortcuts": "Pokaż skróty klawiszowe",
"agentManager.dialog.deleteWorktree.title": "Usuń Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Usunąć Worktree ",
"agentManager.dialog.deleteWorktree.messagePost":
"? Spowoduje to usunięcie Worktree z dysku i odłączenie wszystkich sesji.",
"agentManager.dialog.deleteWorktree.cancel": "Anuluj",
"agentManager.dialog.deleteWorktree.confirm": "Usuń",
"agentManager.dialog.removeStaleWorktree.title": "Usuń nieaktualny Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Usunąć nieaktualny Worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -81,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} worktree będą działać równolegle",
"agentManager.dialog.compareModels": "Porównaj modele",
"agentManager.dialog.compareModels.searchModels": "Szukaj modeli...",
"agentManager.dialog.compareModels.selectModels": "Wybierz modele...",
"agentManager.dialog.creating": "Tworzenie...",
"agentManager.dialog.createWorkspace": "Utwórz Worktree",
"agentManager.dialog.removeImage": "Usuń obraz",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Новый Worktree",
"agentManager.worktree.setupScript": "Скрипт настройки Worktree",
"agentManager.worktree.delete": "Удалить Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "Устаревший",
"agentManager.worktree.staleTooltip": "Отсутствует на диске или больше не отслеживается git worktree",
"agentManager.worktree.removeStale": "Удалить устаревший Worktree",
"agentManager.worktree.doubleClickRename": "Двойной клик для переименования",
"agentManager.worktree.versions": "{{count}} версий",
"agentManager.worktree.advancedOptions": "Расширенные настройки Worktree",
"agentManager.worktree.defaultBaseBranch": "Базовая ветка по умолчанию",
"agentManager.worktree.defaultBaseBranchAuto": "Автоопределение",
"agentManager.hoverCard.branch": "ВЕТКА",
"agentManager.hoverCard.base": "Основа",
"agentManager.hoverCard.sessions": "Сессии",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "Фокус на панели",
"agentManager.shortcuts.openAgentManager": "Открыть Agent Manager",
"agentManager.shortcuts.showShortcuts": "Показать сочетания клавиш",
"agentManager.dialog.deleteWorktree.title": "Удалить Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "Удалить Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "? Это удалит Worktree с диска и отключит все сессии.",
"agentManager.dialog.deleteWorktree.cancel": "Отмена",
"agentManager.dialog.deleteWorktree.confirm": "Удалить",
"agentManager.dialog.removeStaleWorktree.title": "Удалить устаревший Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Удалить устаревший Worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -80,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} Worktree будут выполняться параллельно",
"agentManager.dialog.compareModels": "Сравнить модели",
"agentManager.dialog.compareModels.searchModels": "Поиск моделей...",
"agentManager.dialog.compareModels.selectModels": "Выбрать модели...",
"agentManager.dialog.creating": "Создание...",
"agentManager.dialog.createWorkspace": "Создать worktree",
"agentManager.dialog.removeImage": "Удалить изображение",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "Worktree ใหม่",
"agentManager.worktree.setupScript": "สคริปต์ตั้งค่า Worktree",
"agentManager.worktree.delete": "ลบ Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "ล้าสมัย",
"agentManager.worktree.staleTooltip": "ไม่พบบนดิสก์หรือไม่ได้ถูกติดตามโดย git worktree อีกต่อไป",
"agentManager.worktree.removeStale": "ลบ Worktree ที่ล้าสมัย",
"agentManager.worktree.doubleClickRename": "ดับเบิลคลิกเพื่อเปลี่ยนชื่อ",
"agentManager.worktree.versions": "{{count}} เวอร์ชัน",
"agentManager.worktree.advancedOptions": "ตัวเลือก Worktree ขั้นสูง",
"agentManager.worktree.defaultBaseBranch": "สาขาฐานเริ่มต้น",
"agentManager.worktree.defaultBaseBranchAuto": "ตรวจจับอัตโนมัติ",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "ฐาน",
"agentManager.hoverCard.sessions": "เซสชัน",
@@ -50,13 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "โฟกัสแผง",
"agentManager.shortcuts.openAgentManager": "เปิด Agent Manager",
"agentManager.shortcuts.showShortcuts": "แสดงปุ่มลัดแป้นพิมพ์",
"agentManager.dialog.deleteWorktree.title": "ลบ Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "ลบ Worktree ",
"agentManager.dialog.deleteWorktree.messagePost":
"? การดำเนินการนี้จะลบ Worktree ออกจากดิสก์และยกเลิกการเชื่อมโยงเซสชันทั้งหมด",
"agentManager.dialog.deleteWorktree.cancel": "ยกเลิก",
"agentManager.dialog.deleteWorktree.confirm": "ลบ",
"agentManager.dialog.removeStaleWorktree.title": "ลบ Worktree ที่ล้าสมัย",
"agentManager.dialog.removeStaleWorktree.messagePre": "ลบ Worktree ที่ล้าสมัย ",
"agentManager.dialog.removeStaleWorktree.messagePost":
@@ -81,6 +77,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} Worktrees จะทำงานพร้อมกัน",
"agentManager.dialog.compareModels": "เปรียบเทียบโมเดล",
"agentManager.dialog.compareModels.searchModels": "ค้นหาโมเดล...",
"agentManager.dialog.compareModels.selectModels": "เลือกโมเดล...",
"agentManager.dialog.creating": "กำลังสร้าง...",
"agentManager.dialog.createWorkspace": "สร้าง Worktree",
"agentManager.dialog.removeImage": "ลบรูปภาพ",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "新建 Worktree",
"agentManager.worktree.setupScript": "Worktree 设置脚本",
"agentManager.worktree.delete": "删除 Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "已失效",
"agentManager.worktree.staleTooltip": "在磁盘上缺失,或不再由 git worktree 跟踪",
"agentManager.worktree.removeStale": "移除失效 Worktree",
"agentManager.worktree.doubleClickRename": "双击重命名",
"agentManager.worktree.versions": "{{count}} 个版本",
"agentManager.worktree.advancedOptions": "Worktree 高级选项",
"agentManager.worktree.defaultBaseBranch": "默认基础分支",
"agentManager.worktree.defaultBaseBranchAuto": "自动检测",
"agentManager.hoverCard.branch": "分支",
"agentManager.hoverCard.base": "基础",
"agentManager.hoverCard.sessions": "会话",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "聚焦面板",
"agentManager.shortcuts.openAgentManager": "打开 Agent Manager",
"agentManager.shortcuts.showShortcuts": "显示键盘快捷键",
"agentManager.dialog.deleteWorktree.title": "删除 Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "删除 Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "?这将从磁盘中移除 Worktree 并取消所有会话的关联。",
"agentManager.dialog.deleteWorktree.cancel": "取消",
"agentManager.dialog.deleteWorktree.confirm": "删除",
"agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost": "?这只会移除 Agent Manager 映射,不会触碰磁盘上的文件。",
@@ -79,6 +76,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} 个 Worktree 将并行运行",
"agentManager.dialog.compareModels": "比较模型",
"agentManager.dialog.compareModels.searchModels": "搜索模型...",
"agentManager.dialog.compareModels.selectModels": "选择模型...",
"agentManager.dialog.creating": "创建中...",
"agentManager.dialog.createWorkspace": "创建工作树",
"agentManager.dialog.removeImage": "移除图片",
@@ -7,12 +7,15 @@ export const dict = {
"agentManager.worktree.new": "新建 Worktree",
"agentManager.worktree.setupScript": "Worktree 設定指令碼",
"agentManager.worktree.delete": "刪除 Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.stale": "已失效",
"agentManager.worktree.staleTooltip": "磁碟上不存在,或不再由 git worktree 追蹤",
"agentManager.worktree.removeStale": "移除失效 Worktree",
"agentManager.worktree.doubleClickRename": "按兩下以重新命名",
"agentManager.worktree.versions": "{{count}} 個版本",
"agentManager.worktree.advancedOptions": "Worktree 進階選項",
"agentManager.worktree.defaultBaseBranch": "預設基礎分支",
"agentManager.worktree.defaultBaseBranchAuto": "自動偵測",
"agentManager.hoverCard.branch": "分支",
"agentManager.hoverCard.base": "基底",
"agentManager.hoverCard.sessions": "工作階段",
@@ -50,12 +53,6 @@ export const dict = {
"agentManager.shortcuts.focusPanel": "聚焦面板",
"agentManager.shortcuts.openAgentManager": "開啟 Agent Manager",
"agentManager.shortcuts.showShortcuts": "顯示鍵盤快捷鍵",
"agentManager.dialog.deleteWorktree.title": "刪除 Worktree",
"agentManager.dialog.deleteWorktree.messagePre": "刪除 Worktree ",
"agentManager.dialog.deleteWorktree.messagePost": "?這將從磁碟中移除 Worktree 並取消所有工作階段的關聯。",
"agentManager.dialog.deleteWorktree.cancel": "取消",
"agentManager.dialog.deleteWorktree.confirm": "刪除",
"agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ",
"agentManager.dialog.removeStaleWorktree.messagePost": "?這只會移除 Agent Manager 對應,不會變更磁碟上的檔案。",
@@ -79,6 +76,7 @@ export const dict = {
"agentManager.dialog.versionHint": "{{count}} 個 Worktree 將並行執行",
"agentManager.dialog.compareModels": "比較模型",
"agentManager.dialog.compareModels.searchModels": "搜尋模型...",
"agentManager.dialog.compareModels.selectModels": "選擇模型...",
"agentManager.dialog.creating": "建立中...",
"agentManager.dialog.createWorkspace": "建立工作樹",
"agentManager.dialog.removeImage": "移除圖片",
@@ -0,0 +1,63 @@
import { Component, createMemo, Switch, Match } from "solid-js"
import { Card } from "@kilocode/kilo-ui/card"
import { Button } from "@kilocode/kilo-ui/button"
import type { AssistantMessage } from "@kilocode/sdk/v2"
import { useLanguage } from "../../context/language"
import {
unwrapError,
parseAssistantError,
isUnauthorizedPaidModelError,
isUnauthorizedPromotionLimitError,
} from "../../utils/errorUtils"
interface ErrorDisplayProps {
error: NonNullable<AssistantMessage["error"]>
onLogin?: () => void
}
export const ErrorDisplay: Component<ErrorDisplayProps> = (props) => {
const { t } = useLanguage()
const parsed = createMemo(() => parseAssistantError(props.error))
const errorText = createMemo(() => {
const msg = props.error.data?.message
if (typeof msg === "string") return unwrapError(msg)
if (msg === undefined || msg === null) return ""
return unwrapError(String(msg))
})
return (
<Switch
fallback={
<Card variant="error" class="error-card">
{errorText()}
</Card>
}
>
<Match when={isUnauthorizedPaidModelError(parsed())}>
<div data-component="auth-prompt">
<div data-slot="auth-prompt-header">
<span data-slot="auth-prompt-icon">✨</span>
<span data-slot="auth-prompt-title">{t("error.paidModel.title")}</span>
</div>
<p data-slot="auth-prompt-description">{t("error.paidModel.description")}</p>
<Button variant="primary" onClick={() => props.onLogin?.()}>
{t("error.paidModel.action")}
</Button>
</div>
</Match>
<Match when={isUnauthorizedPromotionLimitError(parsed())}>
<div data-component="auth-prompt">
<div data-slot="auth-prompt-header">
<span data-slot="auth-prompt-icon">🕙</span>
<span data-slot="auth-prompt-title">{t("error.promotionLimit.title")}</span>
</div>
<p data-slot="auth-prompt-description">{t("error.promotionLimit.description")}</p>
<Button variant="primary" onClick={() => props.onLogin?.()}>
{t("error.promotionLimit.action")}
</Button>
</div>
</Match>
</Switch>
)
}
@@ -575,6 +575,20 @@ export const PromptInput: Component = () => {
<ModeSwitcher />
<ModelSelector />
<ThinkingSelector />
<Show when={session.hasModelOverride()}>
<Tooltip value={language.t("prompt.action.resetModel")} placement="top">
<Button
variant="ghost"
size="small"
onClick={() => session.clearModelOverride()}
aria-label={language.t("prompt.action.resetModel")}
>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
</svg>
</Button>
</Tooltip>
</Show>
</div>
<div class="prompt-input-hint-actions">
<Tooltip value={language.t("prompt.action.enhance")} placement="top">
@@ -16,7 +16,6 @@ import { Collapsible } from "@kilocode/kilo-ui/collapsible"
import { Accordion } from "@kilocode/kilo-ui/accordion"
import { DiffChanges } from "@kilocode/kilo-ui/diff-changes"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Card } from "@kilocode/kilo-ui/card"
import { StickyAccordionHeader } from "@kilocode/kilo-ui/sticky-accordion-header"
import { useData } from "@kilocode/kilo-ui/context/data"
import { useDiffComponent } from "@kilocode/kilo-ui/context/diff"
@@ -28,6 +27,9 @@ import type {
Part as SDKPart,
FileDiff,
} from "@kilocode/sdk/v2"
import { ErrorDisplay } from "./ErrorDisplay"
import { useServer } from "../../context/server"
function getDirectory(path: string): string {
const sep = path.includes("/") ? "/" : "\\"
const idx = path.lastIndexOf(sep)
@@ -40,48 +42,6 @@ function getFilename(path: string): string {
return idx === -1 ? path : path.slice(idx + 1)
}
function unwrapError(message: string): string {
const text = message.replace(/^Error:\s*/, "").trim()
const tryParse = (v: string) => {
try {
return JSON.parse(v) as unknown
} catch {
return undefined
}
}
const read = (v: string) => {
const first = tryParse(v)
if (typeof first !== "string") return first
return tryParse(first.trim())
}
let json = read(text)
if (json === undefined) {
const start = text.indexOf("{")
const end = text.lastIndexOf("}")
if (start !== -1 && end > start) json = read(text.slice(start, end + 1))
}
if (!json || typeof json !== "object" || Array.isArray(json)) return message
const rec = json as Record<string, unknown>
const err =
rec.error && typeof rec.error === "object" && !Array.isArray(rec.error)
? (rec.error as Record<string, unknown>)
: undefined
if (err) {
const type = typeof err.type === "string" ? err.type : undefined
const msg = typeof err.message === "string" ? err.message : undefined
if (type && msg) return `${type}: ${msg}`
if (msg) return msg
if (type) return type
const code = typeof err.code === "string" ? err.code : undefined
if (code) return code
}
const msg = typeof rec.message === "string" ? rec.message : undefined
if (msg) return msg
const reason = typeof rec.error === "string" ? rec.error : undefined
if (reason) return reason
return message
}
interface VscodeSessionTurnProps {
sessionID: string
messageID: string
@@ -92,6 +52,7 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
const data = useData()
const i18n = useI18n()
const diffComponent = useDiffComponent()
const server = useServer()
const emptyMessages: SDKMessage[] = []
const emptyParts: SDKPart[] = []
@@ -139,13 +100,6 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
() => assistantMessages().find((m) => m.error && m.error.name !== "MessageAbortedError")?.error,
)
const errorText = createMemo(() => {
const msg = error()?.data?.message
if (typeof msg === "string") return unwrapError(msg)
if (msg === undefined || msg === null) return ""
return unwrapError(String(msg))
})
// Diffs from message summary
const diffs = createMemo(() => {
const rawDiffs = (message() as unknown as { summary?: { diffs?: unknown[] } } | undefined)?.summary?.diffs
@@ -326,11 +280,9 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
</div>
</Show>
{/* Error card */}
{/* Error handling */}
<Show when={error()}>
<Card variant="error" class="error-card">
{errorText()}
</Card>
<ErrorDisplay error={error()!} onLogin={server.startLogin} />
</Show>
</div>
)}
File diff suppressed because it is too large Load Diff
@@ -2,420 +2,393 @@
.migration-wizard {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
align-items: center;
justify-content: center;
min-height: 100%;
font-size: 13px;
color: var(--vscode-foreground);
-webkit-font-smoothing: antialiased;
overflow: hidden;
padding: 16px;
}
.migration-wizard__container {
width: 520px;
}
/* Screen transitions */
.migration-wizard__screen--hidden {
display: none;
}
.migration-wizard__screen--active {
display: block;
animation: mw-screen-enter 0.28s ease-out;
}
@keyframes mw-screen-enter {
from {
opacity: 0;
transform: translateX(18px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Header */
.migration-wizard__header {
padding: 16px 20px 14px;
border-bottom: 1px solid var(--vscode-panel-border, var(--border-weak-base));
flex-shrink: 0;
}
.migration-wizard__title {
font-size: 16px;
font-weight: 600;
margin: 0 0 4px;
}
.migration-wizard__header-subtitle {
font-size: 11px;
color: var(--vscode-descriptionForeground);
margin: 0 0 12px;
line-height: 1.4;
}
/* Body */
.migration-wizard__body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 16px 20px;
gap: 12px;
}
/* Welcome screen — centered hero layout */
.migration-wizard__welcome {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
overflow-y: auto;
padding: 28px 8px 8px;
gap: 0;
min-height: 0;
margin-bottom: 32px;
}
.migration-wizard__welcome-logo {
width: 52px;
height: 52px;
margin-bottom: 16px;
flex-shrink: 0;
.migration-wizard__logo {
width: 56px;
height: 56px;
margin: 0 auto 20px;
}
.migration-wizard__welcome-logo img {
.migration-wizard__logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.migration-wizard__welcome-title {
font-size: 18px;
.migration-wizard__header h1 {
font-size: 22px;
font-weight: 600;
margin: 0 0 8px;
color: var(--vscode-foreground);
letter-spacing: -0.3px;
margin-bottom: 10px;
display: inline-flex;
align-items: center;
gap: 10px;
justify-content: center;
width: 100%;
}
.migration-wizard__welcome-subtitle {
margin: 0 0 20px;
text-align: center;
.migration-wizard__badge {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
padding: 3px 9px;
border-radius: 6px;
background: color-mix(in srgb, var(--vscode-button-background) 15%, transparent);
color: var(--vscode-button-background);
vertical-align: middle;
text-transform: uppercase;
}
.migration-wizard__header p {
font-size: 13px;
color: var(--vscode-descriptionForeground);
font-size: 12px;
line-height: 1.6;
max-width: 440px;
margin: 0 auto;
}
.migration-wizard__welcome-cards {
/* Feature cards — What's New */
.migration-wizard__features {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
text-align: left;
margin-bottom: 8px;
}
/* Step indicator */
.migration-wizard__steps {
.migration-wizard__feature {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 12px;
padding: 16px 18px;
display: flex;
align-items: center;
gap: 0;
flex-shrink: 0;
gap: 14px;
align-items: flex-start;
}
.migration-wizard__step {
.migration-wizard__feature-icon {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
transition: all 0.2s ease;
}
.migration-wizard__feature-icon svg {
width: 18px;
height: 18px;
}
.migration-wizard__feature-icon--blue {
background: color-mix(in srgb, var(--vscode-button-background) 15%, transparent);
color: var(--vscode-button-background);
}
.migration-wizard__feature-icon--purple {
background: rgba(167, 139, 250, 0.12);
color: #a78bfa;
}
.migration-wizard__feature-icon--cyan {
background: rgba(34, 211, 238, 0.1);
color: #22d3ee;
}
.migration-wizard__feature-icon--orange {
background: rgba(251, 146, 60, 0.1);
color: #fb923c;
}
.migration-wizard__feature-text {
flex: 1;
}
.migration-wizard__feature-text .title {
font-size: 13px;
font-weight: 600;
line-height: 1.3;
margin-bottom: 3px;
}
.migration-wizard__feature-text .detail {
font-size: 12px;
color: var(--vscode-descriptionForeground);
line-height: 1.45;
}
/* Blog link */
.migration-wizard__blog-link {
text-align: center;
margin-top: 16px;
}
.migration-wizard__blog-link a {
font-size: 12px;
color: var(--vscode-descriptionForeground);
opacity: 0.65;
text-decoration: none;
transition: opacity 0.15s;
}
.migration-wizard__blog-link a:hover {
opacity: 1;
}
.migration-wizard__blog-link a span {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Migration card */
.migration-wizard__card {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 14px;
overflow: hidden;
}
.migration-wizard__section-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
padding: 16px 20px 8px;
color: var(--vscode-descriptionForeground);
opacity: 0.6;
}
.migration-wizard__item {
display: flex;
align-items: center;
gap: 14px;
padding: 11px 20px;
border-top: 1px solid var(--vscode-panel-border);
}
.migration-wizard__item:first-child,
.migration-wizard__section-label + .migration-wizard__item {
border-top: none;
}
/* Custom checkbox */
.migration-wizard__checkbox {
position: relative;
width: 20px;
height: 20px;
flex-shrink: 0;
cursor: pointer;
}
.migration-wizard__checkbox input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
margin: 0;
}
.migration-wizard__checkmark {
width: 20px;
height: 20px;
border-radius: 5px;
border: 2px solid color-mix(in srgb, var(--vscode-foreground) 15%, transparent);
background: transparent;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.migration-wizard__checkbox input:checked + .migration-wizard__checkmark {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
}
.migration-wizard__checkmark svg {
width: 12px;
height: 12px;
opacity: 0;
transition: opacity 0.15s;
}
.migration-wizard__checkbox input:checked + .migration-wizard__checkmark svg {
opacity: 1;
}
/* Status indicators (during/after migration) */
.migration-wizard__status-icon {
width: 20px;
height: 20px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.migration-wizard__status-icon--success {
background: rgba(52, 199, 89, 0.12);
color: var(--vscode-testing-iconPassed, #34c759);
}
.migration-wizard__status-icon--warning {
background: rgba(255, 159, 10, 0.12);
color: var(--vscode-testing-iconQueued, #ff9f0a);
}
.migration-wizard__status-icon--error {
background: rgba(241, 76, 76, 0.12);
color: var(--vscode-testing-iconFailed, #f14c4c);
}
.migration-wizard__status-icon--pending {
border: 2px solid color-mix(in srgb, var(--vscode-foreground) 10%, transparent);
background: transparent;
}
.migration-wizard__status-icon svg {
width: 12px;
height: 12px;
}
/* Spinner for migrating state */
.migration-wizard__spinner {
width: 14px;
height: 14px;
border: 2px solid color-mix(in srgb, var(--vscode-button-background) 30%, transparent);
border-top-color: var(--vscode-button-background);
border-radius: 50%;
animation: migration-spin 0.6s linear infinite;
}
@keyframes migration-spin {
to {
transform: rotate(360deg);
}
}
/* Item text */
.migration-wizard__item-text {
flex: 1;
min-width: 0;
}
.migration-wizard__item-text .label {
font-size: 13px;
font-weight: 500;
line-height: 1.3;
}
.migration-wizard__item-text .desc {
font-size: 11px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
margin-top: 1px;
}
/* Disabled items */
.migration-wizard__item--disabled {
opacity: 0.45;
pointer-events: none;
}
.migration-wizard__item--disabled .migration-wizard__checkmark {
border-color: color-mix(in srgb, var(--vscode-foreground) 8%, transparent);
}
/* Clickable item (cleanup row) — text area triggers checkbox */
.migration-wizard__item--clickable .migration-wizard__item-text {
cursor: pointer;
user-select: none;
}
/* Connector line rendered as a sibling <span> element in the JSX */
.migration-wizard__step-connector {
width: 28px;
height: 2px;
/* Divider */
.migration-wizard__divider {
height: 1px;
background: var(--vscode-panel-border);
flex-shrink: 0;
margin: 0 2px;
transition: background 0.2s ease;
}
.migration-wizard__step-connector--done {
background: var(--vscode-testing-iconPassed, #89d185);
}
.migration-wizard__step--upcoming {
background: var(--vscode-input-background);
border: 1px solid var(--vscode-panel-border);
color: var(--vscode-descriptionForeground);
}
.migration-wizard__step--current {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
box-shadow:
0 0 0 2px color-mix(in srgb, var(--vscode-button-background) 40%, transparent),
0 0 0 4px var(--vscode-editor-background, var(--vscode-sideBar-background));
}
.migration-wizard__step--done {
background: var(--vscode-testing-iconPassed, #89d185);
color: #fff;
}
/* Scrollable content area */
.migration-wizard__content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 0;
}
/* Lead paragraph */
.migration-wizard__lead {
margin: 0 0 8px;
color: var(--vscode-descriptionForeground);
/* Summary badge */
.migration-wizard__summary {
text-align: center;
font-size: 12px;
line-height: 1.5;
}
/* Info cards */
.migration-wizard__info-card {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
padding: 12px;
font-size: 12px;
line-height: 1.5;
color: var(--vscode-descriptionForeground);
padding: 12px 20px 4px;
}
.migration-wizard__info-card--warning {
border-color: var(--vscode-inputValidation-warningBorder, #cca700);
background: var(--vscode-inputValidation-warningBackground, rgba(204, 167, 0, 0.1));
color: var(--vscode-foreground);
.migration-wizard__summary--success {
color: var(--vscode-testing-iconPassed, #34c759);
}
.migration-wizard__info-card--success {
border-color: var(--vscode-testing-iconPassed, #89d185);
background: rgba(137, 209, 133, 0.08);
color: var(--vscode-foreground);
}
.migration-wizard__list {
margin: 8px 0 0 0;
padding-left: 20px;
}
.migration-wizard__list li {
margin-bottom: 4px;
}
/* Section groups */
.migration-wizard__section {
display: flex;
flex-direction: column;
gap: 4px;
}
.migration-wizard__section-title {
font-size: 11px;
font-weight: 600;
color: var(--vscode-descriptionForeground);
margin: 0 0 4px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--vscode-panel-border);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.migration-wizard__section-subtitle {
font-size: 11px;
font-weight: 600;
color: var(--vscode-descriptionForeground);
margin: 8px 0 2px 0;
text-transform: uppercase;
letter-spacing: 0.04em;
opacity: 0.8;
}
/* Checkbox items */
.migration-wizard__item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 4px;
cursor: pointer;
border-radius: 3px;
}
.migration-wizard__item:hover {
background: var(--vscode-list-hoverBackground);
}
.migration-wizard__item--disabled {
opacity: 0.5;
cursor: not-allowed;
}
.migration-wizard__item input[type="checkbox"] {
margin-top: 2px;
flex-shrink: 0;
}
.migration-wizard__item-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.migration-wizard__item-name {
font-size: 12px;
color: var(--vscode-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.migration-wizard__item-meta {
font-size: 11px;
color: var(--vscode-descriptionForeground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.migration-wizard__item-tag {
display: inline-block;
font-size: 10px;
padding: 1px 5px;
border-radius: 10px;
margin-top: 2px;
}
.migration-wizard__item-tag--warn {
background: rgba(204, 167, 0, 0.15);
color: var(--vscode-testing-iconQueued, #cca700);
border: 1px solid var(--vscode-inputValidation-warningBorder, #cca700);
}
.migration-wizard__empty {
margin: 0;
color: var(--vscode-descriptionForeground);
font-style: italic;
}
/* Approval settings info note */
.migration-wizard__note {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 0 2px;
margin-top: 4px;
}
.migration-wizard__note-icon {
flex-shrink: 0;
margin-top: 1px;
color: var(--vscode-descriptionForeground);
}
.migration-wizard__note-text {
margin: 0;
font-size: 11px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
/* Progress card wrapper */
.migration-wizard__progress-card {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
padding: 12px;
}
/* Progress list */
.migration-wizard__progress-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.migration-wizard__progress-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
}
.migration-wizard__progress-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.migration-wizard__progress-name {
color: var(--vscode-foreground);
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.migration-wizard__progress-msg {
color: var(--vscode-descriptionForeground);
font-size: 11px;
flex-shrink: 0;
}
.migration-wizard__progress-label {
font-size: 11px;
flex-shrink: 0;
margin-left: auto;
}
/* Cleanup checkbox on completion screen */
.migration-wizard__cleanup {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 12px;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
cursor: pointer;
transition: background 0.1s;
}
.migration-wizard__cleanup:hover {
background: var(--vscode-list-hoverBackground);
}
.migration-wizard__cleanup input[type="checkbox"] {
margin-top: 2px;
flex-shrink: 0;
}
.migration-wizard__cleanup-label {
display: block;
font-size: 12px;
color: var(--vscode-foreground);
font-weight: 500;
}
.migration-wizard__cleanup-desc {
display: block;
font-size: 11px;
color: var(--vscode-descriptionForeground);
margin-top: 2px;
line-height: 1.4;
}
/* Footer with action buttons */
/* Footer */
.migration-wizard__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
padding-top: 8px;
border-top: 1px solid var(--vscode-panel-border);
padding: 20px 0;
margin-top: 20px;
}
.migration-wizard__footer--space-between {
justify-content: space-between;
}
.migration-wizard__btn-group {
display: flex;
gap: 10px;
}
/* Buttons */
.migration-wizard__btn {
padding: 6px 14px;
font-size: 12px;
border-radius: 4px;
font-family: var(--vscode-font-family);
font-size: 13px;
font-weight: 500;
padding: 9px 20px;
border-radius: 8px;
border: none;
cursor: pointer;
font-family: var(--vscode-font-family);
font-weight: 500;
transition: opacity 0.1s;
transition: all 0.15s;
}
.migration-wizard__btn:disabled {
@@ -423,21 +396,36 @@
cursor: not-allowed;
}
.migration-wizard__btn--primary {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.migration-wizard__btn--primary:not(:disabled):hover {
background: var(--vscode-button-hoverBackground);
}
.migration-wizard__btn--ghost {
background: transparent;
color: var(--vscode-descriptionForeground);
}
.migration-wizard__btn--ghost:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-foreground);
background: color-mix(in srgb, var(--vscode-foreground) 5%, transparent);
}
.migration-wizard__btn--primary {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.migration-wizard__btn--primary:not(:disabled):hover {
background: var(--vscode-button-hoverBackground);
box-shadow:
0 2px 8px color-mix(in srgb, var(--vscode-button-background) 30%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* Nothing to migrate */
.migration-wizard__empty {
padding: 20px;
text-align: center;
color: var(--vscode-descriptionForeground);
font-style: italic;
font-size: 12px;
}
@@ -6,6 +6,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { useConfig } from "../../context/config"
import { useProvider } from "../../context/provider"
import { useLanguage } from "../../context/language"
import { useSession } from "../../context/session"
import { ModelSelectorBase } from "../shared/ModelSelector"
import type { ModelSelection } from "../../types/messages"
import SettingsRow from "./SettingsRow"
@@ -31,6 +32,7 @@ const ProvidersTab: Component = () => {
const { config, updateConfig } = useConfig()
const provider = useProvider()
const language = useLanguage()
const session = useSession()
const providerOptions = createMemo<ProviderOption[]>(() =>
Object.keys(provider.providers())
@@ -59,13 +61,25 @@ const ProvidersTab: Component = () => {
function handleModelSelect(configKey: "model" | "small_model") {
return (providerID: string, modelID: string) => {
if (!providerID || !modelID) {
updateConfig({ [configKey]: undefined })
updateConfig({ [configKey]: null })
} else {
updateConfig({ [configKey]: `${providerID}/${modelID}` })
}
}
}
const allAgents = createMemo(() => session.agents())
function handleModeModelSelect(agentName: string) {
return (providerID: string, modelID: string) => {
if (!providerID || !modelID) {
updateConfig({ agent: { [agentName]: { model: null } } })
} else {
updateConfig({ agent: { [agentName]: { model: `${providerID}/${modelID}` } } })
}
}
}
return (
<div>
{/* Model selection */}
@@ -97,6 +111,27 @@ const ProvidersTab: Component = () => {
</SettingsRow>
</Card>
{/* Model per Mode */}
<h4 style={{ "margin-top": "24px", "margin-bottom": "8px" }}>{language.t("settings.providers.modeModels")}</h4>
<Card>
<For each={allAgents()}>
{(agent, index) => (
<SettingsRow
title={agent.name.charAt(0).toUpperCase() + agent.name.slice(1)}
last={index() === allAgents().length - 1}
>
<ModelSelectorBase
value={parseModelConfig(config().agent?.[agent.name]?.model ?? undefined)}
onSelect={handleModeModelSelect(agent.name)}
placement="bottom-start"
allowClear
clearLabel={language.t("settings.providers.notSet")}
/>
</SettingsRow>
)}
</For>
</Card>
{/* Disabled providers */}
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>{language.t("settings.providers.disabled")}</h4>
<Card>
@@ -1,6 +1,6 @@
import { Component, JSX } from "solid-js"
const SettingsRow: Component<{ title: string; description: string; last?: boolean; children: JSX.Element }> = (
const SettingsRow: Component<{ title: string; description?: string; last?: boolean; children: JSX.Element }> = (
props,
) => (
<div
@@ -9,11 +9,12 @@ const SettingsRow: Component<{ title: string; description: string; last?: boolea
"margin-bottom": props.last ? "0" : "8px",
"padding-bottom": props.last ? "0" : "8px",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
...(props.description == null ? { "align-items": "center" } : {}),
}}
>
<div data-slot="settings-row-label">
<div data-slot="settings-row-label-title">{props.title}</div>
<div data-slot="settings-row-label-subtitle">{props.description}</div>
<div data-slot="settings-row-label-title" style={props.description == null ? { "margin-bottom": "0" } : {}}>{props.title}</div>
{props.description != null && <div data-slot="settings-row-label-subtitle">{props.description}</div>}
</div>
<div data-slot="settings-row-input">{props.children}</div>
</div>
@@ -31,8 +31,7 @@ export function buildTriggerLabel(
hasProviders: boolean,
labels: { select: string; noProviders: string; notSet: string },
): string {
if (resolvedName)
return providerID === KILO_GATEWAY_ID ? stripSubProviderPrefix(resolvedName) : resolvedName
if (resolvedName) return providerID === KILO_GATEWAY_ID ? stripSubProviderPrefix(resolvedName) : resolvedName
if (raw?.providerID && raw?.modelID) {
return raw.providerID === KILO_GATEWAY_ID ? raw.modelID : `${raw.providerID} / ${raw.modelID}`
}
@@ -14,6 +14,37 @@ interface ConfigContextValue {
updateConfig: (partial: Partial<Config>) => void
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value)
}
/** Deep merge two objects, with source values overriding target values. */
function deepMerge(target: Config, source: Partial<Config>): Config {
const result: Record<string, unknown> = { ...target }
for (const [key, value] of Object.entries(source)) {
if (isRecord(value) && isRecord(result[key])) {
result[key] = deepMerge(result[key] as Config, value as Partial<Config>)
} else {
result[key] = value
}
}
return result as Config
}
/** Recursively remove keys whose value is null (null = "deleted"). */
function stripNulls(obj: Config): Config {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) continue
if (isRecord(value)) {
result[key] = stripNulls(value as Config)
} else {
result[key] = value
}
}
return result as Config
}
const ConfigContext = createContext<ConfigContextValue>()
export const ConfigProvider: ParentComponent = (props) => {
@@ -59,8 +90,8 @@ export const ConfigProvider: ParentComponent = (props) => {
onCleanup(() => clearInterval(retryTimer))
function updateConfig(partial: Partial<Config>) {
// Optimistically update local state
setConfig((prev) => ({ ...prev, ...partial }))
// Optimistically update local state with deep merge + null stripping
setConfig((prev) => stripNulls(deepMerge(prev, partial)))
// Send to extension for persistence
vscode.postMessage({ type: "updateConfig", config: partial })
}
@@ -1,7 +1,7 @@
/**
* Session context
* Manages session state, messages, and handles SSE events from the extension.
* Also owns per-session model selection (provider context is catalog-only).
* Also owns global (extension-lifetime) model selection (provider context is catalog-only).
*/
import {
@@ -20,6 +20,7 @@ import { createStore, produce } from "solid-js/store"
import { useVSCode } from "./vscode"
import { useServer } from "./server"
import { useProvider } from "./provider"
import { useConfig } from "./config"
import { useLanguage } from "./language"
import { showToast } from "@kilocode/kilo-ui/toast"
import type {
@@ -47,7 +48,7 @@ interface SessionStore {
messages: Record<string, Message[]> // sessionID -> messages
parts: Record<string, Part[]> // messageID -> parts
todos: Record<string, TodoItem[]> // sessionID -> todos
modelSelections: Record<string, ModelSelection> // sessionID -> model
modelSelections: Record<string, ModelSelection> // agentName -> model (global, extension-lifetime)
agentSelections: Record<string, string> // sessionID -> agent name
variantSelections: Record<string, string> // "providerID/modelID" -> variant name
}
@@ -96,9 +97,11 @@ interface SessionContextValue {
questions: Accessor<QuestionRequest[]>
questionErrors: Accessor<Set<string>>
// Model selection (per-session)
// Model selection (global, extension-lifetime)
selected: Accessor<ModelSelection | null>
selectModel: (providerID: string, modelID: string) => void
hasModelOverride: Accessor<boolean>
clearModelOverride: () => void
// Cost and context usage for the current session
totalCost: Accessor<number>
@@ -144,6 +147,7 @@ export const SessionProvider: ParentComponent = (props) => {
const vscode = useVSCode()
const server = useServer()
const provider = useProvider()
const { config } = useConfig()
const language = useLanguage()
// Current session ID
@@ -177,15 +181,15 @@ export const SessionProvider: ParentComponent = (props) => {
// Tracks question IDs that failed so the UI can reset sending state
const [questionErrors, setQuestionErrors] = createSignal<Set<string>>(new Set())
// Pending model selection for before a session exists
const [pendingModelSelection, setPendingModelSelection] = createSignal<ModelSelection | null>(null)
const [pendingWasUserSet, setPendingWasUserSet] = createSignal(false)
// Tracks whether the user has explicitly set a model override per agent (to
// prevent the default-sync effect from overwriting it).
const [userSetAgents, setUserSetAgents] = createSignal<Record<string, boolean>>({})
// Agents (modes) loaded from the CLI backend
const [agents, setAgents] = createSignal<AgentInfo[]>([])
const [defaultAgent, setDefaultAgent] = createSignal("code")
// Pending agent selection for before a session exists (mirrors pendingModelSelection)
// Pending agent selection for before a session exists
const [pendingAgentSelection, setPendingAgentSelection] = createSignal<string | null>(null)
// Cloud session preview state
@@ -202,35 +206,35 @@ export const SessionProvider: ParentComponent = (props) => {
variantSelections: {},
})
// Keep pending selection in sync with provider default until the user
// explicitly changes it (or a session exists).
// Keep model selection in sync with provider/mode default until the user
// explicitly overrides it.
createEffect(() => {
const def = provider.defaultSelection()
if (currentSessionID()) {
return
}
const agentName = selectedAgentName()
if (userSetAgents()[agentName]) return
if (pendingWasUserSet()) {
return
}
setPendingModelSelection(def)
// Per-mode config takes priority over global default
const modeModel = getModeModel(agentName)
const sel = modeModel ?? def
if (sel) setStore("modelSelections", agentName, sel)
})
// If we have no pending yet, initialize it from provider default.
createEffect(() => {
if (!pendingModelSelection()) {
setPendingModelSelection(provider.defaultSelection())
}
})
/** Parse a "provider/model" config string into a ModelSelection (or null). */
function getModeModel(agentName: string): ModelSelection | null {
const raw = config().agent?.[agentName]?.model
if (!raw) return null
const slash = raw.indexOf("/")
if (slash <= 0) return null
return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) }
}
// Per-session model selection
// Global model selection per agent/mode
// Precedence: user override > per-mode config > global default > kilo/auto
const selected = createMemo<ModelSelection | null>(() => {
const sessionID = currentSessionID()
if (sessionID) {
return store.modelSelections[sessionID] ?? provider.defaultSelection()
}
return pendingModelSelection()
const agentName = selectedAgentName()
const override = store.modelSelections[agentName]
if (override) return override
return getModeModel(agentName) ?? provider.defaultSelection()
})
// Per-session agent selection
@@ -243,14 +247,39 @@ export const SessionProvider: ParentComponent = (props) => {
})
function selectModel(providerID: string, modelID: string) {
const selection: ModelSelection = { providerID, modelID }
const id = currentSessionID()
if (id) {
setStore("modelSelections", id, selection)
} else {
setPendingWasUserSet(true)
setPendingModelSelection(selection)
}
const agentName = selectedAgentName()
setUserSetAgents((prev) => ({ ...prev, [agentName]: true }))
setStore("modelSelections", agentName, { providerID, modelID })
}
/** The config/default model for the current mode (what settings says). */
const configModel = createMemo<ModelSelection | null>(() => {
const agentName = selectedAgentName()
return getModeModel(agentName) ?? provider.defaultSelection()
})
/** True when the active model differs from what the config dictates. */
const hasModelOverride = createMemo<boolean>(() => {
const sel = selected()
const cfg = configModel()
if (!sel || !cfg) return false
return sel.providerID !== cfg.providerID || sel.modelID !== cfg.modelID
})
/** Clear the per-mode model override, falling back to config default. */
function clearModelOverride() {
const agentName = selectedAgentName()
setUserSetAgents((prev) => {
const next = { ...prev }
delete next[agentName]
return next
})
setStore(
"modelSelections",
produce((selections) => {
delete selections[agentName]
}),
)
}
// Handle agentsLoaded immediately (not in onMount) so we never miss
@@ -432,16 +461,6 @@ export const SessionProvider: ParentComponent = (props) => {
setStore("messages", session.id, [])
}
// If there's a pending model selection, assign it to this new session.
// Guard against duplicate sessionCreated events (HTTP response + SSE)
// which would overwrite the user's selection with the effect-restored default.
const pending = pendingModelSelection()
if (pending && !store.modelSelections[session.id]) {
setStore("modelSelections", session.id, pending)
setPendingModelSelection(null)
setPendingWasUserSet(false)
}
// Transfer pending agent selection to the new session
const pendingAgent = pendingAgentSelection()
if (pendingAgent && !store.agentSelections[session.id]) {
@@ -654,12 +673,6 @@ export const SessionProvider: ParentComponent = (props) => {
delete todos[sessionID]
}),
)
setStore(
"modelSelections",
produce((selections) => {
delete selections[sessionID]
}),
)
setStore(
"agentSelections",
produce((selections) => {
@@ -724,10 +737,6 @@ export const SessionProvider: ParentComponent = (props) => {
batch(() => {
setStore("sessions", session.id, session)
const pending = pendingModelSelection()
if (pending && !store.modelSelections[session.id]) {
setStore("modelSelections", session.id, pending)
}
const pendingAgent = pendingAgentSelection()
if (pendingAgent && !store.agentSelections[session.id]) {
setStore("agentSelections", session.id, pendingAgent)
@@ -788,6 +797,13 @@ export const SessionProvider: ParentComponent = (props) => {
setStore("agentSelections", id, name)
} else {
setPendingAgentSelection(name)
// When switching mode, initialize model for the new mode if the user
// hasn't explicitly set one for it
if (!userSetAgents()[name] && !store.modelSelections[name]) {
const modeModel = getModeModel(name)
const sel = modeModel ?? provider.defaultSelection()
if (sel) setStore("modelSelections", name, sel)
}
}
}
@@ -924,9 +940,7 @@ export const SessionProvider: ParentComponent = (props) => {
return
}
// Reset pending selection to default for the new session
setPendingModelSelection(provider.defaultSelection())
setPendingWasUserSet(false)
// Reset agent selection to default for the new session (model overrides persist)
setPendingAgentSelection(defaultAgent())
vscode.postMessage({ type: "createSession" })
}
@@ -938,8 +952,6 @@ export const SessionProvider: ParentComponent = (props) => {
setPermissions([])
setQuestions([])
setQuestionErrors(new Set<string>())
setPendingModelSelection(provider.defaultSelection())
setPendingWasUserSet(false)
setPendingAgentSelection(defaultAgent())
vscode.postMessage({ type: "clearSession" })
}
@@ -1082,15 +1094,22 @@ export const SessionProvider: ParentComponent = (props) => {
questionErrors,
selected,
selectModel,
hasModelOverride,
clearModelOverride,
totalCost,
contextUsage,
agents,
selectedAgent: selectedAgentName,
selectAgent,
getSessionAgent: (sessionID: string) => store.agentSelections[sessionID] ?? defaultAgent(),
getSessionModel: (sessionID: string) => store.modelSelections[sessionID] ?? provider.defaultSelection(),
setSessionModel: (sessionID: string, providerID: string, modelID: string) => {
setStore("modelSelections", sessionID, { providerID, modelID })
getSessionModel: (sessionID: string) => {
const agentName = store.agentSelections[sessionID] ?? defaultAgent()
return store.modelSelections[agentName] ?? provider.defaultSelection()
},
setSessionModel: (_sessionID: string, providerID: string, modelID: string) => {
const agentName = selectedAgentName()
setUserSetAgents((prev) => ({ ...prev, [agentName]: true }))
setStore("modelSelections", agentName, { providerID, modelID })
},
setSessionAgent: (sessionID: string, name: string) => {
setStore("agentSelections", sessionID, name)
+47 -29
View File
@@ -230,6 +230,7 @@ export const dict = {
"prompt.action.send": "إرسال",
"prompt.action.stop": "توقف",
"prompt.action.enhance": "تحسين النص",
"prompt.action.resetModel": "إعادة تعيين النموذج إلى الافتراضي",
"prompt.action.enhanceDescription":
"زر «حسّن الموجه» يطوّر موجهك بإضافة سياق أو توضيح أو إعادة صياغة. جرّب اكتب موجه هنا ثم اضغط الزر مرة ثانية وشوف النتيجة.",
@@ -405,6 +406,15 @@ export const dict = {
"error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
"error.paidModel.title": "يجب عليك تسجيل الدخول لاستخدام هذا النموذج",
"error.paidModel.description":
"سجّل الدخول أو أنشئ حسابًا للوصول إلى أكثر من 500 نموذج، واستخدم الرصيد بسعر التكلفة، أو استخدم مفتاحك الخاص.",
"error.paidModel.action": "تسجيل الدخول",
"error.promotionLimit.title": "يجب عليك التسجيل للمتابعة",
"error.promotionLimit.description":
"سجّل مجانًا للمتابعة واستكشاف أكثر من 500 نموذج. يستغرق دقيقتين، بدون بطاقة ائتمان. أو عُد لاحقًا.",
"error.promotionLimit.action": "التسجيل",
"error.chain.unknown": "خطأ غير معروف",
"error.chain.causedBy": "بسبب:",
"error.chain.apiError": "خطأ API",
@@ -980,42 +990,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "ترحيل الإصدار القديم",
"settings.aboutKiloCode.legacyMigration.description":
"ترحيل الإعدادات من تثبيت سابق لـ Kilo Code، بما في ذلك مفاتيح API لمقدمي الخدمة والنموذج الافتراضي.",
"migration.welcome.title": "مرحبًا بك في Kilo Code الجديد",
"migration.welcome.detected": "لقد اكتشفنا إعدادات من تثبيت سابق لـ Kilo Code.",
"migration.welcome.sessionsInfo": "لا يمكن ترحيل جلسات الدردشة والسجل — الإصدار الجديد يستخدم بنية مختلفة.",
"migration.welcome.canMigrate": "يمكن ترحيل الإعدادات التالية إلى الإصدار الجديد:",
"migration.welcome.start": "بدء الترحيل",
"migration.welcome.skip": "تخطي الترحيل",
"migration.steps.title": "ترحيل إعداداتك",
"migration.steps.subtitle": "لقد وجدنا إعدادات من تثبيت Kilo Code السابق. اختر ما تريد نقله.",
// Screen 1 — What's New
"migration.whatsNew.title": "ما الجديد في Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "لقد أعدنا بناء الإضافة على أساس أسرع وأكثر كفاءة.",
"migration.whatsNew.features.performance.title": "أداء أسرع للوكيل",
"migration.whatsNew.features.performance.detail":
"استدعاءات الأدوات المتوازية والوكلاء الفرعيون يتيحون لوكيلك إنجاز المزيد في وقت واحد — فتقضي وقتًا أقل في الانتظار ووقتًا أكثر في الإنجاز.",
"migration.whatsNew.features.interface.title": "واجهة مبسّطة",
"migration.whatsNew.features.interface.detail": "أقل تشتيتًا، أسهل وأسرع في القراءة.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"واجهة موحدة لتشغيل عدة وكلاء بالتوازي، كل منهم في شجرة عمل خاصة — راقب التقدم، وبدّل السياق، وراجع التغييرات في مكان واحد.",
"migration.whatsNew.features.foundation.title": "أساس مشترك",
"migration.whatsNew.features.foundation.detail":
"نواة صغيرة وفعالة واحدة عبر كل منتجات Kilo. تجربة مألوفة بغض النظر عن طريقة عملك.",
"migration.whatsNew.blogLink": "اقرأ الإعلان الكامل",
"migration.whatsNew.continue": "متابعة",
// Screen 2 — Migrate Settings
"migration.migrate.title": "ترحيل إعداداتك",
"migration.migrate.subtitle": "لقد وجدنا إعدادات من تثبيتك السابق. إليك ما يمكننا نقله.",
"migration.migrate.selectLabel": "اختر ما تريد ترحيله",
"migration.migrate.cannotMigrate": "لا يمكن ترحيله",
"migration.migrate.chatHistory": "جلسات الدردشة والسجل",
"migration.migrate.chatHistoryDesc": "غير متوافق مع البنية الجديدة",
"migration.migrate.button": "ترحيل الإعدادات",
"migration.migrate.skip": "تخطي",
"migration.migrate.back": "رجوع",
"migration.migrate.keysDetected": "تم اكتشاف {{count}} مفاتيح",
"migration.migrate.serversConfigured": "تم تكوين {{count}} خادم(خوادم)",
"migration.migrate.modesFound": "تم العثور على {{count}} وضع(أوضاع)",
"migration.migrate.nothingToMigrate": "لم يتم العثور على أي شيء لترحيله في الإعدادات القديمة.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "مفاتيح API لمقدمي الخدمة",
"migration.select.mcpServers": "خوادم MCP",
"migration.select.customModes": "الأوضاع المخصصة / الوكلاء",
"migration.select.defaultModel": "النموذج الافتراضي",
"migration.select.unsupported": "غير مدعوم في الإصدار الجديد",
"migration.select.nothingToMigrate": "لم يتم العثور على أي شيء لترحيله في الإعدادات القديمة.",
"migration.select.settings": "الإعدادات",
"migration.select.autoApproval": "الموافقة التلقائية",
"migration.select.autoApproval.commandRules": "قواعد الأوامر",
"migration.select.autoApproval.commandRulesDesc": "مفتاح التبديل الرئيسي وقوائم الأوامر المسموح بها/المرفوضة",
"migration.select.autoApproval.readPermission": "صلاحية القراءة",
"migration.select.autoApproval.readPermissionDesc": "الوصول لقراءة الملفات داخل وخارج مساحة العمل",
"migration.select.autoApproval.writePermission": "صلاحية الكتابة",
"migration.select.autoApproval.writePermissionDesc": "الوصول لكتابة وتعديل الملفات",
"migration.select.autoApproval.executePermission": "صلاحية التنفيذ",
"migration.select.autoApproval.executePermissionDesc": "تنفيذ أوامر الوحدة الطرفية",
"migration.select.autoApproval.mcpPermission": "صلاحية MCP",
"migration.select.autoApproval.mcpPermissionDesc": "استخدام أدوات MCP",
"migration.select.autoApproval.taskPermission": "صلاحية المهام",
"migration.select.autoApproval.taskPermissionDesc": "تبديل الوضع وإنشاء المهام الفرعية",
"migration.select.language": "لغة واجهة المستخدم",
"migration.select.languageDesc": "سيتم تطبيق لغتك المفضلة على الإضافة الجديدة",
"migration.select.autocomplete": "إعدادات الإكمال التلقائي",
"migration.select.autocompleteDesc": "تفضيلات التشغيل التلقائي، واختصارات لوحة المفاتيح، والإكمال التلقائي للدردشة",
"migration.select.continue": "متابعة",
"migration.select.back": "رجوع",
"migration.progress.title": "جاري ترحيل إعداداتك…",
"migration.progress.done": "متابعة",
// Migrate — completion
"migration.complete.summary": "تم ترحيل {{success}} من {{total}} عناصر بنجاح.",
"migration.complete.cleanup": "إزالة بيانات الإعدادات القديمة",
"migration.complete.cleanupDescription":
+47 -31
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "Enviar",
"prompt.action.stop": "Parar",
"prompt.action.enhance": "Melhorar prompt",
"prompt.action.resetModel": "Redefinir modelo para o padrão",
"prompt.action.enhanceDescription":
"O botão 'Aprimorar prompt' ajuda a melhorar seu pedido fornecendo contexto adicional, esclarecimentos ou reformulações. Tente digitar um pedido aqui e clique no botão novamente para ver como funciona.",
@@ -405,6 +406,15 @@ export const dict = {
"error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
"error.paidModel.title": "Você precisa fazer login para usar este modelo",
"error.paidModel.description":
"Faça login ou crie uma conta para acessar mais de 500 modelos, usar créditos a preço de custo ou trazer sua própria chave.",
"error.paidModel.action": "Fazer login",
"error.promotionLimit.title": "Você precisa se cadastrar para continuar",
"error.promotionLimit.description":
"Cadastre-se gratuitamente para continuar e explorar mais de 500 modelos. Leva 2 minutos, sem cartão de crédito. Ou volte mais tarde.",
"error.promotionLimit.action": "Cadastrar-se",
"error.chain.unknown": "Erro desconhecido",
"error.chain.causedBy": "Causado por:",
"error.chain.apiError": "Erro de API",
@@ -994,44 +1004,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Migração Legada",
"settings.aboutKiloCode.legacyMigration.description":
"Migre as configurações de uma instalação anterior do Kilo Code, incluindo chaves de API de provedor e modelo padrão.",
"migration.welcome.title": "Bem-vindo ao Novo Kilo Code",
"migration.welcome.detected": "Detectamos configurações de uma instalação anterior do Kilo Code.",
"migration.welcome.sessionsInfo":
"Sessões de chat e histórico não podem ser migrados — a nova versão usa uma arquitetura diferente.",
"migration.welcome.canMigrate": "As seguintes configurações podem ser migradas para a nova versão:",
"migration.welcome.start": "Iniciar Migração",
"migration.welcome.skip": "Pular Migração",
"migration.steps.title": "Migre suas configurações",
"migration.steps.subtitle":
"Encontramos configurações da sua instalação anterior do Kilo Code. Escolha o que trazer.",
// Screen 1 — What's New
"migration.whatsNew.title": "O Que Há de Novo no Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Reconstruímos a extensão sobre uma base mais rápida e eficiente.",
"migration.whatsNew.features.performance.title": "Desempenho de Agente Mais Rápido",
"migration.whatsNew.features.performance.detail":
"Chamadas de ferramentas paralelas e subagentes permitem que seu agente faça mais de uma vez — assim você gasta menos tempo esperando e mais tempo entregando.",
"migration.whatsNew.features.interface.title": "Interface Simplificada",
"migration.whatsNew.features.interface.detail": "Menos distrações, mais fácil e rápido de ler.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Uma interface unificada para executar múltiplos agentes em paralelo, cada um em sua própria worktree — monitore o progresso, troque de contexto e revise alterações em um só lugar.",
"migration.whatsNew.features.foundation.title": "Base Compartilhada",
"migration.whatsNew.features.foundation.detail":
"Um núcleo pequeno e eficiente em todos os produtos Kilo. Uma experiência familiar independentemente de como você escolha trabalhar.",
"migration.whatsNew.blogLink": "Leia o anúncio completo",
"migration.whatsNew.continue": "Continuar",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migre Suas Configurações",
"migration.migrate.subtitle": "Encontramos configurações da sua instalação anterior. Veja o que podemos trazer.",
"migration.migrate.selectLabel": "Selecione o que migrar",
"migration.migrate.cannotMigrate": "Não pode ser migrado",
"migration.migrate.chatHistory": "Sessões de Chat e Histórico",
"migration.migrate.chatHistoryDesc": "Incompatível com a nova arquitetura",
"migration.migrate.button": "Migrar Configurações",
"migration.migrate.skip": "Pular",
"migration.migrate.back": "Voltar",
"migration.migrate.keysDetected": "{{count}} chaves detectadas",
"migration.migrate.serversConfigured": "{{count}} servidor(es) configurado(s)",
"migration.migrate.modesFound": "{{count}} modo(s) encontrado(s)",
"migration.migrate.nothingToMigrate": "Nada para migrar foi encontrado nas configurações legadas.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "Chaves de API de Provedor",
"migration.select.mcpServers": "Servidores MCP",
"migration.select.customModes": "Modos Personalizados / Agentes",
"migration.select.defaultModel": "Modelo Padrão",
"migration.select.unsupported": "Não suportado na nova versão",
"migration.select.nothingToMigrate": "Nada para migrar foi encontrado nas configurações legadas.",
"migration.select.settings": "Configurações",
"migration.select.autoApproval": "Aprovação Automática",
"migration.select.autoApproval.commandRules": "Regras de Comando",
"migration.select.autoApproval.commandRulesDesc": "Alternador principal e listas de comandos permitidos/negados",
"migration.select.autoApproval.readPermission": "Permissão de Leitura",
"migration.select.autoApproval.readPermissionDesc": "Acesso de leitura de arquivos dentro e fora do workspace",
"migration.select.autoApproval.writePermission": "Permissão de Escrita",
"migration.select.autoApproval.writePermissionDesc": "Acesso de escrita e edição de arquivos",
"migration.select.autoApproval.executePermission": "Permissão de Execução",
"migration.select.autoApproval.executePermissionDesc": "Execução de comandos de terminal",
"migration.select.autoApproval.mcpPermission": "Permissão MCP",
"migration.select.autoApproval.mcpPermissionDesc": "Uso de ferramentas MCP",
"migration.select.autoApproval.taskPermission": "Permissão de Tarefa",
"migration.select.autoApproval.taskPermissionDesc": "Troca de modo e criação de subtarefas",
"migration.select.language": "Idioma da Interface",
"migration.select.languageDesc": "Sua preferência de idioma será aplicada à nova extensão",
"migration.select.autocomplete": "Configurações de Autocomplete",
"migration.select.autocompleteDesc": "Preferências de gatilho automático, atalhos de teclado e autocomplete do chat",
"migration.select.continue": "Continuar",
"migration.select.back": "Voltar",
"migration.progress.title": "Migrando suas configurações…",
"migration.progress.done": "Continuar",
// Migrate — completion
"migration.complete.summary": "{{success}} de {{total}} itens migrados com sucesso.",
"migration.complete.cleanup": "Remover dados de configurações legadas",
"migration.complete.cleanupDescription":
+47 -32
View File
@@ -232,6 +232,7 @@ export const dict = {
"prompt.action.send": "Pošalji",
"prompt.action.stop": "Zaustavi",
"prompt.action.enhance": "Poboljšaj prompt",
"prompt.action.resetModel": "Resetuj model na zadani",
"prompt.action.enhanceDescription":
"Dugme 'Poboljšaj prompt' pomaže poboljšati vaš zahtjev pružajući dodatni kontekst, pojašnjenje ili preformulaciju. Pokušajte upisati zahtjev ovdje i ponovo kliknite na dugme da vidite kako funkcioniše.",
@@ -410,6 +411,15 @@ export const dict = {
"error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
"error.paidModel.title": "Morate se prijaviti da biste koristili ovaj model",
"error.paidModel.description":
"Prijavite se ili kreirajte račun za pristup preko 500 modela, koristite kredite po cijeni koštanja ili donesite vlastiti ključ.",
"error.paidModel.action": "Prijava",
"error.promotionLimit.title": "Morate se registrovati da biste nastavili",
"error.promotionLimit.description":
"Registrujte se besplatno da nastavite i istražite preko 500 modela. Traje 2 minute, bez kreditne kartice. Ili se vratite kasnije.",
"error.promotionLimit.action": "Registracija",
"error.chain.unknown": "Nepoznata greška",
"error.chain.causedBy": "Uzrok:",
"error.chain.apiError": "API greška",
@@ -993,45 +1003,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Migracija sa prethodne verzije",
"settings.aboutKiloCode.legacyMigration.description":
"Migrirajte postavke iz prethodne instalacije Kilo Code, uključujući API ključeve provajdera i podrazumijevani model.",
"migration.welcome.title": "Dobrodošli u novi Kilo Code",
"migration.welcome.detected": "Otkrili smo postavke iz prethodne instalacije Kilo Code.",
"migration.welcome.sessionsInfo":
"Sesije razgovora i historija se ne mogu migrirati — nova verzija koristi drugačiju arhitekturu.",
"migration.welcome.canMigrate": "Sljedeće postavke se mogu migrirati na novu verziju:",
"migration.welcome.start": "Započni migraciju",
"migration.welcome.skip": "Preskoči migraciju",
"migration.steps.title": "Migrirajte svoje postavke",
"migration.steps.subtitle":
"Pronašli smo postavke iz vaše prethodne instalacije Kilo Code. Odaberite šta želite prenijeti.",
// Screen 1 — What's New
"migration.whatsNew.title": "Šta je novo u Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Ponovo smo izgradili ekstenziju na bržem i efikasnijem temelju.",
"migration.whatsNew.features.performance.title": "Brže performanse agenta",
"migration.whatsNew.features.performance.detail":
"Paralelni pozivi alata i podagenti omogućavaju vašem agentu da radi više odjednom — tako trošite manje vremena čekajući, a više isporučujući.",
"migration.whatsNew.features.interface.title": "Pojednostavljeni interfejs",
"migration.whatsNew.features.interface.detail": "Manje distrakcija, lakše i brže za čitanje.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Jedinstveni interfejs za pokretanje više agenata paralelno, svaki na svom worktree-u — pratite napredak, mijenjajte kontekst i pregledajte promjene na jednom mjestu.",
"migration.whatsNew.features.foundation.title": "Zajednički temelj",
"migration.whatsNew.features.foundation.detail":
"Jedno malo, efikasno jezgro kroz sve Kilo proizvode. Poznato iskustvo bez obzira na to kako odaberete raditi.",
"migration.whatsNew.blogLink": "Pročitajte cijeli oglas",
"migration.whatsNew.continue": "Nastavi",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migrirajte svoje postavke",
"migration.migrate.subtitle": "Pronašli smo postavke iz vaše prethodne instalacije. Evo šta možemo prenijeti.",
"migration.migrate.selectLabel": "Odaberite šta želite migrirati",
"migration.migrate.cannotMigrate": "Nije moguće migrirati",
"migration.migrate.chatHistory": "Sesije razgovora i historija",
"migration.migrate.chatHistoryDesc": "Nekompatibilno sa novom arhitekturom",
"migration.migrate.button": "Migriraj postavke",
"migration.migrate.skip": "Preskoči",
"migration.migrate.back": "Nazad",
"migration.migrate.keysDetected": "{{count}} ključeva otkriveno",
"migration.migrate.serversConfigured": "{{count}} server(a) konfigurisano",
"migration.migrate.modesFound": "{{count}} režim(a) pronađeno",
"migration.migrate.nothingToMigrate": "U starim postavkama nije pronađeno ništa za migraciju.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "API ključevi provajdera",
"migration.select.mcpServers": "MCP serveri",
"migration.select.customModes": "Prilagođeni režimi / Agenti",
"migration.select.defaultModel": "Podrazumijevani model",
"migration.select.unsupported": "Nije podržano u novoj verziji",
"migration.select.nothingToMigrate": "U starim postavkama nije pronađeno ništa za migraciju.",
"migration.select.settings": "Postavke",
"migration.select.autoApproval": "Automatsko odobravanje",
"migration.select.autoApproval.commandRules": "Pravila za komande",
"migration.select.autoApproval.commandRulesDesc": "Glavni prekidač i liste dozvoljenih/odbijenih komandi",
"migration.select.autoApproval.readPermission": "Dozvola za čitanje",
"migration.select.autoApproval.readPermissionDesc": "Pristup čitanju datoteka unutar i izvan radnog prostora",
"migration.select.autoApproval.writePermission": "Dozvola za pisanje",
"migration.select.autoApproval.writePermissionDesc": "Pristup za pisanje i uređivanje datoteka",
"migration.select.autoApproval.executePermission": "Dozvola za izvršavanje",
"migration.select.autoApproval.executePermissionDesc": "Izvršavanje komandi u terminalu",
"migration.select.autoApproval.mcpPermission": "MCP dozvola",
"migration.select.autoApproval.mcpPermissionDesc": "Korištenje MCP alata",
"migration.select.autoApproval.taskPermission": "Dozvola za zadatke",
"migration.select.autoApproval.taskPermissionDesc": "Mijenjanje režima i kreiranje podzadataka",
"migration.select.language": "Jezik korisničkog interfejsa",
"migration.select.languageDesc": "Vaše željene postavke jezika će biti primijenjene na novu ekstenziju",
"migration.select.autocomplete": "Postavke automatskog dovršavanja",
"migration.select.autocompleteDesc":
"Postavke automatskog pokretanja, prečica na tastaturi i automatskog dovršavanja u chatu",
"migration.select.continue": "Nastavi",
"migration.select.back": "Nazad",
"migration.progress.title": "Migriranje vaših postavki…",
"migration.progress.done": "Nastavi",
// Migrate — completion
"migration.complete.summary": "Uspješno migrirano {{success}} od {{total}} stavki.",
"migration.complete.cleanup": "Ukloni stare podatke postavki",
"migration.complete.cleanupDescription":
+48 -31
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Forbedr prompt",
"prompt.action.resetModel": "Nulstil model til standard",
"prompt.action.enhanceDescription":
"Knappen 'Forbedr prompt' hjælper med at forbedre din forespørgsel ved at give ekstra kontekst, præcisering eller omformulering. Prøv at skrive en forespørgsel her og klik på knappen igen for at se hvordan det virker.",
@@ -407,6 +408,15 @@ export const dict = {
"error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
"error.paidModel.title": "Du skal logge ind for at bruge denne model",
"error.paidModel.description":
"Log ind eller opret en konto for at få adgang til over 500 modeller, brug kreditter til kostpris, eller medbring din egen nøgle.",
"error.paidModel.action": "Log ind",
"error.promotionLimit.title": "Du skal tilmelde dig for at fortsætte",
"error.promotionLimit.description":
"Tilmeld dig gratis for at fortsætte og udforske over 500 modeller. Tager 2 minutter, intet kreditkort nødvendigt. Eller kom tilbage senere.",
"error.promotionLimit.action": "Tilmeld dig",
"error.chain.unknown": "Ukendt fejl",
"error.chain.causedBy": "Forårsaget af:",
"error.chain.apiError": "API-fejl",
@@ -989,44 +999,51 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Legacy-migrering",
"settings.aboutKiloCode.legacyMigration.description":
"Migrer indstillinger fra en tidligere installation af Kilo Code, herunder udbyder API-nøgler og standardmodel.",
"migration.welcome.title": "Velkommen til det nye Kilo Code",
"migration.welcome.detected": "Vi har fundet indstillinger fra en tidligere installation af Kilo Code.",
"migration.welcome.sessionsInfo":
"Chatsessioner og historik kan ikke migreres — den nye version bruger en anden arkitektur.",
"migration.welcome.canMigrate": "Følgende indstillinger kan migreres til den nye version:",
"migration.welcome.start": "Start migrering",
"migration.welcome.skip": "Spring migrering over",
"migration.steps.title": "Migrer dine indstillinger",
"migration.steps.subtitle":
"Vi har fundet indstillinger fra din tidligere Kilo Code-installation. Vælg, hvad der skal overføres.",
// Screen 1 — What's New
"migration.whatsNew.title": "Nyheder i Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Vi har genopbygget udvidelsen på et hurtigere og mere effektivt fundament.",
"migration.whatsNew.features.performance.title": "Hurtigere agentydelse",
"migration.whatsNew.features.performance.detail":
"Parallelle værktøjskald og underagenter lader din agent tackle mere på én gang — så du bruger mindre tid på at vente og mere tid på at levere.",
"migration.whatsNew.features.interface.title": "Strømlinet brugerflade",
"migration.whatsNew.features.interface.detail": "Færre distraktioner, nemmere og hurtigere at læse.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"En samlet brugerflade til at køre flere agenter parallelt, hver på sit eget worktree — overvåg fremskridt, skift kontekst og gennemgå ændringer ét sted.",
"migration.whatsNew.features.foundation.title": "Fælles fundament",
"migration.whatsNew.features.foundation.detail":
"Én lille, effektiv kerne på tværs af alle Kilo-produkter. En velkendt oplevelse uanset hvordan du vælger at arbejde.",
"migration.whatsNew.blogLink": "Læs den fulde meddelelse",
"migration.whatsNew.continue": "Fortsæt",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migrer dine indstillinger",
"migration.migrate.subtitle":
"Vi har fundet indstillinger fra din tidligere installation. Her er, hvad vi kan overføre.",
"migration.migrate.selectLabel": "Vælg hvad der skal migreres",
"migration.migrate.cannotMigrate": "Kan ikke migreres",
"migration.migrate.chatHistory": "Chatsessioner og historik",
"migration.migrate.chatHistoryDesc": "Ikke kompatibel med den nye arkitektur",
"migration.migrate.button": "Migrer indstillinger",
"migration.migrate.skip": "Spring over",
"migration.migrate.back": "Tilbage",
"migration.migrate.keysDetected": "{{count}} nøgler fundet",
"migration.migrate.serversConfigured": "{{count}} server(e) konfigureret",
"migration.migrate.modesFound": "{{count}} tilstand(e) fundet",
"migration.migrate.nothingToMigrate": "Der blev ikke fundet noget at migrere i legacy-indstillingerne.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "Udbyder API-nøgler",
"migration.select.mcpServers": "MCP-servere",
"migration.select.customModes": "Brugerdefinerede tilstande / Agenter",
"migration.select.defaultModel": "Standardmodel",
"migration.select.unsupported": "Understøttes ikke i den nye version",
"migration.select.nothingToMigrate": "Der blev ikke fundet noget at migrere i legacy-indstillingerne.",
"migration.select.settings": "Indstillinger",
"migration.select.autoApproval": "Automatisk godkendelse",
"migration.select.autoApproval.commandRules": "Kommandoregler",
"migration.select.autoApproval.commandRulesDesc": "Hovedafbryder og lister over tilladte/afviste kommandoer",
"migration.select.autoApproval.readPermission": "Læsetilladelse",
"migration.select.autoApproval.readPermissionDesc": "Fillæseadgang inden for og uden for arbejdsområdet",
"migration.select.autoApproval.writePermission": "Skrivetilladelse",
"migration.select.autoApproval.writePermissionDesc": "Filskrive- og redigeringsadgang",
"migration.select.autoApproval.executePermission": "Udførelsestilladelse",
"migration.select.autoApproval.executePermissionDesc": "Udførelse af terminalkommandoer",
"migration.select.autoApproval.mcpPermission": "MCP-tilladelse",
"migration.select.autoApproval.mcpPermissionDesc": "Brug af MCP-værktøjer",
"migration.select.autoApproval.taskPermission": "Opgavetilladelse",
"migration.select.autoApproval.taskPermissionDesc": "Skift af tilstand og oprettelse af underopgaver",
"migration.select.language": "UI-sprog",
"migration.select.languageDesc": "Din sprogpræference vil blive anvendt på den nye udvidelse",
"migration.select.autocomplete": "Indstillinger for autofuldførelse",
"migration.select.autocompleteDesc": "Præferencer for automatisk udløsning, genvejstaster og chat-autofuldførelse",
"migration.select.continue": "Fortsæt",
"migration.select.back": "Tilbage",
"migration.progress.title": "Migrerer dine indstillinger…",
"migration.progress.done": "Fortsæt",
// Migrate — completion
"migration.complete.summary": "{{success}} af {{total}} elementer blev migreret med succes.",
"migration.complete.cleanup": "Fjern legacy-indstillingsdata",
"migration.complete.cleanupDescription":
+49 -32
View File
@@ -235,6 +235,7 @@ export const dict = {
"prompt.action.send": "Senden",
"prompt.action.stop": "Stopp",
"prompt.action.enhance": "Prompt verbessern",
"prompt.action.resetModel": "Modell auf Standard zurücksetzen",
"prompt.action.enhanceDescription":
"Die Schaltfläche 'Prompt verbessern' hilft, deine Anfrage durch zusätzlichen Kontext, Klarstellungen oder Umformulierungen zu verbessern. Versuche, hier eine Anfrage einzugeben und klicke erneut auf die Schaltfläche, um zu sehen, wie es funktioniert.",
@@ -412,6 +413,15 @@ export const dict = {
"error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
"error.paidModel.title": "Du musst dich anmelden, um dieses Modell zu nutzen",
"error.paidModel.description":
"Melde dich an oder erstelle ein Konto, um auf über 500 Modelle zuzugreifen, Credits zum Selbstkostenpreis zu nutzen oder deinen eigenen Schlüssel mitzubringen.",
"error.paidModel.action": "Anmelden",
"error.promotionLimit.title": "Du musst dich registrieren, um weiterzumachen",
"error.promotionLimit.description":
"Registriere dich kostenlos, um weiterzumachen und über 500 Modelle zu entdecken. Dauert 2 Minuten, keine Kreditkarte nötig. Oder komm später wieder.",
"error.promotionLimit.action": "Registrieren",
"error.chain.unknown": "Unbekannter Fehler",
"error.chain.causedBy": "Verursacht durch:",
"error.chain.apiError": "API-Fehler",
@@ -1006,45 +1016,52 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Legacy-Migration",
"settings.aboutKiloCode.legacyMigration.description":
"Migrieren Sie Einstellungen von einer früheren Installation von Kilo Code, einschließlich Anbieter-API-Schlüsseln und dem Standardmodell.",
"migration.welcome.title": "Willkommen beim neuen Kilo Code",
"migration.welcome.detected": "Wir haben Einstellungen aus einer früheren Installation von Kilo Code erkannt.",
"migration.welcome.sessionsInfo":
"Chat-Sitzungen und Verlauf können nicht migriert werden — die neue Version verwendet eine andere Architektur.",
"migration.welcome.canMigrate": "Die folgenden Einstellungen können in die neue Version migriert werden:",
"migration.welcome.start": "Migration starten",
"migration.welcome.skip": "Migration überspringen",
"migration.steps.title": "Ihre Einstellungen migrieren",
"migration.steps.subtitle":
"Wir haben Einstellungen aus Ihrer vorherigen Kilo Code-Installation gefunden. Wählen Sie aus, was übernommen werden soll.",
// Screen 1 — What's New
"migration.whatsNew.title": "Neuigkeiten in Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle":
"Wir haben die Erweiterung auf einer schnelleren, effizienteren Grundlage neu aufgebaut.",
"migration.whatsNew.features.performance.title": "Schnellere Agentenleistung",
"migration.whatsNew.features.performance.detail":
"Parallele Werkzeugaufrufe und Unteragenten lassen Ihren Agenten mehr gleichzeitig erledigen — so verbringen Sie weniger Zeit mit Warten und mehr Zeit mit Ergebnissen.",
"migration.whatsNew.features.interface.title": "Optimierte Oberfläche",
"migration.whatsNew.features.interface.detail": "Weniger Ablenkungen, einfacher und schneller zu lesen.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Eine einheitliche Oberfläche zum parallelen Ausführen mehrerer Agenten, jeweils in einem eigenen Worktree — Fortschritt überwachen, Kontext wechseln und Änderungen an einem Ort überprüfen.",
"migration.whatsNew.features.foundation.title": "Gemeinsame Grundlage",
"migration.whatsNew.features.foundation.detail":
"Ein kleiner, effizienter Kern über alle Kilo-Produkte hinweg. Ein vertrautes Erlebnis, egal wie Sie arbeiten.",
"migration.whatsNew.blogLink": "Die vollständige Ankündigung lesen",
"migration.whatsNew.continue": "Weiter",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Ihre Einstellungen migrieren",
"migration.migrate.subtitle":
"Wir haben Einstellungen aus Ihrer vorherigen Installation gefunden. Hier ist, was wir übernehmen können.",
"migration.migrate.selectLabel": "Auswählen, was migriert werden soll",
"migration.migrate.cannotMigrate": "Kann nicht migriert werden",
"migration.migrate.chatHistory": "Chat-Sitzungen & Verlauf",
"migration.migrate.chatHistoryDesc": "Nicht kompatibel mit der neuen Architektur",
"migration.migrate.button": "Einstellungen migrieren",
"migration.migrate.skip": "Überspringen",
"migration.migrate.back": "Zurück",
"migration.migrate.keysDetected": "{{count}} Schlüssel erkannt",
"migration.migrate.serversConfigured": "{{count}} Server konfiguriert",
"migration.migrate.modesFound": "{{count}} Modus/Modi gefunden",
"migration.migrate.nothingToMigrate": "In den Legacy-Einstellungen wurde nichts zum Migrieren gefunden.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "Anbieter-API-Schlüssel",
"migration.select.mcpServers": "MCP-Server",
"migration.select.customModes": "Benutzerdefinierte Modi / Agenten",
"migration.select.defaultModel": "Standardmodell",
"migration.select.unsupported": "Wird in der neuen Version nicht unterstützt",
"migration.select.nothingToMigrate": "In den Legacy-Einstellungen wurde nichts gefunden, was migriert werden könnte.",
"migration.select.settings": "Einstellungen",
"migration.select.autoApproval": "Automatische Genehmigung",
"migration.select.autoApproval.commandRules": "Befehlsregeln",
"migration.select.autoApproval.commandRulesDesc": "Hauptschalter und Listen für erlaubte/verweigerte Befehle",
"migration.select.autoApproval.readPermission": "Leseberechtigung",
"migration.select.autoApproval.readPermissionDesc": "Dateilesezugriff innerhalb und außerhalb des Arbeitsbereichs",
"migration.select.autoApproval.writePermission": "Schreibberechtigung",
"migration.select.autoApproval.writePermissionDesc": "Dateischreib- und Bearbeitungszugriff",
"migration.select.autoApproval.executePermission": "Ausführungsberechtigung",
"migration.select.autoApproval.executePermissionDesc": "Ausführung von Terminalbefehlen",
"migration.select.autoApproval.mcpPermission": "MCP-Berechtigung",
"migration.select.autoApproval.mcpPermissionDesc": "Nutzung von MCP-Tools",
"migration.select.autoApproval.taskPermission": "Aufgabenberechtigung",
"migration.select.autoApproval.taskPermissionDesc": "Wechsel des Modus und Erstellung von Teilaufgaben",
"migration.select.language": "UI-Sprache",
"migration.select.languageDesc": "Ihre Spracheinstellung wird auf die neue Erweiterung angewendet",
"migration.select.autocomplete": "Einstellungen für Autovervollständigung",
"migration.select.autocompleteDesc":
"Einstellungen für automatische Auslösung, Tastenkombinationen und Chat-Autovervollständigung",
"migration.select.continue": "Weiter",
"migration.select.back": "Zurück",
"migration.progress.title": "Ihre Einstellungen werden migriert…",
"migration.progress.done": "Weiter",
// Migrate — completion
"migration.complete.summary": "{{success}} von {{total}} Elementen erfolgreich migriert.",
"migration.complete.cleanup": "Legacy-Einstellungsdaten entfernen",
"migration.complete.cleanupDescription":
+49 -30
View File
@@ -232,6 +232,7 @@ export const dict = {
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Enhance prompt",
"prompt.action.resetModel": "Reset model to default",
"prompt.action.enhanceDescription":
"The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.",
@@ -409,6 +410,15 @@ export const dict = {
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
"error.paidModel.title": "You need to sign in to use this model",
"error.paidModel.description":
"Sign in or create an account to access over 500 models, use credits at cost, or bring your own key.",
"error.paidModel.action": "Sign In",
"error.promotionLimit.title": "You need to sign up to keep going",
"error.promotionLimit.description":
"Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later.",
"error.promotionLimit.action": "Sign Up",
"error.chain.unknown": "Unknown error",
"error.chain.causedBy": "Caused by:",
"error.chain.apiError": "API error",
@@ -988,6 +998,8 @@ export const dict = {
"settings.providers.defaultModel.description": "Primary model for conversations",
"settings.providers.smallModel.title": "Small Model",
"settings.providers.smallModel.description": "Lightweight model for title generation and other quick tasks",
"settings.providers.modeModels": "Model per Mode",
"settings.providers.modeModels.description": "Override the default model for specific modes. If not set, the global default model is used.",
"settings.providers.disabled": "Disabled Providers",
"settings.providers.disabled.description": "Providers to hide from the provider list",
"settings.providers.enabled": "Enabled Providers (Allowlist)",
@@ -1008,43 +1020,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Legacy Migration",
"settings.aboutKiloCode.legacyMigration.description":
"Migrate settings from a previous installation of Kilo Code, including provider API keys and default model.",
"migration.welcome.title": "Welcome to the New Kilo Code",
"migration.welcome.detected": "We detected settings from a previous installation of Kilo Code.",
"migration.welcome.sessionsInfo":
"Chat sessions and history cannot be migrated — the new version uses a different architecture.",
"migration.welcome.canMigrate": "The following settings can be migrated to the new version:",
"migration.welcome.start": "Start Migration",
"migration.welcome.skip": "Skip Migration",
"migration.steps.title": "Migrate your settings",
"migration.steps.subtitle": "We found settings from your previous Kilo Code installation. Choose what to bring over.",
// Screen 1 — What's New
"migration.whatsNew.title": "What's New in Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "We've rebuilt the extension on a faster, more efficient foundation.",
"migration.whatsNew.features.performance.title": "Faster Agent Performance",
"migration.whatsNew.features.performance.detail":
"Parallel tool calls and subagents let your agent tackle more at once — so you spend less time watching and more time shipping.",
"migration.whatsNew.features.interface.title": "Streamlined Interface",
"migration.whatsNew.features.interface.detail": "Fewer distractions, easier and quicker to read.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"A unified interface for running multiple agents in parallel, each on its own worktree — monitor progress, switch context, and review changes in one place.",
"migration.whatsNew.features.foundation.title": "Shared Foundation",
"migration.whatsNew.features.foundation.detail":
"One small, efficient core across every Kilo product. A familiar experience however you choose to work.",
"migration.whatsNew.blogLink": "Read the full announcement",
"migration.whatsNew.continue": "Continue",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migrate Your Settings",
"migration.migrate.subtitle": "We found settings from your previous installation. Here's what we can bring over.",
"migration.migrate.selectLabel": "Select what to migrate",
"migration.migrate.cannotMigrate": "Cannot be migrated",
"migration.migrate.chatHistory": "Chat Sessions & History",
"migration.migrate.chatHistoryDesc": "Incompatible with the new architecture",
"migration.migrate.button": "Migrate Settings",
"migration.migrate.skip": "Skip",
"migration.migrate.back": "Back",
"migration.migrate.keysDetected": "{{count}} keys detected",
"migration.migrate.serversConfigured": "{{count}} server(s) configured",
"migration.migrate.modesFound": "{{count}} mode(s) found",
"migration.migrate.nothingToMigrate": "Nothing to migrate was found in the legacy settings.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "Provider API Keys",
"migration.select.mcpServers": "MCP Servers",
"migration.select.customModes": "Custom Modes / Agents",
"migration.select.defaultModel": "Default Model",
"migration.select.unsupported": "Not supported in new version",
"migration.select.nothingToMigrate": "Nothing to migrate was found in the legacy settings.",
"migration.select.settings": "Settings",
"migration.select.autoApproval": "Auto-Approval",
"migration.select.autoApproval.commandRules": "Command Rules",
"migration.select.autoApproval.commandRulesDesc": "Master toggle and allowed/denied command lists",
"migration.select.autoApproval.readPermission": "Read Permission",
"migration.select.autoApproval.readPermissionDesc": "File read access inside and outside workspace",
"migration.select.autoApproval.writePermission": "Write Permission",
"migration.select.autoApproval.writePermissionDesc": "File write and edit access",
"migration.select.autoApproval.executePermission": "Execute Permission",
"migration.select.autoApproval.executePermissionDesc": "Terminal command execution",
"migration.select.autoApproval.mcpPermission": "MCP Permission",
"migration.select.autoApproval.mcpPermissionDesc": "MCP tool usage",
"migration.select.autoApproval.taskPermission": "Task Permission",
"migration.select.autoApproval.taskPermissionDesc": "Mode switching and subtask creation",
"migration.select.language": "UI Language",
"migration.select.languageDesc": "Your language preference will be applied to the new extension",
"migration.select.autocomplete": "Autocomplete Settings",
"migration.select.autocompleteDesc": "Auto-trigger, keybinding, and chat autocomplete preferences",
"migration.select.continue": "Continue",
"migration.select.back": "Back",
"migration.progress.title": "Migrating your settings…",
"migration.progress.done": "Continue",
// Migrate — completion
"migration.complete.summary": "{{success}} of {{total}} items migrated successfully.",
"migration.complete.cleanup": "Remove legacy settings data",
"migration.complete.cleanupDescription":
+48 -33
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "Enviar",
"prompt.action.stop": "Detener",
"prompt.action.enhance": "Mejorar prompt",
"prompt.action.resetModel": "Restablecer modelo al predeterminado",
"prompt.action.enhanceDescription":
"El botón 'Mejorar el mensaje' ayuda a mejorar tu petición proporcionando contexto adicional, aclaraciones o reformulaciones. Intenta escribir una petición aquí y haz clic en el botón nuevamente para ver cómo funciona.",
@@ -408,6 +409,15 @@ export const dict = {
"error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
"error.paidModel.title": "Necesitas iniciar sesión para usar este modelo",
"error.paidModel.description":
"Inicia sesión o crea una cuenta para acceder a más de 500 modelos, usar créditos a precio de coste o traer tu propia clave.",
"error.paidModel.action": "Iniciar sesión",
"error.promotionLimit.title": "Necesitas registrarte para continuar",
"error.promotionLimit.description":
"Regístrate gratis para continuar y explorar más de 500 modelos. Solo 2 minutos, sin tarjeta de crédito. O vuelve más tarde.",
"error.promotionLimit.action": "Registrarse",
"error.chain.unknown": "Error desconocido",
"error.chain.causedBy": "Causado por:",
"error.chain.apiError": "Error de API",
@@ -996,46 +1006,51 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Migración heredada",
"settings.aboutKiloCode.legacyMigration.description":
"Migra la configuración de una instalación anterior de Kilo Code, incluyendo las claves API de proveedores y el modelo predeterminado.",
"migration.welcome.title": "Bienvenido al nuevo Kilo Code",
"migration.welcome.detected": "Hemos detectado una configuración de una instalación anterior de Kilo Code.",
"migration.welcome.sessionsInfo":
"Las sesiones de chat y el historial no se pueden migrar — la nueva versión utiliza una arquitectura diferente.",
"migration.welcome.canMigrate": "Las siguientes configuraciones se pueden migrar a la nueva versión:",
"migration.welcome.start": "Iniciar migración",
"migration.welcome.skip": "Omitir migración",
"migration.steps.title": "Migra tu configuración",
"migration.steps.subtitle":
"Encontramos configuraciones de tu instalación anterior de Kilo Code. Elige qué deseas transferir.",
// Screen 1 — What's New
"migration.whatsNew.title": "Novedades en Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Hemos reconstruido la extensión sobre una base más rápida y eficiente.",
"migration.whatsNew.features.performance.title": "Rendimiento de agente más rápido",
"migration.whatsNew.features.performance.detail":
"Las llamadas a herramientas en paralelo y los subagentes permiten que tu agente aborde más a la vez — así pasas menos tiempo esperando y más tiempo entregando.",
"migration.whatsNew.features.interface.title": "Interfaz optimizada",
"migration.whatsNew.features.interface.detail": "Menos distracciones, más fácil y rápido de leer.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Una interfaz unificada para ejecutar múltiples agentes en paralelo, cada uno en su propio worktree — supervisa el progreso, cambia de contexto y revisa cambios en un solo lugar.",
"migration.whatsNew.features.foundation.title": "Base compartida",
"migration.whatsNew.features.foundation.detail":
"Un núcleo pequeño y eficiente en todos los productos Kilo. Una experiencia familiar sin importar cómo elijas trabajar.",
"migration.whatsNew.blogLink": "Leer el anuncio completo",
"migration.whatsNew.continue": "Continuar",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migra tu configuración",
"migration.migrate.subtitle":
"Encontramos configuraciones de tu instalación anterior. Esto es lo que podemos transferir.",
"migration.migrate.selectLabel": "Selecciona qué migrar",
"migration.migrate.cannotMigrate": "No se puede migrar",
"migration.migrate.chatHistory": "Sesiones de chat e historial",
"migration.migrate.chatHistoryDesc": "Incompatible con la nueva arquitectura",
"migration.migrate.button": "Migrar configuración",
"migration.migrate.skip": "Omitir",
"migration.migrate.back": "Atrás",
"migration.migrate.keysDetected": "{{count}} claves detectadas",
"migration.migrate.serversConfigured": "{{count}} servidor(es) configurado(s)",
"migration.migrate.modesFound": "{{count}} modo(s) encontrado(s)",
"migration.migrate.nothingToMigrate": "No se encontró nada para migrar en la configuración heredada.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "Claves API de proveedores",
"migration.select.mcpServers": "Servidores MCP",
"migration.select.customModes": "Modos personalizados / Agentes",
"migration.select.defaultModel": "Modelo predeterminado",
"migration.select.unsupported": "No compatible con la nueva versión",
"migration.select.nothingToMigrate": "No se encontró nada para migrar en la configuración heredada.",
"migration.select.settings": "Configuración",
"migration.select.autoApproval": "Aprobación automática",
"migration.select.autoApproval.commandRules": "Reglas de comandos",
"migration.select.autoApproval.commandRulesDesc": "Interruptor principal y listas de comandos permitidos/denegados",
"migration.select.autoApproval.readPermission": "Permiso de lectura",
"migration.select.autoApproval.readPermissionDesc":
"Acceso de lectura de archivos dentro y fuera del espacio de trabajo",
"migration.select.autoApproval.writePermission": "Permiso de escritura",
"migration.select.autoApproval.writePermissionDesc": "Acceso de escritura y edición de archivos",
"migration.select.autoApproval.executePermission": "Permiso de ejecución",
"migration.select.autoApproval.executePermissionDesc": "Ejecución de comandos en terminal",
"migration.select.autoApproval.mcpPermission": "Permiso MCP",
"migration.select.autoApproval.mcpPermissionDesc": "Uso de herramientas MCP",
"migration.select.autoApproval.taskPermission": "Permiso de tareas",
"migration.select.autoApproval.taskPermissionDesc": "Cambio de modos y creación de subtareas",
"migration.select.language": "Idioma de la interfaz",
"migration.select.languageDesc": "Tu preferencia de idioma se aplicará a la nueva extensión",
"migration.select.autocomplete": "Configuración de autocompletado",
"migration.select.autocompleteDesc":
"Preferencias de activación automática, atajos de teclado y autocompletado del chat",
"migration.select.continue": "Continuar",
"migration.select.back": "Atrás",
"migration.progress.title": "Migrando tu configuración…",
"migration.progress.done": "Continuar",
// Migrate — completion
"migration.complete.summary": "{{success}} de {{total}} elementos migrados con éxito.",
"migration.complete.cleanup": "Eliminar datos de la configuración heredada",
"migration.complete.cleanupDescription":
+48 -33
View File
@@ -232,6 +232,7 @@ export const dict = {
"prompt.action.send": "Envoyer",
"prompt.action.stop": "Arrêter",
"prompt.action.enhance": "Améliorer le prompt",
"prompt.action.resetModel": "Réinitialiser le modèle par défaut",
"prompt.action.enhanceDescription":
"Le bouton 'Améliorer la requête' aide à améliorer votre demande en fournissant un contexte supplémentaire, des clarifications ou des reformulations. Essayez de taper une demande ici et cliquez à nouveau sur le bouton pour voir comment cela fonctionne.",
@@ -412,6 +413,15 @@ export const dict = {
"error.globalSync.connectFailed":
"Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?",
"error.paidModel.title": "Vous devez vous connecter pour utiliser ce modèle",
"error.paidModel.description":
"Connectez-vous ou créez un compte pour accéder à plus de 500 modèles, utiliser des crédits au prix coûtant ou apporter votre propre clé.",
"error.paidModel.action": "Se connecter",
"error.promotionLimit.title": "Vous devez vous inscrire pour continuer",
"error.promotionLimit.description":
"Inscrivez-vous gratuitement pour continuer et explorer plus de 500 modèles. 2 minutes, sans carte bancaire. Ou revenez plus tard.",
"error.promotionLimit.action": "S'inscrire",
"error.chain.unknown": "Erreur inconnue",
"error.chain.causedBy": "Causé par :",
"error.chain.apiError": "Erreur API",
@@ -1005,46 +1015,51 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Migration de l'ancienne version",
"settings.aboutKiloCode.legacyMigration.description":
"Migrer les paramètres d'une précédente installation de Kilo Code, y compris les clés d'API des fournisseurs et le modèle par défaut.",
"migration.welcome.title": "Bienvenue dans le nouveau Kilo Code",
"migration.welcome.detected": "Nous avons détecté des paramètres d'une précédente installation de Kilo Code.",
"migration.welcome.sessionsInfo":
"Les sessions de discussion et l'historique ne peuvent pas être migrés — la nouvelle version utilise une architecture différente.",
"migration.welcome.canMigrate": "Les paramètres suivants peuvent être migrés vers la nouvelle version :",
"migration.welcome.start": "Démarrer la migration",
"migration.welcome.skip": "Ignorer la migration",
"migration.steps.title": "Migrer vos paramètres",
"migration.steps.subtitle":
"Nous avons trouvé des paramètres de votre précédente installation de Kilo Code. Choisissez ce que vous souhaitez transférer.",
// Screen 1 — What's New
"migration.whatsNew.title": "Nouveautés de Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Nous avons reconstruit l'extension sur une base plus rapide et plus efficace.",
"migration.whatsNew.features.performance.title": "Performance accélérée de l'agent",
"migration.whatsNew.features.performance.detail":
"Les appels d'outils en parallèle et les sous-agents permettent à votre agent de traiter plus de tâches simultanément — vous passez moins de temps à attendre et plus de temps à livrer.",
"migration.whatsNew.features.interface.title": "Interface simplifiée",
"migration.whatsNew.features.interface.detail": "Moins de distractions, plus facile et plus rapide à lire.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Une interface unifiée pour exécuter plusieurs agents en parallèle, chacun sur son propre arbre de travail — surveillez la progression, changez de contexte et examinez les modifications depuis un seul endroit.",
"migration.whatsNew.features.foundation.title": "Base partagée",
"migration.whatsNew.features.foundation.detail":
"Un noyau petit et efficace partagé par tous les produits Kilo. Une expérience familière quelle que soit votre façon de travailler.",
"migration.whatsNew.blogLink": "Lire l'annonce complète",
"migration.whatsNew.continue": "Continuer",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migrer vos paramètres",
"migration.migrate.subtitle":
"Nous avons trouvé des paramètres de votre précédente installation. Voici ce que nous pouvons récupérer.",
"migration.migrate.selectLabel": "Sélectionnez ce que vous souhaitez migrer",
"migration.migrate.cannotMigrate": "Impossible à migrer",
"migration.migrate.chatHistory": "Sessions de discussion et historique",
"migration.migrate.chatHistoryDesc": "Incompatible avec la nouvelle architecture",
"migration.migrate.button": "Migrer les paramètres",
"migration.migrate.skip": "Ignorer",
"migration.migrate.back": "Retour",
"migration.migrate.keysDetected": "{{count}} clés détectées",
"migration.migrate.serversConfigured": "{{count}} serveur(s) configuré(s)",
"migration.migrate.modesFound": "{{count}} mode(s) trouvé(s)",
"migration.migrate.nothingToMigrate": "Rien à migrer n'a été trouvé dans les anciens paramètres.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "Clés d'API des fournisseurs",
"migration.select.mcpServers": "Serveurs MCP",
"migration.select.customModes": "Modes personnalisés / Agents",
"migration.select.defaultModel": "Modèle par défaut",
"migration.select.unsupported": "Non pris en charge dans la nouvelle version",
"migration.select.nothingToMigrate": "Rien à migrer n'a été trouvé dans les anciens paramètres.",
"migration.select.settings": "Paramètres",
"migration.select.autoApproval": "Approbation automatique",
"migration.select.autoApproval.commandRules": "Règles de commande",
"migration.select.autoApproval.commandRulesDesc": "Bascule principale et listes de commandes autorisées/refusées",
"migration.select.autoApproval.readPermission": "Autorisation de lecture",
"migration.select.autoApproval.readPermissionDesc":
"Accès en lecture aux fichiers à l'intérieur et à l'extérieur de l'espace de travail",
"migration.select.autoApproval.writePermission": "Autorisation d'écriture",
"migration.select.autoApproval.writePermissionDesc": "Accès en écriture et modification de fichiers",
"migration.select.autoApproval.executePermission": "Autorisation d'exécution",
"migration.select.autoApproval.executePermissionDesc": "Exécution de commandes dans le terminal",
"migration.select.autoApproval.mcpPermission": "Autorisation MCP",
"migration.select.autoApproval.mcpPermissionDesc": "Utilisation d'outils MCP",
"migration.select.autoApproval.taskPermission": "Autorisation de tâche",
"migration.select.autoApproval.taskPermissionDesc": "Changement de mode et création de sous-tâches",
"migration.select.language": "Langue de l'interface",
"migration.select.languageDesc": "Votre préférence de langue sera appliquée à la nouvelle extension",
"migration.select.autocomplete": "Paramètres de saisie semi-automatique",
"migration.select.autocompleteDesc":
"Préférences de déclenchement automatique, de raccourcis clavier et de saisie semi-automatique du chat",
"migration.select.continue": "Continuer",
"migration.select.back": "Retour",
"migration.progress.title": "Migration de vos paramètres…",
"migration.progress.done": "Continuer",
// Migrate — completion
"migration.complete.summary": "{{success}} éléments sur {{total}} migrés avec succès.",
"migration.complete.cleanup": "Supprimer les données des anciens paramètres",
"migration.complete.cleanupDescription":
+47 -30
View File
@@ -230,6 +230,7 @@ export const dict = {
"prompt.action.send": "送信",
"prompt.action.stop": "停止",
"prompt.action.enhance": "プロンプトを改善",
"prompt.action.resetModel": "モデルをデフォルトにリセット",
"prompt.action.enhanceDescription":
"「プロンプトを強化」ボタンは、追加コンテキスト、説明、または言い換えを提供することで、リクエストを改善します。ここにリクエストを入力し、ボタンを再度クリックして動作を確認してください。",
@@ -405,6 +406,15 @@ export const dict = {
"error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?",
"error.paidModel.title": "このモデルを使用するにはサインインが必要です",
"error.paidModel.description":
"サインインまたはアカウントを作成して、500以上のモデルにアクセスし、原価でクレジットを使用するか、独自のキーを持ち込みましょう。",
"error.paidModel.action": "サインイン",
"error.promotionLimit.title": "続けるにはサインアップが必要です",
"error.promotionLimit.description":
"無料でサインアップして、500以上のモデルを探索しましょう。2分で完了、クレジットカード不要。または後でお戻りください。",
"error.promotionLimit.action": "サインアップ",
"error.chain.unknown": "不明なエラー",
"error.chain.causedBy": "原因:",
"error.chain.apiError": "APIエラー",
@@ -988,43 +998,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "レガシー移行",
"settings.aboutKiloCode.legacyMigration.description":
"プロバイダーのAPIキーやデフォルトモデルなど、以前にインストールしたKilo Codeから設定を移行します。",
"migration.welcome.title": "新しいKilo Codeへようこそ",
"migration.welcome.detected": "以前にインストールされたKilo Codeの設定を検出しました。",
"migration.welcome.sessionsInfo":
"チャットセッションと履歴は移行できません — 新しいバージョンでは異なるアーキテクチャが使用されています。",
"migration.welcome.canMigrate": "以下の設定を新しいバージョンに移行できます:",
"migration.welcome.start": "移行を開始",
"migration.welcome.skip": "移行をスキップ",
"migration.steps.title": "設定の移行",
"migration.steps.subtitle": "以前のKilo Codeインストールの設定が見つかりました。引き継ぐ項目を選択してください。",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Codeの新機能",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "より高速で効率的な基盤上に拡張機能を再構築しました。",
"migration.whatsNew.features.performance.title": "高速なエージェントパフォーマンス",
"migration.whatsNew.features.performance.detail":
"並列ツール呼び出しとサブエージェントにより、エージェントがより多くのタスクを同時に処理できます — 待ち時間が減り、作業効率が向上します。",
"migration.whatsNew.features.interface.title": "洗練されたインターフェース",
"migration.whatsNew.features.interface.detail": "不要な要素を排除し、より読みやすく素早く操作できます。",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"複数のエージェントを並列実行するための統合インターフェース。それぞれ独自のワークツリー上で動作し、進捗の確認、コンテキストの切り替え、変更のレビューを一箇所で行えます。",
"migration.whatsNew.features.foundation.title": "共通基盤",
"migration.whatsNew.features.foundation.detail":
"すべてのKilo製品で共有される小さく効率的なコア。どのような作業スタイルでも馴染みのある体験を提供します。",
"migration.whatsNew.blogLink": "完全なアナウンスを読む",
"migration.whatsNew.continue": "続行",
// Screen 2 — Migrate Settings
"migration.migrate.title": "設定の移行",
"migration.migrate.subtitle": "以前のインストールから設定が見つかりました。移行可能な項目は以下の通りです。",
"migration.migrate.selectLabel": "移行する項目を選択",
"migration.migrate.cannotMigrate": "移行できません",
"migration.migrate.chatHistory": "チャットセッションと履歴",
"migration.migrate.chatHistoryDesc": "新しいアーキテクチャと互換性がありません",
"migration.migrate.button": "設定を移行",
"migration.migrate.skip": "スキップ",
"migration.migrate.back": "戻る",
"migration.migrate.keysDetected": "{{count}}個のキーを検出",
"migration.migrate.serversConfigured": "{{count}}個のサーバーが設定済み",
"migration.migrate.modesFound": "{{count}}個のモードが見つかりました",
"migration.migrate.nothingToMigrate": "レガシー設定に移行する項目が見つかりませんでした。",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "プロバイダーのAPIキー",
"migration.select.mcpServers": "MCPサーバー",
"migration.select.customModes": "カスタムモード / エージェント",
"migration.select.defaultModel": "デフォルトモデル",
"migration.select.unsupported": "新しいバージョンではサポートされていません",
"migration.select.nothingToMigrate": "レガシー設定に移行する項目が見つかりませんでした。",
"migration.select.settings": "設定",
"migration.select.autoApproval": "自動承認",
"migration.select.autoApproval.commandRules": "コマンドルール",
"migration.select.autoApproval.commandRulesDesc": "マスタートグルと許可/拒否されたコマンドリスト",
"migration.select.autoApproval.readPermission": "読み取り権限",
"migration.select.autoApproval.readPermissionDesc": "ワークスペース内外のファイル読み取りアクセス",
"migration.select.autoApproval.writePermission": "書き込み権限",
"migration.select.autoApproval.writePermissionDesc": "ファイルの書き込みと編集アクセス",
"migration.select.autoApproval.executePermission": "実行権限",
"migration.select.autoApproval.executePermissionDesc": "ターミナルコマンドの実行",
"migration.select.autoApproval.mcpPermission": "MCP権限",
"migration.select.autoApproval.mcpPermissionDesc": "MCPツールの使用",
"migration.select.autoApproval.taskPermission": "タスク権限",
"migration.select.autoApproval.taskPermissionDesc": "モードの切り替えとサブタスクの作成",
"migration.select.language": "UI言語",
"migration.select.languageDesc": "言語設定は新しい拡張機能に適用されます",
"migration.select.autocomplete": "オートコンプリート設定",
"migration.select.autocompleteDesc": "自動トリガー、キーバインド、およびチャットのオートコンプリート設定",
"migration.select.continue": "続行",
"migration.select.back": "戻る",
"migration.progress.title": "設定を移行しています…",
"migration.progress.done": "続行",
// Migrate — completion
"migration.complete.summary": "{{total}}個中{{success}}個の項目が正常に移行されました。",
"migration.complete.cleanup": "レガシー設定データを削除する",
"migration.complete.cleanupDescription":
+47 -30
View File
@@ -234,6 +234,7 @@ export const dict = {
"prompt.action.send": "전송",
"prompt.action.stop": "중지",
"prompt.action.enhance": "프롬프트 개선",
"prompt.action.resetModel": "모델을 기본값으로 재설정",
"prompt.action.enhanceDescription":
"'프롬프트 향상' 버튼은 추가 컨텍스트, 명확화 또는 재구성을 제공하여 요청을 개선합니다. 여기에 요청을 입력한 다음 버튼을 다시 클릭하여 작동 방식을 확인해보세요.",
@@ -409,6 +410,15 @@ export const dict = {
"error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?",
"error.paidModel.title": "이 모델을 사용하려면 로그인이 필요합니다",
"error.paidModel.description":
"로그인하거나 계정을 만들어 500개 이상의 모델에 접근하고, 원가로 크레딧을 사용하거나, 자체 키를 가져오세요.",
"error.paidModel.action": "로그인",
"error.promotionLimit.title": "계속하려면 가입이 필요합니다",
"error.promotionLimit.description":
"무료로 가입하여 500개 이상의 모델을 탐색하세요. 2분이면 완료, 신용카드 불필요. 또는 나중에 다시 오세요.",
"error.promotionLimit.action": "가입하기",
"error.chain.unknown": "알 수 없는 오류",
"error.chain.causedBy": "원인:",
"error.chain.apiError": "API 오류",
@@ -985,43 +995,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "레거시 마이그레이션",
"settings.aboutKiloCode.legacyMigration.description":
"공급자 API 키 및 기본 모델을 포함하여 이전 Kilo Code 설치에서 설정을 마이그레이션합니다.",
"migration.welcome.title": "새로운 Kilo Code에 오신 것을 환영합니다",
"migration.welcome.detected": "이전 Kilo Code 설치의 설정이 감지되었습니다.",
"migration.welcome.sessionsInfo":
"채팅 세션 및 기록은 마이그레이션할 수 없습니다 — 새 버전은 다른 아키텍처를 사용합니다.",
"migration.welcome.canMigrate": "다음 설정은 새 버전으로 마이그레이션할 수 있습니다:",
"migration.welcome.start": "마이그레이션 시작",
"migration.welcome.skip": "마이그레이션 건너뛰기",
"migration.steps.title": "설정 마이그레이션",
"migration.steps.subtitle": "이전 Kilo Code 설치에서 설정을 찾았습니다. 가져올 항목을 선택하세요.",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Code의 새로운 기능",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "더 빠르고 효율적인 기반 위에 확장 프로그램을 재구축했습니다.",
"migration.whatsNew.features.performance.title": "더 빠른 에이전트 성능",
"migration.whatsNew.features.performance.detail":
"병렬 도구 호출과 하위 에이전트를 통해 에이전트가 더 많은 작업을 동시에 처리할 수 있습니다 — 대기 시간은 줄이고 작업 효율은 높입니다.",
"migration.whatsNew.features.interface.title": "간소화된 인터페이스",
"migration.whatsNew.features.interface.detail": "불필요한 요소를 줄여 더 쉽고 빠르게 읽을 수 있습니다.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"여러 에이전트를 각자의 작업 트리에서 병렬로 실행할 수 있는 통합 인터페이스 — 진행 상황 모니터링, 컨텍스트 전환, 변경 사항 검토를 한 곳에서 수행합니다.",
"migration.whatsNew.features.foundation.title": "공유 기반",
"migration.whatsNew.features.foundation.detail":
"모든 Kilo 제품에 걸쳐 하나의 작고 효율적인 코어. 어떤 방식으로 작업하든 익숙한 경험을 제공합니다.",
"migration.whatsNew.blogLink": "전체 공지 읽기",
"migration.whatsNew.continue": "계속",
// Screen 2 — Migrate Settings
"migration.migrate.title": "설정 마이그레이션",
"migration.migrate.subtitle": "이전 설치에서 설정을 찾았습니다. 가져올 수 있는 항목은 다음과 같습니다.",
"migration.migrate.selectLabel": "마이그레이션할 항목 선택",
"migration.migrate.cannotMigrate": "마이그레이션할 수 없음",
"migration.migrate.chatHistory": "채팅 세션 및 기록",
"migration.migrate.chatHistoryDesc": "새 아키텍처와 호환되지 않음",
"migration.migrate.button": "설정 마이그레이션",
"migration.migrate.skip": "건너뛰기",
"migration.migrate.back": "뒤로",
"migration.migrate.keysDetected": "{{count}}개의 키 감지됨",
"migration.migrate.serversConfigured": "{{count}}개의 서버 구성됨",
"migration.migrate.modesFound": "{{count}}개의 모드 발견됨",
"migration.migrate.nothingToMigrate": "레거시 설정에서 마이그레이션할 항목을 찾지 못했습니다.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "공급자 API 키",
"migration.select.mcpServers": "MCP 서버",
"migration.select.customModes": "사용자 지정 모드 / 에이전트",
"migration.select.defaultModel": "기본 모델",
"migration.select.unsupported": "새 버전에서는 지원되지 않음",
"migration.select.nothingToMigrate": "레거시 설정에서 마이그레이션할 항목을 찾지 못했습니다.",
"migration.select.settings": "설정",
"migration.select.autoApproval": "자동 승인",
"migration.select.autoApproval.commandRules": "명령 규칙",
"migration.select.autoApproval.commandRulesDesc": "마스터 토글 및 허용/거부된 명령 목록",
"migration.select.autoApproval.readPermission": "읽기 권한",
"migration.select.autoApproval.readPermissionDesc": "작업 공간 내부 및 외부의 파일 읽기 액세스",
"migration.select.autoApproval.writePermission": "쓰기 권한",
"migration.select.autoApproval.writePermissionDesc": "파일 쓰기 및 편집 액세스",
"migration.select.autoApproval.executePermission": "실행 권한",
"migration.select.autoApproval.executePermissionDesc": "터미널 명령 실행",
"migration.select.autoApproval.mcpPermission": "MCP 권한",
"migration.select.autoApproval.mcpPermissionDesc": "MCP 도구 사용",
"migration.select.autoApproval.taskPermission": "작업 권한",
"migration.select.autoApproval.taskPermissionDesc": "모드 전환 및 하위 작업 생성",
"migration.select.language": "UI 언어",
"migration.select.languageDesc": "언어 기본 설정이 새 확장 프로그램에 적용됩니다",
"migration.select.autocomplete": "자동 완성 설정",
"migration.select.autocompleteDesc": "자동 트리거, 키 바인딩 및 채팅 자동 완성 기본 설정",
"migration.select.continue": "계속",
"migration.select.back": "뒤로",
"migration.progress.title": "설정을 마이그레이션하는 중…",
"migration.progress.done": "계속",
// Migrate — completion
"migration.complete.summary": "{{total}}개 중 {{success}}개 항목이 성공적으로 마이그레이션되었습니다.",
"migration.complete.cleanup": "레거시 설정 데이터 제거",
"migration.complete.cleanupDescription":
+47 -31
View File
@@ -234,6 +234,7 @@ export const dict = {
"prompt.action.send": "Send",
"prompt.action.stop": "Stopp",
"prompt.action.enhance": "Forbedre prompt",
"prompt.action.resetModel": "Tilbakestill modell til standard",
"prompt.action.enhanceDescription":
"Knappen 'Forbedre prompt' hjelper med å forbedre forespørselen din ved å gi ekstra kontekst, avklaring eller omformulering. Prøv å skrive en forespørsel her og klikk på knappen igjen for å se hvordan det fungerer.",
@@ -409,6 +410,15 @@ export const dict = {
"error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
"error.paidModel.title": "Du må logge inn for å bruke denne modellen",
"error.paidModel.description":
"Logg inn eller opprett en konto for å få tilgang til over 500 modeller, bruk kreditter til kostpris, eller ta med din egen nøkkel.",
"error.paidModel.action": "Logg inn",
"error.promotionLimit.title": "Du må registrere deg for å fortsette",
"error.promotionLimit.description":
"Registrer deg gratis for å fortsette og utforske over 500 modeller. Tar 2 minutter, ingen kredittkort nødvendig. Eller kom tilbake senere.",
"error.promotionLimit.action": "Registrer deg",
"error.chain.unknown": "Ukjent feil",
"error.chain.causedBy": "Forårsaket av:",
"error.chain.apiError": "API-feil",
@@ -990,44 +1000,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Migrering fra eldre versjon",
"settings.aboutKiloCode.legacyMigration.description":
"Migrer innstillinger fra en tidligere installasjon av Kilo Code, inkludert API-nøkler for leverandører og standardmodell.",
"migration.welcome.title": "Velkommen til nye Kilo Code",
"migration.welcome.detected": "Vi oppdaget innstillinger fra en tidligere installasjon av Kilo Code.",
"migration.welcome.sessionsInfo":
"Chatøkter og historikk kan ikke migreres — den nye versjonen bruker en annen arkitektur.",
"migration.welcome.canMigrate": "Følgende innstillinger kan migreres til den nye versjonen:",
"migration.welcome.start": "Start migrering",
"migration.welcome.skip": "Hopp over migrering",
"migration.steps.title": "Migrer innstillingene dine",
"migration.steps.subtitle":
"Vi fant innstillinger fra din tidligere Kilo Code-installasjon. Velg hva du vil ta med over.",
// Screen 1 — What's New
"migration.whatsNew.title": "Hva er nytt i Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Vi har bygget utvidelsen på nytt med et raskere og mer effektivt grunnlag.",
"migration.whatsNew.features.performance.title": "Raskere agentytelse",
"migration.whatsNew.features.performance.detail":
"Parallelle verktøykall og underagenter lar agenten din håndtere mer samtidig — slik at du bruker mindre tid på å vente og mer tid på å levere.",
"migration.whatsNew.features.interface.title": "Strømlinjeformet grensesnitt",
"migration.whatsNew.features.interface.detail": "Færre forstyrrelser, enklere og raskere å lese.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Et samlet grensesnitt for å kjøre flere agenter parallelt, hver på sitt eget worktree — overvåk fremdrift, bytt kontekst og gjennomgå endringer på ett sted.",
"migration.whatsNew.features.foundation.title": "Delt grunnlag",
"migration.whatsNew.features.foundation.detail":
"Én liten, effektiv kjerne på tvers av alle Kilo-produkter. En kjent opplevelse uansett hvordan du velger å jobbe.",
"migration.whatsNew.blogLink": "Les hele kunngjøringen",
"migration.whatsNew.continue": "Fortsett",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migrer innstillingene dine",
"migration.migrate.subtitle": "Vi fant innstillinger fra din tidligere installasjon. Her er hva vi kan ta med over.",
"migration.migrate.selectLabel": "Velg hva som skal migreres",
"migration.migrate.cannotMigrate": "Kan ikke migreres",
"migration.migrate.chatHistory": "Chatøkter og historikk",
"migration.migrate.chatHistoryDesc": "Ikke kompatibel med den nye arkitekturen",
"migration.migrate.button": "Migrer innstillinger",
"migration.migrate.skip": "Hopp over",
"migration.migrate.back": "Tilbake",
"migration.migrate.keysDetected": "{{count}} nøkler oppdaget",
"migration.migrate.serversConfigured": "{{count}} server(e) konfigurert",
"migration.migrate.modesFound": "{{count}} modus(er) funnet",
"migration.migrate.nothingToMigrate": "Ingenting å migrere ble funnet i de eldre innstillingene.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "API-nøkler for leverandører",
"migration.select.mcpServers": "MCP-servere",
"migration.select.customModes": "Egendefinerte moduser / Agenter",
"migration.select.defaultModel": "Standardmodell",
"migration.select.unsupported": "Støttes ikke i ny versjon",
"migration.select.nothingToMigrate": "Ingenting å migrere ble funnet i de eldre innstillingene.",
"migration.select.settings": "Innstillinger",
"migration.select.autoApproval": "Automatisk godkjenning",
"migration.select.autoApproval.commandRules": "Kommandoregler",
"migration.select.autoApproval.commandRulesDesc": "Hovedbryter og lister for tillatte/avviste kommandoer",
"migration.select.autoApproval.readPermission": "Lesetillatelse",
"migration.select.autoApproval.readPermissionDesc": "Lesetilgang for filer i og utenfor arbeidsområdet",
"migration.select.autoApproval.writePermission": "Skrivetillatelse",
"migration.select.autoApproval.writePermissionDesc": "Skrive- og redigeringstilgang for filer",
"migration.select.autoApproval.executePermission": "Utførelsestillatelse",
"migration.select.autoApproval.executePermissionDesc": "Kjøring av terminalkommandoer",
"migration.select.autoApproval.mcpPermission": "MCP-tillatelse",
"migration.select.autoApproval.mcpPermissionDesc": "Bruk av MCP-verktøy",
"migration.select.autoApproval.taskPermission": "Oppgavetillatelse",
"migration.select.autoApproval.taskPermissionDesc": "Modusbytte og opprettelse av underoppgaver",
"migration.select.language": "UI-språk",
"migration.select.languageDesc": "Din språkpreferanse vil bli brukt i den nye utvidelsen",
"migration.select.autocomplete": "Innstillinger for autofullføring",
"migration.select.autocompleteDesc": "Preferanser for autoutløsing, hurtigtaster og autofullføring i chat",
"migration.select.continue": "Fortsett",
"migration.select.back": "Tilbake",
"migration.progress.title": "Migrerer innstillingene dine…",
"migration.progress.done": "Fortsett",
// Migrate — completion
"migration.complete.summary": "{{success}} av {{total}} elementer ble migrert.",
"migration.complete.cleanup": "Fjern eldre innstillingsdata",
"migration.complete.cleanupDescription":
+47 -33
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "Wyślij",
"prompt.action.stop": "Zatrzymaj",
"prompt.action.enhance": "Ulepsz prompt",
"prompt.action.resetModel": "Zresetuj model do domyślnego",
"prompt.action.enhanceDescription":
"Przycisk 'Ulepsz podpowiedź' pomaga ulepszyć Twoją prośbę, dostarczając dodatkowy kontekst, wyjaśnienia lub przeformułowania. Spróbuj wpisać prośbę tutaj i kliknij przycisk ponownie, aby zobaczyć, jak to działa.",
@@ -407,6 +408,15 @@ export const dict = {
"error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
"error.paidModel.title": "Musisz się zalogować, aby użyć tego modelu",
"error.paidModel.description":
"Zaloguj się lub utwórz konto, aby uzyskać dostęp do ponad 500 modeli, korzystać z kredytów po kosztach lub użyć własnego klucza.",
"error.paidModel.action": "Zaloguj się",
"error.promotionLimit.title": "Musisz się zarejestrować, aby kontynuować",
"error.promotionLimit.description":
"Zarejestruj się za darmo, aby kontynuować i odkryć ponad 500 modeli. Zajmie to 2 minuty, bez karty kredytowej. Lub wróć później.",
"error.promotionLimit.action": "Zarejestruj się",
"error.chain.unknown": "Nieznany błąd",
"error.chain.causedBy": "Spowodowany przez:",
"error.chain.apiError": "Błąd API",
@@ -991,46 +1001,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Migracja starszej wersji",
"settings.aboutKiloCode.legacyMigration.description":
"Migruj ustawienia z poprzedniej instalacji Kilo Code, w tym klucze API dostawców i domyślny model.",
"migration.welcome.title": "Witamy w nowym Kilo Code",
"migration.welcome.detected": "Wykryliśmy ustawienia z poprzedniej instalacji Kilo Code.",
"migration.welcome.sessionsInfo":
"Sesje czatu i historia nie mogą zostać zmigrowane — nowa wersja wykorzystuje inną architekturę.",
"migration.welcome.canMigrate": "Następujące ustawienia mogą zostać zmigrowane do nowej wersji:",
"migration.welcome.start": "Rozpocznij migrację",
"migration.welcome.skip": "Pomiń migrację",
"migration.steps.title": "Migruj swoje ustawienia",
"migration.steps.subtitle":
"Znaleźliśmy ustawienia z poprzedniej instalacji Kilo Code. Wybierz, co chcesz przenieść.",
// Screen 1 — What's New
"migration.whatsNew.title": "Co nowego w Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Przebudowaliśmy rozszerzenie na szybszym i wydajniejszym fundamencie.",
"migration.whatsNew.features.performance.title": "Szybsza wydajność agenta",
"migration.whatsNew.features.performance.detail":
"Równoległe wywołania narzędzi i podagenci pozwalają agentowi robić więcej naraz — mniej czekania, więcej działania.",
"migration.whatsNew.features.interface.title": "Uproszczony interfejs",
"migration.whatsNew.features.interface.detail": "Mniej rozpraszaczy, łatwiejszy i szybszy do czytania.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Zunifikowany interfejs do uruchamiania wielu agentów równolegle, każdy na własnym drzewie roboczym — monitoruj postęp, przełączaj kontekst i przeglądaj zmiany w jednym miejscu.",
"migration.whatsNew.features.foundation.title": "Wspólny fundament",
"migration.whatsNew.features.foundation.detail":
"Jedno małe, wydajne jądro we wszystkich produktach Kilo. Znajome doświadczenie niezależnie od sposobu pracy.",
"migration.whatsNew.blogLink": "Przeczytaj pełne ogłoszenie",
"migration.whatsNew.continue": "Kontynuuj",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Migruj swoje ustawienia",
"migration.migrate.subtitle": "Znaleźliśmy ustawienia z poprzedniej instalacji. Oto co możemy przenieść.",
"migration.migrate.selectLabel": "Wybierz, co zmigrować",
"migration.migrate.cannotMigrate": "Nie można zmigrować",
"migration.migrate.chatHistory": "Sesje czatu i historia",
"migration.migrate.chatHistoryDesc": "Niekompatybilne z nową architekturą",
"migration.migrate.button": "Migruj ustawienia",
"migration.migrate.skip": "Pomiń",
"migration.migrate.back": "Wstecz",
"migration.migrate.keysDetected": "Wykryto {{count}} kluczy",
"migration.migrate.serversConfigured": "Skonfigurowano {{count}} serwer(ów)",
"migration.migrate.modesFound": "Znaleziono {{count}} tryb(ów)",
"migration.migrate.nothingToMigrate": "W starszych ustawieniach nie znaleziono niczego do zmigrowania.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "Klucze API dostawców",
"migration.select.mcpServers": "Serwery MCP",
"migration.select.customModes": "Niestandardowe tryby / Agenci",
"migration.select.defaultModel": "Domyślny model",
"migration.select.unsupported": "Nieobsługiwane w nowej wersji",
"migration.select.nothingToMigrate": "W starszych ustawieniach nie znaleziono niczego do zmigrowania.",
"migration.select.settings": "Ustawienia",
"migration.select.autoApproval": "Automatyczne zatwierdzanie",
"migration.select.autoApproval.commandRules": "Reguły poleceń",
"migration.select.autoApproval.commandRulesDesc": "Główny przełącznik oraz listy dozwolonych/zabronionych poleceń",
"migration.select.autoApproval.readPermission": "Uprawnienia odczytu",
"migration.select.autoApproval.readPermissionDesc":
"Dostęp do odczytu plików wewnątrz i na zewnątrz obszaru roboczego",
"migration.select.autoApproval.writePermission": "Uprawnienia zapisu",
"migration.select.autoApproval.writePermissionDesc": "Dostęp do zapisu i edycji plików",
"migration.select.autoApproval.executePermission": "Uprawnienia wykonywania",
"migration.select.autoApproval.executePermissionDesc": "Wykonywanie poleceń terminala",
"migration.select.autoApproval.mcpPermission": "Uprawnienia MCP",
"migration.select.autoApproval.mcpPermissionDesc": "Korzystanie z narzędzi MCP",
"migration.select.autoApproval.taskPermission": "Uprawnienia zadań",
"migration.select.autoApproval.taskPermissionDesc": "Przełączanie trybów i tworzenie podzadań",
"migration.select.language": "Język interfejsu",
"migration.select.languageDesc": "Twoje preferencje językowe zostaną zastosowane w nowym rozszerzeniu",
"migration.select.autocomplete": "Ustawienia autouzupełniania",
"migration.select.autocompleteDesc":
"Preferencje automatycznego wyzwalania, skrótów klawiszowych i autouzupełniania na czacie",
"migration.select.continue": "Kontynuuj",
"migration.select.back": "Wstecz",
"migration.progress.title": "Migrowanie Twoich ustawień…",
"migration.progress.done": "Kontynuuj",
// Migrate — completion
"migration.complete.summary": "Pomyślnie zmigrowano {{success}} z {{total}} elementów.",
"migration.complete.cleanup": "Usuń dane starszych ustawień",
"migration.complete.cleanupDescription":
+47 -32
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "Отправить",
"prompt.action.stop": "Остановить",
"prompt.action.enhance": "Улучшить промпт",
"prompt.action.resetModel": "Сбросить модель на значение по умолчанию",
"prompt.action.enhanceDescription":
"Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.",
@@ -408,6 +409,15 @@ export const dict = {
"error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
"error.paidModel.title": "Для использования этой модели необходимо войти",
"error.paidModel.description":
"Войдите или создайте аккаунт для доступа к более чем 500 моделям, используйте кредиты по себестоимости или подключите свой ключ.",
"error.paidModel.action": "Войти",
"error.promotionLimit.title": "Для продолжения необходимо зарегистрироваться",
"error.promotionLimit.description":
"Зарегистрируйтесь бесплатно, чтобы продолжить и исследовать более 500 моделей. Займёт 2 минуты, кредитная карта не нужна. Или вернитесь позже.",
"error.promotionLimit.action": "Зарегистрироваться",
"error.chain.unknown": "Неизвестная ошибка",
"error.chain.causedBy": "Причина:",
"error.chain.apiError": "Ошибка API",
@@ -993,45 +1003,50 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Миграция с устаревшей версии",
"settings.aboutKiloCode.legacyMigration.description":
"Перенос настроек из предыдущей установки Kilo Code, включая API-ключи провайдеров и модель по умолчанию.",
"migration.welcome.title": "Добро пожаловать в новый Kilo Code",
"migration.welcome.detected": "Мы обнаружили настройки от предыдущей установки Kilo Code.",
"migration.welcome.sessionsInfo":
"Сессии чата и история не могут быть перенесены — новая версия использует другую архитектуру.",
"migration.welcome.canMigrate": "В новую версию можно перенести следующие настройки:",
"migration.welcome.start": "Начать миграцию",
"migration.welcome.skip": "Пропустить миграцию",
"migration.steps.title": "Перенос ваших настроек",
"migration.steps.subtitle":
"Мы нашли настройки от вашей предыдущей установки Kilo Code. Выберите, что нужно перенести.",
// Screen 1 — What's New
"migration.whatsNew.title": "Что нового в Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "Мы перестроили расширение на более быстрой и эффективной основе.",
"migration.whatsNew.features.performance.title": "Более быстрая работа агента",
"migration.whatsNew.features.performance.detail":
"Параллельные вызовы инструментов и подагенты позволяют вашему агенту делать больше одновременно — меньше ожидания, больше результатов.",
"migration.whatsNew.features.interface.title": "Упрощённый интерфейс",
"migration.whatsNew.features.interface.detail": "Меньше отвлекающих факторов, проще и быстрее для чтения.",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"Единый интерфейс для запуска нескольких агентов параллельно, каждый в своём worktree — отслеживайте прогресс, переключайте контекст и просматривайте изменения в одном месте.",
"migration.whatsNew.features.foundation.title": "Общая основа",
"migration.whatsNew.features.foundation.detail":
"Одно компактное и эффективное ядро для всех продуктов Kilo. Знакомый опыт независимо от выбранного способа работы.",
"migration.whatsNew.blogLink": "Читать полный анонс",
"migration.whatsNew.continue": "Продолжить",
// Screen 2 — Migrate Settings
"migration.migrate.title": "Перенос ваших настроек",
"migration.migrate.subtitle": "Мы нашли настройки от вашей предыдущей установки. Вот что можно перенести.",
"migration.migrate.selectLabel": "Выберите, что перенести",
"migration.migrate.cannotMigrate": "Невозможно перенести",
"migration.migrate.chatHistory": "Сессии чата и история",
"migration.migrate.chatHistoryDesc": "Несовместимы с новой архитектурой",
"migration.migrate.button": "Перенести настройки",
"migration.migrate.skip": "Пропустить",
"migration.migrate.back": "Назад",
"migration.migrate.keysDetected": "Обнаружено ключей: {{count}}",
"migration.migrate.serversConfigured": "Настроено серверов: {{count}}",
"migration.migrate.modesFound": "Найдено режимов: {{count}}",
"migration.migrate.nothingToMigrate": "В старых настройках не найдено данных для переноса.",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "API-ключи провайдеров",
"migration.select.mcpServers": "Серверы MCP",
"migration.select.customModes": "Пользовательские режимы / Агенты",
"migration.select.defaultModel": "Модель по умолчанию",
"migration.select.unsupported": "Не поддерживается в новой версии",
"migration.select.nothingToMigrate": "В старых настройках не найдено данных для переноса.",
"migration.select.settings": "Настройки",
"migration.select.autoApproval": "Автоматическое подтверждение",
"migration.select.autoApproval.commandRules": "Правила команд",
"migration.select.autoApproval.commandRulesDesc": "Главный переключатель и списки разрешенных/запрещенных команд",
"migration.select.autoApproval.readPermission": "Разрешение на чтение",
"migration.select.autoApproval.readPermissionDesc": "Доступ к чтению файлов внутри и вне рабочей области",
"migration.select.autoApproval.writePermission": "Разрешение на запись",
"migration.select.autoApproval.writePermissionDesc": "Доступ к записи и редактированию файлов",
"migration.select.autoApproval.executePermission": "Разрешение на выполнение",
"migration.select.autoApproval.executePermissionDesc": "Выполнение команд в терминале",
"migration.select.autoApproval.mcpPermission": "Разрешение MCP",
"migration.select.autoApproval.mcpPermissionDesc": "Использование инструментов MCP",
"migration.select.autoApproval.taskPermission": "Разрешение на задачи",
"migration.select.autoApproval.taskPermissionDesc": "Переключение режимов и создание подзадач",
"migration.select.language": "Язык интерфейса",
"migration.select.languageDesc": "Ваши языковые настройки будут применены к новому расширению",
"migration.select.autocomplete": "Настройки автодополнения",
"migration.select.autocompleteDesc":
"Параметры автоматического срабатывания, горячие клавиши и автодополнение в чате",
"migration.select.continue": "Продолжить",
"migration.select.back": "Назад",
"migration.progress.title": "Перенос ваших настроек…",
"migration.progress.done": "Продолжить",
// Migrate — completion
"migration.complete.summary": "Успешно перенесено: {{success}} из {{total}} элементов.",
"migration.complete.cleanup": "Удалить данные старых настроек",
"migration.complete.cleanupDescription":
+53 -36
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "ส่ง",
"prompt.action.stop": "หยุด",
"prompt.action.enhance": "ปรับปรุงพรอมต์",
"prompt.action.resetModel": "รีเซ็ตโมเดลเป็นค่าเริ่มต้น",
"prompt.action.enhanceDescription":
"ปุ่ม 'ปรับปรุงพรอมต์' ช่วยปรับปรุงพรอมต์ของคุณโดยให้บริบทเพิ่มเติม ชี้แจง หรือเขียนใหม่ ลองพิมพ์พรอมต์ที่นี่และคลิกปุ่มอีกครั้งเพื่อดูว่ามันทำงานอย่างไร",
@@ -406,6 +407,15 @@ export const dict = {
"error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
"error.paidModel.title": "คุณต้องเข้าสู่ระบบเพื่อใช้โมเดลนี้",
"error.paidModel.description":
"เข้าสู่ระบบหรือสร้างบัญชีเพื่อเข้าถึงโมเดลกว่า 500 รายการ ใช้เครดิตในราคาทุน หรือใช้คีย์ของคุณเอง",
"error.paidModel.action": "เข้าสู่ระบบ",
"error.promotionLimit.title": "คุณต้องสมัครสมาชิกเพื่อดำเนินการต่อ",
"error.promotionLimit.description":
"สมัครฟรีเพื่อดำเนินการต่อและสำรวจโมเดลกว่า 500 รายการ ใช้เวลา 2 นาที ไม่ต้องใช้บัตรเครดิต หรือกลับมาทีหลัง",
"error.promotionLimit.action": "สมัครสมาชิก",
"error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก",
"error.chain.causedBy": "สาเหตุ:",
"error.chain.apiError": "ข้อผิดพลาด API",
@@ -981,47 +991,54 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "การย้ายข้อมูลจากเวอร์ชันดั้งเดิม",
"settings.aboutKiloCode.legacyMigration.description":
"ย้ายการตั้งค่าจากการติดตั้ง Kilo Code ก่อนหน้านี้ รวมถึงคีย์ API ของผู้ให้บริการและโมเดลเริ่มต้น",
"migration.welcome.title": "ยินดีต้อนรับสู่ Kilo Code โฉมใหม่",
"migration.welcome.detected": "เราตรวจพบการตั้งค่าจากการติดตั้ง Kilo Code ก่อนหน้านี้",
"migration.welcome.sessionsInfo": "ไม่สามารถย้ายเซสชันการแชทและประวัติได้ — เวอร์ชันใหม่ใช้สถาปัตยกรรมที่แตกต่างกัน",
"migration.welcome.canMigrate": "การตั้งค่าต่อไปนี้สามารถย้ายไปยังเวอร์ชันใหม่ได้:",
"migration.welcome.start": "เริ่มการย้ายข้อมูล",
"migration.welcome.skip": "ข้ามการย้ายข้อมูล",
"migration.steps.title": "ย้ายการตั้งค่าของคุณ",
"migration.steps.subtitle": "เราพบการตั้งค่าจากการติดตั้ง Kilo Code ก่อนหน้านี้ของคุณ เลือกสิ่งที่จะนำเข้ามาด้วย",
// Screen 1 — What's New
"migration.whatsNew.title": "มีอะไรใหม่ใน Kilo Code",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "เราได้สร้างส่วนขยายใหม่บนรากฐานที่เร็วและมีประสิทธิภาพมากขึ้น",
"migration.whatsNew.features.performance.title": "ประสิทธิภาพเอเจนต์ที่เร็วขึ้น",
"migration.whatsNew.features.performance.detail":
"การเรียกเครื่องมือแบบขนานและเอเจนต์ย่อยช่วยให้เอเจนต์ของคุณจัดการได้มากขึ้นในครั้งเดียว — คุณจึงใช้เวลารอน้อยลงและส่งงานได้มากขึ้น",
"migration.whatsNew.features.interface.title": "อินเทอร์เฟซที่กระชับ",
"migration.whatsNew.features.interface.detail": "สิ่งรบกวนน้อยลง อ่านง่ายและรวดเร็วขึ้น",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"อินเทอร์เฟซรวมสำหรับการเรียกใช้เอเจนต์หลายตัวพร้อมกัน แต่ละตัวบน worktree ของตัวเอง — ติดตามความคืบหน้า สลับบริบท และตรวจสอบการเปลี่ยนแปลงได้ในที่เดียว",
"migration.whatsNew.features.foundation.title": "รากฐานที่ใช้ร่วมกัน",
"migration.whatsNew.features.foundation.detail":
"แกนหลักขนาดเล็กและมีประสิทธิภาพเดียวกันในทุกผลิตภัณฑ์ Kilo ประสบการณ์ที่คุ้นเคยไม่ว่าคุณจะเลือกทำงานอย่างไร",
"migration.whatsNew.blogLink": "อ่านประกาศฉบับเต็ม",
"migration.whatsNew.continue": "ดำเนินการต่อ",
// Screen 2 — Migrate Settings
"migration.migrate.title": "ย้ายการตั้งค่าของคุณ",
"migration.migrate.subtitle": "เราพบการตั้งค่าจากการติดตั้งก่อนหน้านี้ของคุณ นี่คือสิ่งที่เราสามารถนำมาได้",
"migration.migrate.selectLabel": "เลือกสิ่งที่จะย้าย",
"migration.migrate.cannotMigrate": "ไม่สามารถย้ายได้",
"migration.migrate.chatHistory": "เซสชันแชทและประวัติ",
"migration.migrate.chatHistoryDesc": "เข้ากันไม่ได้กับสถาปัตยกรรมใหม่",
"migration.migrate.button": "ย้ายการตั้งค่า",
"migration.migrate.skip": "ข้าม",
"migration.migrate.back": "ย้อนกลับ",
"migration.migrate.keysDetected": "ตรวจพบ {{count}} คีย์",
"migration.migrate.serversConfigured": "กำหนดค่า {{count}} เซิร์ฟเวอร์",
"migration.migrate.modesFound": "พบ {{count}} โหมด",
"migration.migrate.nothingToMigrate": "ไม่พบสิ่งที่จะย้ายในการตั้งค่าดั้งเดิม",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "คีย์ API ของผู้ให้บริการ",
"migration.select.mcpServers": "เซิร์ฟเวอร์ MCP",
"migration.select.customModes": "โหมดกำหนดเอง / Agents",
"migration.select.customModes": "โหมดที่กำหนดเอง / เอเจนต์",
"migration.select.defaultModel": "โมเดลเริ่มต้น",
"migration.select.unsupported": "ไม่รองรับในเวอร์ชันใหม่",
"migration.select.nothingToMigrate": "ไม่พบสิ่งที่จะย้ายในการตั้งค่าดั้งเดิม",
"migration.select.settings": "การตั้งค่า",
"migration.select.autoApproval": "การอนุมัติอัตโนมัติ",
"migration.select.autoApproval.commandRules": "กฎคำสั่ง",
"migration.select.autoApproval.commandRulesDesc": "สวิตช์หลักและรายการคำสั่งที่อนุญาต/ปฏิเสธ",
"migration.select.autoApproval.readPermission": "สิทธิ์การอ่าน",
"migration.select.autoApproval.readPermissionDesc": "การเข้าถึงการอ่านไฟล์ภายในและภายนอกเวิร์กสเปซ",
"migration.select.autoApproval.writePermission": "สิทธิ์การเขียน",
"migration.select.autoApproval.writePermissionDesc": "การเข้าถึงการเขียนและแก้ไขไฟล์",
"migration.select.autoApproval.executePermission": "สิทธิ์การดำเนินการ",
"migration.select.autoApproval.executePermissionDesc": "การเรียกใช้งานคำสั่ง Terminal",
"migration.select.autoApproval.mcpPermission": "สิทธิ์ MCP",
"migration.select.autoApproval.mcpPermissionDesc": "การใช้งานเครื่องมือ MCP",
"migration.select.autoApproval.taskPermission": "สิทธิ์งาน",
"migration.select.autoApproval.taskPermissionDesc": "การสลับโหมดและการสร้างงานย่อย",
"migration.select.language": "ภาษา UI",
"migration.select.languageDesc": "การตั้งค่าภาษาของคุณจะถูกนำไปใช้กับส่วนขยายใหม่",
"migration.select.autocomplete": "การตั้งค่าเติมข้อความอัตโนมัติ",
"migration.select.autocompleteDesc":
"การเรียกใช้อัตโนมัติ, การผูกแป้นพิมพ์ และการตั้งค่าการเติมข้อความอัตโนมัติในการแชท",
"migration.select.continue": "ดำเนินการต่อ",
"migration.select.back": "ย้อนกลับ",
"migration.progress.title": "กำลังย้ายการตั้งค่าของคุณ…",
"migration.progress.done": "ดำเนินการต่อ",
"migration.complete.summary": "ย้ายรายการสำเร็จ {{success}} จาก {{total}} รายการ",
"migration.complete.cleanup": "ลบข้อมูลการตั้งค่าดั้งเดิม",
"migration.select.language": "ภาษาของ UI",
"migration.select.autocomplete": "การตั้งค่าการเติมข้อความอัตโนมัติ",
// Migrate — completion
"migration.complete.summary": "ย้ายข้อมูลสำเร็จ {{success}} จาก {{total}} รายการ",
"migration.complete.cleanup": "ลบข้อมูลการตั้งค่าเดิม",
"migration.complete.cleanupDescription":
"การดำเนินการนี้จะลบการตั้งค่าเก่าออกจากพื้นที่เก็บข้อมูลของ VS Code คุณจะไม่สามารถเรียกใช้การย้ายข้อมูลนี้ซ้ำได้อีก",
"การดำเนินการนี้จะลบการตั้งค่าเดิมจากที่เก็บข้อมูล VS Code คุณจะไม่สามารถเรียกใช้การย้ายข้อมูลนี้ได้อีก",
"migration.complete.done": "เสร็จสิ้น",
// legacy-migration end
}
+54 -38
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "发送",
"prompt.action.stop": "停止",
"prompt.action.enhance": "优化提示词",
"prompt.action.resetModel": "重置模型为默认值",
"prompt.action.enhanceDescription":
"'增强提示'按钮通过提供额外上下文、澄清或重新表述来帮助改进您的请求。尝试在此处输入请求,然后再次点击按钮查看其工作原理。",
@@ -404,6 +405,13 @@ export const dict = {
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
"error.paidModel.title": "您需要登录才能使用此模型",
"error.paidModel.description": "登录或创建账户以访问500多个模型,按成本使用积分,或使用您自己的密钥。",
"error.paidModel.action": "登录",
"error.promotionLimit.title": "您需要注册才能继续",
"error.promotionLimit.description": "免费注册以继续探索500多个模型。只需2分钟,无需信用卡。或稍后再来。",
"error.promotionLimit.action": "注册",
"error.chain.unknown": "未知错误",
"error.chain.causedBy": "原因:",
"error.chain.apiError": "API 错误",
@@ -972,49 +980,57 @@ export const dict = {
"common.review": "审查",
// legacy-migration start
"settings.legacyMigration.link": "从旧版本迁移",
"settings.aboutKiloCode.legacyMigration.title": "旧版本迁移",
"settings.legacyMigration.link": "从旧版迁移",
"settings.aboutKiloCode.legacyMigration.title": "旧版迁移",
"settings.aboutKiloCode.legacyMigration.description":
"从之前安装的 Kilo Code 迁移设置,包括提供商 API 密钥和默认模型。",
"migration.welcome.title": "欢迎使用全新 Kilo Code",
"migration.welcome.detected": "我们检测到了来自之前安装的 Kilo Code 的设置。",
"migration.welcome.sessionsInfo": "聊天会话和历史记录无法迁移 — 新版本使用了不同的架构。",
"migration.welcome.canMigrate": "以下设置可以迁移到新版本:",
"migration.welcome.start": "开始迁移",
"migration.welcome.skip": "跳过迁移",
"migration.steps.title": "迁移您的设置",
"migration.steps.subtitle": "我们发现了您之前 Kilo Code 安装的设置。请选择要引入的内容。",
"从旧版 Kilo Code 安装迁移设置,包括提供商 API 密钥和默认模型。",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Code 新功能",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "我们在更快、更高效的基础上重新构建了扩展。",
"migration.whatsNew.features.performance.title": "更快的智能体性能",
"migration.whatsNew.features.performance.detail":
"并行工具调用和子智能体让你的智能体可以同时处理更多任务——减少等待时间,更快交付成果。",
"migration.whatsNew.features.interface.title": "简洁的界面",
"migration.whatsNew.features.interface.detail": "更少干扰,更易阅读,更快上手。",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"一个统一的界面,可以并行运行多个智能体,每个智能体各自使用独立的 worktree——在同一个地方监控进度、切换上下文和审查变更。",
"migration.whatsNew.features.foundation.title": "共享基础",
"migration.whatsNew.features.foundation.detail":
"所有 Kilo 产品共享一个小巧高效的核心。无论你选择哪种方式工作,都能获得熟悉的体验。",
"migration.whatsNew.blogLink": "阅读完整公告",
"migration.whatsNew.continue": "继续",
// Screen 2 — Migrate Settings
"migration.migrate.title": "迁移你的设置",
"migration.migrate.subtitle": "我们发现了你之前安装的设置。以下是可以迁移的内容。",
"migration.migrate.selectLabel": "选择要迁移的内容",
"migration.migrate.cannotMigrate": "无法迁移",
"migration.migrate.chatHistory": "聊天会话和历史记录",
"migration.migrate.chatHistoryDesc": "与新架构不兼容",
"migration.migrate.button": "迁移设置",
"migration.migrate.skip": "跳过",
"migration.migrate.back": "返回",
"migration.migrate.keysDetected": "检测到 {{count}} 个密钥",
"migration.migrate.serversConfigured": "已配置 {{count}} 个服务器",
"migration.migrate.modesFound": "发现 {{count}} 个模式",
"migration.migrate.nothingToMigrate": "在旧版设置中未找到要迁移的内容。",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "提供商 API 密钥",
"migration.select.mcpServers": "MCP 服务器",
"migration.select.customModes": "自定义模式 / 智能体",
"migration.select.customModes": "自定义模式 / 代理",
"migration.select.defaultModel": "默认模型",
"migration.select.unsupported": "新版本不支持",
"migration.select.nothingToMigrate": "在旧版设置中未找到要迁移的内容。",
"migration.select.settings": "设置",
"migration.select.autoApproval": "自动批准",
"migration.select.autoApproval.commandRules": "命令规则",
"migration.select.autoApproval.commandRulesDesc": "主开关以及允许/拒绝的命令列表",
"migration.select.autoApproval.readPermission": "读取权限",
"migration.select.autoApproval.readPermissionDesc": "工作区内外文件读取访问权限",
"migration.select.autoApproval.writePermission": "写入权限",
"migration.select.autoApproval.writePermissionDesc": "文件写入和编辑访问权限",
"migration.select.autoApproval.executePermission": "执行权限",
"migration.select.autoApproval.executePermissionDesc": "终端命令执行",
"migration.select.autoApproval.mcpPermission": "MCP 权限",
"migration.select.autoApproval.mcpPermissionDesc": "MCP 工具使用",
"migration.select.autoApproval.taskPermission": "任务权限",
"migration.select.autoApproval.taskPermissionDesc": "模式切换和子任务创建",
"migration.select.language": "UI 语言",
"migration.select.languageDesc": "您的语言偏好将应用于新的扩展",
"migration.select.autoApproval": "自动审批",
"migration.select.language": "界面语言",
"migration.select.autocomplete": "自动补全设置",
"migration.select.autocompleteDesc": "自动触发、键绑定和聊天自动补全偏好设置",
"migration.select.continue": "继续",
"migration.select.back": "返回",
"migration.progress.title": "正在迁移您的设置…",
"migration.progress.done": "继续",
"migration.complete.summary": "{{total}} 项中的 {{success}} 项已成功迁移。",
"migration.complete.cleanup": "移除旧版设置数据",
"migration.complete.cleanupDescription": "这将从 VS Code 存储中移除旧设置。您将无法再次运行此迁移。",
// Migrate — completion
"migration.complete.summary": "成功迁移 {{success}}/{{total}} 项。",
"migration.complete.cleanup": "清除旧版设置数据",
"migration.complete.cleanupDescription": "此操作将从 VS Code 存储中删除旧版设置。您将无法再次运行此迁移。",
"migration.complete.done": "完成",
// legacy-migration end
} satisfies Partial<Record<Keys, string>>
+52 -36
View File
@@ -231,6 +231,7 @@ export const dict = {
"prompt.action.send": "傳送",
"prompt.action.stop": "停止",
"prompt.action.enhance": "改善提示詞",
"prompt.action.resetModel": "重置模型為預設值",
"prompt.action.enhanceDescription":
"「強化提示詞」按鈕可透過提供額外內容、說明或改寫來協助改善提示詞。試著在這裡輸入提示詞,再點選一次按鈕以了解其運作方式。",
@@ -405,6 +406,13 @@ export const dict = {
"error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
"error.paidModel.title": "您需要登入才能使用此模型",
"error.paidModel.description": "登入或建立帳戶以存取500多個模型,按成本使用點數,或使用您自己的金鑰。",
"error.paidModel.action": "登入",
"error.promotionLimit.title": "您需要註冊才能繼續",
"error.promotionLimit.description": "免費註冊以繼續探索500多個模型。只需2分鐘,無需信用卡。或稍後再來。",
"error.promotionLimit.action": "註冊",
"error.chain.unknown": "未知錯誤",
"error.chain.causedBy": "原因:",
"error.chain.apiError": "API 錯誤",
@@ -973,49 +981,57 @@ export const dict = {
"common.review": "審查",
// legacy-migration start
"settings.legacyMigration.link": "從舊版轉移",
"settings.aboutKiloCode.legacyMigration.title": "舊版轉移",
"settings.legacyMigration.link": "從舊版遷移",
"settings.aboutKiloCode.legacyMigration.title": "舊版遷移",
"settings.aboutKiloCode.legacyMigration.description":
"從先前安裝的 Kilo Code 轉移設定,包含供應商 API 金鑰與預設模型。",
"migration.welcome.title": "歡迎使用全新的 Kilo Code",
"migration.welcome.detected": "我們偵測到先前安裝的 Kilo Code 設定。",
"migration.welcome.sessionsInfo": "聊天對話與歷史紀錄無法轉移 — 新版本使用了不同的架構。",
"migration.welcome.canMigrate": "下列設定可以轉移至新版本:",
"migration.welcome.start": "開始轉移",
"migration.welcome.skip": "略過轉移",
"migration.steps.title": "轉移您的設定",
"migration.steps.subtitle": "我們找到了您先前 Kilo Code 安裝的設定。請選擇要保留的項目。",
"從舊版 Kilo Code 安裝遷移設定,包括供應商 API 金鑰和預設模型。",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Code 新功能",
"migration.whatsNew.badge": "Beta",
"migration.whatsNew.subtitle": "我們在更快、更高效的基礎上重新建構了擴充功能。",
"migration.whatsNew.features.performance.title": "更快的 Agent 效能",
"migration.whatsNew.features.performance.detail":
"平行工具呼叫和子 Agent 讓你的 Agent 可以同時處理更多任務——減少等待時間,更快交付成果。",
"migration.whatsNew.features.interface.title": "簡潔的介面",
"migration.whatsNew.features.interface.detail": "更少干擾,更易閱讀,更快上手。",
"migration.whatsNew.features.agentManager.title": "Agent Manager",
"migration.whatsNew.features.agentManager.detail":
"一個統一的介面,可以平行執行多個 Agent,每個 Agent 各自使用獨立的 worktree——在同一個地方監控進度、切換上下文和審查變更。",
"migration.whatsNew.features.foundation.title": "共享基礎",
"migration.whatsNew.features.foundation.detail":
"所有 Kilo 產品共享一個小巧高效的核心。無論你選擇哪種方式工作,都能獲得熟悉的體驗。",
"migration.whatsNew.blogLink": "閱讀完整公告",
"migration.whatsNew.continue": "繼續",
// Screen 2 — Migrate Settings
"migration.migrate.title": "遷移你的設定",
"migration.migrate.subtitle": "我們發現了你之前安裝的設定。以下是可以遷移的內容。",
"migration.migrate.selectLabel": "選擇要遷移的內容",
"migration.migrate.cannotMigrate": "無法遷移",
"migration.migrate.chatHistory": "聊天工作階段與歷史紀錄",
"migration.migrate.chatHistoryDesc": "與新架構不相容",
"migration.migrate.button": "遷移設定",
"migration.migrate.skip": "略過",
"migration.migrate.back": "返回",
"migration.migrate.keysDetected": "偵測到 {{count}} 個金鑰",
"migration.migrate.serversConfigured": "已設定 {{count}} 個伺服器",
"migration.migrate.modesFound": "發現 {{count}} 個模式",
"migration.migrate.nothingToMigrate": "在舊版設定中找不到可遷移的項目。",
// Migrate — item labels (reused from old select keys)
"migration.select.providers": "供應商 API 金鑰",
"migration.select.mcpServers": "MCP 伺服器",
"migration.select.customModes": "自訂模式 / 代理",
"migration.select.defaultModel": "預設模型",
"migration.select.unsupported": "新版本不支援",
"migration.select.nothingToMigrate": "在舊版設定中找不到可轉移的項目。",
"migration.select.settings": "設定",
"migration.select.autoApproval": "自動核准",
"migration.select.autoApproval.commandRules": "指令規則",
"migration.select.autoApproval.commandRulesDesc": "主要開關與允許/拒絕的指令清單",
"migration.select.autoApproval.readPermission": "讀取權限",
"migration.select.autoApproval.readPermissionDesc": "工作區內外的檔案讀取存取權",
"migration.select.autoApproval.writePermission": "寫入權限",
"migration.select.autoApproval.writePermissionDesc": "檔案寫入與編輯存取權",
"migration.select.autoApproval.executePermission": "執行權限",
"migration.select.autoApproval.executePermissionDesc": "終端機指令執行",
"migration.select.autoApproval.mcpPermission": "MCP 權限",
"migration.select.autoApproval.mcpPermissionDesc": "MCP 工具使用",
"migration.select.autoApproval.taskPermission": "任務權限",
"migration.select.autoApproval.taskPermissionDesc": "模式切換與子任務建立",
"migration.select.language": "介面語言",
"migration.select.languageDesc": "您的語言偏好將會套用至新擴充功能",
"migration.select.autocomplete": "自動完成設定",
"migration.select.autocompleteDesc": "自動觸發、按鍵綁定與聊天自動完成偏好設定",
"migration.select.continue": "繼續",
"migration.select.back": "返回",
"migration.progress.title": "正在轉移您的設定…",
"migration.progress.done": "繼續",
"migration.complete.summary": "已成功轉移 {{total}} 個項目中的 {{success}} 個。",
"migration.complete.cleanup": "移除舊版設定資料",
"migration.complete.cleanupDescription": "這將從 VS Code 儲存空間移除舊的設定。您將無法重新執行此轉移程序。",
"migration.select.autocomplete": "自動補全設定",
// Migrate — completion
"migration.complete.summary": "成功遷移 {{success}}/{{total}} 項。",
"migration.complete.cleanup": "清除舊版設定資料",
"migration.complete.cleanupDescription": "此操作將從 VS Code 儲存中刪除舊版設定。您將無法再次執行此遷移。",
"migration.complete.done": "完成",
// legacy-migration end
} satisfies Partial<Record<Keys, string>>
@@ -27,14 +27,16 @@ import { SessionContext } from "../context/session"
import { LanguageContext } from "../context/language"
import { dict as uiEn } from "@kilocode/kilo-ui/i18n/en"
import { dict as appEn } from "../i18n/en"
import { dict as amEn } from "../../agent-manager/i18n/en"
import { dict as kiloEn } from "@kilocode/kilo-i18n/en"
import { resolveTemplate } from "../context/language-utils"
import type { PermissionRequest, QuestionRequest } from "../types/messages"
// Merged English dictionary (same merge order as the real LanguageProvider)
const dict: Record<string, string> = { ...appEn, ...uiEn, ...kiloEn }
const dict: Record<string, string> = { ...appEn, ...amEn, ...uiEn, ...kiloEn }
function t(key: string) {
return dict[key] ?? key
function t(key: string, params?: Record<string, string | number | boolean | undefined>) {
return resolveTemplate(dict[key] ?? key, params)
}
// ---------------------------------------------------------------------------
@@ -134,6 +136,8 @@ export function mockSessionValue(overrides?: {
questionErrors: () => new Set<string>(),
selected: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }),
selectModel: noop,
hasModelOverride: () => false,
clearModelOverride: noop,
totalCost: () => 0,
contextUsage: () => undefined,
agents: () => [{ name: "code", description: "Code mode", mode: "primary" as const }],
@@ -1,7 +1,7 @@
/** @jsxImportSource solid-js */
/**
* Stories for Agent Manager components:
* FileTree, DiffPanel, FullScreenDiffView
* FileTree, DiffPanel, FullScreenDiffView, WorktreeItem
*/
import type { Meta, StoryObj } from "storybook-solidjs-vite"
@@ -9,7 +9,9 @@ import { StoryProviders } from "./StoryProviders"
import { FileTree } from "../../agent-manager/FileTree"
import { DiffPanel } from "../../agent-manager/DiffPanel"
import { FullScreenDiffView } from "../../agent-manager/FullScreenDiffView"
import type { WorktreeFileDiff } from "../types/messages"
import { WorktreeItem } from "../../agent-manager/WorktreeItem"
import type { WorktreeFileDiff, WorktreeState, WorktreeGitStats } from "../types/messages"
import "../../agent-manager/agent-manager.css"
// ---------------------------------------------------------------------------
// Shared mock data
@@ -125,3 +127,168 @@ export const FullScreenDiffWithChanges: Story = {
</StoryProviders>
),
}
// ---------------------------------------------------------------------------
// WorktreeItem — shared mock helpers
// ---------------------------------------------------------------------------
const noop = () => {}
const baseWorktree: WorktreeState = {
id: "wt-abc123",
branch: "feat/inline-delete",
path: "/tmp/worktrees/feat-inline-delete",
parentBranch: "main",
createdAt: new Date(Date.now() - 3600_000).toISOString(),
}
const baseStats: WorktreeGitStats = {
worktreeId: "wt-abc123",
files: 4,
additions: 32,
deletions: 8,
ahead: 2,
behind: 0,
}
const defaultProps = {
worktree: baseWorktree,
label: "feat/inline-delete",
active: false,
pendingDelete: false,
busy: false,
stale: false,
shortcut: 2,
sessions: 1,
grouped: false,
groupStart: false,
groupEnd: false,
groupSize: 0,
renaming: false,
renameValue: "",
closeKeybind: "⌘⇧W",
onClick: noop,
onDelete: noop,
onStartRename: noop,
onRenameInput: noop,
onCommitRename: noop,
onCancelRename: noop,
onRemoveStale: noop,
}
// ---------------------------------------------------------------------------
// WorktreeItem stories
// ---------------------------------------------------------------------------
export const WorktreeItemDefault: Story = {
name: "WorktreeItem — default",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "200px" }}>
<WorktreeItem {...defaultProps} />
</div>
</StoryProviders>
),
}
export const WorktreeItemActive: Story = {
name: "WorktreeItem — active",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "200px" }}>
<WorktreeItem {...defaultProps} active />
</div>
</StoryProviders>
),
}
export const WorktreeItemPendingDelete: Story = {
name: "WorktreeItem — pending delete",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "200px" }}>
<WorktreeItem {...defaultProps} active pendingDelete />
</div>
</StoryProviders>
),
}
export const WorktreeItemBusy: Story = {
name: "WorktreeItem — busy (spinner)",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "200px" }}>
<WorktreeItem {...defaultProps} busy />
</div>
</StoryProviders>
),
}
export const WorktreeItemStale: Story = {
name: "WorktreeItem — stale",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "200px" }}>
<WorktreeItem {...defaultProps} stale />
</div>
</StoryProviders>
),
}
export const WorktreeItemWithStats: Story = {
name: "WorktreeItem — with git stats",
render: () => (
<StoryProviders noPadding>
<div style={{ width: "200px" }}>
<WorktreeItem {...defaultProps} stats={baseStats} />
</div>
</StoryProviders>
),
}
export const WorktreeItemGrouped: Story = {
name: "WorktreeItem — grouped (3 versions)",
render: () => {
const group: WorktreeState[] = [
{ ...baseWorktree, id: "wt-g1", branch: "feat/v1", groupId: "g1" },
{ ...baseWorktree, id: "wt-g2", branch: "feat/v2", groupId: "g1" },
{ ...baseWorktree, id: "wt-g3", branch: "feat/v3", groupId: "g1" },
]
return (
<StoryProviders noPadding>
<div style={{ width: "200px" }}>
<WorktreeItem
{...defaultProps}
worktree={group[0]}
label="feat/v1"
grouped
groupStart
groupEnd={false}
groupSize={3}
shortcut={2}
/>
<WorktreeItem
{...defaultProps}
worktree={group[1]}
label="feat/v2"
grouped
groupStart={false}
groupEnd={false}
groupSize={0}
shortcut={3}
/>
<WorktreeItem
{...defaultProps}
worktree={group[2]}
label="feat/v3"
grouped
groupStart={false}
groupEnd
groupSize={0}
shortcut={4}
/>
</div>
</StoryProviders>
)
},
}
@@ -66,6 +66,8 @@ const WithSessions: ParentComponent<{ sessions?: typeof mockSessions }> = (props
questionErrors: () => new Set<string>(),
selected: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }),
selectModel: noop,
hasModelOverride: () => false,
clearModelOverride: noop,
totalCost: () => 0,
contextUsage: () => undefined,
agents: () => [{ name: "code", description: "Code mode", mode: "primary" as const }],
@@ -23,7 +23,9 @@ const agents = [
{ name: "architect", description: "Plan and design before implementation", mode: "primary" as const },
]
const PromptProviders: ParentComponent<{ variants?: boolean }> = (props) => {
const noop = () => {}
const PromptProviders: ParentComponent<{ variants?: boolean; modelOverride?: boolean }> = (props) => {
const base = mockSessionValue({ status: "idle" })
const session = {
...base,
@@ -31,6 +33,8 @@ const PromptProviders: ParentComponent<{ variants?: boolean }> = (props) => {
selectedAgent: () => "code",
variantList: () => (props.variants ? ["low", "medium", "high"] : []),
currentVariant: () => (props.variants ? ("medium" as string | undefined) : undefined),
hasModelOverride: () => props.modelOverride ?? false,
clearModelOverride: noop,
}
return (
@@ -97,3 +101,25 @@ export const WithThinking200: Story = {
</PromptProviders>
),
}
// ---------------------------------------------------------------------------
// Stories — model override active (reset button visible)
// ---------------------------------------------------------------------------
export const WithModelOverride420: Story = {
name: "With model override — 420px",
render: () => (
<PromptProviders modelOverride>
<PromptInput />
</PromptProviders>
),
}
export const WithModelOverride200: Story = {
name: "With model override — 200px",
render: () => (
<PromptProviders modelOverride>
<PromptInput />
</PromptProviders>
),
}
@@ -1284,6 +1284,40 @@
}
}
/* ============================================
Auth Prompt (sign in / sign up error displays)
============================================ */
[data-component="auth-prompt"] {
display: flex;
flex-direction: column;
gap: 8px;
}
[data-component="auth-prompt"] [data-slot="auth-prompt-header"] {
display: flex;
align-items: center;
gap: 8px;
}
[data-component="auth-prompt"] [data-slot="auth-prompt-icon"] {
font-size: 16px;
line-height: 1;
}
[data-component="auth-prompt"] [data-slot="auth-prompt-title"] {
font-weight: 600;
color: var(--text-strong, var(--vscode-foreground));
font-size: 13px;
}
[data-component="auth-prompt"] [data-slot="auth-prompt-description"] {
color: var(--text-weak, var(--vscode-descriptionForeground));
font-size: 12px;
margin: 0;
line-height: 1.4;
}
/* ============================================
Enhance Prompt Spinner
============================================ */
@@ -257,7 +257,7 @@ export type PermissionLevel = "allow" | "ask" | "deny"
export type PermissionConfig = Partial<Record<string, PermissionLevel>>
export interface AgentConfig {
model?: string
model?: string | null
prompt?: string
temperature?: number
top_p?: number
@@ -309,8 +309,8 @@ export interface ExperimentalConfig {
export interface Config {
permission?: PermissionConfig
model?: string
small_model?: string
model?: string | null
small_model?: string | null
default_agent?: string
agent?: Record<string, AgentConfig>
provider?: Record<string, ProviderConfig>
@@ -626,6 +626,7 @@ export interface AgentManagerSessionMetaMessage {
export interface AgentManagerRepoInfoMessage {
type: "agentManager.repoInfo"
branch: string
defaultBranch?: string
}
// Agent Manager worktree setup progress
@@ -674,6 +675,7 @@ export interface AgentManagerStateMessage {
sessionsCollapsed?: boolean
reviewDiffStyle?: "unified" | "split"
isGitRepo?: boolean
defaultBaseBranch?: string
}
// Resolved keybindings for agent manager actions
@@ -703,6 +705,7 @@ export interface BranchInfo {
isRemote: boolean
isDefault: boolean
lastCommitDate?: string
isCheckedOut?: boolean
}
export interface AgentManagerBranchesMessage {
@@ -1328,6 +1331,7 @@ export interface AgentManagerOpenFileRequest {
sessionId: string
filePath: string
line?: number
column?: number
}
/**
@@ -1449,6 +1453,12 @@ export interface EnhancePromptRequest {
requestId: string
}
// Set default base branch (webview → extension)
export interface SetDefaultBaseBranchRequest {
type: "agentManager.setDefaultBaseBranch"
branch?: string
}
export type WebviewMessage =
| SendMessageRequest
| AbortRequest
@@ -1531,6 +1541,7 @@ export type WebviewMessage =
// legacy-migration end
| ApplyWorktreeDiffMessage
| EnhancePromptRequest
| SetDefaultBaseBranchRequest
// ============================================
// VS Code API type
@@ -0,0 +1,98 @@
import type { AssistantMessage } from "@kilocode/sdk/v2"
export function unwrapError(message: string): string {
const text = message.replace(/^Error:\s*/, "").trim()
const tryParse = (v: string) => {
try {
return JSON.parse(v) as unknown
} catch {
return undefined
}
}
const read = (v: string) => {
const first = tryParse(v)
if (typeof first !== "string") return first
return tryParse(first.trim())
}
let json = read(text)
if (json === undefined) {
const start = text.indexOf("{")
const end = text.lastIndexOf("}")
if (start !== -1 && end > start) json = read(text.slice(start, end + 1))
}
if (!json || typeof json !== "object" || Array.isArray(json)) return message
const rec = json as Record<string, unknown>
const err =
rec.error && typeof rec.error === "object" && !Array.isArray(rec.error)
? (rec.error as Record<string, unknown>)
: undefined
if (err) {
const type = typeof err.type === "string" ? err.type : undefined
const msg = typeof err.message === "string" ? err.message : undefined
if (type && msg) return `${type}: ${msg}`
if (msg) return msg
if (type) return type
const code = typeof err.code === "string" ? err.code : undefined
if (code) return code
}
const msg = typeof rec.message === "string" ? rec.message : undefined
if (msg) return msg
const reason = typeof rec.error === "string" ? rec.error : undefined
if (reason) return reason
return message
}
export const errorCodes = {
PAID_MODEL_AUTH_REQUIRED: "PAID_MODEL_AUTH_REQUIRED",
PROMOTION_MODEL_LIMIT_REACHED: "PROMOTION_MODEL_LIMIT_REACHED",
} as const
export interface ParsedError {
statusCode?: number
code?: string
message?: string
}
export function parseAssistantError(error: AssistantMessage["error"] | null | undefined): ParsedError | null {
if (!error) return null
if (error.name !== "APIError") return null
const data = error.data
if (!data) return null
const statusCode = typeof data.statusCode === "number" ? data.statusCode : undefined
const message = typeof data.message === "string" ? data.message : undefined
let code: string | undefined
if (typeof data.responseBody === "string") {
try {
const body = JSON.parse(data.responseBody) as Record<string, unknown>
const bodyError = body.error as Record<string, unknown> | undefined
if (bodyError && typeof bodyError.code === "string") {
code = bodyError.code
} else if (typeof body.code === "string") {
code = body.code
}
} catch {
// responseBody is not valid JSON — ignore
}
}
return { statusCode, code, message }
}
export function isUnauthorizedPaidModelError(parsed: ParsedError | null): boolean {
if (!parsed) return false
return parsed.statusCode === 401 && parsed.code === errorCodes.PAID_MODEL_AUTH_REQUIRED
}
/**
* Accepts both 401 (current backend) and 429 (future backend) to support
* the transition. Keep 401 until the backend is updated to return 429.
*/
export function isUnauthorizedPromotionLimitError(parsed: ParsedError | null): boolean {
if (!parsed) return false
return (
(parsed.statusCode === 401 || parsed.statusCode === 429) && parsed.code === errorCodes.PROMOTION_MODEL_LIMIT_REACHED
)
}
+25 -6
View File
@@ -774,7 +774,7 @@ export namespace Config {
export const Agent = z
.object({
model: ModelId.optional(),
model: ModelId.nullable().optional(), // kilocode_change - nullable for delete sentinel
variant: z
.string()
.optional()
@@ -1129,10 +1129,12 @@ export namespace Config {
.array(z.string())
.optional()
.describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
small_model: ModelId.describe(
// kilocode_change start - nullable for delete sentinel
model: ModelId.nullable().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
small_model: ModelId.nullable().describe(
"Small model to use for tasks like title generation in the format of provider/model",
).optional(),
// kilocode_change end
// kilocode_change start - renamed from "build" to "code"
default_agent: z
.string()
@@ -1407,7 +1409,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, mergeDeep(existing, config))
await Filesystem.writeJson(filepath, stripNulls(mergeDeep(existing, config) as Record<string, unknown>)) // kilocode_change - strip null delete sentinels
await Instance.dispose()
}
@@ -1427,9 +1429,26 @@ export namespace Config {
return !!value && typeof value === "object" && !Array.isArray(value)
}
// kilocode_change start - strip null delete sentinels after merge
/** Recursively remove keys whose value is null (used after mergeDeep to honor delete sentinels). */
function stripNulls(obj: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
if (value === null) continue
if (isRecord(value)) {
result[key] = stripNulls(value)
} else {
result[key] = value
}
}
return result
}
// kilocode_change end
function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
if (!isRecord(patch)) {
const edits = modify(input, path, patch, {
// kilocode_change - null means "delete this key" — pass undefined to jsonc-parser's modify()
const edits = modify(input, path, patch === null ? undefined : patch, {
formattingOptions: {
insertSpaces: true,
tabSize: 2,
@@ -1488,7 +1507,7 @@ export namespace Config {
const next = await (async () => {
if (!filepath.endsWith(".jsonc")) {
const existing = parseConfig(before, filepath)
const merged = mergeDeep(existing, config)
const merged = stripNulls(mergeDeep(existing, config) as Record<string, unknown>) as Info // kilocode_change - strip null delete sentinels
await Filesystem.writeJson(filepath, merged)
return merged
}
-20
View File
@@ -1,20 +0,0 @@
// kilocode_change - new file
/**
* Debounced Instance.disposeAll() helper.
*
* Bulk operations like legacy migration call client.auth.set() for each provider
* in rapid succession. Calling Instance.disposeAll() immediately on every auth
* change causes repeated disposal/recreation cycles. This module coalesces all
* dispose requests within a 300ms window into a single disposal call.
*/
import { Instance } from "../project/instance"
let timer: ReturnType<typeof setTimeout> | undefined
export function scheduleDisposeAll() {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = undefined
void Instance.disposeAll().catch(() => undefined)
}, 300)
}
-3
View File
@@ -8,7 +8,6 @@ import { NamedError } from "@opencode-ai/util/error"
import { Auth } from "@/auth"
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
import { ModelCache } from "./model-cache" // kilocode_change
import { scheduleDisposeAll } from "../kilocode/dispose" // kilocode_change
export namespace ProviderAuth {
const state = Instance.state(async () => {
@@ -125,7 +124,6 @@ export namespace ProviderAuth {
// kilocode_change start - invalidate provider/model cache after auth change
ModelCache.clear(input.providerID)
scheduleDisposeAll()
// kilocode_change end
return
@@ -147,7 +145,6 @@ export namespace ProviderAuth {
})
// kilocode_change start - invalidate provider/model cache after auth change
ModelCache.clear(input.providerID)
scheduleDisposeAll()
// kilocode_change end
},
)
-3
View File
@@ -19,7 +19,6 @@ import { Agent } from "../agent/agent"
import { Skill } from "../skill/skill"
import { Auth } from "../auth"
import { ModelCache } from "../provider/model-cache" // kilocode_change
import { scheduleDisposeAll } from "../kilocode/dispose" // kilocode_change
import { Flag } from "../flag/flag"
import { Command } from "../command"
import { Global } from "../global"
@@ -171,7 +170,6 @@ export namespace Server {
await Auth.set(providerID, info)
// kilocode_change start - invalidate provider/model cache after auth change
ModelCache.clear(providerID)
scheduleDisposeAll()
// kilocode_change end
return c.json(true)
},
@@ -205,7 +203,6 @@ export namespace Server {
await Auth.remove(providerID)
// kilocode_change start - invalidate provider/model cache after auth removal
ModelCache.clear(providerID)
scheduleDisposeAll()
// kilocode_change end
return c.json(true)
},
+1 -1
View File
@@ -2,7 +2,7 @@ import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./batch.txt"
const DISALLOWED = new Set(["batch"])
const DISALLOWED = new Set(["batch", "plan_exit"]) // kilocode_change - plan_exit is agent-gated, not registry-gated
const FILTERED_FROM_SUGGESTIONS = new Set(["invalid", "patch", ...DISALLOWED])
export const BatchTool = Tool.define("batch", async () => {
+1 -1
View File
@@ -119,7 +119,7 @@ export namespace ToolRegistry {
ApplyPatchTool,
...(Flag.KILO_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
...(Flag.KILO_EXPERIMENTAL_PLAN_MODE && Flag.KILO_CLIENT === "cli" ? [PlanExitTool] : []),
PlanExitTool, // kilocode_change - always registered; gated by agent permission instead
...custom,
]
}
+5 -3
View File
@@ -1083,7 +1083,7 @@ export type PermissionConfig =
| PermissionActionConfig
export type AgentConfig = {
model?: string
model?: string | null
/**
* Default model variant for this agent (applies only when using the agent's configured model).
*/
@@ -1126,6 +1126,8 @@ export type AgentConfig = {
[key: string]:
| unknown
| string
| null
| string
| number
| {
[key: string]: boolean
@@ -1375,11 +1377,11 @@ export type Config = {
/**
* Model to use in the format of provider/model, eg anthropic/claude-2
*/
model?: string
model?: string | null
/**
* Small model to use for tasks like title generation in the format of provider/model
*/
small_model?: string
small_model?: string | null
/**
* Default agent to use when none is specified. Must be a primary agent. Falls back to 'code' if not set or if the specified agent is invalid.
*/
+66 -53
View File
@@ -43,6 +43,7 @@ import { Checkbox } from "./checkbox"
import { DiffChanges } from "./diff-changes"
import { Markdown } from "./markdown"
import { ImagePreview } from "./image-preview"
import { extractFilePathFromHref } from "../file-path" // kilocode_change
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { Tooltip } from "./tooltip"
@@ -976,53 +977,51 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
<Show when={!hideQuestion()}>
<Show when={dismissed()}>
<div style="width: 100%; display: flex; justify-content: flex-end; padding: 4px 8px 4px 0;">
<span class="text-13-regular text-text-weak cursor-default">
{i18n.t("ui.tool.questions")} dismissed
</span>
<span class="text-13-regular text-text-weak cursor-default">{i18n.t("ui.tool.questions")} dismissed</span>
</div>
</Show>
<Show when={!dismissed()}>
<div data-component="tool-part-wrapper">
<Switch>
<Match when={part.state.status === "error" && part.state.error}>
{(error) => {
const cleaned = error().replace("Error: ", "")
const [title, ...rest] = cleaned.split(": ")
return (
<Card variant="error">
<div data-component="tool-error">
<Icon name="circle-ban-sign" size="small" />
<Switch>
<Match when={title && title.length < 30}>
<div data-slot="message-part-tool-error-content">
<div data-slot="message-part-tool-error-title">{title}</div>
<span data-slot="message-part-tool-error-message">{rest.join(": ")}</span>
</div>
</Match>
<Match when={true}>
<span data-slot="message-part-tool-error-message">{cleaned}</span>
</Match>
</Switch>
</div>
</Card>
)
}}
</Match>
<Match when={true}>
<Dynamic
component={render}
input={input()}
tool={part.tool}
metadata={partMetadata()}
// @ts-expect-error
output={part.state.output}
status={part.state.status}
hideDetails={props.hideDetails}
defaultOpen={props.defaultOpen}
/>
</Match>
</Switch>
</div>
<div data-component="tool-part-wrapper">
<Switch>
<Match when={part.state.status === "error" && part.state.error}>
{(error) => {
const cleaned = error().replace("Error: ", "")
const [title, ...rest] = cleaned.split(": ")
return (
<Card variant="error">
<div data-component="tool-error">
<Icon name="circle-ban-sign" size="small" />
<Switch>
<Match when={title && title.length < 30}>
<div data-slot="message-part-tool-error-content">
<div data-slot="message-part-tool-error-title">{title}</div>
<span data-slot="message-part-tool-error-message">{rest.join(": ")}</span>
</div>
</Match>
<Match when={true}>
<span data-slot="message-part-tool-error-message">{cleaned}</span>
</Match>
</Switch>
</div>
</Card>
)
}}
</Match>
<Match when={true}>
<Dynamic
component={render}
input={input()}
tool={part.tool}
metadata={partMetadata()}
// @ts-expect-error
output={part.state.output}
status={part.state.status}
hideDetails={props.hideDetails}
defaultOpen={props.defaultOpen}
/>
</Match>
</Switch>
</div>
</Show>
</Show>
)
@@ -1103,15 +1102,29 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
if (!data.openFile) return
const target = e.target
if (!(target instanceof HTMLElement)) return
const link = target.closest(".file-link[data-file-path]")
if (!link) return
const path = link.getAttribute("data-file-path")
if (!path) return
const lineAttr = link.getAttribute("data-file-line")
const colAttr = link.getAttribute("data-file-col")
const line = lineAttr ? parseInt(lineAttr, 10) : undefined
const column = colAttr ? parseInt(colAttr, 10) : undefined
data.openFile(path, line, column)
// Handle .file-link code spans (e.g. `src/foo.ts:42`)
const fileLink = target.closest(".file-link[data-file-path]")
if (fileLink) {
const path = fileLink.getAttribute("data-file-path")
if (!path) return
const lineAttr = fileLink.getAttribute("data-file-line")
const colAttr = fileLink.getAttribute("data-file-col")
const line = lineAttr ? parseInt(lineAttr, 10) : undefined
const column = colAttr ? parseInt(colAttr, 10) : undefined
data.openFile(path, line, column)
return
}
// Handle markdown links whose href looks like a relative file path
// (e.g. [AGENTS.md](AGENTS.md) or [src/foo.ts](src/foo.ts))
const anchor = target.closest("a.external-link") as HTMLAnchorElement | null
if (anchor) {
const href = anchor.getAttribute("href")
if (!href) return
const filePath = extractFilePathFromHref(href)
if (!filePath) return
e.preventDefault()
data.openFile(filePath)
}
}
// kilocode_change end
+2 -19
View File
@@ -3,6 +3,7 @@ import markedKatex from "marked-katex-extension"
import markedShiki from "marked-shiki"
import katex from "katex"
import { bundledLanguages, type BundledLanguage } from "shiki"
import { parseFilePath } from "../file-path" // kilocode_change
import { createSimpleContext } from "./helper"
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
@@ -461,25 +462,7 @@ async function highlightCodeBlocks(html: string): Promise<string> {
export type NativeMarkdownParser = (markdown: string) => Promise<string>
// kilocode_change start
// Matches text that looks like a file path: contains "/" and ends with a file extension,
// or starts with "./" or "../" or "/". Supports optional :line or :line:col suffix.
const FILE_PATH_RE =
/^((?:\/|\.\.?\/)?(?:[a-zA-Z0-9_@-][a-zA-Z0-9_@./-]*\/)*[a-zA-Z0-9_@.-]+\.[a-zA-Z0-9]+)(?::(\d+)(?::(\d+))?)?$/
function parseFilePath(text: string): { path: string; line?: number; column?: number } | undefined {
if (text.includes("://")) return undefined
if (text.includes(" ")) return undefined
const match = FILE_PATH_RE.exec(text)
if (!match) return undefined
if (!match[1].includes("/")) return undefined
return {
path: match[1],
line: match[2] ? parseInt(match[2], 10) : undefined,
column: match[3] ? parseInt(match[3], 10) : undefined,
}
}
// kilocode_change end
// kilocode_change: parseFilePath imported from ../file-path
export const { use: useMarked, provider: MarkedProvider } = createSimpleContext({
name: "Marked",
+237
View File
@@ -0,0 +1,237 @@
import { describe, expect, it } from "bun:test"
import { parseFilePath, extractFilePathFromHref } from "./file-path"
describe("parseFilePath", () => {
describe("Unix paths", () => {
it("bare filename", () => {
expect(parseFilePath("foo.ts")).toEqual({ path: "foo.ts", line: undefined, column: undefined })
})
it("relative path", () => {
expect(parseFilePath("src/index.ts")).toEqual({ path: "src/index.ts", line: undefined, column: undefined })
})
it("dot-relative path", () => {
expect(parseFilePath("./src/foo.ts")).toEqual({ path: "./src/foo.ts", line: undefined, column: undefined })
})
it("parent-relative path", () => {
expect(parseFilePath("../lib/bar.ts")).toEqual({ path: "../lib/bar.ts", line: undefined, column: undefined })
})
it("absolute path", () => {
expect(parseFilePath("/Users/dev/project/main.ts")).toEqual({
path: "/Users/dev/project/main.ts",
line: undefined,
column: undefined,
})
})
it("with line number", () => {
expect(parseFilePath("src/foo.ts:42")).toEqual({ path: "src/foo.ts", line: 42, column: undefined })
})
it("with line and column", () => {
expect(parseFilePath("src/foo.ts:42:10")).toEqual({ path: "src/foo.ts", line: 42, column: 10 })
})
it("deeply nested path", () => {
expect(parseFilePath("packages/ui/src/context/marked.tsx")).toEqual({
path: "packages/ui/src/context/marked.tsx",
line: undefined,
column: undefined,
})
})
it("path with @ scope", () => {
expect(parseFilePath("@scope/pkg/index.js")).toEqual({
path: "@scope/pkg/index.js",
line: undefined,
column: undefined,
})
})
it("dotfile with path", () => {
expect(parseFilePath("src/.eslintrc.json")).toEqual({
path: "src/.eslintrc.json",
line: undefined,
column: undefined,
})
})
})
describe("Windows paths", () => {
it("drive letter with backslash", () => {
expect(parseFilePath("C:\\Users\\dev\\file.ts")).toEqual({
path: "C:\\Users\\dev\\file.ts",
line: undefined,
column: undefined,
})
})
it("drive letter with forward slash", () => {
expect(parseFilePath("C:/Users/dev/file.ts")).toEqual({
path: "C:/Users/dev/file.ts",
line: undefined,
column: undefined,
})
})
it("lowercase drive", () => {
expect(parseFilePath("d:\\projects\\app.tsx")).toEqual({
path: "d:\\projects\\app.tsx",
line: undefined,
column: undefined,
})
})
it("UNC path", () => {
expect(parseFilePath("\\\\server\\share\\file.ts")).toEqual({
path: "\\\\server\\share\\file.ts",
line: undefined,
column: undefined,
})
})
it("Windows path with line number", () => {
expect(parseFilePath("C:\\src\\file.ts:12")).toEqual({ path: "C:\\src\\file.ts", line: 12, column: undefined })
})
it("Windows path with line and column", () => {
expect(parseFilePath("C:\\src\\file.ts:12:5")).toEqual({ path: "C:\\src\\file.ts", line: 12, column: 5 })
})
})
describe("rejects non-paths", () => {
it("URL with protocol", () => {
expect(parseFilePath("https://example.com/path.html")).toBeUndefined()
})
it("text with spaces", () => {
expect(parseFilePath("not a path.ts")).toBeUndefined()
})
it("bare word without extension", () => {
expect(parseFilePath("README")).toBeUndefined()
})
it("empty string", () => {
expect(parseFilePath("")).toBeUndefined()
})
it("file:// URL", () => {
expect(parseFilePath("file:///foo/bar.ts")).toBeUndefined()
})
it("just a number", () => {
expect(parseFilePath("42")).toBeUndefined()
})
it("path without extension", () => {
expect(parseFilePath("src/Makefile")).toBeUndefined()
})
})
})
describe("extractFilePathFromHref", () => {
describe("accepts file-like hrefs", () => {
it("bare filename", () => {
expect(extractFilePathFromHref("AGENTS.md")).toBe("AGENTS.md")
})
it("relative path", () => {
expect(extractFilePathFromHref("src/foo.ts")).toBe("src/foo.ts")
})
it("dot-relative path", () => {
expect(extractFilePathFromHref("./README.md")).toBe("./README.md")
})
it("parent-relative path", () => {
expect(extractFilePathFromHref("../docs/guide.md")).toBe("../docs/guide.md")
})
it("path with multiple extensions", () => {
expect(extractFilePathFromHref("config.test.ts")).toBe("config.test.ts")
})
})
describe("strips fragments and queries", () => {
it("strips #fragment", () => {
expect(extractFilePathFromHref("AGENTS.md#worktrees")).toBe("AGENTS.md")
})
it("strips ?query", () => {
expect(extractFilePathFromHref("README.md?plain=1")).toBe("README.md")
})
it("strips both fragment and query", () => {
expect(extractFilePathFromHref("docs/guide.md?v=2#section")).toBe("docs/guide.md")
})
it("strips fragment from path with directory", () => {
expect(extractFilePathFromHref("src/foo.ts#L42")).toBe("src/foo.ts")
})
})
describe("rejects URLs and schemes", () => {
it("https URL", () => {
expect(extractFilePathFromHref("https://example.com/path.html")).toBeUndefined()
})
it("http URL", () => {
expect(extractFilePathFromHref("http://localhost:3000/index.html")).toBeUndefined()
})
it("mailto scheme", () => {
expect(extractFilePathFromHref("mailto:user@example.com")).toBeUndefined()
})
it("tel scheme", () => {
expect(extractFilePathFromHref("tel:+1234567890")).toBeUndefined()
})
it("javascript scheme", () => {
expect(extractFilePathFromHref("javascript:void(0)")).toBeUndefined()
})
it("file:// URL", () => {
expect(extractFilePathFromHref("file:///foo/bar.ts")).toBeUndefined()
})
it("ftp URL", () => {
expect(extractFilePathFromHref("ftp://server/file.txt")).toBeUndefined()
})
it("custom scheme", () => {
expect(extractFilePathFromHref("vscode://extension/id")).toBeUndefined()
})
})
describe("rejects anchors and non-file values", () => {
it("pure anchor", () => {
expect(extractFilePathFromHref("#section")).toBeUndefined()
})
it("anchor with nested path", () => {
expect(extractFilePathFromHref("#/some/path")).toBeUndefined()
})
it("empty string", () => {
expect(extractFilePathFromHref("")).toBeUndefined()
})
it("no extension (bare word)", () => {
expect(extractFilePathFromHref("README")).toBeUndefined()
})
it("directory-only path (no extension)", () => {
expect(extractFilePathFromHref("src/components/")).toBeUndefined()
})
it("fragment-only after stripping resolves to empty", () => {
// href is "#foo" — starts with # so rejected
expect(extractFilePathFromHref("#foo.md")).toBeUndefined()
})
})
})
+51
View File
@@ -0,0 +1,51 @@
// kilocode_change - new file
// Matches text that looks like a file path:
// - Unix: /foo/bar.ts, ./foo.ts, ../foo.ts, foo.ts
// - Windows drive: C:\foo\bar.ts, C:/foo/bar.ts
// - Windows UNC: \\server\share\file.ts
// Supports optional :line or :line:col suffix.
const FILE_PATH_UNIX_RE =
/^((?:\/|\.\.?\/)?(?:[a-zA-Z0-9_@-][a-zA-Z0-9_@./-]*\/)*[a-zA-Z0-9_@.-]+\.[a-zA-Z0-9]+)(?::(\d+)(?::(\d+))?)?$/
const FILE_PATH_WIN_RE = /^((?:[a-zA-Z]:[/\\]|\\\\)(?:[^\\/]+[/\\])*[^\\/]+\.[a-zA-Z0-9]+)(?::(\d+)(?::(\d+))?)?$/
/**
* Parse an inline code span into a file path with optional line/column.
* Returns undefined when the text does not look like a file reference.
*
* Handles Unix paths (`/foo/bar.ts`, `./foo.ts`, `foo.ts`),
* Windows drive paths (`C:\foo\bar.ts`), and UNC paths (`\\server\share\file.ts`).
*/
export function parseFilePath(text: string): { path: string; line?: number; column?: number } | undefined {
if (text.includes("://")) return undefined
if (text.includes(" ")) return undefined
const match = FILE_PATH_UNIX_RE.exec(text) ?? FILE_PATH_WIN_RE.exec(text)
if (!match) return undefined
return {
path: match[1],
line: match[2] ? parseInt(match[2], 10) : undefined,
column: match[3] ? parseInt(match[3], 10) : undefined,
}
}
const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/
/**
* Extract a file path from a markdown link href, or return undefined
* when the href is a URL, anchor, scheme, or otherwise not a file reference.
*
* Strips `#fragment` and `?query` suffixes before returning the path.
*/
export function extractFilePathFromHref(href: string): string | undefined {
if (!href) return undefined
// Skip actual URLs and non-file schemes (mailto:, tel:, etc.)
if (href.includes("://") || SCHEME_RE.test(href)) return undefined
// Skip pure anchors
if (href.startsWith("#")) return undefined
// Strip fragment and query before treating as file path
const cleaned = href.replace(/[#?].*$/, "")
if (!cleaned) return undefined
// Must look like a file path (has a dot for extension)
if (!cleaned.includes(".")) return undefined
return cleaned
}