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