Merge branch 'main' into imanolmaiztegui/kilo-opencode-v1.15.13

This commit is contained in:
Imanol Maiztegui
2026-06-19 10:50:34 +02:00
97 changed files with 2825 additions and 1250 deletions
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
---
Deny provider data collection for Kilo Gateway requests when prompt-training models are hidden.
+117 -810
View File
File diff suppressed because it is too large Load Diff
+9 -18
View File
@@ -7,7 +7,6 @@
"packageManager": "bun@1.3.14",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"typecheck": "bun turbo typecheck",
@@ -15,7 +14,7 @@
"postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts",
"prepare": "husky",
"random": "echo 'Random script'",
"sso": "aws sso login --sso-session=opencode --no-browser",
"hello": "echo 'Hello World!'",
"test": "echo 'do not run tests from root' && exit 1",
"extension": "bun --cwd packages/kilo-vscode script/launch.ts",
"dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup"
@@ -32,8 +31,8 @@
"@types/bun": "1.3.14",
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@opentui/core": "0.2.16",
"@opentui/solid": "0.2.16",
"@opentui/core": "0.2.15",
"@opentui/solid": "0.2.15",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
@@ -77,11 +76,9 @@
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"solid-js": "1.9.12",
"vite-plugin-solid": "2.11.10",
"@lydell/node-pty": "1.2.0-beta.12",
"@opentui/keymap": "0.2.16",
"@effect/sql-sqlite-bun": "4.0.0-beta.66",
"@hono/zod-validator": "0.4.2",
"sst": "4.13.1"
"@lydell/node-pty": "1.2.0-beta.10",
"@opentui/keymap": "0.2.15",
"@effect/sql-sqlite-bun": "4.0.0-beta.66"
}
},
"devDependencies": {
@@ -97,17 +94,13 @@
"turbo": "2.9.14",
"@types/bun": "catalog:",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.27.10",
"@actions/artifact": "5.0.1",
"sst": "catalog:"
"@changesets/cli": "^2.27.10"
},
"dependencies": {
"@kilocode/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@kilocode/sdk": "workspace:*",
"typescript": "catalog:",
"@aws-sdk/client-s3": "3.933.0",
"heap-snapshot-toolkit": "1.1.3"
"typescript": "catalog:"
},
"repository": {
"type": "git",
@@ -153,10 +146,8 @@
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
},
"version": "7.3.46",
"version": "7.3.49",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.46",
"version": "7.3.49",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.46",
"version": "7.3.49",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.3.48"
version = "7.3.49"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.46",
"version": "7.3.49",
"name": "@opencode-ai/http-recorder",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-console",
"version": "7.3.46",
"version": "7.3.49",
"private": true,
"type": "module",
"scripts": {
+44
View File
@@ -11,6 +11,7 @@ import type {
GlobalHealthResponse,
GlobalEvent,
KiloEmbeddingModelCatalog,
KiloProfileResponse,
LspStatusResponse,
McpStatusResponse,
Pty as PtyInfo,
@@ -42,6 +43,8 @@ export type Query = {
export type ProjectQuery = Pick<Query, "url" | "dir">
export type KiloProfileData = KiloProfileResponse
export type ProjectItem = KiloProject
export type RecentProjectItem = ProjectItem & {
sessions: number
@@ -210,6 +213,11 @@ function model(input: unknown) {
return { provider: input.slice(0, index), model: input.slice(index + 1) }
}
function directory(input: ProjectQuery) {
const dir = value(input.dir)
return dir ? { directory: dir } : undefined
}
function title(input: string) {
return input
.split("_")
@@ -407,6 +415,42 @@ export async function loadEmbeddingModels(input: Query): Promise<KiloEmbeddingMo
return demand("Kilo embedding models", await client(input).indexing.models())
}
export async function loadKiloProfile(input: ProjectQuery): Promise<KiloProfileData> {
const sdk = client(input)
const result = await sdk.kilo.profile(directory(input))
return demand("Kilo profile", result)
}
export async function setKiloOrganization(input: ProjectQuery, organizationId: string | null) {
const sdk = client(input)
const result = await sdk.kilo.organization.set({ ...directory(input), organizationId })
demand("Switch Kilo account", result)
await sdk.global.dispose()
}
export async function logoutKilo(input: ProjectQuery) {
const sdk = client(input)
const result = await sdk.auth.remove({ providerID: "kilo" })
demand("Log out of Kilo", result)
await sdk.global.dispose()
}
export async function startKiloLogin(input: ProjectQuery): Promise<ProviderAuthAuthorization> {
const sdk = client(input)
const result = await sdk.provider.oauth.authorize({ ...directory(input), providerID: "kilo", method: 0 })
return demand("Start Kilo login", result)
}
export async function completeKiloLogin(input: ProjectQuery, signal?: AbortSignal) {
const sdk = client(input)
const result = await sdk.provider.oauth.callback(
{ ...directory(input), providerID: "kilo", method: 0 },
signal ? { signal } : undefined,
)
demand("Complete Kilo login", result)
await sdk.global.dispose()
}
export async function loadProjects(input: ProjectQuery): Promise<ProjectItem[]> {
const sdk = client(input)
const dir = value(input.dir)
+2
View File
@@ -6,6 +6,7 @@ import "./styles.css"
import { ProjectConsoleRoute } from "./routes/projects/ProjectConsoleRoute"
import { ProjectsRoute } from "./routes/projects/ProjectsRoute"
import { ProfileRoute } from "./routes/profile/ProfileRoute"
import { LoginRoute } from "./routes/profile/LoginRoute"
import { ConfigLayout } from "./layouts/ConfigLayout"
import { configSections } from "./routes/config/sections"
@@ -27,6 +28,7 @@ render(
{routes()}
</Route>
<Route path="/profile" component={ProfileRoute} />
<Route path="/kilo/login" component={LoginRoute} />
<Route path="/settings" component={ConfigLayout}>
{routes()}
</Route>
@@ -0,0 +1,244 @@
import { useLocation, useNavigate } from "@solidjs/router"
import { Button } from "@kilocode/kilo-web-ui/button"
import { Card } from "@kilocode/kilo-web-ui/card"
import { createEffect, createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
import { LoadingScreen } from "../../components/LoadingScreen"
import { completeKiloLogin, loadKiloProfile, startKiloLogin, type ProjectQuery } from "../../client"
import { errMsg } from "../../shared/utils"
import { markDisconnected, page, parseDeviceCode, safeReturn } from "./profile-utils"
import { useProfileServer } from "./server"
type Status = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled"
type State = {
status: Status
code?: string
url?: string
expiresIn?: number
error?: string
}
function time(input: number) {
const min = Math.floor(input / 60)
const sec = input % 60
return `${min}:${sec.toString().padStart(2, "0")}`
}
export function LoginRoute() {
const loc = useLocation()
const nav = useNavigate()
const params = createMemo(() => new URLSearchParams(loc.search))
const server = useProfileServer(params)
const [auth, setAuth] = createSignal<State>({ status: "idle" })
const [left, setLeft] = createSignal(900)
const [attempt, setAttempt] = createSignal(0)
const [active, setActive] = createSignal<AbortController>()
const [copied, setCopied] = createSignal("")
const ret = () => safeReturn(params().get("return"))
const profile = () => page(params(), "/profile")
function abort() {
active()?.abort()
setActive(undefined)
}
function done() {
window.setTimeout(() => nav(ret(), { replace: true }), 650)
}
function start(input: ProjectQuery | undefined = server.query()) {
if (!input) return
abort()
const ctl = new AbortController()
const rev = attempt() + 1
setAttempt(rev)
setActive(ctl)
setAuth({ status: "initiating" })
void startKiloLogin(input)
.then((info) => {
if (attempt() !== rev) return false
setAuth({
status: "pending",
code: parseDeviceCode(info.instructions),
url: info.url,
expiresIn: 900,
})
return completeKiloLogin(input, ctl.signal).then(() => true)
})
.then((ok) => {
if (!ok || attempt() !== rev) return
setAuth({ status: "success" })
void loadKiloProfile(input)
.then(() => markDisconnected(false))
.catch(() => markDisconnected(false))
.finally(done)
})
.catch((err) => {
if (attempt() !== rev) return
setAuth({ status: "error", error: errMsg(err) })
})
.finally(() => {
if (active() === ctl) setActive(undefined)
})
}
function cancel() {
abort()
setAttempt((value) => value + 1)
setAuth({ status: "cancelled" })
}
function copy(label: string, value: string | undefined) {
if (!value) return
void navigator.clipboard
.writeText(value)
.then(() => {
setCopied(label)
window.setTimeout(() => {
if (copied() === label) setCopied("")
}, 1400)
})
.catch((err) => setAuth({ status: "error", error: errMsg(err) }))
}
function open() {
const url = auth().url
if (!url) return
window.open(url, "_blank", "noopener,noreferrer")
}
createEffect(() => {
const current = server.query()
if (!current || auth().status !== "idle") return
start(current)
})
createEffect(() => {
const state = auth()
if (state.status !== "pending") return
setLeft(state.expiresIn ?? 900)
const timer = window.setInterval(() => setLeft((value) => Math.max(0, value - 1)), 1000)
onCleanup(() => window.clearInterval(timer))
})
createEffect(() => {
if (auth().status === "success") server.remember()
})
onCleanup(() => {
abort()
setAttempt((value) => value + 1)
})
return (
<section class="route-empty">
<div class="profile-login-page">
<header class="profile-login-header">
<p class="eyebrow">Kilo Login</p>
<h1>Login to Kilo</h1>
<p>Authorize this Kilo Console through the same device auth flow used by the editor clients.</p>
</header>
<Show when={!server.query() && server.discoverable()}>
<LoadingScreen variant="fullscreen" />
</Show>
<Show when={!server.query() && !server.discoverable()}>
<Card class="profile-login-card" variant="warning">
<strong>Kilo server not found</strong>
<p>Start a local Kilo server or pass a server URL with ?server=.</p>
<a class="profile-link-button" href={profile()}>
Back to Profile
</a>
</Card>
</Show>
<Show when={server.query()}>
<Card class="profile-login-card">
<Switch>
<Match when={auth().status === "idle" || auth().status === "initiating"}>
<div class="profile-login-state">
<span class="profile-spinner" aria-hidden="true" />
<strong>Starting login...</strong>
<p>Preparing a secure browser authorization request.</p>
</div>
</Match>
<Match when={auth().status === "pending"}>
<div class="profile-login-flow">
<div class="profile-login-step">
<span>Step 1</span>
<strong>Open this URL</strong>
<div class="profile-login-url">
<span>{auth().url}</span>
<Button variant="secondary" type="button" onClick={() => copy("url", auth().url)}>
{copied() === "url" ? "Copied" : "Copy"}
</Button>
</div>
<Button variant="primary" type="button" onClick={open}>
Open Browser
</Button>
</div>
<Show when={auth().code}>
{(code) => (
<div class="profile-login-step">
<span>Step 2</span>
<strong>Enter this code</strong>
<button type="button" class="profile-login-code" onClick={() => copy("code", code())}>
{code()}
<small>{copied() === "code" ? "Copied" : "Click to copy"}</small>
</button>
</div>
)}
</Show>
<div class="profile-login-waiting">
<span class="profile-spinner" aria-hidden="true" />
<span>Waiting for authorization ({time(left())})</span>
</div>
<Button variant="ghost" type="button" onClick={cancel}>
Cancel
</Button>
</div>
</Match>
<Match when={auth().status === "success"}>
<div class="profile-login-state success">
<strong>Login successful</strong>
<p>Redirecting back to your profile.</p>
</div>
</Match>
<Match when={auth().status === "error"}>
<div class="profile-login-state error">
<strong>Login failed</strong>
<p>{auth().error}</p>
<div class="profile-login-actions">
<Button variant="primary" type="button" onClick={() => start()}>
Try Again
</Button>
<a class="profile-link-button" href={profile()}>
Back to Profile
</a>
</div>
</div>
</Match>
<Match when={auth().status === "cancelled"}>
<div class="profile-login-state">
<strong>Login cancelled</strong>
<p>No credentials were saved.</p>
<Button variant="primary" type="button" onClick={() => start()}>
Start Again
</Button>
</div>
</Match>
</Switch>
</Card>
</Show>
</div>
</section>
)
}
@@ -1,9 +1,315 @@
import { A, useLocation, useNavigate } from "@solidjs/router"
import { Button } from "@kilocode/kilo-web-ui/button"
import { Card } from "@kilocode/kilo-web-ui/card"
import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js"
import { LoadingScreen } from "../../components/LoadingScreen"
import { loadKiloProfile, logoutKilo, setKiloOrganization, type KiloProfileData, type ProjectQuery } from "../../client"
import { errMsg } from "../../shared/utils"
import {
authError,
cloud,
credits,
initials,
markDisconnected,
money,
page,
personal,
usage,
wasDisconnected,
} from "./profile-utils"
import { useProfileServer } from "./server"
type Org = NonNullable<KiloProfileData["profile"]["organizations"]>[number]
type State = { kind: "connected"; data: KiloProfileData } | { kind: "disconnected" }
async function load(input: ProjectQuery): Promise<State> {
if (wasDisconnected()) return { kind: "disconnected" }
try {
const data = await loadKiloProfile(input)
markDisconnected(false)
return { kind: "connected", data }
} catch (err) {
if (authError(err)) {
markDisconnected(true)
return { kind: "disconnected" }
}
throw err
}
}
function org(data: KiloProfileData | undefined) {
if (!data?.currentOrgId) return undefined
return data.profile.organizations?.find((item) => item.id === data.currentOrgId)
}
function account(data: KiloProfileData | undefined) {
return org(data)?.name ?? "Personal Account"
}
function role(input: Org) {
if (input.role === "owner") return "Owner"
if (input.role === "admin") return "Admin"
if (input.role === "billing_manager") return "Billing"
return "Member"
}
export function ProfileRoute() {
const loc = useLocation()
const nav = useNavigate()
const params = createMemo(() => new URLSearchParams(loc.search))
const server = useProfileServer(params)
const [data, actions] = createResource(server.query, load)
const [saving, setSaving] = createSignal<string>()
const [error, setError] = createSignal("")
const profile = createMemo(() => {
const state = data()
if (state?.kind === "connected") return state.data
return undefined
})
const orgs = createMemo(() => profile()?.profile.organizations ?? [])
const active = createMemo(() => org(profile()))
const disconnected = createMemo(() => data()?.kind === "disconnected")
const scope = createMemo(() => {
if (profile()) return account(profile())
if (data.loading) return "Loading..."
return "Not connected"
})
const login = () => page(params(), "/kilo/login")
const overview = () => page(params(), "/profile")
const usageUrl = createMemo(() => usage(profile()?.currentOrgId))
const creditsUrl = createMemo(() => credits(profile()?.currentOrgId))
createEffect(() => {
if (profile()) server.remember()
})
createEffect(() => {
const err = data.error
if (!err || authError(err)) return
server.recover()
})
function refresh() {
setError("")
void actions.refetch()
}
function choose(id: string | null) {
const current = server.query()
if (!current) return
const next = id ?? personal
const selected = profile()?.currentOrgId ?? null
if (selected === id) return
setSaving(next)
setError("")
void setKiloOrganization(current, id)
.then(() => actions.refetch())
.catch((err) => setError(errMsg(err)))
.finally(() => setSaving(undefined))
}
function logout() {
const current = server.query()
if (!current) return
setSaving("logout")
setError("")
void logoutKilo(current)
.then(() => {
markDisconnected(true)
actions.mutate({ kind: "disconnected" })
nav(overview(), { replace: true })
})
.catch((err) => setError(errMsg(err)))
.finally(() => setSaving(undefined))
}
return (
<section class="route-empty">
<p class="eyebrow">Profile</p>
<h1>Profile Settings</h1>
<p>Profile identity, account preferences, and workspace defaults will live here.</p>
<section class="config-shell profile-shell" classList={{ disconnected: disconnected() }}>
<Show when={!disconnected()}>
<aside class="config-sidebar" aria-label="Profile sections">
<div class="config-sidebar-title">
<span>Profile</span>
<span class="config-sidebar-scope">
<span>{scope()}</span>
</span>
</div>
<nav class="config-options">
<A class="config-top-option active" href={overview()} aria-current="page">
<span>Overview</span>
</A>
<a class="config-top-option" href={usageUrl()} target="_blank" rel="noreferrer">
<span>Usage</span>
</a>
<a class="config-top-option" href={creditsUrl()} target="_blank" rel="noreferrer">
<span>Buy Credits</span>
</a>
</nav>
</aside>
</Show>
<section class="content">
<div class="profile-page">
<Show when={!disconnected()}>
<header class="profile-header">
<div>
<p class="eyebrow">Kilo Account</p>
<h1>Your Profile</h1>
<p>Manage your Kilo identity, account context, credits, and billing shortcuts.</p>
</div>
<div class="profile-actions">
<Button variant="secondary" type="button" onClick={refresh} disabled={data.loading || !server.query()}>
Refresh
</Button>
<a
class="profile-primary-link"
data-component="button"
data-size="default"
data-variant="primary"
href={cloud()}
target="_blank"
rel="noreferrer"
>
Open Dashboard
</a>
</div>
</header>
</Show>
<Show when={!server.query() && server.discoverable()}>
<LoadingScreen variant="fullscreen" />
</Show>
<Show when={data.loading && !data()}>
<LoadingScreen variant="fullscreen" />
</Show>
<Show when={disconnected()}>
<Card class="profile-connect-card">
<span class="profile-connect-mark" aria-hidden="true">
KG
</span>
<div class="profile-connect-copy">
<p class="eyebrow">Kilo Account</p>
<h1>Connect your Kilo account</h1>
<p>Sign in to view your credits, organizations, and account details in Kilo Console.</p>
</div>
<A
class="profile-primary-link"
data-component="button"
data-size="default"
data-variant="primary"
href={login()}
>
Connect
</A>
</Card>
</Show>
<Show when={!server.query() && !server.discoverable()}>
<Card class="profile-banner" variant="warning">
<strong>Kilo server not found</strong>
<span>Start Kilo Console from a running Kilo server or pass a server URL with ?server=.</span>
</Card>
</Show>
<Show when={data.error && !authError(data.error)}>
<Card class="profile-banner" variant="error">
<strong>Profile request failed</strong>
<span>{errMsg(data.error)}</span>
</Card>
</Show>
<Show when={error()}>
<Card class="profile-banner" variant="error">
<strong>Account update failed</strong>
<span>{error()}</span>
</Card>
</Show>
<Show when={profile()}>
{(info) => (
<div class="profile-grid">
<Card class="profile-card profile-account-card">
<div class="profile-account-head">
<span class="profile-avatar" aria-hidden="true">
{initials(info().profile.name, info().profile.email)}
</span>
<div class="profile-account-text">
<strong>{info().profile.name || info().profile.email}</strong>
<span>{info().profile.email}</span>
</div>
</div>
<div class="profile-meta-row">
<span>Active account</span>
<strong>{account(info())}</strong>
</div>
<div class="profile-card-actions">
<Button variant="ghost" type="button" onClick={logout} disabled={Boolean(saving())}>
Log Out
</Button>
</div>
</Card>
<Card class="profile-card profile-balance-card">
<span class="profile-card-label">Remaining Credits</span>
<strong>{money(info().balance?.balance)}</strong>
<p>
Credits shown for {account(info())}. Switch accounts below to see organization balances when
available.
</p>
</Card>
<Card class="profile-card profile-wide-card">
<header class="profile-card-head">
<div>
<span class="profile-card-label">Organizations</span>
<h2>Your Accounts</h2>
</div>
<span class="profile-pill">{orgs().length === 1 ? "1 org" : `${orgs().length} orgs`}</span>
</header>
<div class="profile-org-list">
<button
type="button"
class="profile-org-row"
classList={{ active: !info().currentOrgId }}
onClick={() => choose(null)}
disabled={Boolean(saving())}
>
<span class="profile-org-icon" aria-hidden="true">
KG
</span>
<span class="profile-org-body">
<strong>Personal Account</strong>
<span>Your personal Kilo credits and settings</span>
</span>
<span class="profile-org-state">{!info().currentOrgId ? "Current" : "Use"}</span>
</button>
<For each={orgs()}>
{(item) => (
<button
type="button"
class="profile-org-row"
classList={{ active: active()?.id === item.id }}
onClick={() => choose(item.id)}
disabled={Boolean(saving())}
>
<span class="profile-org-icon" aria-hidden="true">
{initials(item.name, item.name)}
</span>
<span class="profile-org-body">
<strong>{item.name}</strong>
<span>{role(item)}</span>
</span>
<span class="profile-org-state">{active()?.id === item.id ? "Current" : "Use"}</span>
</button>
)}
</For>
</div>
</Card>
</div>
)}
</Show>
</div>
</section>
</section>
)
}
@@ -0,0 +1,44 @@
import { expect, test } from "bun:test"
import { authError, credits, initials, money, page, parseDeviceCode, safeReturn, usage } from "./profile-utils"
test("parses device auth codes from instructions", () => {
expect(parseDeviceCode("Open https://app.kilo.ai/device-auth and enter code: ABCD-2345")).toBe("ABCD-2345")
expect(parseDeviceCode("Use ABCD-2345 to continue")).toBe("ABCD-2345")
expect(parseDeviceCode(undefined)).toBeUndefined()
})
test("detects Kilo auth failures", () => {
expect(authError(new Error("Kilo profile: Unauthorized"))).toBe(true)
expect(authError({ status: 401 })).toBe(true)
expect(authError(new Error("Kilo profile: temporary network failure"))).toBe(false)
})
test("formats account display helpers", () => {
expect(money(12.5)).toBe("$12.50")
expect(money(null)).toBe("Unknown")
expect(initials("Jane Developer", "jane@example.com")).toBe("JD")
expect(initials("", "solo@example.com")).toBe("SE")
})
test("keeps login returns internal", () => {
expect(safeReturn("/profile?server=http%3A%2F%2F127.0.0.1%3A4097")).toBe(
"/profile?server=http%3A%2F%2F127.0.0.1%3A4097",
)
expect(safeReturn("https://app.kilo.ai/profile")).toBe("/profile")
expect(safeReturn("//app.kilo.ai/profile")).toBe("/profile")
})
test("builds local links while preserving server", () => {
const params = new URLSearchParams({ server: "http://127.0.0.1:4097", ignored: "1" })
expect(page(params, "/kilo/login", { return: "/profile" })).toBe(
"/kilo/login?server=http%3A%2F%2F127.0.0.1%3A4097&return=%2Fprofile",
)
})
test("builds account-aware cloud links", () => {
const id = "9d4b144c-0a2b-477b-973d-24fa02bebf13"
expect(usage(null)).toBe("https://app.kilo.ai/usage")
expect(usage(id)).toBe(`https://app.kilo.ai/organizations/${id}/usage-details`)
expect(credits(undefined)).toBe("https://app.kilo.ai/profile")
expect(credits(id)).toBe(`https://app.kilo.ai/organizations/${id}`)
})
@@ -0,0 +1,80 @@
export const personal = "personal"
let marked = false
export function markDisconnected(input: boolean) {
marked = input
}
export function wasDisconnected() {
return marked
}
function text(input: unknown) {
if (input instanceof Error) return input.message
if (typeof input === "string") return input
if (input === undefined || input === null) return ""
return JSON.stringify(input)
}
export function parseDeviceCode(input: string | undefined) {
if (!input) return undefined
const code = input.match(/code:\s*([A-Z0-9-]+)/i)?.[1]
if (code) return code.toUpperCase()
return input.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i)?.[1]?.toUpperCase()
}
export function authError(input: unknown) {
const value = text(input).toLowerCase()
return value.includes("unauthorized") || value.includes("invalid token") || value.includes('status":401')
}
export function money(input: number | null | undefined) {
if (typeof input !== "number" || !Number.isFinite(input)) return "Unknown"
return `$${input.toFixed(2)}`
}
export function initials(name: string | undefined, email: string) {
const parts = (name?.trim() || email)
.split(/[\s._@-]+/)
.filter(Boolean)
.slice(0, 2)
const value = parts.map((part) => part[0] ?? "").join("")
return value.toUpperCase() || "KG"
}
export function safeReturn(input: string | null | undefined) {
if (!input || !input.startsWith("/") || input.startsWith("//")) return "/profile"
try {
const url = new URL(input, "http://localhost")
if (url.origin !== "http://localhost") return "/profile"
return `${url.pathname}${url.search}${url.hash}`
} catch {
return "/profile"
}
}
export function page(params: URLSearchParams, path: string, extra?: Record<string, string | null | undefined>) {
const next = new URLSearchParams()
const server = params.get("server")
if (server) next.set("server", server)
for (const [key, value] of Object.entries(extra ?? {})) {
if (value) next.set(key, value)
}
const query = next.toString()
return `${path}${query ? `?${query}` : ""}`
}
export function cloud(path = "/profile") {
return `https://app.kilo.ai${path}`
}
export function usage(id: string | null | undefined) {
if (!id) return cloud("/usage")
return cloud(`/organizations/${encodeURIComponent(id)}/usage-details`)
}
export function credits(id: string | null | undefined) {
if (!id) return cloud("/profile")
return cloud(`/organizations/${encodeURIComponent(id)}`)
}
@@ -0,0 +1,71 @@
import { createEffect, createMemo, createSignal } from "solid-js"
import {
discover,
forgetCached,
loadCached,
resolveServer,
saveCached,
type ProjectQuery,
} from "../../client"
import { clean } from "../../shared/utils"
const ports = new Set(["3017", "3018"])
function shouldDiscover(input: URLSearchParams) {
if (input.get("server")) return false
return ports.has(window.location.port)
}
function base(input: URLSearchParams) {
const param = input.get("server")
if (param) return param
const cached = shouldDiscover(input) ? loadCached() : ""
if (cached) return cached
if (shouldDiscover(input)) return ""
return window.location.origin
}
export function useProfileServer(params: () => URLSearchParams) {
const [url, setUrl] = createSignal(base(params()))
const discoverable = () => shouldDiscover(params())
const query = createMemo<ProjectQuery | undefined>(() => {
const target = clean(url()) || base(params())
if (!target) return undefined
return { url: target, dir: "" }
})
createEffect(() => {
const next = params().get("server")
if (next && next !== url()) setUrl(next)
})
createEffect(() => {
if (!discoverable()) return
void resolveServer().then((value) => {
if (!value) return
saveCached(value)
setUrl(value)
})
})
function remember() {
const current = query()
if (!current || !discoverable()) return
saveCached(current.url)
}
function recover() {
if (!discoverable()) return
const cached = loadCached()
if (!cached || cached !== url()) return
forgetCached()
setUrl("")
void discover().then((value) => {
if (!value) return
saveCached(value)
setUrl(value)
})
}
return { query, discoverable, remember, recover }
}
@@ -11,6 +11,7 @@ test("classifies routes after stripping the console base", () => {
expect(path("/console/projects/demo/settings/agents", "/console")).toBe("/project")
expect(path("/console/settings/agents", "/console")).toBe("/settings")
expect(path("/console/profile", "/console")).toBe("/profile")
expect(path("/console/kilo/login", "/console")).toBe("/profile")
})
test("builds settings roots without preserving the console base", () => {
@@ -21,7 +21,7 @@ export function settings(input: string, prefix = base()) {
export function path(input: string, prefix = base()): Path {
const route = strip(input, prefix)
if (route === "/profile") return "/profile"
if (route === "/profile" || route.startsWith("/kilo/login")) return "/profile"
if (route.startsWith("/settings") || route.startsWith("/config")) return "/settings"
if (route.startsWith("/projects/")) return "/project"
return "/projects"
+1
View File
@@ -14,6 +14,7 @@
@import "./styles/agents-tools.css";
@import "./styles/projects.css";
@import "./styles/project-console.css";
@import "./styles/profile.css";
@import "./styles/servers.css";
@import "./styles/sources.css";
@import "./styles/responsive.css";
@@ -0,0 +1,480 @@
.kilo-console .profile-page,
.kilo-console .profile-login-page {
display: grid;
gap: 1rem;
max-width: 64rem;
margin: 0 auto;
}
.kilo-console .profile-login-page {
max-width: 34rem;
}
.kilo-console .profile-shell.disconnected {
grid-template-columns: minmax(0, 1fr);
}
.kilo-console .profile-header,
.kilo-console .profile-login-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 1rem;
align-items: start;
padding-bottom: 0.5rem;
}
.kilo-console .profile-login-header {
grid-template-columns: 1fr;
text-align: center;
}
.kilo-console .profile-header h1,
.kilo-console .profile-login-header h1 {
margin: 0;
color: var(--foreground);
font-size: 1.25rem;
font-weight: 600;
letter-spacing: -0.02em;
}
.kilo-console .profile-header p:not(.eyebrow),
.kilo-console .profile-login-header p:not(.eyebrow) {
max-width: 42rem;
margin: 0.625rem 0 0;
color: var(--muted-foreground);
font-size: 0.8125rem;
line-height: 1.45;
}
.kilo-console .profile-actions,
.kilo-console .profile-card-actions,
.kilo-console .profile-login-actions {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
}
.kilo-console .profile-primary-link {
text-decoration: none;
}
.kilo-console .profile-connect-card {
display: grid;
min-height: 18rem;
place-items: center;
align-content: center;
gap: 1.25rem;
text-align: center;
}
.kilo-console .profile-connect-mark {
display: grid;
width: 3.5rem;
height: 3.5rem;
place-items: center;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background:
linear-gradient(135deg, color-mix(in oklab, var(--primary) 32%, transparent), transparent),
color-mix(in oklab, var(--muted) 70%, transparent);
color: var(--foreground);
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.08em;
}
.kilo-console .profile-connect-copy {
display: grid;
max-width: 28rem;
gap: 0.625rem;
}
.kilo-console .profile-connect-copy h1,
.kilo-console .profile-connect-copy p {
margin: 0;
}
.kilo-console .profile-connect-copy h1 {
color: var(--foreground);
font-size: 1.125rem;
font-weight: 600;
letter-spacing: -0.02em;
}
.kilo-console .profile-connect-copy p:not(.eyebrow) {
color: var(--muted-foreground);
font-size: 0.8125rem;
line-height: 1.5;
}
.kilo-console .profile-link-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.75rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--input-base);
color: var(--foreground);
font-size: 0.75rem;
font-weight: 500;
line-height: 1.45;
padding: 0.25rem 0.625rem;
text-decoration: none;
transition:
background 120ms ease,
border-color 120ms ease,
color 120ms ease;
}
.kilo-console .profile-link-button:hover,
.kilo-console .profile-link-button:focus-visible {
border-color: var(--ring);
background: var(--muted);
color: var(--foreground);
outline: 0;
}
.kilo-console .profile-banner {
margin-bottom: 0;
}
.kilo-console .profile-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.kilo-console .profile-card {
gap: 0.875rem;
}
.kilo-console .profile-wide-card {
grid-column: 1 / -1;
}
.kilo-console .profile-account-card,
.kilo-console .profile-balance-card {
min-height: 10rem;
}
.kilo-console .profile-account-head {
display: flex;
gap: 0.875rem;
align-items: center;
min-width: 0;
}
.kilo-console .profile-avatar,
.kilo-console .profile-org-icon {
display: grid;
flex: 0 0 auto;
place-items: center;
border: 1px solid var(--border);
background:
linear-gradient(135deg, color-mix(in oklab, var(--primary) 28%, transparent), transparent),
color-mix(in oklab, var(--muted) 70%, transparent);
color: var(--foreground);
font-weight: 700;
letter-spacing: 0.06em;
}
.kilo-console .profile-avatar {
width: 3.25rem;
height: 3.25rem;
border-radius: var(--radius-lg);
font-size: 1rem;
}
.kilo-console .profile-org-icon {
width: 2rem;
height: 2rem;
border-radius: var(--radius-sm);
font-size: 0.6875rem;
}
.kilo-console .profile-account-text {
display: grid;
min-width: 0;
gap: 0.125rem;
}
.kilo-console .profile-account-text strong,
.kilo-console .profile-card-head h2,
.kilo-console .profile-login-state strong,
.kilo-console .profile-login-step strong {
margin: 0;
color: var(--foreground);
font-size: 0.9375rem;
font-weight: 600;
line-height: 1.25;
}
.kilo-console .profile-account-text span,
.kilo-console .profile-login-state p,
.kilo-console .profile-login-step > span,
.kilo-console .profile-login-waiting {
color: var(--muted-foreground);
font-size: 0.75rem;
line-height: 1.45;
}
.kilo-console .profile-account-text span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.kilo-console .profile-meta-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: color-mix(in oklab, var(--muted) 35%, transparent);
padding: 0.75rem;
}
.kilo-console .profile-meta-row span,
.kilo-console .profile-card-label {
color: var(--muted-foreground);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.kilo-console .profile-meta-row strong {
color: var(--foreground);
font-size: 0.75rem;
font-weight: 600;
}
.kilo-console .profile-balance-card strong {
color: var(--foreground);
font-size: clamp(2rem, 6vw, 3.25rem);
font-weight: 700;
letter-spacing: -0.05em;
line-height: 0.95;
}
.kilo-console .profile-balance-card p,
.kilo-console .profile-login-state p {
margin: 0;
}
.kilo-console .profile-card-head {
display: flex;
gap: 1rem;
align-items: flex-start;
justify-content: space-between;
}
.kilo-console .profile-pill {
display: inline-flex;
align-items: center;
min-height: 1.5rem;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted-foreground);
font-size: 0.6875rem;
font-weight: 600;
padding: 0 0.625rem;
text-decoration: none;
text-transform: uppercase;
}
.kilo-console .profile-org-list {
display: grid;
gap: 0.5rem;
}
.kilo-console .profile-org-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
width: 100%;
min-height: 3.5rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--card);
color: var(--foreground);
cursor: pointer;
font: inherit;
padding: 0.75rem;
text-align: left;
transition:
background 120ms ease,
border-color 120ms ease;
}
.kilo-console .profile-org-row:hover,
.kilo-console .profile-org-row:focus-visible,
.kilo-console .profile-org-row.active {
border-color: var(--ring);
background: color-mix(in oklab, var(--muted) 45%, transparent);
outline: 0;
}
.kilo-console .profile-org-row:disabled {
cursor: not-allowed;
opacity: 0.65;
}
.kilo-console .profile-org-body {
display: grid;
min-width: 0;
gap: 0.125rem;
}
.kilo-console .profile-org-body strong {
overflow: hidden;
color: var(--foreground);
font-size: 0.8125rem;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.kilo-console .profile-org-body span,
.kilo-console .profile-org-state {
color: var(--muted-foreground);
font-size: 0.6875rem;
}
.kilo-console .profile-org-state {
font-weight: 600;
}
.kilo-console .profile-login-card {
min-height: 18rem;
}
.kilo-console .profile-login-state,
.kilo-console .profile-login-flow,
.kilo-console .profile-login-step {
display: grid;
gap: 0.875rem;
}
.kilo-console .profile-login-state {
place-items: center;
min-height: 14rem;
text-align: center;
}
.kilo-console .profile-login-state.error strong {
color: var(--destructive);
}
.kilo-console .profile-login-state.success strong {
color: var(--success, var(--primary));
}
.kilo-console .profile-login-step {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: color-mix(in oklab, var(--muted) 25%, transparent);
padding: 1rem;
}
.kilo-console .profile-login-url {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.5rem;
align-items: center;
}
.kilo-console .profile-login-url span {
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--input-base);
color: var(--foreground);
font-family: var(--font-family-mono, monospace);
font-size: 0.75rem;
padding: 0.5rem 0.625rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.kilo-console .profile-login-code {
display: grid;
gap: 0.25rem;
place-items: center;
border: 1px solid var(--ring);
border-radius: var(--radius-lg);
background: color-mix(in oklab, var(--primary) 12%, var(--input-base));
color: var(--foreground);
cursor: pointer;
font-family: var(--font-family-mono, monospace);
font-size: 2rem;
font-weight: 700;
letter-spacing: 0.16em;
padding: 1rem;
}
.kilo-console .profile-login-code small {
color: var(--muted-foreground);
font-family: var(--font-family-sans, system-ui, sans-serif);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0;
}
.kilo-console .profile-login-waiting {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.kilo-console .profile-spinner {
width: 1rem;
height: 1rem;
border: 2px solid color-mix(in oklab, var(--muted-foreground) 30%, transparent);
border-top-color: var(--primary);
border-radius: 999px;
animation: profile-spin 900ms linear infinite;
}
@keyframes profile-spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 760px) {
.kilo-console .profile-header,
.kilo-console .profile-grid {
grid-template-columns: 1fr;
}
.kilo-console .profile-actions,
.kilo-console .profile-card-actions,
.kilo-console .profile-login-actions {
justify-content: flex-start;
}
.kilo-console .profile-login-url {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.kilo-console .profile-org-row {
grid-template-columns: auto minmax(0, 1fr);
}
.kilo-console .profile-org-state {
grid-column: 2;
}
.kilo-console .profile-login-code {
font-size: 1.5rem;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.3.46",
"version": "7.3.49",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -11,6 +11,7 @@ Auto Model is a smart routing system that selects an underlying model for each r
|---|---|---|
| `kilo-auto/frontier` | Maximum capability with the best available models | Paid |
| `kilo-auto/balanced` | Strong performance at a lower cost | Paid |
| `kilo-auto/efficient` | Lowest cost per task, with capability matched to difficulty | Paid |
| `kilo-auto/free` | The best free models available | Free |
## How It Works
@@ -31,8 +32,17 @@ The underlying models behind each Auto Model tier are updated server-side as bet
- **Frontier** — Routes to the latest and most capable paid models. Uses different models for reasoning-heavy tasks (planning, architecture, debugging) versus implementation tasks (coding, building, exploring), pairing the right capability to each type of work.
- **Balanced** — Routes to a cost-effective model for all modes. The specific model is selected based on the API interface in use, but does not vary by mode. A good default for most developers who want strong AI assistance without paying frontier prices.
- **Efficient** — Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to the Balanced tier, so quality never drops below Balanced.
- **Free** — Routes to the best available free models on OpenRouter, splitting traffic across them. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side — you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the models may change over time.
### How Auto Efficient routing works
1. Kilo observes your coding session in context
2. It classifies each task by complexity
3. It routes to the benchmark-proven best model for that task automatically
You get lean costs on routine work and stronger models when the work demands it — with no manual switching.
{% callout type="warning" title="Data handling for Auto Free" %}
Auto Free may route your requests to providers that log prompts and outputs and use them to improve their services. Do not submit personal or confidential data when using Auto Free. In particular, it may route to NVIDIA's free endpoints.
@@ -51,7 +61,7 @@ No need to manually switch models when changing modes. Auto Model handles routin
### Flexible Cost Control
Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Free costs nothing.
Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Efficient minimizes cost per task by matching model capability to task difficulty; Free costs nothing.
## Requirements
@@ -60,7 +60,7 @@ While the specifics change constantly, some principles stay consistent:
## Free and Budget Model Picks
You don't need a paid API key to use Kilo Code productively. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup.
You don't need a paid API key to use Kilo Code productively. For the lowest cost on paid work, [Auto Efficient](/docs/code-with-ai/agents/auto-model#tiers) (`kilo-auto/efficient`) routes each request to the cheapest model proven accurate enough for that task. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup.
If you prefer to pick models yourself, type `free` in the model picker to filter by free models, or browse the full list at [kilo.ai/models](https://kilo.ai/models).
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06c6cd2c2c63745d3cbaec756ae852db3ef0749c005926b75fe3eb03f06b0495
size 22169
-2
View File
@@ -81,8 +81,6 @@
<!-- packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceContribute.tsx -->
- <https://github.com/Kilo-Org/kilocode>
<!-- packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx -->
- <https://github.com/Kilo-Org/kilocode/>
<!-- packages/kilo-vscode/webview-ui/src/context/notifications.tsx -->
- <https://github.com/Kilo-Org/kilocode/issues/6986>
<!-- packages/kilo-vscode/src/agent-manager/constants.ts -->
- <https://github.com/Kilo-Org/kilocode/issues/9618>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+2 -2
View File
@@ -9,7 +9,7 @@ import { getApiKey } from "./auth/token.js"
import { buildKiloHeaders, getDefaultHeaders } from "./headers.js"
import { ANONYMOUS_API_KEY } from "./api/constants.js"
import { resolveKiloOpenRouterBaseUrl } from "./api/url.js"
import { sanitizeResponsesBody } from "./responses.js"
import { transformRequestBody } from "./responses.js"
export function buildRequestHeaders(defaultHeaders: Record<string, string>, requestHeaders?: HeadersInit): Headers {
const headers = new Headers(defaultHeaders)
@@ -55,7 +55,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider {
const originalFetch = options.fetch ?? fetch
const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => {
const headers = buildRequestHeaders(customHeaders, init?.headers)
const body = sanitizeResponsesBody(input, init?.body)
const body = transformRequestBody(input, init?.body, options.dataCollection)
// Add authorization if API key exists
if (apiKey) {
+16 -7
View File
@@ -28,8 +28,13 @@ function strip(input: unknown[]) {
return { kept, changed }
}
export function sanitizeResponsesBody(input: string | URL | Request, body: BodyInit | null | undefined) {
if (!endpoint(input)) return body
export function transformRequestBody(
input: string | URL | Request,
body: BodyInit | null | undefined,
value?: "allow" | "deny",
) {
const responses = endpoint(input)
if (!responses && !value) return body
if (typeof body !== "string") return body
const data = (() => {
@@ -40,10 +45,14 @@ export function sanitizeResponsesBody(input: string | URL | Request, body: BodyI
}
})()
if (!record(data)) return body
if (data.store === true) return body
if (!Array.isArray(data.input)) return body
const result = strip(data.input)
if (!result.changed) return body
return JSON.stringify({ ...data, input: result.kept })
const result = responses && data.store !== true && Array.isArray(data.input) ? strip(data.input) : undefined
if (!result?.changed && !value) return body
const provider = record(data.provider) ? data.provider : {}
return JSON.stringify({
...data,
...(result?.changed ? { input: result.kept } : {}),
...(value ? { provider: { ...provider, data_collection: value } } : {}),
})
}
+5
View File
@@ -95,6 +95,11 @@ export interface KiloProviderOptions {
*/
name?: string
/**
* Data collection preference for upstream provider routing
*/
dataCollection?: "allow" | "deny"
/**
* Custom fetch function
*/
+29 -6
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { sanitizeResponsesBody } from "../src/responses"
import { transformRequestBody } from "../src/responses"
describe("Responses request sanitization", () => {
test("strips item ids when storage is disabled", () => {
@@ -30,7 +30,7 @@ describe("Responses request sanitization", () => {
],
})
const result = sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)
const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)
const data = JSON.parse(result as string)
expect(data.input).toHaveLength(3)
@@ -60,19 +60,19 @@ describe("Responses request sanitization", () => {
],
})
expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body)
expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body)
})
test("leaves non-responses requests unchanged", () => {
const body = "not json"
expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body)
expect(transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body)
})
test("leaves invalid responses JSON unchanged", () => {
const body = "not json"
expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body)
expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body)
})
test("matches relative responses paths without a placeholder host", () => {
@@ -86,9 +86,32 @@ describe("Responses request sanitization", () => {
},
],
})
const result = sanitizeResponsesBody("/api/openrouter/responses?stream=true", body)
const result = transformRequestBody("/api/openrouter/responses?stream=true", body)
const data = JSON.parse(result as string)
expect(data.input[0].id).toBeUndefined()
})
test("sanitizes responses and denies data collection in one transform", () => {
const body = JSON.stringify({
input: [{ type: "message", role: "assistant", id: "msg_tmp_123" }],
provider: { order: ["anthropic"] },
})
const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body, "deny")
expect(JSON.parse(result as string)).toEqual({
input: [{ type: "message", role: "assistant" }],
provider: { order: ["anthropic"], data_collection: "deny" },
})
})
test("denies data collection for non-responses requests", () => {
const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" })
const result = transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body, "deny")
expect(JSON.parse(result as string)).toEqual({
model: "anthropic/claude-sonnet-4",
provider: { data_collection: "deny" },
})
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-indexing",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code",
+17
View File
@@ -88,6 +88,23 @@
## [Unreleased]
## [7.0.1-rc.12] - 2026-06-18
### Added
- Provider settings management, including searchable provider lists, API-key configuration, OAuth provider login, provider enable/disable controls, disconnect actions, and shared provider metadata.
- Add copy controls to session messages so prompts and assistant responses can be copied directly from the transcript.
- Share codebase indexes across worktrees so Agent Manager and worktree sessions can use semantic search without duplicating the full index.
### Fixed
- Keep long JetBrains prompt input usable by capping growth, preserving scrolling, and hiding soft-wrap glyphs.
- Copy actions correctly in session.
### Changed
- Update the bundled CLI runtime to OpenCode 1.15.9
## [7.0.1-rc.11] - 2026-06-17
### Added
+1 -1
View File
@@ -1,5 +1,5 @@
kotlin.stdlib.default.dependency=false
kilo.jetbrains.version=7.0.1-rc.11
kilo.jetbrains.version=7.0.1-rc.12
org.gradle.configuration-cache=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
+1 -1
View File
@@ -8,7 +8,7 @@
"test": "./gradlew test",
"test:ci": "bun script/test-ci.ts"
},
"version": "7.3.46",
"version": "7.3.49",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"exports": {
+6
View File
@@ -1,5 +1,11 @@
# kilo-code
## 7.3.49
### Minor Changes
- [#11303](https://github.com/Kilo-Org/kilocode/pull/11303) [`6faa3f1`](https://github.com/Kilo-Org/kilocode/commit/6faa3f14b619144a1353f169a0a2855dcbf69bbb) - Add an "Import Sessions from Roo Code" button in the About settings tab that discovers and imports conversation history from an existing Roo Code installation, reusing the existing migration wizard.
## 7.3.48
### Minor Changes
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.3.46",
"version": "7.3.49",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
+10 -15
View File
@@ -94,8 +94,8 @@ import { hasGit } from "./kilo-provider/git-status"
// legacy-migration start
import {
checkAndShowMigrationWizard,
handleRequestLegacyMigrationData,
handleStartLegacyMigration,
handleRequestMigrationData,
handleStartMigration,
handleFinalizeLegacyMigration,
handleSkipLegacyMigration,
handleClearLegacyData,
@@ -320,8 +320,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private readonly confirmations = new MessageConfirmation()
private unsubscribeEvent: (() => void) | null = null
private unsubscribeState: (() => void) | null = null
/** Cached legacy migration data so migrate() doesn't re-read from disk/SecretStorage. */ // legacy-migration
private cachedLegacyData: import("./legacy-migration/legacy-types").LegacyMigrationData | null = null // legacy-migration
/** Cached migration data so migration doesn't re-read from disk/SecretStorage. */ // legacy-migration
private migrationCache: MigrationContext["migrationCache"] = new Map()
/** Guard to prevent checkAndShowMigrationWizard running concurrently. */ // legacy-migration
private migrationCheckInFlight = false // legacy-migration
private unsubscribeNotificationDismiss: (() => void) | null = null
@@ -1206,11 +1206,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
break
}
// legacy-migration start
case "requestLegacyMigrationData":
void handleRequestLegacyMigrationData(this.migrationCtx)
case "requestMigrationData":
void handleRequestMigrationData(this.migrationCtx, message.source, message.operationId)
break
case "startLegacyMigration":
void handleStartLegacyMigration(this.migrationCtx, message.selections)
case "startMigration":
void handleStartMigration(this.migrationCtx, message.source, message.operationId, message.selections)
break
case "skipLegacyMigration":
void handleSkipLegacyMigration(this.migrationCtx)
@@ -1385,7 +1385,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// legacy-migration start
// Subscribe to migration-complete broadcast from any KiloProvider instance
this.unsubscribeMigrationComplete = this.connectionService.onMigrationComplete(() => {
this.postMessage({ type: "migrationState", needed: false })
this.postMessage({ type: "migrationState", needed: false, source: "legacy" })
})
// legacy-migration end
@@ -3512,12 +3512,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
client: this.client,
extensionContext: this.extensionContext,
postMessage: (msg) => this.postMessage(msg),
get cachedLegacyData() {
return self.cachedLegacyData
},
set cachedLegacyData(data) {
self.cachedLegacyData = data
},
migrationCache: self.migrationCache,
get migrationCheckInFlight() {
return self.migrationCheckInFlight
},
+1 -1
View File
@@ -387,7 +387,7 @@ export function activate(context: vscode.ExtensionContext) {
}),
// legacy-migration start
vscode.commands.registerCommand("kilo-code.new.openMigrationWizard", () => {
provider.postMessage({ type: "migrationState", needed: true })
provider.postMessage({ type: "migrationState", needed: true, source: "legacy" })
}),
// legacy-migration end
vscode.commands.registerCommand("kilo-code.new.generateTerminalCommand", async () => {
@@ -2,7 +2,7 @@
* Legacy migration handlers — extracted from KiloProvider.
*
* Manages the migration wizard for users upgrading from Kilo Code v5.x.
* No vscode dependency — all vscode access is injected via MigrationContext.
* VS Code access is limited to migration service helpers and injected context.
*/
import type { KiloClient } from "@kilocode/sdk/v2/client"
@@ -10,8 +10,13 @@ import type {
LegacyMigrationData,
MigrationSelections,
MigrationSessionProgress,
MigrationSessionSelection,
} from "../../legacy-migration/legacy-types"
import * as MigrationService from "../../legacy-migration/migration-service"
import { runSessionBatch } from "../../legacy-migration/session-batch"
import { migrate as migrateSession } from "../../legacy-migration/sessions/migrate"
import { resolveSession } from "../../legacy-migration/task-store"
import { detectRooCodeSessions, type RooImportSource } from "../../roo-import/service"
/** Subset of vscode.ExtensionContext needed by migration handlers. */
interface MigrationExtensionContext {
@@ -28,21 +33,59 @@ interface MigrationExtensionContext {
globalStorageUri: { fsPath: string }
}
export type MigrationSource = "legacy" | "roo"
export type MigrationCacheEntry =
| { operationId: string; source: "legacy"; data: LegacyMigrationData }
| { operationId: string; source: "roo"; data: RooImportSource | null }
export type MigrationCache = Map<string, MigrationCacheEntry>
export function getMigrationCache(
cache: MigrationCache,
source: "legacy",
operationId: string,
): Extract<MigrationCacheEntry, { source: "legacy" }> | undefined
export function getMigrationCache(
cache: MigrationCache,
source: "roo",
operationId: string,
): Extract<MigrationCacheEntry, { source: "roo" }> | undefined
export function getMigrationCache(cache: MigrationCache, source: MigrationSource, operationId: string) {
const entry = cache.get(operationId)
return entry?.source === source ? entry : undefined
}
export interface MigrationContext {
readonly client: KiloClient | null
readonly extensionContext: MigrationExtensionContext | undefined
postMessage(msg: unknown): void
refreshSessions(): void
cachedLegacyData: LegacyMigrationData | null
migrationCache: MigrationCache
migrationCheckInFlight: boolean
lastMigrationHadErrors?: boolean
disposeGlobal(): Promise<void>
broadcastComplete(): void
}
function postSessionProgress(ctx: MigrationContext, progress: MigrationSessionProgress): void {
function emptyData(sessions: LegacyMigrationData["sessions"] = []): LegacyMigrationData {
return {
hasData: sessions.length > 0,
providers: [],
mcpServers: [],
customModes: [],
sessions,
}
}
function postSessionProgress(
ctx: MigrationContext,
source: MigrationSource,
operationId: string,
progress: MigrationSessionProgress,
): void {
ctx.postMessage({
type: "legacyMigrationSessionProgress",
type: "migrationSessionProgress",
source,
operationId,
session: progress.session,
index: progress.index,
total: progress.total,
@@ -75,13 +118,42 @@ export async function checkAndShowMigrationWizard(ctx: MigrationContext): Promis
if (!data.hasData) return
// Cache so migrate() doesn't re-read from SecretStorage/disk
ctx.cachedLegacyData = data
console.log("[Kilo New] KiloProvider: 🔄 Legacy data detected, showing migration wizard")
// The wizard re-requests the data via requestMigrationData on mount, so only the flag is sent here.
ctx.postMessage({
type: "migrationState",
needed: true,
source: "legacy",
})
}
/** Send migration data for the requested source to the webview. */
export async function handleRequestMigrationData(
ctx: MigrationContext,
source: MigrationSource,
operationId: string,
): Promise<void> {
if (!ctx.extensionContext) return
// A new request means a new wizard session; drop any entry from an abandoned one.
for (const key of ctx.migrationCache.keys()) {
if (key !== operationId) ctx.migrationCache.delete(key)
}
const data = await (async () => {
if (source === "roo") {
const roo = await detectRooCodeSessions(ctx.extensionContext as Parameters<typeof detectRooCodeSessions>[0])
ctx.migrationCache.set(operationId, { operationId, source, data: roo })
return emptyData(roo?.sessions ?? [])
}
const legacy = await MigrationService.detectLegacyData(
ctx.extensionContext as Parameters<typeof MigrationService.detectLegacyData>[0],
)
ctx.migrationCache.set(operationId, { operationId, source, data: legacy })
return legacy
})()
ctx.postMessage({
type: "migrationData",
source,
operationId,
data: {
providers: data.providers,
mcpServers: data.mcpServers,
@@ -93,54 +165,83 @@ export async function checkAndShowMigrationWizard(ctx: MigrationContext): Promis
})
}
/** Send the detected legacy data to the webview on explicit request. */
export async function handleRequestLegacyMigrationData(ctx: MigrationContext): Promise<void> {
if (!ctx.extensionContext) return
const data = await MigrationService.detectLegacyData(
ctx.extensionContext as Parameters<typeof MigrationService.detectLegacyData>[0],
)
ctx.cachedLegacyData = data
ctx.postMessage({
type: "legacyMigrationData",
data: {
providers: data.providers,
mcpServers: data.mcpServers,
customModes: data.customModes,
sessions: data.sessions,
defaultModel: data.defaultModel,
settings: data.settings,
async function startRooMigration(
ctx: MigrationContext,
operationId: string,
selections: { sessions?: MigrationSessionSelection[] },
): Promise<void> {
if (!ctx.extensionContext || !ctx.client) return
const cached = getMigrationCache(ctx.migrationCache, "roo", operationId)
const source = cached
? cached.data
: await detectRooCodeSessions(ctx.extensionContext as Parameters<typeof detectRooCodeSessions>[0])
if (!cached) ctx.migrationCache.set(operationId, { operationId, source: "roo", data: source })
if (!source) {
ctx.postMessage({
type: "migrationComplete",
source: "roo",
operationId,
results: [
{ item: "Roo Code sessions", category: "session", status: "warning", message: "No Roo Code sessions found." },
],
})
return
}
const results = await runSessionBatch({
selections: selections.sessions ?? [],
sessions: source.sessions,
resolve: (id) => resolveSession(source.catalog, id),
migrate: (selection, resolved, progress) =>
migrateSession(
selection,
ctx.extensionContext as Parameters<typeof migrateSession>[1],
ctx.client as KiloClient,
progress,
resolved,
),
onProgress: (item, status, message) => {
ctx.postMessage({ type: "migrationProgress", source: "roo", operationId, item, status, message })
},
onSessionProgress: (progress) => postSessionProgress(ctx, "roo", operationId, progress),
})
ctx.lastMigrationHadErrors = results.some((item) => item.status === "error")
ctx.postMessage({ type: "migrationComplete", source: "roo", operationId, results })
}
/** Run the migration for the selected items. */
export async function handleStartLegacyMigration(
async function startLegacyMigration(
ctx: MigrationContext,
operationId: string,
selections: MigrationSelections,
): Promise<void> {
if (!ctx.extensionContext || !ctx.client) return
try {
const cached = getMigrationCache(ctx.migrationCache, "legacy", operationId)
const results = await MigrationService.migrate(
ctx.extensionContext as Parameters<typeof MigrationService.migrate>[0],
ctx.client,
selections,
(item, status, message) => {
ctx.postMessage({ type: "legacyMigrationProgress", item, status, message })
ctx.postMessage({ type: "migrationProgress", source: "legacy", operationId, item, status, message })
},
(progress: MigrationSessionProgress) => {
postSessionProgress(ctx, progress)
postSessionProgress(ctx, "legacy", operationId, progress)
},
ctx.cachedLegacyData?.settings,
ctx.cachedLegacyData?.sessions,
cached?.data.settings,
cached?.data.sessions,
)
ctx.lastMigrationHadErrors = results.some((item) => item.status === "error")
ctx.postMessage({ type: "legacyMigrationComplete", results })
ctx.postMessage({ type: "migrationComplete", source: "legacy", operationId, results })
} catch (error) {
ctx.lastMigrationHadErrors = true
console.error("[Kilo New] KiloProvider: ❌ Migration failed", error)
ctx.postMessage({
type: "legacyMigrationComplete",
type: "migrationComplete",
source: "legacy",
operationId,
results: [
{
item: "Migration",
@@ -153,6 +254,24 @@ export async function handleStartLegacyMigration(
}
}
export async function handleStartMigration(
ctx: MigrationContext,
source: MigrationSource,
operationId: string,
selections: MigrationSelections,
): Promise<void> {
try {
if (source === "roo") {
await startRooMigration(ctx, operationId, selections)
return
}
await startLegacyMigration(ctx, operationId, selections)
} finally {
// The operation has finished (or thrown); its cached discovery is no longer needed.
ctx.migrationCache.delete(operationId)
}
}
export async function handleFinalizeLegacyMigration(ctx: MigrationContext): Promise<void> {
if (!ctx.extensionContext) return
await ctx.disposeGlobal()
@@ -36,9 +36,11 @@ import type {
MigrationSessionInfo,
MigrationSessionProgress,
} from "./legacy-types"
import { buildSessionMeta, buildSessionProgress } from "./migration-session-progress"
import type { MigrationResultItem } from "./migration-types"
import { runSessionBatch } from "./session-batch"
import { listSessions, resolveSession, scanTaskStore } from "./task-store"
import { createSessionID } from "./sessions/lib/ids"
import type { LegacyHistoryItem } from "./sessions/lib/legacy-types"
import { migrate as migrateSession } from "./sessions/migrate"
// ---------------------------------------------------------------------------
@@ -77,7 +79,7 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis
const customModes = await readLegacyCustomModes(context)
const prompts = readLegacyCustomModePrompts(context)
const settings = readLegacySettings(context)
const sessions = await readSessionsInGlobalStorage(context)
const sessions = listSessions(await readSessionCatalog(context))
const oauthProviders = new Set<string>()
const codexRaw = await context.secrets.get(CODEX_OAUTH_SECRET_KEY)
@@ -116,28 +118,10 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis
}
}
async function readSessionsInGlobalStorage(context: vscode.ExtensionContext) {
const items = context.globalState.get<{ id: string; task?: string; workspace?: string; ts?: number }[]>(
"taskHistory",
[],
)
const base = vscode.Uri.joinPath(context.globalStorageUri, "tasks")
const sessions: MigrationSessionInfo[] = []
for (const item of items) {
const file = vscode.Uri.joinPath(base, item.id, "api_conversation_history.json")
const exists = await vscode.workspace.fs.stat(file).then(
() => true,
() => false,
)
if (!exists) continue
sessions.push({
id: item.id,
title: item.task?.trim() || item.id,
directory: item.workspace?.trim() || "",
time: item.ts ?? 0,
})
}
return sessions
async function readSessionCatalog(context: vscode.ExtensionContext) {
const items = context.globalState.get<LegacyHistoryItem[]>("taskHistory", [])
const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath
return (await scanTaskStore(dir, items, { mode: "history" })).catalog
}
// ---------------------------------------------------------------------------
@@ -152,9 +136,6 @@ export type ProgressCallback = (
export type SessionProgressCallback = (progress: MigrationSessionProgress) => void
const SESSION_DELAY = 300
const SESSION_SUMMARY_DELAY = 1000
/**
* Executes migration for the selected items.
* Calls onProgress for each item with real-time status updates.
@@ -176,7 +157,8 @@ export async function migrate(
const customModes = await readLegacyCustomModes(context)
const prompts = readLegacyCustomModePrompts(context)
const legacySettings = cachedSettings ?? readLegacySettings(context)
const sessions = cachedSessions ?? (await readSessionsInGlobalStorage(context))
const catalog = await readSessionCatalog(context)
const sessions = cachedSessions ?? listSessions(catalog)
const results: MigrationResultItem[] = []
@@ -277,36 +259,16 @@ export async function migrate(
}
if (selections.sessions?.length) {
const list = selections.sessions
for (const [index, item] of list.entries()) {
onProgress(item.id, "migrating")
const session = sessions.find((entry: MigrationSessionInfo) => entry.id === item.id)
const meta = buildSessionMeta(session, index, list.length)
const progress = buildSessionProgress(meta, onSessionProgress)
const result = await migrateSession(item, context, client, meta, progress)
const reason = result.ok ? "Session migrated" : result.message
results.push({
item: item.id,
category: "session",
status: result.ok ? "success" : "error",
message: reason,
})
onProgress(item.id, result.ok ? "success" : "error", reason)
if (index < list.length - 1) {
await new Promise((resolve) => setTimeout(resolve, SESSION_DELAY))
}
}
const last = list.at(-1)
const session = last ? sessions.find((item: MigrationSessionInfo) => item.id === last.id) : undefined
if (session && onSessionProgress) {
onSessionProgress({
session,
index: list.length,
total: list.length,
phase: "summary",
})
await new Promise((resolve) => setTimeout(resolve, SESSION_SUMMARY_DELAY))
}
results.push(
...(await runSessionBatch({
selections: selections.sessions,
sessions,
resolve: (id) => resolveSession(catalog, id),
migrate: (selection, source, progress) => migrateSession(selection, context, client, progress, source),
onProgress,
onSessionProgress,
})),
)
}
// Migrate default model
@@ -0,0 +1,63 @@
import type { MigrationSessionInfo, MigrationSessionProgress, MigrationSessionSelection } from "./legacy-types"
import type { MigrationResultItem } from "./migration-types"
import { buildSessionMeta, buildSessionProgress } from "./migration-session-progress"
import type { SessionSource } from "./task-store"
import type { migrate as migrateSession } from "./sessions/migrate"
const DELAY = 300
const SUMMARY_DELAY = 1000
type Result = Awaited<ReturnType<typeof migrateSession>>
interface BatchOptions {
selections: MigrationSessionSelection[]
sessions: MigrationSessionInfo[]
resolve(id: string): SessionSource | undefined
migrate(
selection: MigrationSessionSelection,
source: SessionSource,
progress: ReturnType<typeof buildSessionProgress>,
): Promise<Result>
onProgress(item: string, status: "migrating" | "success" | "warning" | "error", message?: string): void
onSessionProgress?: (progress: MigrationSessionProgress) => void
delay?: (ms: number) => Promise<void>
}
export async function runSessionBatch(options: BatchOptions): Promise<MigrationResultItem[]> {
const results: MigrationResultItem[] = []
const wait = options.delay ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))
for (const [index, selection] of options.selections.entries()) {
options.onProgress(selection.id, "migrating")
const session = options.sessions.find((entry) => entry.id === selection.id)
const source = options.resolve(selection.id)
if (!source) {
const message = "Session source not found"
results.push({ item: session?.title ?? selection.id, category: "session", status: "error", message })
options.onProgress(selection.id, "error", message)
continue
}
const meta = buildSessionMeta(session, index, options.selections.length)
const result = await options.migrate(selection, source, buildSessionProgress(meta, options.onSessionProgress))
const status = result.ok ? (result.skipped ? "warning" : "success") : "error"
const message = result.ok ? (result.skipped ? "Already imported." : undefined) : result.message
results.push({ item: session?.title ?? selection.id, category: "session", status, message })
options.onProgress(selection.id, status, message)
if (index < options.selections.length - 1) await wait(DELAY)
}
const last = options.selections.at(-1)
const session = last ? options.sessions.find((entry) => entry.id === last.id) : undefined
if (session && options.onSessionProgress) {
options.onSessionProgress({
session,
index: options.selections.length,
total: options.selections.length,
phase: "summary",
})
await wait(SUMMARY_DELAY)
}
return results
}
@@ -7,10 +7,11 @@ export function createSession(
item: LegacyHistoryItem | undefined,
projectID: string,
dir: string,
key = id,
): NonNullable<Session["body"]> {
const session = makeSession()
session.id = createSessionID(id)
session.id = createSessionID(key)
session.projectID = projectID
@@ -1,8 +1,9 @@
import * as vscode from "vscode"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import { getMigrationErrorMessage } from "../errors/migration-error"
import type { MigrationSessionInfo, MigrationSessionProgress, MigrationSessionSelection } from "../legacy-types"
import type { MigrationSessionProgress, MigrationSessionSelection } from "../legacy-types"
import { createSessionID } from "./lib/ids"
import type { SessionSource } from "../task-store"
import type { LegacyHistoryItem } from "./lib/legacy-types"
import { parseSession } from "./parser"
@@ -30,19 +31,19 @@ export async function migrate(
input: MigrationSessionSelection,
context: vscode.ExtensionContext,
client: KiloClient,
meta?: {
session: MigrationSessionInfo
index: number
total: number
},
onProgress?: ProgressCallback,
resolved?: SessionSource,
): Promise<Result> {
const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath
const items = context.globalState.get<LegacyHistoryItem[]>("taskHistory", [])
const item = items.find((item) => item.id === input.id)
const source = resolved ?? {
id: input.id,
dir: vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath,
item: items.find((item) => item.id === input.id),
}
const key = source.namespace ? `${source.namespace}:${source.id}` : source.id
const progress = (next: Progress) => {
if (!meta || !onProgress) return
if (!onProgress) return
onProgress(next)
}
@@ -68,12 +69,12 @@ export async function migrate(
try {
if (!input.force) {
const result = await client.session.get({ sessionID: createSessionID(input.id) })
const result = await client.session.get({ sessionID: createSessionID(key) })
if (result.data) return skip()
}
progress({ phase: "preparing" })
const payload = await parseSession(input.id, dir, item)
const payload = await parseSession(source.id, source.dir, source.item, undefined, key)
progress({ phase: "storing" })
const project = await client.kilocode.sessionImport.project(payload.project, { throwOnError: true })
const projectID = project.data?.id ?? payload.project.id
@@ -1,4 +1,4 @@
import type { LegacyHistoryItem } from "./lib/legacy-types"
import type { LegacyApiMessage, LegacyHistoryItem } from "./lib/legacy-types"
import type {
KilocodeSessionImportMessageData as Message,
KilocodeSessionImportPartData as Part,
@@ -19,15 +19,20 @@ export interface NormalizedSession {
parts: Array<NonNullable<Part["body"]>>
}
export async function parseSession(id: string, dir: string, item?: LegacyHistoryItem): Promise<NormalizedSession> {
export async function parseSession(
id: string,
dir: string,
item?: LegacyHistoryItem,
input?: LegacyApiMessage[],
key = id,
): Promise<NormalizedSession> {
const root = await normalizeLegacyPath(item?.workspace)
const next = item ? { ...item, workspace: root } : undefined
const project = createProject(next)
const session = createSession(id, next, project.id, root)
const file = await getApiConversationHistory(id, dir)
const conversation = parseFile(file)
const messages = parseMessagesFromConversation(conversation, id, root, next)
const parts = parsePartsFromConversation(conversation, id, item)
const session = createSession(id, next, project.id, root, key)
const conversation = input ?? parseFile(await getApiConversationHistory(id, dir))
const messages = parseMessagesFromConversation(conversation, key, root, next)
const parts = parsePartsFromConversation(conversation, key, item)
return {
project,
@@ -0,0 +1,244 @@
import * as path from "node:path"
import * as vscode from "vscode"
import type { MigrationSessionInfo } from "./legacy-types"
import type { LegacyHistoryItem } from "./sessions/lib/legacy-types"
const API_FILE = "api_conversation_history.json"
const UI_FILE = "ui_messages.json"
export interface SessionSource {
id: string
dir: string
item?: LegacyHistoryItem
namespace?: string
mtime?: number
}
export interface SessionEntry {
id: string
session: MigrationSessionInfo
source: SessionSource
}
export type SessionCatalog = Map<string, SessionEntry>
export interface ScanDiagnostic {
id: string
dir: string
reason: "ui-only" | "malformed" | "missing-workspace"
}
export interface TaskScan {
catalog: SessionCatalog
diagnostics: ScanDiagnostic[]
}
export function listSessions(catalog: SessionCatalog) {
return [...catalog.values()].map((entry) => entry.session).sort((a, b) => b.time - a.time || a.id.localeCompare(b.id))
}
export function resolveSession(catalog: SessionCatalog, id: string) {
return catalog.get(id)?.source
}
export type ScanMode = "history" | "discover"
export interface ScanOptions {
namespace?: string
/**
* "history" trusts the provided history items and only checks that each task's
* conversation file still exists (cheap stat, used by legacy migration).
* "discover" enumerates every task directory on disk and parses conversation
* files to recover titles (used when no history is available, e.g. Roo import).
*/
mode?: ScanMode
}
export async function scanTaskStore(
dir: string,
items: LegacyHistoryItem[] = [],
options: ScanOptions = {},
): Promise<TaskScan> {
const mode = options.mode ?? (items.length ? "history" : "discover")
return mode === "history"
? scanFromHistory(dir, items, options.namespace)
: scanFromDisk(dir, items, options.namespace)
}
/** Builds a catalog from known history items, only confirming each conversation file exists. */
async function scanFromHistory(dir: string, items: LegacyHistoryItem[], namespace?: string): Promise<TaskScan> {
const catalog: SessionCatalog = new Map()
for (const item of items) {
if (catalog.has(item.id)) continue
if (!(await exists(path.join(dir, item.id, API_FILE)))) continue
catalog.set(item.id, {
id: item.id,
session: {
id: item.id,
title: item.task?.trim() || fallbackTitle(item.id),
directory: item.workspace?.trim() || "",
time: item.ts ?? timestamp(item.id),
},
source: { id: item.id, dir, item, namespace },
})
}
return { catalog, diagnostics: [] }
}
/** Enumerates every task directory on disk and parses conversation files to recover titles. */
async function scanFromDisk(dir: string, items: LegacyHistoryItem[], namespace?: string): Promise<TaskScan> {
const entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dir)).then(
(value) => value,
() => [] as [string, vscode.FileType][],
)
const history = new Map(items.map((item) => [item.id, item]))
const catalog: SessionCatalog = new Map()
const diagnostics: ScanDiagnostic[] = []
for (const [id, type] of entries.sort(([a], [b]) => a.localeCompare(b))) {
if (type !== vscode.FileType.Directory) continue
const root = path.join(dir, id)
const api = path.join(root, API_FILE)
const valid = await readHistory(api)
if (!valid.exists) {
if (await exists(path.join(root, UI_FILE))) diagnostics.push({ id, dir, reason: "ui-only" })
continue
}
if (!valid.valid) {
diagnostics.push({ id, dir, reason: "malformed" })
continue
}
const item = await readHistoryItem(root, id, valid.data, history.get(id))
const workspace = item.workspace?.trim()
if (!workspace) {
diagnostics.push({ id, dir, reason: "missing-workspace" })
continue
}
catalog.set(id, {
id,
session: {
id,
title: item.task?.trim() || fallbackTitle(id),
directory: workspace,
time: item.ts ?? timestamp(id),
},
source: { id, dir, item, namespace, mtime: valid.mtime },
})
}
return { catalog, diagnostics }
}
export async function readTaskIndex(dir: string): Promise<LegacyHistoryItem[]> {
return Promise.resolve(vscode.workspace.fs.readFile(vscode.Uri.file(path.join(dir, "_index.json"))))
.then((bytes) => {
const json = JSON.parse(Buffer.from(bytes).toString("utf8")) as { entries?: unknown }
if (!Array.isArray(json.entries)) return []
return json.entries.flatMap((value) => {
if (!value || typeof value !== "object") return []
const item = value as Record<string, unknown>
if (typeof item.id !== "string") return []
return [parseRecord(item, item.id)]
})
})
.catch(() => [])
}
async function readHistory(file: string) {
const uri = vscode.Uri.file(file)
return Promise.all([vscode.workspace.fs.readFile(uri), vscode.workspace.fs.stat(uri)])
.then(([bytes, stat]) => {
const json = JSON.parse(Buffer.from(bytes).toString("utf8")) as unknown
return {
exists: true as const,
valid: Array.isArray(json),
data: Array.isArray(json) ? json : [],
mtime: stat.mtime,
}
})
.catch((error) => {
if (error instanceof SyntaxError) {
return { exists: true as const, valid: false, data: [] as unknown[], mtime: 0 }
}
return { exists: false as const, valid: false, data: [] as unknown[], mtime: 0 }
})
}
async function readHistoryItem(
root: string,
id: string,
messages: unknown[],
indexed?: LegacyHistoryItem,
): Promise<LegacyHistoryItem> {
const file = path.join(root, "history_item.json")
const stored = await Promise.resolve(vscode.workspace.fs.readFile(vscode.Uri.file(file)))
.then((bytes) => parseItem(Buffer.from(bytes).toString("utf8"), id))
.catch(() => undefined)
if (stored) return stored
if (indexed) return indexed
return {
id,
task: titleFromMessages(messages) || fallbackTitle(id),
workspace: "",
ts: timestamp(id),
}
}
function parseItem(input: string, id: string): LegacyHistoryItem | undefined {
return parseRecord(JSON.parse(input) as Record<string, unknown>, id)
}
function parseRecord(json: Record<string, unknown>, id: string): LegacyHistoryItem {
return {
id,
task: typeof json.task === "string" ? json.task.trim().slice(0, 120) : fallbackTitle(id),
workspace: typeof json.workspace === "string" ? json.workspace : "",
ts: typeof json.ts === "number" ? json.ts : timestamp(id),
mode: typeof json.mode === "string" ? json.mode : undefined,
rootTaskId: typeof json.rootTaskId === "string" ? json.rootTaskId : undefined,
parentTaskId: typeof json.parentTaskId === "string" ? json.parentTaskId : undefined,
}
}
function titleFromMessages(messages: unknown[]) {
for (const value of messages) {
if (!value || typeof value !== "object") continue
const msg = value as { role?: string; content?: unknown }
if (msg.role !== "user") continue
const text = textFromContent(msg.content)
if (text) return text.slice(0, 120).replace(/\n/g, " ").trim()
}
return ""
}
function textFromContent(content: unknown): string {
if (typeof content === "string") return content
if (!Array.isArray(content)) return ""
for (const value of content) {
if (!value || typeof value !== "object") continue
const block = value as { type?: string; text?: unknown }
if (block.type === "text" && typeof block.text === "string" && block.text.trim()) return block.text
}
return ""
}
function exists(file: string) {
return vscode.workspace.fs.stat(vscode.Uri.file(file)).then(
() => true,
() => false,
)
}
function timestamp(id: string) {
const value = Number(id)
return Number.isFinite(value) && value > 1_000_000_000_000 ? value : 0
}
function fallbackTitle(id: string) {
const time = timestamp(id)
return time ? new Date(time).toLocaleString() : id
}
@@ -0,0 +1,64 @@
import * as path from "node:path"
import * as vscode from "vscode"
import {
listSessions,
readTaskIndex,
scanTaskStore,
type ScanDiagnostic,
type SessionCatalog,
type SessionEntry,
} from "../legacy-migration/task-store"
const ROOTS = [
"roovscode.roo-cline",
"roovscode.roo-code",
"rooveterinaryinc.roo-cline",
"rooveterinaryinc.roo-code",
"rooveterinaryinc.roo-code-nightly",
]
export interface RooImportSource {
catalog: SessionCatalog
sessions: ReturnType<typeof listSessions>
diagnostics: ScanDiagnostic[]
}
/** Scans every known Roo storage root and keeps the most recent, complete copy of duplicate task IDs. */
export async function detectRooCodeSessions(
context: vscode.ExtensionContext,
customPath?: string,
): Promise<RooImportSource | null> {
const parent = path.dirname(context.globalStorageUri.fsPath)
const configured = customPath ?? vscode.workspace.getConfiguration("roo-cline").get<unknown>("customStoragePath", "")
const custom = typeof configured === "string" ? configured.trim() : ""
const roots = [
...ROOTS.map((root) => path.join(parent, root, "tasks")),
...(custom ? [path.join(custom, "tasks")] : []),
]
const dirs = [...new Map(roots.map((dir) => [path.resolve(dir), dir])).values()]
const catalog: SessionCatalog = new Map()
const diagnostics: ScanDiagnostic[] = []
for (const dir of dirs) {
const items = await readTaskIndex(dir)
const scan = await scanTaskStore(dir, items, { namespace: "roo", mode: "discover" })
diagnostics.push(...scan.diagnostics)
for (const [id, entry] of [...scan.catalog].sort(([a], [b]) => a.localeCompare(b))) {
const current = catalog.get(id)
if (!current || compare(entry, current) >= 0) catalog.set(id, entry)
}
}
for (const diagnostic of diagnostics) {
console.warn(`[Kilo New] Roo import skipped ${diagnostic.reason} task ${diagnostic.id} in ${diagnostic.dir}`)
}
const sessions = listSessions(catalog)
return sessions.length ? { catalog, sessions, diagnostics } : null
}
function compare(a: SessionEntry, b: SessionEntry) {
const complete = (entry: SessionEntry) =>
Number(Boolean(entry.source.item?.task?.trim())) + Number(Boolean(entry.source.item?.mode))
return (a.source.mtime ?? 0) - (b.source.mtime ?? 0) || a.session.time - b.session.time || complete(a) - complete(b)
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "7.3.46",
"version": "7.3.49",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
@@ -123,6 +123,12 @@ const mockVscode = {
Workspace: 2,
WorkspaceFolder: 3,
},
FileType: {
Unknown: 0,
File: 1,
Directory: 2,
SymbolicLink: 64,
},
TabInputText: class {
constructor(public uri: { scheme: string; fsPath: string }) {}
},
@@ -0,0 +1,87 @@
import { describe, expect, it } from "bun:test"
import {
getMigrationCache,
handleRequestMigrationData,
handleStartMigration,
type MigrationCache,
type MigrationCacheEntry,
type MigrationContext,
} from "../../../src/kilo-provider/handlers/migration"
const legacy = {
hasData: false,
providers: [],
mcpServers: [],
customModes: [],
}
function makeContext(cache: MigrationCache): MigrationContext {
return {
client: null,
extensionContext: { globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } },
postMessage: () => {},
refreshSessions: () => {},
migrationCache: cache,
migrationCheckInFlight: false,
disposeGlobal: async () => {},
broadcastComplete: () => {},
} as unknown as MigrationContext
}
describe("migration cache", () => {
it("isolates entries by operation and source", () => {
const cache: MigrationCache = new Map()
const entry: MigrationCacheEntry = { operationId: "new", source: "legacy", data: legacy }
cache.set("new", entry)
expect(getMigrationCache(cache, "legacy", "new")).toBe(entry)
expect(getMigrationCache(cache, "roo", "new")).toBeUndefined()
expect(getMigrationCache(cache, "legacy", "stale")).toBeUndefined()
})
it("retains an empty Roo discovery for its operation", () => {
const cache: MigrationCache = new Map()
const entry: MigrationCacheEntry = { operationId: "empty", source: "roo", data: null }
cache.set("empty", entry)
expect(getMigrationCache(cache, "roo", "empty")).toBe(entry)
expect(getMigrationCache(cache, "roo", "empty")?.data).toBeNull()
})
it("drops entries from abandoned operations when a new request arrives", async () => {
const cache: MigrationCache = new Map()
cache.set("abandoned", { operationId: "abandoned", source: "roo", data: null })
await handleRequestMigrationData(makeContext(cache), "roo", "fresh")
expect(cache.has("abandoned")).toBe(false)
expect(getMigrationCache(cache, "roo", "fresh")).toBeDefined()
})
it("evicts an operation's entry once the migration completes", async () => {
const cache: MigrationCache = new Map()
cache.set("op", { operationId: "op", source: "roo", data: null })
await handleStartMigration(makeContext(cache), "roo", "op", {
providers: [],
mcpServers: [],
customModes: [],
sessions: [],
defaultModel: false,
settings: {
autoApproval: {
commandRules: false,
readPermission: false,
writePermission: false,
executePermission: false,
mcpPermission: false,
taskPermission: false,
},
language: false,
autocomplete: false,
},
})
expect(cache.has("op")).toBe(false)
})
})
@@ -0,0 +1,36 @@
import { describe, expect, it } from "bun:test"
import { runSessionBatch } from "../../../src/legacy-migration/session-batch"
const sessions = [
{ id: "one", title: "One", directory: "/repo", time: 2 },
{ id: "two", title: "Two", directory: "/repo", time: 1 },
]
describe("migration session batch", () => {
it("uses consistent progress, skipped, error, and summary semantics", async () => {
const progress: unknown[] = []
const phases: unknown[] = []
const results = await runSessionBatch({
selections: [{ id: "one" }, { id: "two" }],
sessions,
resolve: (id) => ({ id, dir: "/tasks" }),
migrate: async (selection) =>
selection.id === "one" ? { ok: true, skipped: true } : { ok: false, message: "broken" },
onProgress: (...args) => progress.push(args),
onSessionProgress: (value) => phases.push(value),
delay: async () => undefined,
})
expect(results).toEqual([
{ item: "One", category: "session", status: "warning", message: "Already imported." },
{ item: "Two", category: "session", status: "error", message: "broken" },
])
expect(progress).toEqual([
["one", "migrating"],
["one", "warning", "Already imported."],
["two", "migrating"],
["two", "error", "broken"],
])
expect(phases.at(-1)).toMatchObject({ session: sessions[1], index: 2, total: 2, phase: "summary" })
})
})
@@ -0,0 +1,49 @@
import { afterEach, describe, expect, it } from "bun:test"
import * as vscode from "vscode"
import { listSessions, resolveSession, scanTaskStore } from "../../../src/legacy-migration/task-store"
type Fs = typeof vscode.workspace.fs
const fs = vscode.workspace.fs as Fs
const original = { readDirectory: fs.readDirectory, readFile: fs.readFile, stat: fs.stat }
const dir = "/storage/kilocode.kilo-code/tasks"
const api = (id: string) => `${dir}/${id}/api_conversation_history.json`
describe("task store history scan", () => {
afterEach(() => {
fs.readDirectory = original.readDirectory
fs.readFile = original.readFile
fs.stat = original.stat
})
it("includes only history items whose conversation file exists, without scanning or parsing disk", async () => {
let listed = false
let read = false
fs.readDirectory = async () => {
listed = true
return []
}
fs.readFile = async () => {
read = true
throw new Error("history scan must not read files")
}
fs.stat = async (uri) => {
if (uri.fsPath === api("keep")) return { type: vscode.FileType.File, ctime: 0, mtime: 0, size: 1 }
throw new Error(`missing ${uri.fsPath}`)
}
const items = [
{ id: "keep", task: "Keep me", workspace: "/repo", ts: 1700000000000 },
{ id: "gone", task: "Deleted on disk", workspace: "/repo", ts: 1700000000001 },
]
const scan = await scanTaskStore(dir, items, { mode: "history" })
// "gone" is dropped (no file) and on-disk orphans are never considered (no enumeration).
expect(listSessions(scan.catalog)).toEqual([
{ id: "keep", title: "Keep me", directory: "/repo", time: 1700000000000 },
])
expect(resolveSession(scan.catalog, "keep")).toMatchObject({ id: "keep", dir })
expect(scan.diagnostics).toEqual([])
expect(listed).toBe(false)
expect(read).toBe(false)
})
})
@@ -0,0 +1,172 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
import * as vscode from "vscode"
import { createSessionID } from "../../src/legacy-migration/sessions/lib/ids"
import { parseSession } from "../../src/legacy-migration/sessions/parser"
import { detectRooCodeSessions } from "../../src/roo-import/service"
const enc = new TextEncoder()
const first = "/storage/roovscode.roo-cline/tasks"
const second = "/storage/rooveterinaryinc.roo-cline/tasks"
const customRoot = "/custom/roo/tasks"
const id = "1781613537275"
const other = "1781613537276"
const missing = "1781613537277"
const customId = "1781613537278"
const special = "__proto__"
type Fs = typeof vscode.workspace.fs
const fs = vscode.workspace.fs as Fs
const original = {
readDirectory: fs.readDirectory,
readFile: fs.readFile,
stat: fs.stat,
}
let files: Map<string, { value: string; mtime: number }>
let dirs: Map<string, string[]>
describe("roo import", () => {
beforeEach(() => {
files = new Map([
[
`${first}/${id}/api_conversation_history.json`,
{ value: JSON.stringify([{ role: "user", content: "First root" }]), mtime: 10 },
],
[
`${first}/${special}/api_conversation_history.json`,
{ value: JSON.stringify([{ role: "user", content: "Special" }]), mtime: 10 },
],
[
`${first}/${id}/history_item.json`,
{
value: JSON.stringify({ id, ts: Number(id), task: "First root", workspace: "/old", mode: "architect" }),
mtime: 10,
},
],
[
`${first}/_index.json`,
{
value: JSON.stringify({
version: 1,
entries: [{ id: special, ts: 1, task: "Special", workspace: "/indexed" }],
}),
mtime: 10,
},
],
[
`${second}/${id}/api_conversation_history.json`,
{ value: JSON.stringify([{ role: "user", content: "New root" }]), mtime: 20 },
],
[
`${second}/${id}/history_item.json`,
{ value: JSON.stringify({ id, ts: Number(id) + 1, task: "New root", workspace: "/new" }), mtime: 20 },
],
[
`${second}/_index.json`,
{
value: JSON.stringify({
version: 1,
entries: [{ id, ts: Number(id), task: "Stale index", workspace: "/stale" }],
}),
mtime: 10,
},
],
[`${second}/${other}/ui_messages.json`, { value: JSON.stringify([{ type: "say", text: "UI only" }]), mtime: 20 }],
[`${second}/bad/api_conversation_history.json`, { value: "not json", mtime: 20 }],
[
`${second}/${missing}/api_conversation_history.json`,
{ value: JSON.stringify([{ role: "user", content: "No workspace" }]), mtime: 20 },
],
[
`${customRoot}/${customId}/api_conversation_history.json`,
{ value: JSON.stringify([{ role: "user", content: "Custom root" }]), mtime: 30 },
],
[
`${customRoot}/_index.json`,
{
value: JSON.stringify({
version: 1,
entries: [{ id: customId, ts: Number(customId), task: "Custom root", workspace: "/custom-repo" }],
}),
mtime: 30,
},
],
])
dirs = new Map([
[first, [id, special]],
[second, [id, other, "bad", missing]],
[customRoot, [customId]],
])
fs.readDirectory = async (uri) => {
const entries = dirs.get(uri.fsPath)
if (entries) return entries.map((entry) => [entry, vscode.FileType.Directory])
throw new Error(`missing dir ${uri.fsPath}`)
}
fs.stat = async (uri) => {
const file = files.get(uri.fsPath)
if (file) return { type: vscode.FileType.File, ctime: 0, mtime: file.mtime, size: file.value.length }
throw new Error(`missing file ${uri.fsPath}`)
}
fs.readFile = async (uri) => {
const file = files.get(uri.fsPath)
if (file) return enc.encode(file.value)
throw new Error(`missing file ${uri.fsPath}`)
}
})
afterEach(() => {
fs.readDirectory = original.readDirectory
fs.readFile = original.readFile
fs.stat = original.stat
})
it("recovers indexed metadata, selects the newest duplicate, and diagnoses unusable tasks", async () => {
const source = await detectRooCodeSessions({ globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } } as never)
expect(source?.sessions).toEqual([
{ id, title: "New root", directory: "/new", time: Number(id) + 1 },
{ id: special, title: "Special", directory: "/indexed", time: 1 },
])
expect(source?.catalog.get(id)?.source).toMatchObject({ id, dir: second, namespace: "roo", mtime: 20 })
expect(source?.catalog.get(special)?.source).toMatchObject({ id: special, dir: first, namespace: "roo" })
expect(source?.diagnostics.map((item) => [item.id, item.reason])).toEqual([
[other, "ui-only"],
[missing, "missing-workspace"],
["bad", "malformed"],
])
})
it("discovers sessions in Roo's configured custom storage path", async () => {
const source = await detectRooCodeSessions(
{ globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } } as never,
"/custom/roo",
)
expect(source?.sessions).toContainEqual({
id: customId,
title: "Custom root",
directory: "/custom-repo",
time: Number(customId),
})
expect(source?.catalog.get(customId)?.source.dir).toBe(customRoot)
})
it("namespaces generated Roo IDs without changing the visible session slug", async () => {
const source = await detectRooCodeSessions({ globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } } as never)
const entry = source?.catalog.get(id)
expect(entry).toBeDefined()
const payload = await parseSession(
id,
entry!.source.dir,
entry!.source.item,
[{ role: "user", content: "Hi" }],
`roo:${id}`,
)
expect(payload.session.id).toBe(createSessionID(`roo:${id}`))
expect(payload.session.slug).toBe(id)
expect(payload.messages[0].sessionID).toBe(createSessionID(`roo:${id}`))
})
})
+9 -3
View File
@@ -201,6 +201,7 @@ const AppContent: Component = () => {
// legacy-migration: state-driven flag independent of currentView to avoid
// race conditions with SettingsEditorProvider's navigate messages.
const [migrationNeeded, setMigrationNeeded] = createSignal(false)
const [migrationSource, setMigrationSource] = createSignal<"legacy" | "roo">("legacy")
const session = useSession()
const server = useServer()
const vscode = useVSCode()
@@ -278,6 +279,7 @@ const AppContent: Component = () => {
// legacy-migration: state-driven migration wizard
if (message?.type === "migrationState") {
console.log("[Kilo New] App: 🔄 migrationState:", message.needed)
setMigrationSource(message.source)
setMigrationNeeded(message.needed)
}
}
@@ -338,9 +340,9 @@ const AppContent: Component = () => {
<Settings
tab={settingsTab()}
onTabChange={setSettingsTab}
onMigrateClick={() => {
onMigrationClick={(source) => {
setMigrationSource(source)
setMigrationNeeded(true)
vscode.postMessage({ type: "requestLegacyMigrationData" })
}}
/>
</Match>
@@ -350,7 +352,11 @@ const AppContent: Component = () => {
</Switch>
}
>
<MigrationWizard onBack={() => setMigrationNeeded(false)} onComplete={() => setMigrationNeeded(false)} />
<MigrationWizard
source={migrationSource()}
onBack={() => setMigrationNeeded(false)}
onComplete={() => setMigrationNeeded(false)}
/>
</Show>
{/* legacy-migration end */}
</div>
@@ -30,10 +30,11 @@ import type {
MigrationResultItem,
MigrationAutoApprovalSelections,
LegacySettings,
LegacyMigrationDataMessage,
LegacyMigrationProgressMessage,
LegacyMigrationSessionProgressMessage,
LegacyMigrationCompleteMessage,
MigrationDataMessage,
MigrationProgressMessage,
MigrationSessionProgressMessage,
MigrationCompleteMessage,
MigrationSource,
} from "../../types/messages"
import "./migration.css"
@@ -182,16 +183,36 @@ interface ProgressEntry {
// ---------------------------------------------------------------------------
export interface MigrationWizardProps {
source?: MigrationSource
operationId?: string
onBack: () => void
onComplete: () => void
}
const capabilities = {
legacy: {
intro: true,
persist: true,
cleanup: true,
categories: new Set(["providers", "mcpServers", "customModes", "sessions", "defaultModel", "settings"]),
},
roo: {
intro: false,
persist: false,
cleanup: false,
categories: new Set(["sessions"]),
},
} satisfies Record<MigrationSource, { intro: boolean; persist: boolean; cleanup: boolean; categories: Set<string> }>
const MigrationWizard: Component<MigrationWizardProps> = (props) => {
const vscode = useVSCode()
const dialog = useDialog()
const language = useLanguage()
const source = props.source ?? "legacy"
const config = capabilities[source]
const operationId = props.operationId ?? crypto.randomUUID()
const [screen, setScreen] = createSignal<Screen>("whats-new")
const [screen, setScreen] = createSignal<Screen>(config.intro ? "whats-new" : "migrate")
const [phase, setPhase] = createSignal<MigratePhase>("selecting")
// Data from extension
@@ -226,46 +247,49 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
// Message handling
// ---------------------------------------------------------------------------
const applyData = (data: MigrationDataMessage["data"]) => {
setProviders(config.categories.has("providers") ? data.providers : [])
setMcpServers(config.categories.has("mcpServers") ? data.mcpServers : [])
setCustomModes(config.categories.has("customModes") ? data.customModes : [])
setSessions(config.categories.has("sessions") ? (data.sessions ?? []) : [])
setDefaultModel(config.categories.has("defaultModel") ? data.defaultModel : undefined)
setLegacySettings(config.categories.has("settings") ? data.settings : undefined)
setMigrateProviders(
config.categories.has("providers") && data.providers.some((item) => item.supported && item.hasApiKey),
)
setMigrateMcpServers(config.categories.has("mcpServers") && data.mcpServers.length > 0)
setMigrateModes(config.categories.has("customModes") && data.customModes.length > 0)
setMigrateSessions(config.categories.has("sessions") && (data.sessions?.length ?? 0) > 0)
setMigrateDefaultModel(config.categories.has("defaultModel") && Boolean(data.defaultModel))
const settings = config.categories.has("settings") ? data.settings : undefined
if (!settings) return
setMigrateAutoApproval(
settings.autoApprovalEnabled !== undefined ||
Boolean(settings.allowedCommands?.length) ||
Boolean(settings.deniedCommands?.length) ||
settings.alwaysAllowReadOnly !== undefined ||
settings.alwaysAllowReadOnlyOutsideWorkspace !== undefined ||
settings.alwaysAllowWrite !== undefined ||
settings.alwaysAllowExecute !== undefined ||
settings.alwaysAllowMcp !== undefined ||
settings.alwaysAllowModeSwitch !== undefined ||
settings.alwaysAllowSubtasks !== undefined,
)
setMigrateLanguage(Boolean(settings.language))
setMigrateAutocomplete(Boolean(settings.autocomplete))
}
onMount(() => {
const handler = (event: MessageEvent) => {
const msg = event.data
if (msg?.type === "legacyMigrationData") {
const data = (msg as LegacyMigrationDataMessage).data
setProviders(data.providers)
setMcpServers(data.mcpServers)
setCustomModes(data.customModes)
setSessions(data.sessions ?? [])
setDefaultModel(data.defaultModel)
setLegacySettings(data.settings)
// Pre-select groups that have data
setMigrateProviders(data.providers.some((p) => p.supported && p.hasApiKey))
setMigrateMcpServers(data.mcpServers.length > 0)
setMigrateModes(data.customModes.length > 0)
setMigrateSessions((data.sessions?.length ?? 0) > 0)
setMigrateDefaultModel(Boolean(data.defaultModel))
const s = data.settings
if (s) {
setMigrateAutoApproval(
s.autoApprovalEnabled !== undefined ||
Boolean(s.allowedCommands?.length) ||
Boolean(s.deniedCommands?.length) ||
s.alwaysAllowReadOnly !== undefined ||
s.alwaysAllowReadOnlyOutsideWorkspace !== undefined ||
s.alwaysAllowWrite !== undefined ||
s.alwaysAllowExecute !== undefined ||
s.alwaysAllowMcp !== undefined ||
s.alwaysAllowModeSwitch !== undefined ||
s.alwaysAllowSubtasks !== undefined,
)
setMigrateLanguage(Boolean(s.language))
setMigrateAutocomplete(Boolean(s.autocomplete))
}
if (msg?.source !== source || msg?.operationId !== operationId) return
if (msg.type === "migrationData") {
applyData((msg as MigrationDataMessage).data)
}
if (msg?.type === "legacyMigrationProgress") {
const update = msg as LegacyMigrationProgressMessage
if (msg.type === "migrationProgress") {
const update = msg as MigrationProgressMessage
setProgressEntries((prev) => {
const existing = prev.findIndex((e) => e.item === update.item)
const entry: ProgressEntry = {
@@ -278,8 +302,8 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
})
}
if (msg?.type === "legacyMigrationSessionProgress") {
const update = msg as LegacyMigrationSessionProgressMessage
if (msg.type === "migrationSessionProgress") {
const update = msg as MigrationSessionProgressMessage
setSessionSummary((prev) =>
updateSessionSummary(prev, createSessionItem(update.session, update.error), update.phase),
)
@@ -292,8 +316,8 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
})
}
if (msg?.type === "legacyMigrationComplete") {
const complete = msg as LegacyMigrationCompleteMessage
if (msg.type === "migrationComplete") {
const complete = msg as MigrationCompleteMessage
setResults(complete.results)
setRunning(false)
const hasErrors = complete.results.some((r) => r.status === "error")
@@ -305,7 +329,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
}
window.addEventListener("message", handler)
vscode.postMessage({ type: "requestLegacyMigrationData" })
vscode.postMessage({ type: "requestMigrationData", source, operationId })
onCleanup(() => window.removeEventListener("message", handler))
})
@@ -314,105 +338,91 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
// ---------------------------------------------------------------------------
const handleSkip = () => {
if (!config.persist) {
props.onBack()
return
}
vscode.postMessage({ type: "skipLegacyMigration" })
props.onBack()
}
const handleStartMigration = () => {
const selectedProviderNames = migrateProviders()
const emptyAutoApproval = (): MigrationAutoApprovalSelections => ({
commandRules: false,
readPermission: false,
writePermission: false,
executePermission: false,
mcpPermission: false,
taskPermission: false,
})
const autoApprovalSelections = (): MigrationAutoApprovalSelections => {
if (!migrateAutoApproval()) return emptyAutoApproval()
const settings = legacySettings()
return {
commandRules:
settings?.autoApprovalEnabled !== undefined ||
Boolean(settings?.allowedCommands?.length) ||
Boolean(settings?.deniedCommands?.length),
readPermission:
settings?.alwaysAllowReadOnly !== undefined || settings?.alwaysAllowReadOnlyOutsideWorkspace !== undefined,
writePermission: settings?.alwaysAllowWrite !== undefined,
executePermission: settings?.alwaysAllowExecute !== undefined,
mcpPermission: settings?.alwaysAllowMcp !== undefined,
taskPermission: settings?.alwaysAllowModeSwitch !== undefined || settings?.alwaysAllowSubtasks !== undefined,
}
}
const selections = (sessions: { id: string; force?: boolean }[] = []) => ({
providers: migrateProviders()
? providers()
.filter((p) => p.supported && p.hasApiKey)
.map((p) => p.profileName)
: []
const selectedMcpNames = migrateMcpServers() ? mcpServers().map((s) => s.name) : []
const selectedModesSlugs = migrateModes() ? customModes().map((m) => m.slug) : []
.filter((provider) => provider.supported && provider.hasApiKey)
.map((provider) => provider.profileName)
: [],
mcpServers: migrateMcpServers() ? mcpServers().map((server) => server.name) : [],
customModes: migrateModes() ? customModes().map((mode) => mode.slug) : [],
sessions,
defaultModel: migrateDefaultModel(),
settings: {
autoApproval: autoApprovalSelections(),
language: migrateLanguage(),
autocomplete: migrateAutocomplete(),
},
})
const autoApproval: MigrationAutoApprovalSelections = migrateAutoApproval()
? {
commandRules:
legacySettings()?.autoApprovalEnabled !== undefined ||
Boolean(legacySettings()?.allowedCommands?.length) ||
Boolean(legacySettings()?.deniedCommands?.length),
readPermission:
legacySettings()?.alwaysAllowReadOnly !== undefined ||
legacySettings()?.alwaysAllowReadOnlyOutsideWorkspace !== undefined,
writePermission: legacySettings()?.alwaysAllowWrite !== undefined,
executePermission: legacySettings()?.alwaysAllowExecute !== undefined,
mcpPermission: legacySettings()?.alwaysAllowMcp !== undefined,
taskPermission:
legacySettings()?.alwaysAllowModeSwitch !== undefined ||
legacySettings()?.alwaysAllowSubtasks !== undefined,
}
: {
commandRules: false,
readPermission: false,
writePermission: false,
executePermission: false,
mcpPermission: false,
taskPermission: false,
}
// Build progress entries
const handleStartMigration = () => {
const selected = selections(migrateSessions() ? sessions().map((session) => ({ id: session.id })) : [])
const approval = selected.settings.autoApproval
const entries: ProgressEntry[] = [
...selectedProviderNames.map((name) => ({ item: name, group: "providers", status: "pending" as const })),
...selectedMcpNames.map((name) => ({ item: name, group: "mcpServers", status: "pending" as const })),
...selectedModesSlugs.map((slug) => {
const mode = customModes().find((m) => m.slug === slug)
return { item: mode?.name ?? slug, group: "customModes", status: "pending" as const }
}),
...(migrateSessions()
? sessions().map((session) => ({ item: session.id, group: "sessions", status: "pending" as const }))
: []),
...(migrateDefaultModel() && defaultModel()
...selected.providers.map((item) => ({ item, group: "providers", status: "pending" as const })),
...selected.mcpServers.map((item) => ({ item, group: "mcpServers", status: "pending" as const })),
...selected.customModes.map((item) => ({ item, group: "customModes", status: "pending" as const })),
...selected.sessions.map((session) => ({ item: session.id, group: "sessions", status: "pending" as const })),
...(selected.defaultModel && defaultModel()
? [{ item: "Default model", group: "defaultModel", status: "pending" as const }]
: []),
...(autoApproval.commandRules
? [{ item: "Command rules", group: "autoApproval", status: "pending" as const }]
: []),
...(autoApproval.readPermission
? [{ item: "Read permission", group: "autoApproval", status: "pending" as const }]
: []),
...(autoApproval.writePermission
? [{ item: "Write permission", group: "autoApproval", status: "pending" as const }]
: []),
...(autoApproval.executePermission
? [{ item: "Execute permission", group: "autoApproval", status: "pending" as const }]
: []),
...(autoApproval.mcpPermission
? [{ item: "MCP permission", group: "autoApproval", status: "pending" as const }]
: []),
...(autoApproval.taskPermission
? [{ item: "Task permission", group: "autoApproval", status: "pending" as const }]
: []),
...(migrateLanguage() && legacySettings()?.language
...(
[
[approval.commandRules, "Command rules"],
[approval.readPermission, "Read permission"],
[approval.writePermission, "Write permission"],
[approval.executePermission, "Execute permission"],
[approval.mcpPermission, "MCP permission"],
[approval.taskPermission, "Task permission"],
] as const
).flatMap(([enabled, item]) => (enabled ? [{ item, group: "autoApproval", status: "pending" as const }] : [])),
...(selected.settings.language && legacySettings()?.language
? [{ item: "Language preference", group: "language", status: "pending" as const }]
: []),
...(migrateAutocomplete() && legacySettings()?.autocomplete
...(selected.settings.autocomplete && legacySettings()?.autocomplete
? [{ item: "Autocomplete settings", group: "autocomplete", status: "pending" as const }]
: []),
]
setProgressEntries(entries)
setSessionSummary(createSessionSummary())
setSessionProgress(undefined)
setRunning(true)
setPhase("migrating")
vscode.postMessage({
type: "startLegacyMigration",
selections: {
providers: selectedProviderNames,
mcpServers: selectedMcpNames,
customModes: selectedModesSlugs,
sessions: migrateSessions() ? sessions().map((session) => ({ id: session.id })) : [],
defaultModel: migrateDefaultModel(),
settings: {
autoApproval,
language: migrateLanguage(),
autocomplete: migrateAutocomplete(),
},
},
})
vscode.postMessage({ type: "startMigration", source, operationId, selections: selected })
}
const handleForceReimport = (ids: string[]) => {
@@ -424,13 +434,14 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
setPhase("migrating")
setSessionProgress(undefined)
setResults((prev) => prev.filter((item) => item.category !== "session"))
setProgressEntries((prev) => {
const keep = prev.filter((item) => item.group !== "sessions")
const next = ids.map((id) => ({ item: id, group: "sessions", status: "pending" as const }))
return [...keep, ...next]
})
setProgressEntries((prev) => [
...prev.filter((item) => item.group !== "sessions"),
...ids.map((item) => ({ item, group: "sessions", status: "pending" as const })),
])
vscode.postMessage({
type: "startLegacyMigration",
type: "startMigration",
source,
operationId,
selections: {
providers: [],
mcpServers: [],
@@ -438,14 +449,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
sessions: ids.map((id) => ({ id, force: true })),
defaultModel: false,
settings: {
autoApproval: {
commandRules: false,
readPermission: false,
writePermission: false,
executePermission: false,
mcpPermission: false,
taskPermission: false,
},
autoApproval: emptyAutoApproval(),
language: false,
autocomplete: false,
},
@@ -458,12 +462,18 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
}
const handleDone = () => {
if (!config.persist) {
vscode.postMessage({ type: "loadSessions" })
props.onComplete()
return
}
if (running()) {
dialog.show(() => (
<RunningMigrationDialog
onConfirm={() => {
vscode.postMessage({ type: "finalizeLegacyMigration" })
if (clearLegacyData()) {
if (config.cleanup && clearLegacyData()) {
vscode.postMessage({ type: "clearLegacyData" })
}
props.onComplete()
@@ -473,7 +483,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
return
}
vscode.postMessage({ type: "finalizeLegacyMigration" })
if (clearLegacyData()) {
if (config.cleanup && clearLegacyData()) {
vscode.postMessage({ type: "clearLegacyData" })
}
props.onComplete()
@@ -916,7 +926,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
<button
type="button"
class="migration-wizard__btn migration-wizard__btn--ghost"
onClick={() => setScreen("whats-new")}
onClick={() => (config.intro ? setScreen("whats-new") : props.onBack())}
>
{language.t("common.goBack")}
</button>
@@ -5,14 +5,14 @@ import { showToast } from "@kilocode/kilo-ui/toast"
import { useLanguage } from "../../context/language"
import { useVSCode } from "../../context/vscode"
import { useConfig } from "../../context/config"
import type { Config, ConnectionState, ExtensionMessage } from "../../types/messages"
import type { Config, ConnectionState, ExtensionMessage, MigrationSource } from "../../types/messages"
import { buildExport, parseImport, MAX_IMPORT_SIZE } from "./settings-io"
export interface AboutKiloCodeTabProps {
port: number | null
connectionState: ConnectionState
extensionVersion?: string
onMigrateClick?: () => void // legacy-migration
onMigrationClick?: (source: MigrationSource) => void // legacy-migration
}
const AboutKiloCodeTab: Component<AboutKiloCodeTabProps> = (props) => {
@@ -328,21 +328,19 @@ const AboutKiloCodeTab: Component<AboutKiloCodeTabProps> = (props) => {
>
{language.t("settings.aboutKiloCode.legacyMigration.description")}
</p>
<button
type="button"
onClick={() => props.onMigrateClick?.()}
style={{
background: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
border: "none",
padding: "6px 14px",
"border-radius": "2px",
cursor: "pointer",
"font-size": "var(--kilo-font-size-12)",
}}
>
{language.t("settings.legacyMigration.link")}
</button>
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
<Button variant="secondary" size="small" onClick={() => props.onMigrationClick?.("legacy")}>
{language.t("settings.legacyMigration.link")}
</Button>
<Button
variant="secondary"
size="small"
onClick={() => props.onMigrationClick?.("roo")}
title={language.t("settings.aboutKiloCode.rooImport.description")}
>
{language.t("settings.aboutKiloCode.rooImport.button")}
</Button>
</div>
</div>
{/* legacy-migration end */}
@@ -24,11 +24,12 @@ import LanguageTab from "./LanguageTab"
import AboutKiloCodeTab from "./AboutKiloCodeTab"
import IndexingTab from "./IndexingTab"
import { useServer } from "../../context/server"
import type { MigrationSource } from "../../types/messages"
export interface SettingsProps {
tab?: string
onTabChange?: (tab: string) => void
onMigrateClick?: () => void // legacy-migration
onMigrationClick?: (source: MigrationSource) => void // legacy-migration
}
const Settings: Component<SettingsProps> = (props) => {
@@ -275,7 +276,7 @@ const Settings: Component<SettingsProps> = (props) => {
port={server.serverInfo()?.port ?? null}
connectionState={server.connectionState()}
extensionVersion={server.extensionVersion()}
onMigrateClick={props.onMigrateClick}
onMigrationClick={props.onMigrationClick}
/>
</Tabs.Content>
</Tabs>
@@ -11,16 +11,6 @@ import {
import { useVSCode } from "./vscode"
import type { KilocodeNotification, ExtensionMessage } from "../types/messages"
// Static notifications always shown unconditionally (not fetched from API)
const STATIC_NOTIFICATIONS: KilocodeNotification[] = [
{
id: "star-giveaway-june-2026",
title: "GitHub Star Giveaway",
message: "We're giving away $500 of AI Credits when we reach 25,000 stars on GitHub. Support us:",
action: { actionText: "github.com/Kilo-Org/kilocode", actionURL: "https://github.com/Kilo-Org/kilocode/" },
},
]
interface NotificationsContextValue {
notifications: Accessor<KilocodeNotification[]>
filteredNotifications: Accessor<KilocodeNotification[]>
@@ -63,8 +53,7 @@ export const NotificationsProvider: ParentComponent = (props) => {
const filteredNotifications = createMemo(() => {
const dismissed = dismissedIds()
const all = [...STATIC_NOTIFICATIONS, ...notifications()]
return all.filter((n) => !dismissed.includes(n.id))
return notifications().filter((n) => !dismissed.includes(n.id))
})
const dismiss = (id: string) => {
+2
View File
@@ -1520,6 +1520,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "ترحيل الإصدار القديم",
"settings.aboutKiloCode.legacyMigration.description":
"ترحيل الإعدادات من تثبيت سابق لـ Kilo Code، بما في ذلك مفاتيح API لمقدمي الخدمة والنموذج الافتراضي.",
"settings.aboutKiloCode.rooImport.description": "استيراد سجل المحادثات من تثبيت Roo Code.",
"settings.aboutKiloCode.rooImport.button": "استيراد الجلسات من Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "ما الجديد في Kilo Code",
+2
View File
@@ -1565,6 +1565,8 @@ 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.",
"settings.aboutKiloCode.rooImport.description": "Importe o histórico de conversas de uma instalação do Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Importar sessões do Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "O Que Há de Novo no Kilo Code",
+2
View File
@@ -1559,6 +1559,8 @@ 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.",
"settings.aboutKiloCode.rooImport.description": "Uvezite historiju razgovora iz instalacije Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Uvezi sesije iz Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Šta je novo u Kilo Code",
+2
View File
@@ -1549,6 +1549,8 @@ 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.",
"settings.aboutKiloCode.rooImport.description": "Importer samtalehistorik fra en installation af Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Importer sessioner fra Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Nyheder i Kilo Code",
+3
View File
@@ -1580,6 +1580,9 @@ 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.",
"settings.aboutKiloCode.rooImport.description":
"Importieren Sie den Konversationsverlauf von einer Installation von Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Sitzungen aus Roo Code importieren",
// Screen 1 — What's New
"migration.whatsNew.title": "Neuigkeiten in Kilo Code",
@@ -1546,6 +1546,8 @@ 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.",
"settings.aboutKiloCode.rooImport.description": "Import conversation history from a Roo Code installation.",
"settings.aboutKiloCode.rooImport.button": "Import Sessions from Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "What's New in Kilo Code",
+3
View File
@@ -1572,6 +1572,9 @@ 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.",
"settings.aboutKiloCode.rooImport.description":
"Importa el historial de conversaciones de una instalación de Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Importar sesiones desde Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Novedades en Kilo Code",
+3
View File
@@ -1592,6 +1592,9 @@ 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.",
"settings.aboutKiloCode.rooImport.description":
"Importer l'historique des conversations depuis une installation de Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Importer les sessions depuis Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Nouveautés de Kilo Code",
+3
View File
@@ -1383,6 +1383,9 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Migrazione legacy",
"settings.aboutKiloCode.legacyMigration.description":
"Migra le impostazioni da una precedente installazione di Kilo Code, incluse API key dei provider e modello predefinito.",
"settings.aboutKiloCode.rooImport.description":
"Importa la cronologia delle conversazioni da una installazione di Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Importa sessioni da Roo Code",
"migration.whatsNew.title": "Novità in Kilo Code",
"migration.whatsNew.subtitle": "Abbiamo ricostruito l'estensione su fondamenta più veloci ed efficienti.",
"migration.whatsNew.features.performance.title": "Prestazioni agente più rapide",
+2
View File
@@ -1543,6 +1543,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "レガシー移行",
"settings.aboutKiloCode.legacyMigration.description":
"プロバイダーのAPIキーやデフォルトモデルなど、以前にインストールしたKilo Codeから設定を移行します。",
"settings.aboutKiloCode.rooImport.description": "インストールしたRoo Codeから会話履歴を移行します。",
"settings.aboutKiloCode.rooImport.button": "Roo Codeからセッションをインポート",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Codeの新機能",
+2
View File
@@ -1527,6 +1527,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "레거시 마이그레이션",
"settings.aboutKiloCode.legacyMigration.description":
"공급자 API 키 및 기본 모델을 포함하여 이전 Kilo Code 설치에서 설정을 마이그레이션합니다.",
"settings.aboutKiloCode.rooImport.description": "Roo Code 설치에서 대화 기록을 가져옵니다.",
"settings.aboutKiloCode.rooImport.button": "Roo Code에서 세션 가져오기",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Code의 새로운 기능",
+2
View File
@@ -1540,6 +1540,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Legacy Migratie",
"settings.aboutKiloCode.legacyMigration.description":
"Migreer instellingen van een eerdere installatie van Kilo Code, inclusief provider API-sleutels en standaard model.",
"settings.aboutKiloCode.rooImport.description": "Importeer gespreksgeschiedenis van een Roo Code installatie.",
"settings.aboutKiloCode.rooImport.button": "Sessies importeren uit Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Wat is er Nieuw in Kilo Code",
+2
View File
@@ -1546,6 +1546,8 @@ 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.",
"settings.aboutKiloCode.rooImport.description": "Importer samtalehistorikk fra en installasjon av Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Importer økter fra Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Hva er nytt i Kilo Code",
+2
View File
@@ -1556,6 +1556,8 @@ 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.",
"settings.aboutKiloCode.rooImport.description": "Importuj historię rozmów z instalacji Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Importuj sesje z Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Co nowego w Kilo Code",
+2
View File
@@ -1556,6 +1556,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Миграция с устаревшей версии",
"settings.aboutKiloCode.legacyMigration.description":
"Перенос настроек из предыдущей установки Kilo Code, включая API-ключи провайдеров и модель по умолчанию.",
"settings.aboutKiloCode.rooImport.description": "Импорт истории переписки из установки Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Импортировать сеансы из Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Что нового в Kilo Code",
+2
View File
@@ -1525,6 +1525,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "การย้ายข้อมูลจากเวอร์ชันดั้งเดิม",
"settings.aboutKiloCode.legacyMigration.description":
"ย้ายการตั้งค่าจากการติดตั้ง Kilo Code ก่อนหน้านี้ รวมถึงคีย์ API ของผู้ให้บริการและโมเดลเริ่มต้น",
"settings.aboutKiloCode.rooImport.description": "นำเข้าประวัติการสนทนาจากการติดตั้ง Roo Code",
"settings.aboutKiloCode.rooImport.button": "นำเข้าเซสชันจาก Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "มีอะไรใหม่ใน Kilo Code",
+2
View File
@@ -1528,6 +1528,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Eski Sürüm Taşıma",
"settings.aboutKiloCode.legacyMigration.description":
"Sağlayıcı API anahtarları ve varsayılan model dahil olmak üzere Kilo Code'un önceki yüklemesinden ayarları taşıyın.",
"settings.aboutKiloCode.rooImport.description": "Bir Roo Code yüklemesinden konuşma geçmişini içe aktarın.",
"settings.aboutKiloCode.rooImport.button": "Roo Code'dan Oturumları İçe Aktar",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Code'daki Yenilikler",
+2
View File
@@ -1525,6 +1525,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "Перенесення зі старої версії",
"settings.aboutKiloCode.legacyMigration.description":
"Перенести налаштування з попередньої версії Kilo Code, включаючи API-ключі провайдерів та стандартну модель.",
"settings.aboutKiloCode.rooImport.description": "Імпортувати історію розмов з інсталяції Roo Code.",
"settings.aboutKiloCode.rooImport.button": "Імпортувати сесії з Roo Code",
// Screen 1 — What's New
"migration.whatsNew.title": "Що нового в Kilo Code",
+2
View File
@@ -1489,6 +1489,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "旧版迁移",
"settings.aboutKiloCode.legacyMigration.description":
"从旧版 Kilo Code 安装迁移设置,包括提供商 API 密钥和默认模型。",
"settings.aboutKiloCode.rooImport.description": "从 Roo Code 安装导入对话历史记录。",
"settings.aboutKiloCode.rooImport.button": "从 Roo Code 导入会话",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Code 新功能",
+2
View File
@@ -1495,6 +1495,8 @@ export const dict = {
"settings.aboutKiloCode.legacyMigration.title": "舊版遷移",
"settings.aboutKiloCode.legacyMigration.description":
"從舊版 Kilo Code 安裝遷移設定,包括供應商 API 金鑰和預設模型。",
"settings.aboutKiloCode.rooImport.description": "從 Roo Code 安裝匯入對話歷史記錄。",
"settings.aboutKiloCode.rooImport.button": "從 Roo Code 匯入工作階段",
// Screen 1 — What's New
"migration.whatsNew.title": "Kilo Code 新功能",
@@ -0,0 +1,62 @@
/** @jsxImportSource solid-js */
/**
* Stories for migration flows.
*/
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import { onMount, type Component } from "solid-js"
import MigrationWizard from "../components/migration/MigrationWizard"
import { StoryProviders } from "./StoryProviders"
const meta: Meta = {
title: "Migration",
parameters: { layout: "fullscreen" },
}
export default meta
type Story = StoryObj
const operationId = "roo-migration-story"
const RooWizard: Component = () => {
onMount(() => {
queueMicrotask(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
type: "migrationData",
source: "roo",
operationId,
data: {
providers: [],
mcpServers: [],
customModes: [],
sessions: [
{
id: "roo-1",
title: "Refactor authentication flow",
directory: "/workspace/app",
time: 1760443200000,
},
{ id: "roo-2", title: "Add billing dashboard", directory: "/workspace/app", time: 1760356800000 },
{ id: "roo-3", title: "Investigate flaky tests", directory: "/workspace/app", time: 1760270400000 },
],
},
},
}),
)
})
})
return <MigrationWizard source="roo" operationId={operationId} onBack={() => {}} onComplete={() => {}} />
}
export const RooWizardSelecting: Story = {
name: "Roo wizard — selecting sessions",
render: () => (
<StoryProviders>
<div style={{ "max-height": "720px", overflow: "auto" }}>
<RooWizard />
</div>
</StoryProviders>
),
}
@@ -39,10 +39,10 @@ import type {
WorktreeState,
} from "./agent-manager"
import type {
LegacyMigrationCompleteMessage,
LegacyMigrationDataMessage,
LegacyMigrationProgressMessage,
LegacyMigrationSessionProgressMessage,
MigrationCompleteMessage,
MigrationDataMessage,
MigrationProgressMessage,
MigrationSessionProgressMessage,
MigrationStateMessage,
} from "./migration"
@@ -1069,10 +1069,10 @@ export type ExtensionMessage =
| AgentManagerTerminalErrorMessage
// legacy-migration start
| MigrationStateMessage
| LegacyMigrationDataMessage
| LegacyMigrationProgressMessage
| LegacyMigrationSessionProgressMessage
| LegacyMigrationCompleteMessage
| MigrationDataMessage
| MigrationProgressMessage
| MigrationSessionProgressMessage
| MigrationCompleteMessage
// legacy-migration end
| EnhancePromptResultMessage
| EnhancePromptErrorMessage
@@ -54,21 +54,18 @@ export interface MigrationResultItem {
message?: string
}
export type MigrationSource = "legacy" | "roo"
export interface MigrationStateMessage {
type: "migrationState"
needed: boolean
data?: {
providers: MigrationProviderInfo[]
mcpServers: MigrationMcpServerInfo[]
customModes: MigrationCustomModeInfo[]
sessions?: MigrationSessionInfo[]
defaultModel?: { provider: string; model: string }
settings?: LegacySettings
}
source: "legacy"
}
export interface LegacyMigrationDataMessage {
type: "legacyMigrationData"
export interface MigrationDataMessage {
type: "migrationData"
source: MigrationSource
operationId: string
data: {
providers: MigrationProviderInfo[]
mcpServers: MigrationMcpServerInfo[]
@@ -79,8 +76,10 @@ export interface LegacyMigrationDataMessage {
}
}
export interface LegacyMigrationProgressMessage {
type: "legacyMigrationProgress"
export interface MigrationProgressMessage {
type: "migrationProgress"
source: MigrationSource
operationId: string
item: string
status: "migrating" | "success" | "warning" | "error"
message?: string
@@ -88,8 +87,10 @@ export interface LegacyMigrationProgressMessage {
export type LegacyMigrationSessionPhase = "preparing" | "storing" | "skipped" | "done" | "summary" | "error"
export interface LegacyMigrationSessionProgressMessage {
type: "legacyMigrationSessionProgress"
export interface MigrationSessionProgressMessage {
type: "migrationSessionProgress"
source: MigrationSource
operationId: string
session: MigrationSessionInfo
index: number
total: number
@@ -97,13 +98,17 @@ export interface LegacyMigrationSessionProgressMessage {
error?: string
}
export interface LegacyMigrationCompleteMessage {
type: "legacyMigrationComplete"
export interface MigrationCompleteMessage {
type: "migrationComplete"
source: MigrationSource
operationId: string
results: MigrationResultItem[]
}
export interface RequestLegacyMigrationDataMessage {
type: "requestLegacyMigrationData"
export interface RequestMigrationDataMessage {
type: "requestMigrationData"
source: MigrationSource
operationId: string
}
export interface MigrationAutoApprovalSelections {
@@ -120,8 +125,10 @@ export interface MigrationSessionSelection {
force?: boolean
}
export interface StartLegacyMigrationMessage {
type: "startLegacyMigration"
export interface StartMigrationMessage {
type: "startMigration"
source: MigrationSource
operationId: string
selections: {
providers: string[]
mcpServers: string[]
@@ -10,9 +10,9 @@ import type { WorkStyle, WorkStyleState } from "../../../../src/shared/work-styl
import type {
ClearLegacyDataMessage,
FinalizeLegacyMigrationMessage,
RequestLegacyMigrationDataMessage,
RequestMigrationDataMessage,
SkipLegacyMigrationMessage,
StartLegacyMigrationMessage,
StartMigrationMessage,
} from "./migration"
// ============================================
@@ -1224,8 +1224,8 @@ export type WebviewMessage =
| RefreshPRMessage
| OpenPRMessage
// legacy-migration start
| RequestLegacyMigrationDataMessage
| StartLegacyMigrationMessage
| RequestMigrationDataMessage
| StartMigrationMessage
| SkipLegacyMigrationMessage
| ClearLegacyDataMessage
| FinalizeLegacyMigrationMessage
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-web-ui",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"exports": {
+2 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.46",
"version": "7.3.49",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
@@ -47,6 +47,5 @@
"@smithy/util-utf8": "4.2.2",
"aws4fetch": "1.0.20",
"effect": "catalog:"
},
"peerDependencies": {}
}
}
+2
View File
@@ -1,5 +1,7 @@
# @kilocode/cli
## 7.3.49
## 7.3.48
### Minor Changes
+8 -23
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.46",
"version": "7.3.49",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -68,25 +68,22 @@
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",
"@types/npmcli__arborist": "6.3.3",
"@opencode-ai/http-recorder": "workspace:*",
"@babel/core": "7.28.4",
"@standard-schema/spec": "1.0.0",
"@types/babel__core": "7.20.5"
"@opencode-ai/http-recorder": "workspace:*"
},
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.54",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53",
@@ -145,7 +142,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.8.0",
"gitlab-ai-provider": "6.7.0",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
"htmlparser2": "8.0.2",
@@ -171,25 +168,13 @@
"tree-sitter-wasms": "^0.1.12",
"turndown": "7.2.0",
"ulid": "catalog:",
"venice-ai-sdk-provider": "2.0.2",
"venice-ai-sdk-provider": "2.0.1",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10",
"which": "6.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yargs": "18.0.0",
"zod": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
"@pierre/diffs": "catalog:",
"@standard-schema/spec": "1.0.0",
"@types/ws": "8.18.1",
"chokidar": "4.0.3",
"glob": "13.0.5",
"minimatch": "10.0.3",
"partial-json": "0.1.7",
"ws": "8.21.0",
"xdg-basedir": "5.1.0"
"zod": "catalog:"
},
"overrides": {
"drizzle-orm": "catalog:"
@@ -115,6 +115,11 @@ function useLanguageModel(sdk: any) {
return sdk.responses === undefined && sdk.chat === undefined
}
export function patchKiloProviderPrivacy(provider: { options?: Record<string, any> } | undefined, config: any) {
if (!provider || config.hide_prompt_training_models !== true) return
provider.options = { ...provider.options, dataCollection: "deny" }
}
export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader> {
return {
"github-copilot-enterprise": () =>
@@ -129,10 +134,11 @@ export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader>
kilo: Effect.fnUntraced(function* (input: any) {
const env = yield* dep.env()
const config = yield* dep.config()
const hasKey = yield* Effect.gen(function* () {
if (input.env.some((item: string) => env[item])) return true
if (yield* dep.auth(input.id)) return true
if ((yield* dep.config()).provider?.["kilo"]?.options?.apiKey) return true
if (config.provider?.["kilo"]?.options?.apiKey) return true
return false
})
@@ -140,6 +146,9 @@ export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader>
if (env.KILO_ORG_ID) {
options.kilocodeOrganizationId = env.KILO_ORG_ID
}
if (config.hide_prompt_training_models === true) {
options.dataCollection = "deny"
}
if (!hasKey) {
options.apiKey = "anonymous"
}
@@ -36,6 +36,7 @@ import {
patchModelsDevModel as patchKiloModel,
patchConfigModel as patchKiloConfigModel,
patchCustomLoaderResult,
patchKiloProviderPrivacy,
kiloSmallModelPriority,
buildTimeoutSignal,
} from "@/kilocode/provider/provider"
@@ -1525,6 +1526,7 @@ export const layer = Layer.effect(
if (provider.options) partial.options = provider.options
mergeProvider(providerID, partial)
}
patchKiloProviderPrivacy(providers[ProviderID.make("kilo")], cfg) // kilocode_change
const gitlab = ProviderID.make("gitlab")
if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
@@ -7,7 +7,7 @@ import { ModelsDev } from "../../src/provider/models"
import * as CoreModels from "@opencode-ai/core/models-dev"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { kiloCustomLoaders } from "../../src/kilocode/provider/provider"
import { kiloCustomLoaders, patchKiloProviderPrivacy } from "../../src/kilocode/provider/provider"
import { Auth } from "../../src/auth"
import { ModelCache } from "../../src/provider/model-cache"
import { Provider } from "../../src/provider/provider"
@@ -165,6 +165,21 @@ it.effect("enables a paid catalog when config apiKey is present", () =>
}),
)
it.effect("denies provider data collection when prompt-training models are hidden", () =>
Effect.gen(function* () {
const result = yield* load({ config: { hide_prompt_training_models: true } })
expect(result.options).toEqual({ apiKey: "anonymous", dataCollection: "deny" })
}),
)
it.effect("keeps data collection denied after configured options are applied", () =>
Effect.sync(() => {
const provider = { options: { dataCollection: "allow", baseURL: "https://api.kilo.ai" } }
patchKiloProviderPrivacy(provider, { hide_prompt_training_models: true })
expect(provider.options).toEqual({ dataCollection: "deny", baseURL: "https://api.kilo.ai" })
}),
)
it.effect("enables a paid catalog when auth exists", () =>
Effect.gen(function* () {
const result = yield* load({ auth: { type: "api", key: "test-key" } })
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/plugin-atomic-chat",
"version": "7.3.46",
"version": "7.3.49",
"description": "Kilo Code plugin for Atomic Chat: auto-detection and dynamic model discovery (OpenAI-compatible local API)",
"type": "module",
"license": "MIT",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/plugin",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"scripts": {
@@ -22,9 +22,9 @@
"zod": "catalog:"
},
"peerDependencies": {
"@opentui/core": ">=0.2.16",
"@opentui/solid": ">=0.2.16",
"@opentui/keymap": ">=0.2.16"
"@opentui/core": ">=0.2.15",
"@opentui/solid": ">=0.2.15",
"@opentui/keymap": ">=0.2.15"
},
"peerDependenciesMeta": {
"@opentui/core": {
+1 -1
View File
@@ -12,6 +12,6 @@
"exports": {
".": "./src/index.ts"
},
"version": "7.3.46",
"version": "7.3.49",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/sdk",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -26,7 +26,7 @@
"typescript": "catalog:",
"vite": "catalog:"
},
"version": "7.3.46",
"version": "7.3.49",
"dependencies": {},
"peerDependencies": {}
}
+3 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "7.3.46",
"version": "7.3.49",
"type": "module",
"license": "MIT",
"exports": {
@@ -42,8 +42,7 @@
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-icons-spritesheet": "3.0.1",
"vite-plugin-solid": "catalog:",
"@types/luxon": "catalog:"
"vite-plugin-solid": "catalog:"
},
"dependencies": {
"@kobalte/core": "catalog:",
@@ -72,12 +71,7 @@
"strip-ansi": "7.1.2",
"virtua": "catalog:",
"@typescript/native-preview": "catalog:",
"@opencode-ai/core": "workspace:*",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3",
"luxon": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:"
"@opencode-ai/core": "workspace:*"
},
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/upstream-merge",
"version": "7.3.46",
"version": "7.3.49",
"private": true,
"type": "module",
"description": "Scripts for automating upstream opencode merges into Kilo",