diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index a147e0a7ca3..d9130992ca8 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -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}} متاحة الآن للاستخدام.",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index 3ea4a19a9be..1f3dd6b04d2 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -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",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index 5072733aade..f1d695f0ee4 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -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}} พร้อมใช้งานแล้ว",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index 2e624f2071b..69577b3d234 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -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}} 模型了。",
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx
index 9bbb58f3fbe..3b8f0c3c8d3 100644
--- a/packages/app/src/pages/layout/sidebar-items.tsx
+++ b/packages/app/src/pages/layout/sidebar-items.tsx
@@ -32,9 +32,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
0 && props.notify }}
diff --git a/packages/kilo-gateway/src/cloud-sessions.ts b/packages/kilo-gateway/src/cloud-sessions.ts
new file mode 100644
index 00000000000..fd5e9d4c991
--- /dev/null
+++ b/packages/kilo-gateway/src/cloud-sessions.ts
@@ -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 {
+ 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 {
+ 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(callback: (db: DrizzleDb) => T): T
+ effect(fn: () => void | Promise): void
+ }
+ Instance: {
+ readonly directory: string
+ readonly project: { readonly id: string }
+ }
+ SessionTable: object
+ MessageTable: object
+ PartTable: object
+ SessionToRow: (info: any) => Record
+ Bus: { publish(event: { type: string; properties: unknown }, payload: unknown): void | Promise }
+ 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()
+ 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))
+ .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
+}
diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts
index 7393122c71b..bf37784fc81 100644
--- a/packages/kilo-gateway/src/server/routes.ts
+++ b/packages/kilo-gateway/src/server/routes.ts
@@ -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 = {}
+ 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)
+ }
+ },
+ )
}
diff --git a/packages/kilo-ui/src/components/dialog.css b/packages/kilo-ui/src/components/dialog.css
index a1306d92f77..e571f5cc7f8 100644
--- a/packages/kilo-ui/src/components/dialog.css
+++ b/packages/kilo-ui/src/components/dialog.css
@@ -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;
+}
diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json
index cf7ca684354..2f2da3059f8 100644
--- a/packages/kilo-vscode/package.json
+++ b/packages/kilo-vscode/package.json
@@ -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": [
diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts
index 5fd3af3c8c3..a7be3ed06ab 100644
--- a/packages/kilo-vscode/src/KiloProvider.ts
+++ b/packages/kilo-vscode/src/KiloProvider.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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.
*/
diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts
index 42444f61adf..866e495f503 100644
--- a/packages/kilo-vscode/src/extension.ts
+++ b/packages/kilo-vscode/src/extension.ts
@@ -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" })
}),
diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts
index 072bf09a708..27f7f795ad2 100644
--- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts
+++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts
@@ -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 {
+ 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("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 {
+ try {
+ return await this.request("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 {
+ try {
+ return await this.request("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.
diff --git a/packages/kilo-vscode/src/services/cli-backend/index.ts b/packages/kilo-vscode/src/services/cli-backend/index.ts
index 10e9ac968da..b2446bde9ad 100644
--- a/packages/kilo-vscode/src/services/cli-backend/index.ts
+++ b/packages/kilo-vscode/src/services/cli-backend/index.ts
@@ -28,6 +28,7 @@ export type {
Config,
KilocodeNotification,
KilocodeNotificationAction,
+ CloudSessionData,
} from "./types"
export { ServerManager } from "./server-manager"
diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts
index 33f8d87b403..401013b3997 100644
--- a/packages/kilo-vscode/src/services/cli-backend/types.ts
+++ b/packages/kilo-vscode/src/services/cli-backend/types.ts
@@ -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 */
diff --git a/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts b/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts
index d905f9d6e49..af36c3c7b5b 100644
--- a/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts
+++ b/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts
@@ -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)
diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx
index 1826be90736..fdbc3c5e71f 100644
--- a/packages/kilo-vscode/webview-ui/src/App.tsx
+++ b/packages/kilo-vscode/webview-ui/src/App.tsx
@@ -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(["newTask", "marketplace", "history", "profile", "settings"])
+type ViewType = "newTask" | "marketplace" | "history" | "cloudHistory" | "profile" | "settings"
+const VALID_VIEWS = new Set(["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 = () => {
+
+ {
+ session.selectCloudSession(cloudSessionId)
+ setCurrentView("newTask")
+ }}
+ />
+
void
+}
+
+export const CloudImportDialog: Component = (props) => {
+ const language = useLanguage()
+ const dialog = useDialog()
+ const [value, setValue] = createSignal("")
+ const [error, setError] = createSignal(undefined)
+
+ function submit() {
+ const id = extractSessionId(value())
+ if (!id) {
+ setError(language.t("session.cloud.import.invalid"))
+ return
+ }
+ props.onImport(id)
+ dialog.close()
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx
index fbbe63dadbd..c02417286b0 100644
--- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx
+++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx
@@ -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 = (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 = (props) => {
+
diff --git a/packages/kilo-vscode/webview-ui/src/components/history/CloudSessionList.tsx b/packages/kilo-vscode/webview-ui/src/components/history/CloudSessionList.tsx
new file mode 100644
index 00000000000..e6c1c0a119c
--- /dev/null
+++ b/packages/kilo-vscode/webview-ui/src/components/history/CloudSessionList.tsx
@@ -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 = (props) => {
+ const vscode = useVSCode()
+ const language = useLanguage()
+
+ const [sessions, setSessions] = createSignal([])
+ const [loading, setLoading] = createSignal(false)
+ const [nextCursor, setNextCursor] = createSignal(null)
+ const [gitUrl, setGitUrl] = createSignal(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 (
+
+
+
+ {language.t("session.cloud.repoOnly") ?? "Only this repository"}
+
+
+
+ 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) => (
+ <>
+ {s.title}
+ {formatRelativeDate(s.updatedAt)}
+ >
+ )}
+
+
+
+
+
+
+
+ )
+}
+
+export default CloudSessionList
diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx
index 3333daccfa7..dc8ecf709d8 100644
--- a/packages/kilo-vscode/webview-ui/src/context/session.tsx
+++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx
@@ -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
+ selectCloudSession: (cloudSessionId: string) => void
}
const SessionContext = createContext()
@@ -175,6 +180,9 @@ export const SessionProvider: ParentComponent = (props) => {
// Pending agent selection for before a session exists (mirrors pendingModelSelection)
const [pendingAgentSelection, setPendingAgentSelection] = createSignal(null)
+ // Cloud session preview state
+ const [cloudPreviewId, setCloudPreviewId] = createSignal(null)
+
// Store for sessions, messages, parts, todos, modelSelections, agentSelections
const [store, setStore] = createStore({
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[""] = [...]). 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 component
+ // calls getParts("") — 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 {props.children}
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
index ab8e3611836..2449efea7cf 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts
@@ -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": "محلي",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts
index b3d609bb1f4..b7c47b6ca29 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
index 499df4c2fd9..2c492e6f01a 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts
index ff4d93e3a65..bbebc706aea 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts
index b74f9673840..a0fd13f093e 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts
index 522ef9e241a..2dcd3222dda 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts
index 843f0453a75..fe795895044 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
index 7589704f058..a3b4ec9c326 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
index f4ba6f52d26..2bebaad73f0 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts
@@ -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": "ローカル",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
index 47c80359bec..086bad57374 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts
@@ -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": "로컬",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts
index 11e485b8965..9b1a09f2ac9 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
index 4b370d21d29..eec0eed2130 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts
@@ -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",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
index 27bd3b08a71..4635e404a3b 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts
@@ -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": "локальное",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts
index 828035cfb2d..32ab6a97f1b 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts
@@ -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": "ในเครื่อง",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
index 931115c492e..7ad871d3efd 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts
@@ -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": "本地",
diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
index 9794d484c3b..55a09d6bda6 100644
--- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
+++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts
@@ -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": "本地",
diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css
index adf9bca17c0..cbd3dad19ed 100644
--- a/packages/kilo-vscode/webview-ui/src/styles/chat.css
+++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css
@@ -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
============================================ */
diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts
index d085b6f6616..795d8e2a19c 100644
--- a/packages/kilo-vscode/webview-ui/src/types/messages.ts
+++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts
@@ -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
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index e5d2b09d8f5..622db2eea6f 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -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"
diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts
index d3d916fd62f..479bffed51e 100644
--- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts
+++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts
@@ -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")
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index be42471a1f1..a14c7b2084c 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -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
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index c5fa4de1e09..365e9c9585b 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -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)
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index ec117c527ea..98b7840d74c 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -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,
) {
@@ -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(
+ parameters: {
+ id: string
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "id" },
+ { in: "query", key: "directory" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get({
+ 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(
+ parameters?: {
+ directory?: string
+ sessionId?: string
+ },
+ options?: Options,
+ ) {
+ 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(
+ parameters?: {
+ directory?: string
+ cursor?: string
+ limit?: number
+ gitUrl?: string
+ },
+ options?: Options,
+ ) {
+ 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({
+ 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 {
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 0685e4e4ea2..a7fce92571d 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -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
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 4ef3e49b93d..6dac5d7b220 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -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"]