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 ( + +
+ { + if (e.key === "Enter") submit() + }} + validationState={error() ? "invalid" : undefined} + error={error()} + /> +
+ + +
+
+
+ ) +} 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"]