mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
Merge branch 'main' into imanolmaiztegui/kilo-opencode-v1.15.13
This commit is contained in:
@@ -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.
|
||||
+9
-18
@@ -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,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,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",
|
||||
|
||||
@@ -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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.3.46",
|
||||
"version": "7.3.49",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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).
|
||||
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:06c6cd2c2c63745d3cbaec756ae852db3ef0749c005926b75fe3eb03f06b0495
|
||||
size 22169
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 } } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ export interface KiloProviderOptions {
|
||||
*/
|
||||
name?: string
|
||||
|
||||
/**
|
||||
* Data collection preference for upstream provider routing
|
||||
*/
|
||||
dataCollection?: "allow" | "deny"
|
||||
|
||||
/**
|
||||
* Custom fetch function
|
||||
*/
|
||||
|
||||
@@ -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,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,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",
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.46",
|
||||
"version": "7.3.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,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}`))
|
||||
})
|
||||
})
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-web-ui",
|
||||
"version": "7.3.46",
|
||||
"version": "7.3.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @kilocode/cli
|
||||
|
||||
## 7.3.49
|
||||
|
||||
## 7.3.48
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"version": "7.3.46",
|
||||
"version": "7.3.49",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"version": "7.3.46",
|
||||
"version": "7.3.49",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
Reference in New Issue
Block a user