Merge pull request #6196 from Kilo-Org/feat/cloud-sessions-list

This commit is contained in:
Igor Šćekić
2026-02-24 15:13:07 +01:00
committed by GitHub
45 changed files with 1917 additions and 31 deletions
+1 -2
View File
@@ -132,8 +132,7 @@ export const dict = {
"provider.connect.oauth.code.invalid": "رمز التفويض غير صالح",
"provider.connect.oauth.auto.visit.prefix": "قم بزيارة ",
"provider.connect.oauth.auto.visit.link": "هذا الرابط",
"provider.connect.oauth.auto.visit.suffix":
" وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
"provider.connect.oauth.auto.visit.suffix": " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
"provider.connect.oauth.auto.confirmationCode": "رمز التأكيد",
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
+2 -4
View File
@@ -385,8 +385,7 @@ export const dict = {
"toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session",
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
"toast.update.title": "Mise à jour disponible",
"toast.update.description":
"Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
"toast.update.description": "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
"toast.update.action.installRestart": "Installer et redémarrer",
"toast.update.action.notYet": "Pas encore",
"error.page.title": "Quelque chose s'est mal passé",
@@ -517,8 +516,7 @@ export const dict = {
"sidebar.workspaces.enable": "Activer les espaces de travail",
"sidebar.workspaces.disable": "Désactiver les espaces de travail",
"sidebar.gettingStarted.title": "Commencer",
"sidebar.gettingStarted.line1":
"Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
"sidebar.gettingStarted.line1": "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
"sidebar.gettingStarted.line2":
"Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Sessions récentes",
+1 -2
View File
@@ -144,8 +144,7 @@ export const dict = {
"provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง",
"provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ",
"provider.connect.oauth.auto.visit.link": "ลิงก์นี้",
"provider.connect.oauth.auto.visit.suffix":
" และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
"provider.connect.oauth.auto.visit.suffix": " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
"provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน",
"provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว",
"provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว",
+1 -2
View File
@@ -145,8 +145,7 @@ export const dict = {
"provider.connect.oauth.code.invalid": "授權碼無效",
"provider.connect.oauth.auto.visit.prefix": "造訪 ",
"provider.connect.oauth.auto.visit.link": "此連結",
"provider.connect.oauth.auto.visit.suffix":
" 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
"provider.connect.oauth.auto.visit.suffix": " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
"provider.connect.oauth.auto.confirmationCode": "確認碼",
"provider.connect.toast.connected.title": "{{provider}} 已連線",
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
@@ -32,9 +32,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
src={
props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override
}
src={props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
classList={{ "badge-mask": unseenCount() > 0 && props.notify }}
+158
View File
@@ -0,0 +1,158 @@
import { buildKiloHeaders } from "./headers.js"
export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
export interface DrizzleDb {
insert(table: object): { values(data: object): { onConflictDoNothing(): { run(): void } } }
}
const INGEST_BASE = process.env.KILO_SESSION_INGEST_URL ?? "https://ingest.kilosessions.ai"
function exportUrl(sessionId: string) {
return UUID_RE.test(sessionId)
? `${INGEST_BASE}/session/${sessionId}`
: `${INGEST_BASE}/api/session/${sessionId}/export`
}
export type FetchResult = { ok: true; data: any } | { ok: false; status: number; error: string }
export async function fetchCloudSession(token: string, sessionId: string): Promise<FetchResult> {
const response = await fetch(exportUrl(sessionId), {
headers: {
Authorization: `Bearer ${token}`,
...buildKiloHeaders(),
},
})
if (response.status === 404) return { ok: false, status: 404, error: "Session not found" }
if (!response.ok) return { ok: false, status: response.status, error: "Failed to fetch session" }
const data = await response.json()
return { ok: true, data }
}
export async function fetchCloudSessionForImport(token: string, sessionId: string): Promise<FetchResult> {
const response = await fetch(exportUrl(sessionId), {
headers: {
Authorization: `Bearer ${token}`,
...buildKiloHeaders(),
},
})
if (response.status === 404) return { ok: false, status: 404, error: "Session not found in cloud" }
if (!response.ok) {
const text = await response.text()
console.error("[Kilo Gateway] cloud/session/import: export failed", {
status: response.status,
body: text.slice(0, 500),
})
return { ok: false, status: response.status, error: `Import failed: ${response.status}` }
}
const data = await response.json()
return { ok: true, data }
}
export interface ImportDeps {
Database: {
transaction<T>(callback: (db: DrizzleDb) => T): T
effect(fn: () => void | Promise<unknown>): void
}
Instance: {
readonly directory: string
readonly project: { readonly id: string }
}
SessionTable: object
MessageTable: object
PartTable: object
SessionToRow: (info: any) => Record<string, unknown>
Bus: { publish(event: { type: string; properties: unknown }, payload: unknown): void | Promise<unknown> }
SessionCreatedEvent: { type: string; properties: unknown }
Identifier: {
ascending(prefix: "session" | "message" | "part", given?: string): string
descending(prefix: "session" | "message" | "part", given?: string): string
}
}
export function importSessionToDb(data: any, deps: ImportDeps) {
const {
Database,
Instance,
SessionTable,
MessageTable,
PartTable,
SessionToRow,
Bus,
SessionCreatedEvent,
Identifier,
} = deps
const localSessionID = Identifier.descending("session")
const msgMap = new Map<string, string>()
const projectID = Instance.project.id
const now = Date.now()
const time = {
created: data.info.time?.created ?? now,
updated: now,
...(data.info.time?.compacting !== undefined && { compacting: data.info.time.compacting }),
...(data.info.time?.archived !== undefined && { archived: data.info.time.archived }),
}
const info = {
...data.info,
id: localSessionID,
projectID,
slug: data.info.slug,
directory: Instance.directory,
version: data.info.version,
time,
}
Database.transaction((db) => {
db.insert(SessionTable)
.values(SessionToRow(info as Record<string, unknown>))
.onConflictDoNothing()
.run()
const messages = Array.isArray(data.messages) ? data.messages : []
for (const msg of messages.filter((m: any) => m.info)) {
const msgID = Identifier.ascending("message")
msgMap.set(msg.info.id, msgID)
msg.info.id = msgID
msg.info.sessionID = localSessionID
if (msg.info.parentID) msg.info.parentID = msgMap.get(msg.info.parentID) ?? msg.info.parentID
db.insert(MessageTable)
.values({
id: msgID,
session_id: localSessionID,
time_created: msg.info.time?.created ?? Date.now(),
data: msg.info,
})
.onConflictDoNothing()
.run()
for (const part of msg.parts ?? []) {
const partID = Identifier.ascending("part")
part.id = partID
part.messageID = msgID
part.sessionID = localSessionID
db.insert(PartTable)
.values({
id: partID,
message_id: msgID,
session_id: localSessionID,
data: part,
})
.onConflictDoNothing()
.run()
}
}
Database.effect(() => Bus.publish(SessionCreatedEvent, { info }))
})
return info
}
+224 -2
View File
@@ -10,6 +10,8 @@ import { fetchProfile, fetchBalance } from "../api/profile.js"
import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js"
import { KILO_API_BASE, HEADER_FEATURE } from "../api/constants.js" // kilocode_change - added HEADER_FEATURE
import { buildKiloHeaders } from "../headers.js" // kilocode_change
import type { ImportDeps, DrizzleDb } from "../cloud-sessions.js" // kilocode_change
import { fetchCloudSession, fetchCloudSessionForImport, importSessionToDb } from "../cloud-sessions.js" // kilocode_change
// Type definitions for OpenCode dependencies (injected at runtime)
type Hono = any
@@ -20,7 +22,7 @@ type Errors = any
type Auth = any
type Z = any
interface KiloRoutesDeps {
interface KiloRoutesDeps extends ImportDeps {
Hono: new () => Hono
describeRoute: DescribeRoute
validator: Validator
@@ -54,7 +56,24 @@ interface KiloRoutesDeps {
* ```
*/
export function createKiloRoutes(deps: KiloRoutesDeps) {
const { Hono, describeRoute, validator, resolver, errors, Auth, z } = deps
const {
Hono,
describeRoute,
validator,
resolver,
errors,
Auth,
z,
Database,
Instance,
SessionTable,
MessageTable,
PartTable,
SessionToRow,
Bus,
SessionCreatedEvent,
Identifier,
} = deps
const Organization = z.object({
id: z.string(),
@@ -281,4 +300,207 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
return c.json(notifications)
},
)
.get(
"/cloud/session/:id",
describeRoute({
summary: "Get cloud session",
description: "Fetch full session data from the Kilo cloud for preview",
operationId: "kilo.cloud.session.get",
responses: {
200: {
description: "Cloud session data",
content: {
"application/json": {
schema: resolver(z.unknown()),
},
},
},
...errors(401, 404),
},
}),
validator("param", z.object({ id: z.string() })),
async (c: any) => {
try {
const auth = await Auth.get("kilo")
if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401)
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
if (!token) return c.json({ error: "No valid token found" }, 401)
const { id } = c.req.valid("param")
const result = await fetchCloudSession(token, id)
if (!result.ok) return c.json({ error: result.error }, result.status)
return c.json(result.data)
} catch (err: any) {
console.error("[Kilo Gateway] cloud/session/get: unhandled error", err?.message ?? err)
return c.json({ error: "Internal error" }, 500)
}
},
)
.post(
"/cloud/session/import",
describeRoute({
summary: "Import session from cloud",
description: "Download a cloud-synced session and write it to local storage with fresh IDs.",
operationId: "kilo.cloud.session.import",
responses: {
200: {
description: "Imported session info",
content: {
"application/json": {
schema: resolver(z.unknown()),
},
},
},
...errors(400, 401, 404),
},
}),
validator(
"json",
z.object({
sessionId: z.string(),
}),
),
async (c: any) => {
try {
const { sessionId } = c.req.valid("json")
const auth = await Auth.get("kilo")
if (!auth) return c.json({ error: "Not authenticated with Kilo" }, 401)
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
if (!token) return c.json({ error: "No valid token found" }, 401)
const fetched = await fetchCloudSessionForImport(token, sessionId)
if (!fetched.ok) return c.json({ error: fetched.error }, fetched.status as any)
const data = fetched.data
if (!data?.info?.id) return c.json({ error: "Invalid export data" }, 400)
const info = importSessionToDb(data, {
Database,
Instance,
SessionTable,
MessageTable,
PartTable,
SessionToRow,
Bus,
SessionCreatedEvent,
Identifier,
})
return c.json(info)
} catch (err: any) {
console.error("[Kilo Gateway] cloud/session/import: unhandled error", err?.message ?? err)
return c.json({ error: "Internal error" }, 500)
}
},
)
.get(
"/cloud-sessions",
describeRoute({
summary: "Get cloud sessions",
description: "Fetch cloud CLI sessions from Kilo API",
operationId: "kilo.cloudSessions",
responses: {
200: {
description: "Cloud sessions list",
content: {
"application/json": {
schema: resolver(
z.object({
cliSessions: z.array(
z.object({
session_id: z.string(),
title: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
version: z.number(),
}),
),
nextCursor: z.string().nullable(),
}),
),
},
},
},
...errors(400, 401),
},
}),
validator(
"query",
z.object({
cursor: z.string().optional(),
limit: z.coerce.number().optional(),
gitUrl: z.string().optional(),
}),
),
async (c: any) => {
try {
const auth = await Auth.get("kilo")
if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401)
const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined
if (!token) return c.json({ error: "No valid token found" }, 401)
const { cursor, limit, gitUrl } = c.req.valid("query")
const input: Record<string, unknown> = {}
if (cursor) input.cursor = cursor
if (limit) input.limit = limit
if (gitUrl) input.gitUrl = gitUrl
const params = new URLSearchParams({
batch: "1",
input: JSON.stringify({ "0": input }),
})
const url = `${KILO_API_BASE}/api/trpc/cliSessionsV2.list?${params.toString()}`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...buildKiloHeaders(),
},
})
if (!response.ok) {
const text = await response.text()
console.error("[Kilo Gateway] cloud-sessions: tRPC request failed", {
status: response.status,
body: text.slice(0, 500),
})
return c.json({ error: `Cloud sessions fetch failed: ${response.status}` }, response.status as any)
}
const raw = await response.text()
const json = JSON.parse(raw)
const data = Array.isArray(json) ? json[0]?.result?.data : null
const result = data?.json ?? data
if (!result) return c.json({ cliSessions: [], nextCursor: null })
const sessions = (result.cliSessions ?? []).map((s: any) => ({
session_id: s.session_id,
title: s.title ?? null,
created_at:
typeof s.created_at === "string"
? s.created_at
: s.created_at
? new Date(s.created_at).toISOString()
: new Date().toISOString(),
updated_at:
typeof s.updated_at === "string"
? s.updated_at
: s.updated_at
? new Date(s.updated_at).toISOString()
: new Date().toISOString(),
version: s.version ?? 0,
}))
return c.json({ cliSessions: sessions, nextCursor: result.nextCursor ?? null })
} catch (err: any) {
console.error("[Kilo Gateway] cloud-sessions: unhandled error", err?.message ?? err)
return c.json({ error: "Internal error" }, 500)
}
},
)
}
@@ -21,3 +21,12 @@
font-weight: 600;
}
}
[data-component="dialog"][data-fit] [data-slot="dialog-container"] {
height: auto;
}
[data-component="dialog"][data-fit] [data-slot="dialog-content"] {
min-height: 0;
align-self: center;
}
+13 -3
View File
@@ -59,6 +59,11 @@
"title": "History",
"icon": "$(history)"
},
{
"command": "kilo-code.new.cloudHistoryButtonClicked",
"title": "Cloud History",
"icon": "$(cloud)"
},
{
"command": "kilo-code.new.profileButtonClicked",
"title": "Profile",
@@ -217,19 +222,24 @@
"when": "view == kilo-code.new.sidebarView"
},
{
"command": "kilo-code.new.historyButtonClicked",
"command": "kilo-code.new.cloudHistoryButtonClicked",
"group": "navigation@3",
"when": "view == kilo-code.new.sidebarView"
},
{
"command": "kilo-code.new.profileButtonClicked",
"command": "kilo-code.new.historyButtonClicked",
"group": "navigation@4",
"when": "view == kilo-code.new.sidebarView"
},
{
"command": "kilo-code.new.settingsButtonClicked",
"command": "kilo-code.new.profileButtonClicked",
"group": "navigation@5",
"when": "view == kilo-code.new.sidebarView"
},
{
"command": "kilo-code.new.settingsButtonClicked",
"group": "navigation@6",
"when": "view == kilo-code.new.sidebarView"
}
],
"scm/title": [
+222
View File
@@ -444,6 +444,39 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
console.error("[Kilo New] fetchAndSendNotifications failed:", e),
)
break
case "requestCloudSessions":
await this.handleRequestCloudSessions(message)
break
case "requestGitRemoteUrl":
void this.getGitRemoteUrl().then((url) => {
this.postMessage({ type: "gitRemoteUrlLoaded", gitUrl: url ?? null })
})
break
case "requestCloudSessionData":
void this.handleRequestCloudSessionData(message.sessionId)
break
case "importAndSend": {
const files = z
.array(
z.object({
mime: z.string(),
url: z.string().refine((u) => u.startsWith("file://") || u.startsWith("data:")),
}),
)
.optional()
.catch(undefined)
.parse(message.files)
void this.handleImportAndSend(
message.cloudSessionId,
message.text,
message.providerID,
message.modelID,
message.agent,
message.variant,
files,
)
break
}
case "dismissNotification":
await this.handleDismissNotification(message.notificationId)
break
@@ -967,6 +1000,175 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
/**
* Handle cloud sessions request from webview.
* Fetches sessions from the Kilo cloud API and sends them back.
*/
private async handleRequestCloudSessions(message: {
cursor?: string
limit?: number
gitUrl?: string
}): Promise<void> {
if (!this.httpClient) {
this.postMessage({
type: "error",
message: "Not connected to CLI backend",
})
return
}
try {
const result = await this.httpClient.getCloudSessions({
cursor: message.cursor,
limit: message.limit,
gitUrl: message.gitUrl,
})
this.postMessage({
type: "cloudSessionsLoaded",
sessions: result?.cliSessions ?? [],
nextCursor: result?.nextCursor ?? null,
})
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch cloud sessions:", error)
this.postMessage({
type: "error",
message: error instanceof Error ? error.message : "Failed to fetch cloud sessions",
})
}
}
/**
* Fetch full cloud session data for read-only preview.
* Transforms the export data into webview message format and sends it back.
*/
private async handleRequestCloudSessionData(sessionId: string): Promise<void> {
if (!this.httpClient) {
this.postMessage({
type: "cloudSessionImportFailed",
cloudSessionId: sessionId,
error: "Not connected to CLI backend",
})
return
}
try {
const data = await this.httpClient.getCloudSession(sessionId)
if (!data) {
this.postMessage({
type: "cloudSessionImportFailed",
cloudSessionId: sessionId,
error: "Failed to fetch cloud session",
})
return
}
const messages = (data.messages ?? [])
.filter((m) => m.info)
.map((m) => ({
id: m.info.id,
sessionID: m.info.sessionID,
role: m.info.role as "user" | "assistant",
parts: m.parts,
createdAt: m.info.time?.created ? new Date(m.info.time.created).toISOString() : new Date().toISOString(),
cost: m.info.cost,
tokens: m.info.tokens,
}))
this.postMessage({
type: "cloudSessionDataLoaded",
cloudSessionId: sessionId,
title: data.info.title ?? "Untitled",
messages,
})
} catch (err) {
console.error("[Kilo New] Failed to load cloud session data:", err)
this.postMessage({
type: "cloudSessionImportFailed",
cloudSessionId: sessionId,
error: err instanceof Error ? err.message : "Failed to load cloud session",
})
}
}
/**
* Import a cloud session to local storage, then send a new message on it.
* This is the "clone on first message" flow — the cloud session becomes a
* local session only when the user decides to continue it.
*/
private async handleImportAndSend(
cloudSessionId: string,
text: string,
providerID?: string,
modelID?: string,
agent?: string,
variant?: string,
files?: Array<{ mime: string; url: string }>,
): Promise<void> {
if (!this.httpClient) {
this.postMessage({
type: "cloudSessionImportFailed",
cloudSessionId,
error: "Not connected to CLI backend",
})
return
}
const workspaceDir = this.getWorkspaceDirectory()
// Step 1: Import the cloud session with fresh IDs
const session = await this.httpClient.importCloudSession(cloudSessionId, workspaceDir)
if (!session) {
this.postMessage({
type: "cloudSessionImportFailed",
cloudSessionId,
error: "Failed to import session from cloud",
})
return
}
// Track the new local session
this.currentSession = session
this.trackedSessionIds.add(session.id)
// Notify webview of the import success
this.postMessage({
type: "cloudSessionImported",
cloudSessionId,
session: this.sessionToWebview(session),
})
// Step 2: Send the user's message on the new local session
const parts: Array<{ type: "text"; text: string } | { type: "file"; mime: string; url: string }> = []
if (files) {
for (const f of files) {
parts.push({ type: "file", mime: f.mime, url: f.url })
}
}
parts.push({ type: "text", text })
try {
const editorContext = await this.gatherEditorContext()
await this.httpClient.sendMessage(session.id, parts, workspaceDir, {
providerID,
modelID,
agent,
variant,
editorContext,
})
} catch (err) {
console.error("[Kilo New] Failed to send message after cloud import:", err)
this.postMessage({
type: "error",
message: err instanceof Error ? err.message : "Failed to send message after import",
sessionID: session.id,
})
}
}
/**
* Persist a dismissed notification ID in globalState and push updated lists to webview.
*/
@@ -1514,6 +1716,26 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
})
}
/**
* Get the git remote URL for the current workspace using VS Code's built-in Git API.
* Returns undefined if not in a git repo or no remotes are configured.
*/
private async getGitRemoteUrl(): Promise<string | undefined> {
try {
const extension = vscode.extensions.getExtension("vscode.git")
if (!extension) return undefined
const api = extension.isActive ? extension.exports?.getAPI(1) : (await extension.activate())?.getAPI(1)
if (!api) return undefined
const repo = api.repositories?.[0]
if (!repo) return undefined
const remote = repo.state?.remotes?.find((r: { name: string }) => r.name === "origin")
return remote?.fetchUrl ?? remote?.pushUrl
} catch (error) {
console.warn("[Kilo New] KiloProvider: Failed to get git remote URL:", error)
return undefined
}
}
/**
* Gather VS Code editor context to send alongside messages to the CLI backend.
*/
+3
View File
@@ -61,6 +61,9 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.historyButtonClicked", () => {
provider.postMessage({ type: "action", action: "historyButtonClicked" })
}),
vscode.commands.registerCommand("kilo-code.new.cloudHistoryButtonClicked", () => {
provider.postMessage({ type: "action", action: "cloudHistoryButtonClicked" })
}),
vscode.commands.registerCommand("kilo-code.new.profileButtonClicked", () => {
provider.postMessage({ type: "action", action: "profileButtonClicked" })
}),
@@ -12,6 +12,8 @@ import type {
McpConfig,
Config,
KilocodeNotification,
CloudSessionsResponse,
CloudSessionData,
EditorContext,
} from "./types"
import { extractHttpErrorMessage, parseSSEDataLine } from "./http-utils"
@@ -353,6 +355,54 @@ export class HttpClient {
}
}
/**
* Fetch cloud CLI sessions from the Kilo cloud API.
* Returns null if not logged in or if the request fails.
*/
async getCloudSessions(params?: {
cursor?: string
limit?: number
gitUrl?: string
}): Promise<CloudSessionsResponse | null> {
try {
const query = new URLSearchParams()
if (params?.cursor) query.set("cursor", params.cursor)
if (params?.limit) query.set("limit", String(params.limit))
if (params?.gitUrl) query.set("gitUrl", params.gitUrl)
const qs = query.toString()
return await this.request<CloudSessionsResponse>("GET", `/kilo/cloud-sessions${qs ? `?${qs}` : ""}`)
} catch (err) {
console.warn("[Kilo] Failed to fetch cloud sessions:", err)
return null
}
}
/**
* Fetch full cloud session data for read-only preview.
* Returns null if the session is not found or the request fails.
*/
async getCloudSession(sessionId: string): Promise<CloudSessionData | null> {
try {
return await this.request<CloudSessionData>("GET", `/kilo/cloud/session/${sessionId}`)
} catch (err) {
console.warn("[Kilo] Failed to fetch cloud session:", err)
return null
}
}
/**
* Import a cloud session into local storage with fresh IDs.
* Returns the imported session info, or null on failure.
*/
async importCloudSession(sessionId: string, directory: string): Promise<SessionInfo | null> {
try {
return await this.request<SessionInfo>("POST", "/kilo/cloud/session/import", { sessionId }, { directory })
} catch (err) {
console.warn("[Kilo] Failed to import cloud session:", err)
return null
}
}
/**
* Switch the active organization.
* Pass null to switch back to personal account.
@@ -28,6 +28,7 @@ export type {
Config,
KilocodeNotification,
KilocodeNotificationAction,
CloudSessionData,
} from "./types"
export { ServerManager } from "./server-manager"
@@ -337,6 +337,48 @@ export interface Config {
experimental?: ExperimentalConfig
}
// Cloud session from the Kilo cloud API (cli_sessions_v2)
export interface CloudSessionInfo {
session_id: string
title: string | null
created_at: string
updated_at: string
version: number
}
export interface CloudSessionsResponse {
cliSessions: CloudSessionInfo[]
nextCursor: string | null
}
// Full cloud session data for preview (from /kilo/cloud/session/:id)
export interface CloudSessionData {
info: {
id: string
title: string
time: { created: number; updated: number }
[key: string]: unknown
}
messages: Array<{
info: {
id: string
sessionID: string
role: "user" | "assistant"
time: { created: number; completed?: number }
cost?: { input: number; output: number; reasoning?: number; cache?: { read: number; write: number } }
tokens?: { input: number; output: number; reasoning?: number; cache?: { read: number; write: number } }
[key: string]: unknown
}
parts: Array<{
id: string
sessionID: string
messageID: string
type: string
[key: string]: unknown
}>
}>
}
/** VS Code editor context sent alongside messages to the CLI backend */
export interface EditorContext {
/** Workspace-relative paths of currently visible editors */
@@ -168,6 +168,7 @@ describe("WorktreeStateManager", () => {
const fresh = path.join(root, "subdir")
const mgr = new WorktreeStateManager(fresh, () => {})
mgr.addWorktree({ branch: "test", path: "/tmp/test", parentBranch: "main" })
await mgr.flush()
await mgr.save()
expect(fs.existsSync(path.join(fresh, ".kilocode", "agent-manager.json"))).toBe(true)
+14 -2
View File
@@ -19,12 +19,13 @@ import { LanguageProvider } from "./context/language"
import { ChatView } from "./components/chat"
import { KiloNotifications } from "./components/chat/KiloNotifications"
import SessionList from "./components/history/SessionList"
import CloudSessionList from "./components/history/CloudSessionList"
import { NotificationsProvider } from "./context/notifications"
import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2"
import "./styles/chat.css"
type ViewType = "newTask" | "marketplace" | "history" | "profile" | "settings"
const VALID_VIEWS = new Set<string>(["newTask", "marketplace", "history", "profile", "settings"])
type ViewType = "newTask" | "marketplace" | "history" | "cloudHistory" | "profile" | "settings"
const VALID_VIEWS = new Set<string>(["newTask", "marketplace", "history", "cloudHistory", "profile", "settings"])
const DummyView: Component<{ title: string }> = (props) => {
return (
@@ -121,6 +122,9 @@ const AppContent: Component = () => {
case "historyButtonClicked":
setCurrentView("history")
break
case "cloudHistoryButtonClicked":
setCurrentView("cloudHistory")
break
case "profileButtonClicked":
setCurrentView("profile")
break
@@ -166,6 +170,14 @@ const AppContent: Component = () => {
<Match when={currentView() === "history"}>
<SessionList onSelectSession={handleSelectSession} />
</Match>
<Match when={currentView() === "cloudHistory"}>
<CloudSessionList
onSelectSession={(cloudSessionId) => {
session.selectCloudSession(cloudSessionId)
setCurrentView("newTask")
}}
/>
</Match>
<Match when={currentView() === "profile"}>
<ProfileView
profileData={server.profileData()}
@@ -0,0 +1,65 @@
import { Component, createSignal } from "solid-js"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import { Button } from "@kilocode/kilo-ui/button"
import { TextField } from "@kilocode/kilo-ui/text-field"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { useLanguage } from "../../context/language"
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
const SES_RE = /ses_[A-Za-z0-9]+/
function extractSessionId(raw: string): string | null {
const trimmed = raw.trim()
const ses = trimmed.match(SES_RE)
if (ses) return ses[0]
const uuid = trimmed.match(UUID_RE)
if (uuid) return uuid[0]
return null
}
interface CloudImportDialogProps {
onImport: (sessionId: string) => void
}
export const CloudImportDialog: Component<CloudImportDialogProps> = (props) => {
const language = useLanguage()
const dialog = useDialog()
const [value, setValue] = createSignal("")
const [error, setError] = createSignal<string | undefined>(undefined)
function submit() {
const id = extractSessionId(value())
if (!id) {
setError(language.t("session.cloud.import.invalid"))
return
}
props.onImport(id)
dialog.close()
}
return (
<Dialog title={language.t("session.cloud.import.title")} fit>
<div class="dialog-confirm-body">
<TextField
autofocus
placeholder={language.t("session.cloud.import.placeholder")}
value={value()}
onChange={setValue}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === "Enter") submit()
}}
validationState={error() ? "invalid" : undefined}
error={error()}
/>
<div class="dialog-confirm-actions">
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</Button>
<Button variant="primary" size="large" onClick={submit}>
{language.t("session.cloud.import.button")}
</Button>
</div>
</div>
</Dialog>
)
}
@@ -6,11 +6,14 @@
import { Component, For, Show, createSignal, createEffect, createMemo, onCleanup, JSX } from "solid-js"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { Button } from "@kilocode/kilo-ui/button"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { useSession } from "../../context/session"
import { useServer } from "../../context/server"
import { useLanguage } from "../../context/language"
import { formatRelativeDate } from "../../utils/date"
import { Message } from "./Message"
import { CloudImportDialog } from "./CloudImportDialog"
/** Inline working/retry indicator shown below messages while the agent is active. */
const WorkingIndicator: Component = () => {
@@ -73,6 +76,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
const session = useSession()
const server = useServer()
const language = useLanguage()
const dialog = useDialog()
let containerRef: HTMLDivElement | undefined
const [isAtBottom, setIsAtBottom] = createSignal(true)
@@ -181,6 +185,21 @@ export const MessageList: Component<MessageListProps> = (props) => {
</For>
</div>
</Show>
<Button
variant="ghost"
size="small"
onClick={() =>
dialog.show(() => (
<CloudImportDialog
onImport={(id) => {
session.selectCloudSession(id)
}}
/>
))
}
>
{language.t("session.cloud.import")}
</Button>
</div>
</Show>
<Show when={!session.loading()}>
@@ -0,0 +1,169 @@
/**
* CloudSessionList component
* Displays cloud sessions from the Kilo cloud API, grouped by date.
* Supports filtering by repository (git URL) and search by title.
*/
import { Component, createSignal, createEffect, onMount, onCleanup, Show } from "solid-js"
import { List } from "@kilocode/kilo-ui/list"
import { Button } from "@kilocode/kilo-ui/button"
import { Checkbox } from "@kilocode/kilo-ui/checkbox"
import { useVSCode } from "../../context/vscode"
import { useLanguage } from "../../context/language"
import { formatRelativeDate } from "../../utils/date"
import type { CloudSessionInfo, ExtensionMessage } from "../../types/messages"
const DATE_GROUP_KEYS = ["time.today", "time.yesterday", "time.thisWeek", "time.thisMonth", "time.older"] as const
function dateGroupKey(iso: string): (typeof DATE_GROUP_KEYS)[number] {
const now = new Date()
const then = new Date(iso)
const DAY_MS = 24 * 60 * 60 * 1000
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterday = new Date(today.getTime() - DAY_MS)
const weekAgo = new Date(today.getTime() - 7 * DAY_MS)
const monthAgo = new Date(today.getTime() - 30 * DAY_MS)
if (then >= today) return DATE_GROUP_KEYS[0]
if (then >= yesterday) return DATE_GROUP_KEYS[1]
if (then >= weekAgo) return DATE_GROUP_KEYS[2]
if (then >= monthAgo) return DATE_GROUP_KEYS[3]
return DATE_GROUP_KEYS[4]
}
interface DisplaySession {
id: string
title: string
updatedAt: string
createdAt: string
}
function toDisplay(s: CloudSessionInfo): DisplaySession {
return {
id: s.session_id,
title: s.title ?? "Untitled",
updatedAt: s.updated_at,
createdAt: s.created_at,
}
}
interface CloudSessionListProps {
onSelectSession?: (id: string) => void
}
const CloudSessionList: Component<CloudSessionListProps> = (props) => {
const vscode = useVSCode()
const language = useLanguage()
const [sessions, setSessions] = createSignal<DisplaySession[]>([])
const [loading, setLoading] = createSignal(false)
const [nextCursor, setNextCursor] = createSignal<string | null>(null)
const [gitUrl, setGitUrl] = createSignal<string | null>(null)
const [repoOnly, setRepoOnly] = createSignal(true)
const [initialized, setInitialized] = createSignal(false)
let loadGen = 0
let activeGen = 0
const unsub = vscode.onMessage((message: ExtensionMessage) => {
if (message.type === "cloudSessionsLoaded") {
if (activeGen !== loadGen) return
const incoming = message.sessions.map(toDisplay)
const cursor = nextCursor()
if (cursor && incoming.length > 0) {
setSessions((prev) => {
const seen = new Set(prev.map((s) => s.id))
return [...prev, ...incoming.filter((s) => !seen.has(s.id))]
})
} else if (!cursor) {
setSessions(incoming)
}
setNextCursor(message.nextCursor)
setLoading(false)
}
if (message.type === "gitRemoteUrlLoaded") {
setGitUrl(message.gitUrl)
setInitialized(true)
}
})
onCleanup(unsub)
onMount(() => {
vscode.postMessage({ type: "requestGitRemoteUrl" })
})
createEffect(() => {
if (!initialized()) return
const url = repoOnly() ? gitUrl() : undefined
loadGen++
activeGen = loadGen
setLoading(true)
setSessions([])
setNextCursor(null)
vscode.postMessage({
type: "requestCloudSessions",
limit: 50,
gitUrl: url ?? undefined,
})
})
function loadMore() {
const cursor = nextCursor()
if (!cursor || loading()) return
const url = repoOnly() ? gitUrl() : undefined
activeGen = loadGen
setLoading(true)
vscode.postMessage({
type: "requestCloudSessions",
cursor,
limit: 50,
gitUrl: url ?? undefined,
})
}
return (
<div class="session-list cloud-session-list">
<div class="cloud-session-filters">
<Checkbox checked={repoOnly()} onChange={setRepoOnly}>
{language.t("session.cloud.repoOnly") ?? "Only this repository"}
</Checkbox>
</div>
<List<DisplaySession>
items={sessions()}
key={(s) => s.id}
filterKeys={["title"]}
onSelect={(s) => {
if (s) props.onSelectSession?.(s.id)
}}
search={{ placeholder: language.t("session.search.placeholder"), autofocus: false }}
emptyMessage={
loading() ? (language.t("common.loading") ?? "Loading...") : (language.t("session.empty") ?? "No sessions")
}
groupBy={(s) => language.t(dateGroupKey(s.updatedAt))}
sortGroupsBy={(a, b) => {
const rank = Object.fromEntries(DATE_GROUP_KEYS.map((k, i) => [language.t(k), i]))
return (rank[a.category] ?? 99) - (rank[b.category] ?? 99)
}}
>
{(s) => (
<>
<span data-slot="list-item-title">{s.title}</span>
<span data-slot="list-item-description">{formatRelativeDate(s.updatedAt)}</span>
</>
)}
</List>
<Show when={nextCursor() && !loading()}>
<div class="cloud-session-load-more">
<Button variant="ghost" size="small" onClick={loadMore}>
{language.t("common.loadMore") ?? "Load more"}
</Button>
</div>
</Show>
</div>
)
}
export default CloudSessionList
@@ -21,6 +21,7 @@ import { useVSCode } from "./vscode"
import { useServer } from "./server"
import { useProvider } from "./provider"
import { useLanguage } from "./language"
import { showToast } from "@kilocode/kilo-ui/toast"
import type {
SessionInfo,
Message,
@@ -125,6 +126,10 @@ interface SessionContextValue {
deleteSession: (id: string) => void
renameSession: (id: string, title: string) => void
syncSession: (sessionID: string) => void
// Cloud session preview
cloudPreviewId: Accessor<string | null>
selectCloudSession: (cloudSessionId: string) => void
}
const SessionContext = createContext<SessionContextValue>()
@@ -175,6 +180,9 @@ export const SessionProvider: ParentComponent = (props) => {
// Pending agent selection for before a session exists (mirrors pendingModelSelection)
const [pendingAgentSelection, setPendingAgentSelection] = createSignal<string | null>(null)
// Cloud session preview state
const [cloudPreviewId, setCloudPreviewId] = createSignal<string | null>(null)
// Store for sessions, messages, parts, todos, modelSelections, agentSelections
const [store, setStore] = createStore<SessionStore>({
sessions: {},
@@ -376,6 +384,26 @@ export const SessionProvider: ParentComponent = (props) => {
// (or has no sessionID for backwards compatibility)
if (!message.sessionID || message.sessionID === currentSessionID()) setLoading(false)
break
case "cloudSessionDataLoaded":
handleCloudSessionDataLoaded(message.cloudSessionId, message.title, message.messages)
break
case "cloudSessionImported":
handleCloudSessionImported(message.cloudSessionId, message.session)
break
case "cloudSessionImportFailed":
setCloudPreviewId(null)
setCurrentSessionID(undefined)
setLoading(false)
showToast({
variant: "error",
title: language.t("session.cloud.import.failed") ?? "Failed to import cloud session",
description: message.error,
})
console.error("[Kilo New] Cloud session import failed:", message.error)
break
}
})
@@ -386,7 +414,15 @@ export const SessionProvider: ParentComponent = (props) => {
function handleSessionCreated(session: SessionInfo) {
batch(() => {
setStore("sessions", session.id, session)
setStore("messages", session.id, [])
// Only initialize messages if none exist yet — a cloud session import
// (handleCloudSessionImported) may have already populated messages for
// this session ID. The SSE session.created event can race with the
// cloudSessionImported message, and wiping to [] causes a flash of
// the empty/welcome screen.
if (!store.messages[session.id]?.length) {
setStore("messages", session.id, [])
}
// If there's a pending model selection, assign it to this new session.
// Guard against duplicate sessionCreated events (HTTP response + SSE)
@@ -641,6 +677,90 @@ export const SessionProvider: ParentComponent = (props) => {
})
}
function handleCloudSessionDataLoaded(cloudSessionId: string, title: string, messages: Message[]) {
if (cloudPreviewId() !== cloudSessionId) return
const key = `cloud:${cloudSessionId}`
batch(() => {
setStore("sessions", key, {
id: key,
title,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
setStore("messages", key, messages)
for (const msg of messages) {
if (msg.parts && msg.parts.length > 0) {
setStore("parts", msg.id, msg.parts)
}
}
setCurrentSessionID(key)
setLoading(false)
})
}
function handleCloudSessionImported(cloudSessionId: string, session: SessionInfo) {
const cloudKey = `cloud:${cloudSessionId}`
const cloudMessages = store.messages[cloudKey] ?? []
batch(() => {
setStore("sessions", session.id, session)
const pending = pendingModelSelection()
if (pending && !store.modelSelections[session.id]) {
setStore("modelSelections", session.id, pending)
}
const pendingAgent = pendingAgentSelection()
if (pendingAgent && !store.agentSelections[session.id]) {
setStore("agentSelections", session.id, pendingAgent)
}
// Carry over cloud messages so there's no loading flash
setStore("messages", session.id, cloudMessages)
setCloudPreviewId(null)
setCurrentSessionID(session.id)
// Clean up synthetic cloud: entries from sessions/messages stores.
//
// Why we do NOT delete cloud parts here:
//
// During preview, parts are stored keyed by the original cloud message IDs
// (e.g. store.parts["<cloud-msg-id>"] = [...]). When the import completes
// we carry cloudMessages into the new local session (above) so the UI
// renders immediately without a loading flash. Those carried-over message
// objects still hold their original cloud IDs, so every <Message> component
// calls getParts("<cloud-msg-id>") — which means the parts must remain in
// the store for now.
//
// If we deleted them here, every message would temporarily render with no
// parts (parts().length === 0), showing only the WorkingIndicator spinner
// until the real data arrives.
//
// Instead, right after this batch we dispatch a "loadMessages" request
// (below). When the extension responds with the "messagesLoaded" event,
// handleMessagesLoaded() replaces the messages array with server-assigned
// IDs and writes new parts keyed by those IDs. The old cloud-keyed part
// entries become orphans — no message in the store references them anymore.
// They remain in store.parts until the webview reloads or the store is
// reset, which is a bounded, one-session-worth amount of data that does
// not accumulate over time.
setStore(
"sessions",
produce((sessions) => {
delete sessions[cloudKey]
}),
)
setStore(
"messages",
produce((messages) => {
delete messages[cloudKey]
}),
)
})
// Load real messages in the background (picks up server-assigned IDs
// and the new user message once the send completes via SSE)
vscode.postMessage({ type: "loadMessages", sessionID: session.id })
}
// Actions
function selectAgent(name: string) {
const id = currentSessionID()
@@ -657,7 +777,22 @@ export const SessionProvider: ParentComponent = (props) => {
return
}
// Phase 4: optimistic user message
const preview = cloudPreviewId()
if (preview) {
const agent = selectedAgentName() !== defaultAgent() ? selectedAgentName() : undefined
vscode.postMessage({
type: "importAndSend",
cloudSessionId: preview,
text,
providerID,
modelID,
agent,
variant: currentVariant(),
files,
})
return
}
const sid = currentSessionID()
if (sid) {
const tempId = `optimistic-${crypto.randomUUID()}`
@@ -776,6 +911,7 @@ export const SessionProvider: ParentComponent = (props) => {
function clearCurrentSession() {
setCurrentSessionID(undefined)
setCloudPreviewId(null)
setLoading(false)
setPermissions([])
setQuestions([])
@@ -799,11 +935,27 @@ export const SessionProvider: ParentComponent = (props) => {
console.warn("[Kilo New] Cannot select session: not connected")
return
}
if (id.startsWith("cloud:")) {
console.warn("[Kilo New] Cannot select cloud preview session via selectSession")
return
}
setCurrentSessionID(id)
setLoading(true)
vscode.postMessage({ type: "loadMessages", sessionID: id })
}
function selectCloudSession(cloudSessionId: string) {
if (!server.isConnected()) {
console.warn("[Kilo New] Cannot select cloud session: not connected")
return
}
const key = `cloud:${cloudSessionId}`
setCloudPreviewId(cloudSessionId)
setCurrentSessionID(key)
setLoading(true)
vscode.postMessage({ type: "requestCloudSessionData", sessionId: cloudSessionId })
}
function deleteSession(id: string) {
if (!server.isConnected()) {
console.warn("[Kilo New] Cannot delete session: not connected")
@@ -849,7 +1001,9 @@ export const SessionProvider: ParentComponent = (props) => {
}
const sessions = createMemo(() =>
Object.values(store.sessions).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()),
Object.values(store.sessions)
.filter((s) => !s.id.startsWith("cloud:"))
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()),
)
const totalCost = createMemo(() => calcTotalCost(messages()))
@@ -932,6 +1086,8 @@ export const SessionProvider: ParentComponent = (props) => {
deleteSession,
renameSession,
syncSession,
cloudPreviewId,
selectCloudSession,
}
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
@@ -688,6 +688,13 @@ export const dict = {
"session.recent": "الأخيرة",
"session.search.placeholder": "البحث في الجلسات...",
"session.empty": "لا توجد جلسات بعد. انقر + لبدء محادثة جديدة.",
"session.cloud.repoOnly": "هذا المستودع فقط",
"session.cloud.import": "استيراد من السحابة",
"session.cloud.import.title": "استيراد من السحابة",
"session.cloud.import.placeholder": "معرّف الجلسة أو الرابط أو أمر kilo import",
"session.cloud.import.button": "استيراد",
"session.cloud.import.invalid": "تنسيق معرّف الجلسة غير صالح",
"session.cloud.import.failed": "فشل استيراد جلسة السحابة",
"workspace.new": "مساحة عمل جديدة",
"workspace.type.local": "محلي",
@@ -695,6 +695,13 @@ export const dict = {
"session.recent": "Recentes",
"session.search.placeholder": "Buscar sessões...",
"session.empty": "Nenhuma sessão ainda. Clique + para iniciar uma nova conversa.",
"session.cloud.repoOnly": "Apenas este repositório",
"session.cloud.import": "Importar da nuvem",
"session.cloud.import.title": "Importar da nuvem",
"session.cloud.import.placeholder": "ID da sessão, URL ou comando kilo import",
"session.cloud.import.button": "Importar",
"session.cloud.import.invalid": "Formato de ID de sessão inválido",
"session.cloud.import.failed": "Falha ao importar sessão da nuvem",
"workspace.new": "Novo espaço de trabalho",
"workspace.type.local": "local",
@@ -722,6 +722,13 @@ export const dict = {
"session.recent": "Nedavne",
"session.search.placeholder": "Pretraži sesije...",
"session.empty": "Još nema sesija. Kliknite + za početak novog razgovora.",
"session.cloud.repoOnly": "Samo ovaj repozitorij",
"session.cloud.import": "Uvezi iz oblaka",
"session.cloud.import.title": "Uvezi iz oblaka",
"session.cloud.import.placeholder": "ID sesije, URL ili kilo import naredba",
"session.cloud.import.button": "Uvezi",
"session.cloud.import.invalid": "Neispravan format ID-a sesije",
"session.cloud.import.failed": "Uvoz sesije iz oblaka nije uspio",
"workspace.new": "Novi radni prostor",
"workspace.type.local": "lokalno",
@@ -694,6 +694,13 @@ export const dict = {
"session.recent": "Seneste",
"session.search.placeholder": "Søg sessioner...",
"session.empty": "Ingen sessioner endnu. Klik + for at starte en ny samtale.",
"session.cloud.repoOnly": "Kun dette repository",
"session.cloud.import": "Importér fra skyen",
"session.cloud.import.title": "Importér fra skyen",
"session.cloud.import.placeholder": "Sessions-ID, URL eller kilo import-kommando",
"session.cloud.import.button": "Importér",
"session.cloud.import.invalid": "Ugyldigt sessions-ID-format",
"session.cloud.import.failed": "Kunne ikke importere cloud-session",
"workspace.new": "Nyt arbejdsområde",
"workspace.type.local": "lokal",
@@ -700,6 +700,13 @@ export const dict = {
"session.recent": "Kürzlich",
"session.search.placeholder": "Sitzungen suchen...",
"session.empty": "Noch keine Sitzungen. Klicke + um eine neue Unterhaltung zu starten.",
"session.cloud.repoOnly": "Nur dieses Repository",
"session.cloud.import": "Aus der Cloud importieren",
"session.cloud.import.title": "Aus der Cloud importieren",
"session.cloud.import.placeholder": "Sitzungs-ID, URL oder kilo import-Befehl",
"session.cloud.import.button": "Importieren",
"session.cloud.import.invalid": "Ungültiges Sitzungs-ID-Format",
"session.cloud.import.failed": "Fehler beim Importieren der Cloud-Sitzung",
"workspace.new": "Neuer Arbeitsbereich",
"workspace.type.local": "lokal",
@@ -727,6 +727,13 @@ export const dict = {
"session.recent": "Recent",
"session.search.placeholder": "Search sessions...",
"session.empty": "No sessions yet. Click + to start a new conversation.",
"session.cloud.repoOnly": "Only this repository",
"session.cloud.import": "Import from cloud",
"session.cloud.import.title": "Import from cloud",
"session.cloud.import.placeholder": "Session ID, URL, or kilo import command",
"session.cloud.import.button": "Import",
"session.cloud.import.invalid": "Invalid session ID format",
"session.cloud.import.failed": "Failed to import cloud session",
"workspace.new": "New workspace",
"workspace.type.local": "local",
@@ -700,6 +700,13 @@ export const dict = {
"session.recent": "Recientes",
"session.search.placeholder": "Buscar sesiones...",
"session.empty": "Aún no hay sesiones. Haz clic en + para iniciar una nueva conversación.",
"session.cloud.repoOnly": "Solo este repositorio",
"session.cloud.import": "Importar desde la nube",
"session.cloud.import.title": "Importar desde la nube",
"session.cloud.import.placeholder": "ID de sesión, URL o comando kilo import",
"session.cloud.import.button": "Importar",
"session.cloud.import.invalid": "Formato de ID de sesión no válido",
"session.cloud.import.failed": "Error al importar la sesión de la nube",
"workspace.new": "Nuevo espacio de trabajo",
"workspace.type.local": "local",
@@ -704,6 +704,13 @@ export const dict = {
"session.recent": "Récentes",
"session.search.placeholder": "Rechercher des sessions...",
"session.empty": "Aucune session pour l'instant. Cliquez + pour démarrer une nouvelle conversation.",
"session.cloud.repoOnly": "Uniquement ce dépôt",
"session.cloud.import": "Importer depuis le cloud",
"session.cloud.import.title": "Importer depuis le cloud",
"session.cloud.import.placeholder": "ID de session, URL ou commande kilo import",
"session.cloud.import.button": "Importer",
"session.cloud.import.invalid": "Format d'ID de session invalide",
"session.cloud.import.failed": "Échec de l'importation de la session cloud",
"workspace.new": "Nouvel espace de travail",
"workspace.type.local": "local",
@@ -688,6 +688,13 @@ export const dict = {
"session.recent": "最近",
"session.search.placeholder": "セッションを検索...",
"session.empty": "セッションがありません。+ をクリックして新しい会話を始めましょう。",
"session.cloud.repoOnly": "このリポジトリのみ",
"session.cloud.import": "クラウドからインポート",
"session.cloud.import.title": "クラウドからインポート",
"session.cloud.import.placeholder": "セッションID、URL、またはkilo importコマンド",
"session.cloud.import.button": "インポート",
"session.cloud.import.invalid": "セッションIDの形式が無効です",
"session.cloud.import.failed": "クラウドセッションのインポートに失敗しました",
"workspace.new": "新しいワークスペース",
"workspace.type.local": "ローカル",
@@ -693,6 +693,13 @@ export const dict = {
"session.recent": "최근",
"session.search.placeholder": "세션 검색...",
"session.empty": "아직 세션이 없습니다. +를 클릭하여 새 대화를 시작하세요.",
"session.cloud.repoOnly": "이 저장소만",
"session.cloud.import": "클라우드에서 가져오기",
"session.cloud.import.title": "클라우드에서 가져오기",
"session.cloud.import.placeholder": "세션 ID, URL 또는 kilo import 명령어",
"session.cloud.import.button": "가져오기",
"session.cloud.import.invalid": "잘못된 세션 ID 형식입니다",
"session.cloud.import.failed": "클라우드 세션 가져오기 실패",
"workspace.new": "새 작업 공간",
"workspace.type.local": "로컬",
@@ -697,6 +697,13 @@ export const dict = {
"session.recent": "Nylige",
"session.search.placeholder": "Søk i sesjoner...",
"session.empty": "Ingen sesjoner ennå. Klikk + for å starte en ny samtale.",
"session.cloud.repoOnly": "Kun dette repositoriet",
"session.cloud.import": "Importer fra skyen",
"session.cloud.import.title": "Importer fra skyen",
"session.cloud.import.placeholder": "Økt-ID, URL eller kilo import-kommando",
"session.cloud.import.button": "Importer",
"session.cloud.import.invalid": "Ugyldig økt-ID-format",
"session.cloud.import.failed": "Kunne ikke importere skyøkt",
"workspace.new": "Nytt arbeidsområde",
"workspace.type.local": "lokal",
@@ -696,6 +696,13 @@ export const dict = {
"session.recent": "Ostatnie",
"session.search.placeholder": "Szukaj sesji...",
"session.empty": "Brak sesji. Kliknij + aby rozpocząć nową rozmowę.",
"session.cloud.repoOnly": "Tylko to repozytorium",
"session.cloud.import": "Importuj z chmury",
"session.cloud.import.title": "Importuj z chmury",
"session.cloud.import.placeholder": "ID sesji, URL lub polecenie kilo import",
"session.cloud.import.button": "Importuj",
"session.cloud.import.invalid": "Nieprawidłowy format ID sesji",
"session.cloud.import.failed": "Nie udało się zaimportować sesji chmurowej",
"workspace.new": "Nowa przestrzeń robocza",
"workspace.type.local": "lokalna",
@@ -700,6 +700,13 @@ export const dict = {
"session.recent": "Недавние",
"session.search.placeholder": "Поиск сессий...",
"session.empty": "Сессий пока нет. Нажмите + чтобы начать новый разговор.",
"session.cloud.repoOnly": "Только этот репозиторий",
"session.cloud.import": "Импорт из облака",
"session.cloud.import.title": "Импорт из облака",
"session.cloud.import.placeholder": "ID сессии, URL или команда kilo import",
"session.cloud.import.button": "Импортировать",
"session.cloud.import.invalid": "Неверный формат ID сессии",
"session.cloud.import.failed": "Не удалось импортировать облачную сессию",
"workspace.new": "Новое рабочее пространство",
"workspace.type.local": "локальное",
@@ -686,6 +686,13 @@ export const dict = {
"session.recent": "ล่าสุด",
"session.search.placeholder": "ค้นหาเซสชัน...",
"session.empty": "ยังไม่มีเซสชัน คลิก + เพื่อเริ่มการสนทนาใหม่",
"session.cloud.repoOnly": "เฉพาะรีโพซิทอรีนี้",
"session.cloud.import": "นำเข้าจากคลาวด์",
"session.cloud.import.title": "นำเข้าจากคลาวด์",
"session.cloud.import.placeholder": "ID เซสชัน, URL หรือคำสั่ง kilo import",
"session.cloud.import.button": "นำเข้า",
"session.cloud.import.invalid": "รูปแบบ ID เซสชันไม่ถูกต้อง",
"session.cloud.import.failed": "ไม่สามารถนำเข้าเซสชันคลาวด์ได้",
"workspace.new": "พื้นที่ทำงานใหม่",
"workspace.type.local": "ในเครื่อง",
@@ -689,6 +689,13 @@ export const dict = {
"session.recent": "最近",
"session.search.placeholder": "搜索会话...",
"session.empty": "暂无会话。点击 + 开始新的对话。",
"session.cloud.repoOnly": "仅此仓库",
"session.cloud.import": "从云端导入",
"session.cloud.import.title": "从云端导入",
"session.cloud.import.placeholder": "会话 ID、URL 或 kilo import 命令",
"session.cloud.import.button": "导入",
"session.cloud.import.invalid": "会话 ID 格式无效",
"session.cloud.import.failed": "导入云会话失败",
"workspace.new": "新建工作区",
"workspace.type.local": "本地",
@@ -686,6 +686,13 @@ export const dict = {
"session.recent": "最近",
"session.search.placeholder": "搜尋工作階段...",
"session.empty": "尚無工作階段。點擊 + 開始新的對話。",
"session.cloud.repoOnly": "僅此儲存庫",
"session.cloud.import": "從雲端匯入",
"session.cloud.import.title": "從雲端匯入",
"session.cloud.import.placeholder": "工作階段 ID、URL 或 kilo import 指令",
"session.cloud.import.button": "匯入",
"session.cloud.import.invalid": "工作階段 ID 格式無效",
"session.cloud.import.failed": "無法匯入雲端工作階段",
"workspace.new": "新增工作區",
"workspace.type.local": "本地",
@@ -764,6 +764,27 @@
white-space: nowrap;
}
/* Cloud Session List */
.cloud-session-list {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.cloud-session-filters {
flex-shrink: 0;
padding: 8px 12px;
border-bottom: 1px solid var(--vscode-panel-border);
}
.cloud-session-load-more {
flex-shrink: 0;
display: flex;
justify-content: center;
padding: 8px;
}
/* ============================================
Kilo Notifications
============================================ */
@@ -104,6 +104,14 @@ export interface SessionInfo {
updatedAt: string
}
// Cloud session info (from Kilo cloud API)
export interface CloudSessionInfo {
session_id: string
title: string | null
created_at: string
updated_at: string
}
// Permission request
export interface PermissionRequest {
id: string
@@ -399,6 +407,36 @@ export interface SessionsLoadedMessage {
sessions: SessionInfo[]
}
export interface CloudSessionsLoadedMessage {
type: "cloudSessionsLoaded"
sessions: CloudSessionInfo[]
nextCursor: string | null
}
export interface GitRemoteUrlLoadedMessage {
type: "gitRemoteUrlLoaded"
gitUrl: string | null
}
export interface CloudSessionDataLoadedMessage {
type: "cloudSessionDataLoaded"
cloudSessionId: string
title: string
messages: Message[]
}
export interface CloudSessionImportedMessage {
type: "cloudSessionImported"
cloudSessionId: string
session: SessionInfo
}
export interface CloudSessionImportFailedMessage {
type: "cloudSessionImportFailed"
cloudSessionId: string
error: string
}
export interface ActionMessage {
type: "action"
action: string
@@ -441,7 +479,7 @@ export interface DeviceAuthCancelledMessage {
export interface NavigateMessage {
type: "navigate"
view: "newTask" | "marketplace" | "history" | "profile" | "settings"
view: "newTask" | "marketplace" | "history" | "cloudHistory" | "profile" | "settings"
}
export interface ProvidersLoadedMessage {
@@ -675,6 +713,8 @@ export type ExtensionMessage =
| MessagesLoadedMessage
| MessageCreatedMessage
| SessionsLoadedMessage
| CloudSessionsLoadedMessage
| GitRemoteUrlLoadedMessage
| ActionMessage
| ProfileDataMessage
| DeviceAuthStartedMessage
@@ -706,6 +746,9 @@ export type ExtensionMessage =
| SetChatBoxMessage
| TriggerTaskMessage
| VariantsLoadedMessage
| CloudSessionDataLoadedMessage
| CloudSessionImportedMessage
| CloudSessionImportFailedMessage
| AgentManagerBranchesMessage
| AgentManagerExternalWorktreesMessage
| AgentManagerImportResultMessage
@@ -759,6 +802,33 @@ export interface LoadSessionsRequest {
type: "loadSessions"
}
export interface RequestCloudSessionsMessage {
type: "requestCloudSessions"
cursor?: string
limit?: number
gitUrl?: string
}
export interface RequestGitRemoteUrlMessage {
type: "requestGitRemoteUrl"
}
export interface RequestCloudSessionDataMessage {
type: "requestCloudSessionData"
sessionId: string
}
export interface ImportAndSendMessage {
type: "importAndSend"
cloudSessionId: string
text: string
providerID?: string
modelID?: string
agent?: string
variant?: string
files?: FileAttachment[]
}
export interface LoginRequest {
type: "login"
}
@@ -1051,6 +1121,8 @@ export type WebviewMessage =
| ClearSessionRequest
| LoadMessagesRequest
| LoadSessionsRequest
| RequestCloudSessionsMessage
| RequestGitRemoteUrlMessage
| LoginRequest
| LogoutRequest
| RefreshProfileRequest
@@ -1098,6 +1170,8 @@ export type WebviewMessage =
| SetSessionsCollapsedRequest
| PersistVariantRequest
| RequestVariantsMessage
| RequestCloudSessionDataMessage
| ImportAndSendMessage
| RequestBranchesMessage
| RequestExternalWorktreesMessage
| ImportFromBranchRequest
+6 -6
View File
@@ -78,13 +78,15 @@
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@kilocode/kilo-gateway": "workspace:*",
"@kilocode/kilo-telemetry": "workspace:*",
"@kilocode/plugin": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@modelcontextprotocol/sdk": "1.25.2",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@kilocode/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.79",
@@ -115,6 +117,7 @@
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"simple-git": "3.31.1",
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",
@@ -125,10 +128,7 @@
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
"zod-to-json-schema": "3.24.5",
"@kilocode/kilo-gateway": "workspace:*",
"@kilocode/kilo-telemetry": "workspace:*",
"simple-git": "3.31.1"
"zod-to-json-schema": "3.24.5"
},
"overrides": {
"drizzle-orm": "1.0.0-beta.12-a5629fb"
@@ -343,6 +343,7 @@ export namespace KiloSessions {
const session = await Session.get(sessionId)
const diffs = await Session.diff(sessionId)
const messages = await Array.fromAsync(MessageV2.stream(sessionId))
messages.reverse()
const models = await Promise.all(
messages
.filter((m) => m.info.role === "user")
+13
View File
@@ -31,6 +31,10 @@ import { ExperimentalRoutes } from "./routes/experimental"
import { TelemetryRoutes } from "./routes/telemetry" // kilocode_change
import { ProviderRoutes } from "./routes/provider"
import { createKiloRoutes } from "@kilocode/kilo-gateway" // kilocode_change
import { Database } from "../storage/db" // kilocode_change
import { Session } from "../session" // kilocode_change
import { Identifier } from "../id/id" // kilocode_change
import { SessionTable, MessageTable, PartTable } from "../session/session.sql" // kilocode_change
import { lazy } from "../util/lazy"
import { InstanceBootstrap } from "../project/bootstrap"
import { NotFoundError } from "../storage/db"
@@ -248,6 +252,15 @@ export namespace Server {
errors,
Auth,
z,
Database, // kilocode_change
Instance, // kilocode_change
SessionTable, // kilocode_change
MessageTable, // kilocode_change
PartTable, // kilocode_change
SessionToRow: Session.toRow, // kilocode_change
Bus, // kilocode_change
SessionCreatedEvent: Session.Event.Created, // kilocode_change
Identifier, // kilocode_change
}),
)
// kilocode_change end
+4 -1
View File
@@ -676,7 +676,10 @@ export namespace SessionPrompt {
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages })
// Build system prompt, adding structured output instruction if needed
const system = [...(await SystemPrompt.environment(model, lastUser.editorContext)), ...(await InstructionPrompt.system())] // kilocode_change
const system = [
...(await SystemPrompt.environment(model, lastUser.editorContext)),
...(await InstructionPrompt.system()),
] // kilocode_change
const format = lastUser.format ?? { type: "text" }
if (format.type === "json_schema") {
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
+125
View File
@@ -43,6 +43,12 @@ import type {
GlobalEventResponses,
GlobalHealthResponses,
InstanceDisposeResponses,
KiloCloudSessionGetErrors,
KiloCloudSessionGetResponses,
KiloCloudSessionImportErrors,
KiloCloudSessionImportResponses,
KiloCloudSessionsErrors,
KiloCloudSessionsResponses,
KiloFimErrors,
KiloFimResponses,
KiloNotificationsErrors,
@@ -986,6 +992,7 @@ export class Session extends HeyApiClient {
parentID?: string
title?: string
permission?: PermissionRuleset
platform?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -998,6 +1005,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "parentID" },
{ in: "body", key: "title" },
{ in: "body", key: "permission" },
{ in: "body", key: "platform" },
],
},
],
@@ -2319,6 +2327,84 @@ export class Organization extends HeyApiClient {
}
}
export class Session2 extends HeyApiClient {
/**
* Get cloud session
*
* Fetch full session data from the Kilo cloud for preview
*/
public get<ThrowOnError extends boolean = false>(
parameters: {
id: string
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "id" },
{ in: "query", key: "directory" },
],
},
],
)
return (options?.client ?? this.client).get<KiloCloudSessionGetResponses, KiloCloudSessionGetErrors, ThrowOnError>({
url: "/kilo/cloud/session/{id}",
...options,
...params,
})
}
/**
* Import session from cloud
*
* Download a cloud-synced session and write it to local storage with fresh IDs.
*/
public import<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
sessionId?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "body", key: "sessionId" },
],
},
],
)
return (options?.client ?? this.client).post<
KiloCloudSessionImportResponses,
KiloCloudSessionImportErrors,
ThrowOnError
>({
url: "/kilo/cloud/session/import",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Cloud extends HeyApiClient {
private _session?: Session2
get session(): Session2 {
return (this._session ??= new Session2({ client: this.client }))
}
}
export class Kilo extends HeyApiClient {
/**
* Get Kilo Gateway profile
@@ -2401,10 +2487,49 @@ export class Kilo extends HeyApiClient {
})
}
/**
* Get cloud sessions
*
* Fetch cloud CLI sessions from Kilo API
*/
public cloudSessions<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
cursor?: string
limit?: number
gitUrl?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "cursor" },
{ in: "query", key: "limit" },
{ in: "query", key: "gitUrl" },
],
},
],
)
return (options?.client ?? this.client).get<KiloCloudSessionsResponses, KiloCloudSessionsErrors, ThrowOnError>({
url: "/kilo/cloud-sessions",
...options,
...params,
})
}
private _organization?: Organization
get organization(): Organization {
return (this._organization ??= new Organization({ client: this.client }))
}
private _cloud?: Cloud
get cloud(): Cloud {
return (this._cloud ??= new Cloud({ client: this.client }))
}
}
export class Find extends HeyApiClient {
+98
View File
@@ -2972,6 +2972,7 @@ export type SessionCreateData = {
parentID?: string
title?: string
permission?: PermissionRuleset
platform?: string
}
path?: never
query?: {
@@ -4465,6 +4466,103 @@ export type KiloNotificationsResponses = {
export type KiloNotificationsResponse = KiloNotificationsResponses[keyof KiloNotificationsResponses]
export type KiloCloudSessionGetData = {
body?: never
path: {
id: string
}
query?: {
directory?: string
}
url: "/kilo/cloud/session/{id}"
}
export type KiloCloudSessionGetErrors = {
/**
* Not found
*/
404: NotFoundError
}
export type KiloCloudSessionGetError = KiloCloudSessionGetErrors[keyof KiloCloudSessionGetErrors]
export type KiloCloudSessionGetResponses = {
/**
* Cloud session data
*/
200: unknown
}
export type KiloCloudSessionImportData = {
body?: {
sessionId: string
}
path?: never
query?: {
directory?: string
}
url: "/kilo/cloud/session/import"
}
export type KiloCloudSessionImportErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type KiloCloudSessionImportError = KiloCloudSessionImportErrors[keyof KiloCloudSessionImportErrors]
export type KiloCloudSessionImportResponses = {
/**
* Imported session info
*/
200: unknown
}
export type KiloCloudSessionsData = {
body?: never
path?: never
query?: {
directory?: string
cursor?: string
limit?: number
gitUrl?: string
}
url: "/kilo/cloud-sessions"
}
export type KiloCloudSessionsErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type KiloCloudSessionsError = KiloCloudSessionsErrors[keyof KiloCloudSessionsErrors]
export type KiloCloudSessionsResponses = {
/**
* Cloud sessions list
*/
200: {
cliSessions: Array<{
session_id: string
title: string | null
created_at: string
updated_at: string
version: number
}>
nextCursor: string | null
}
}
export type KiloCloudSessionsResponse = KiloCloudSessionsResponses[keyof KiloCloudSessionsResponses]
export type FindTextData = {
body?: never
path?: never
+307
View File
@@ -1361,6 +1361,9 @@
},
"permission": {
"$ref": "#/components/schemas/PermissionRuleset"
},
"platform": {
"type": "string"
}
}
}
@@ -2429,6 +2432,32 @@
"variant": {
"type": "string"
},
"editorContext": {
"type": "object",
"properties": {
"visibleFiles": {
"type": "array",
"items": {
"type": "string"
}
},
"openTabs": {
"type": "array",
"items": {
"type": "string"
}
},
"activeFile": {
"type": "string"
},
"shell": {
"type": "string"
},
"timezone": {
"type": "string"
}
}
},
"parts": {
"type": "array",
"items": {
@@ -2808,6 +2837,32 @@
"variant": {
"type": "string"
},
"editorContext": {
"type": "object",
"properties": {
"visibleFiles": {
"type": "array",
"items": {
"type": "string"
}
},
"openTabs": {
"type": "array",
"items": {
"type": "string"
}
},
"activeFile": {
"type": "string"
},
"shell": {
"type": "string"
},
"timezone": {
"type": "string"
}
}
},
"parts": {
"type": "array",
"items": {
@@ -4514,6 +4569,232 @@
]
}
},
"/kilo/cloud/session/{id}": {
"get": {
"operationId": "kilo.cloud.session.get",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "id",
"schema": {
"type": "string"
},
"required": true
}
],
"summary": "Get cloud session",
"description": "Fetch full session data from the Kilo cloud for preview",
"responses": {
"200": {
"description": "Cloud session data",
"content": {
"application/json": {
"schema": {}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.cloud.session.get({\n ...\n})"
}
]
}
},
"/kilo/cloud/session/import": {
"post": {
"operationId": "kilo.cloud.session.import",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
}
],
"summary": "Import session from cloud",
"description": "Download a cloud-synced session and write it to local storage with fresh IDs.",
"responses": {
"200": {
"description": "Imported session info",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
}
},
"required": ["sessionId"]
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.cloud.session.import({\n ...\n})"
}
]
}
},
"/kilo/cloud-sessions": {
"get": {
"operationId": "kilo.cloudSessions",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
}
},
{
"in": "query",
"name": "gitUrl",
"schema": {
"type": "string"
}
}
],
"summary": "Get cloud sessions",
"description": "Fetch cloud CLI sessions from Kilo API",
"responses": {
"200": {
"description": "Cloud sessions list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"cliSessions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"session_id": {
"type": "string"
},
"title": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"created_at": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"version": {
"type": "number"
}
},
"required": ["session_id", "title", "created_at", "updated_at", "version"]
}
},
"nextCursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": ["cliSessions", "nextCursor"]
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.cloudSessions({\n ...\n})"
}
]
}
},
"/find": {
"get": {
"operationId": "find.text",
@@ -6687,6 +6968,32 @@
},
"variant": {
"type": "string"
},
"editorContext": {
"type": "object",
"properties": {
"visibleFiles": {
"type": "array",
"items": {
"type": "string"
}
},
"openTabs": {
"type": "array",
"items": {
"type": "string"
}
},
"activeFile": {
"type": "string"
},
"shell": {
"type": "string"
},
"timezone": {
"type": "string"
}
}
}
},
"required": ["id", "sessionID", "role", "time", "agent", "model"]