Skip to content

Commit 6852e80

Browse files
authored
Merge pull request #4145 from Kilo-Org/add-session-commands
2 parents e70b967 + aa3c6b5 commit 6852e80

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+923
-849
lines changed

.changeset/late-paths-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kilo-code": patch
3+
---
4+
5+
update shared session url

.changeset/twenty-rocks-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kilo-code": minor
3+
---
4+
5+
add session sharing and forking

cli/src/commands/__tests__/session.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ describe("sessionCommand", () => {
100100
expect(sessionCommand.examples).toContain("/session search <query>")
101101
expect(sessionCommand.examples).toContain("/session select <sessionId>")
102102
expect(sessionCommand.examples).toContain("/session share")
103-
expect(sessionCommand.examples).toContain("/session fork <shareId>")
103+
expect(sessionCommand.examples).toContain("/session fork <id>")
104104
expect(sessionCommand.examples).toContain("/session delete <sessionId>")
105105
expect(sessionCommand.examples).toContain("/session rename <new name>")
106106
})
@@ -621,7 +621,7 @@ describe("sessionCommand", () => {
621621

622622
const replacedMessages = (mockContext.replaceMessages as ReturnType<typeof vi.fn>).mock.calls[0][0]
623623
expect(replacedMessages).toHaveLength(2)
624-
expect(replacedMessages[1].content).toContain("Forking session from share ID")
624+
expect(replacedMessages[1].content).toContain("Forking session from ID")
625625
expect(replacedMessages[1].content).toContain("share-123")
626626
})
627627

@@ -633,7 +633,7 @@ describe("sessionCommand", () => {
633633
expect(mockContext.addMessage).toHaveBeenCalledTimes(1)
634634
const message = (mockContext.addMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]
635635
expect(message.type).toBe("error")
636-
expect(message.content).toContain("Usage: /session fork <shareId>")
636+
expect(message.content).toContain("Usage: /session fork <id>")
637637
expect(mockSessionManager.forkSession).not.toHaveBeenCalled()
638638
})
639639

@@ -645,7 +645,7 @@ describe("sessionCommand", () => {
645645
expect(mockContext.addMessage).toHaveBeenCalledTimes(1)
646646
const message = (mockContext.addMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]
647647
expect(message.type).toBe("error")
648-
expect(message.content).toContain("Usage: /session fork <shareId>")
648+
expect(message.content).toContain("Usage: /session fork <id>")
649649
})
650650

651651
it("should handle fork error gracefully", async () => {

cli/src/commands/session.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@ async function listSessions(context: CommandContext): Promise<void> {
4141
const sessionClient = sessionService.sessionClient
4242

4343
try {
44-
const result = await sessionClient.list({ limit: 50 })
45-
const { cliSessions } = result
46-
47-
if (cliSessions.length === 0) {
44+
const result = await sessionClient?.list({ limit: 50 })
45+
if (!result || result.cliSessions.length === 0) {
4846
addMessage({
4947
...generateMessage(),
5048
type: "system",
@@ -53,6 +51,8 @@ async function listSessions(context: CommandContext): Promise<void> {
5351
return
5452
}
5553

54+
const { cliSessions } = result
55+
5656
// Format and display sessions
5757
let content = `**Available Sessions:**\n\n`
5858
cliSessions.forEach((session, index) => {
@@ -148,10 +148,9 @@ async function searchSessions(context: CommandContext, query: string): Promise<v
148148
}
149149

150150
try {
151-
const result = await sessionClient.search({ search_string: query, limit: 20 })
152-
const { results, total } = result
151+
const result = await sessionClient?.search({ search_string: query, limit: 20 })
153152

154-
if (results.length === 0) {
153+
if (!result || result.results.length === 0) {
155154
addMessage({
156155
...generateMessage(),
157156
type: "system",
@@ -160,6 +159,8 @@ async function searchSessions(context: CommandContext, query: string): Promise<v
160159
return
161160
}
162161

162+
const { results, total } = result
163+
163164
let content = `**Search Results** (${results.length} of ${total}):\n\n`
164165
results.forEach((session, index) => {
165166
const isActive = session.session_id === sessionService.sessionId ? " * [Active]" : ""
@@ -198,7 +199,7 @@ async function shareSession(context: CommandContext): Promise<void> {
198199
addMessage({
199200
...generateMessage(),
200201
type: "system",
201-
content: `✅ Session shared successfully!\n\n\`https://kilo.ai/share/${result.share_id}\``,
202+
content: `✅ Session shared successfully!\n\n\`https://app.kilo.ai/share/${result.share_id}\``,
202203
})
203204
} catch (error) {
204205
addMessage({
@@ -212,15 +213,15 @@ async function shareSession(context: CommandContext): Promise<void> {
212213
/**
213214
* Fork a shared session by share ID
214215
*/
215-
async function forkSession(context: CommandContext, shareId: string): Promise<void> {
216+
async function forkSession(context: CommandContext, id: string): Promise<void> {
216217
const { addMessage, replaceMessages, refreshTerminal } = context
217218
const sessionService = SessionManager.init()
218219

219-
if (!shareId) {
220+
if (!id) {
220221
addMessage({
221222
...generateMessage(),
222223
type: "error",
223-
content: "Usage: /session fork <shareId>",
224+
content: "Usage: /session fork <id>",
224225
})
225226
return
226227
}
@@ -238,14 +239,14 @@ async function forkSession(context: CommandContext, shareId: string): Promise<vo
238239
{
239240
id: `system-${now + 1}`,
240241
type: "system",
241-
content: `Forking session from share ID \`${shareId}\`...`,
242+
content: `Forking session from ID \`${id}\`...`,
242243
ts: 2,
243244
},
244245
])
245246

246247
await refreshTerminal()
247248

248-
await sessionService.forkSession(shareId, true)
249+
await sessionService.forkSession(id, true)
249250

250251
// Success message handled by restoreSession via extension messages
251252
} catch (error) {
@@ -275,6 +276,10 @@ async function deleteSession(context: CommandContext, sessionId: string): Promis
275276
}
276277

277278
try {
279+
if (!sessionClient) {
280+
throw new Error("SessionManager used before initialization")
281+
}
282+
278283
await sessionClient.delete({ session_id: sessionId })
279284

280285
addMessage({
@@ -340,7 +345,11 @@ async function sessionIdAutocompleteProvider(context: ArgumentProviderContext):
340345
}
341346

342347
try {
343-
const response = await sessionClient.search({ search_string: prefix, limit: 20 })
348+
const response = await sessionClient?.search({ search_string: prefix, limit: 20 })
349+
350+
if (!response) {
351+
return []
352+
}
344353

345354
return response.results.map((session, index) => {
346355
const title = session.title || "Untitled"
@@ -373,7 +382,7 @@ export const sessionCommand: Command = {
373382
"/session search <query>",
374383
"/session select <sessionId>",
375384
"/session share",
376-
"/session fork <shareId>",
385+
"/session fork <id>",
377386
"/session delete <sessionId>",
378387
"/session rename <new name>",
379388
],
@@ -390,7 +399,7 @@ export const sessionCommand: Command = {
390399
{ value: "search", description: "Search sessions by title or ID" },
391400
{ value: "select", description: "Restore a session" },
392401
{ value: "share", description: "Share current session publicly" },
393-
{ value: "fork", description: "Fork a shared session" },
402+
{ value: "fork", description: "Fork a session" },
394403
{ value: "delete", description: "Delete a session" },
395404
{ value: "rename", description: "Rename the current session" },
396405
],

cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ program
4444
.option("-pv, --provider <id>", "Select provider by ID (e.g., 'kilocode-1')")
4545
.option("-mo, --model <model>", "Override model for the selected provider")
4646
.option("-s, --session <sessionId>", "Restore a session by ID")
47-
.option("-f, --fork <shareId>", "Fork a shared session by share ID")
47+
.option("-f, --fork <shareId>", "Fork a session by ID")
4848
.option("--nosplash", "Disable the welcome message and update notifications", false)
4949
.argument("[prompt]", "The prompt or command to execute")
5050
.action(async (prompt, options) => {

src/activate/handleUri.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ export const handleUri = async (uri: vscode.Uri) => {
4646
})
4747
break
4848
}
49+
case "/kilo/fork": {
50+
const id = query.get("id")
51+
if (id) {
52+
await visibleProvider.postMessageToWebview({
53+
type: "invoke",
54+
invoke: "setChatBoxMessage",
55+
text: `/session fork ${id}`,
56+
})
57+
await visibleProvider.postMessageToWebview({
58+
type: "action",
59+
action: "focusInput",
60+
})
61+
}
62+
break
63+
}
4964
// kilocode_change end
5065
case "/requesty": {
5166
const code = query.get("code")

src/core/webview/webviewMessageHandler.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import { getSapAiCoreDeployments } from "../../api/providers/fetchers/sap-ai-cor
9393
import { AutoPurgeScheduler } from "../../services/auto-purge" // kilocode_change
9494
import { setPendingTodoList } from "../tools/UpdateTodoListTool"
9595
import { ManagedIndexer } from "../../services/code-index/managed/ManagedIndexer"
96+
import { SessionManager } from "../../shared/kilocode/cli-sessions/core/SessionManager" // kilocode_change
9697

9798
export const webviewMessageHandler = async (
9899
provider: ClineProvider,
@@ -3842,6 +3843,72 @@ export const webviewMessageHandler = async (
38423843
}
38433844
break
38443845
}
3846+
case "sessionShare": {
3847+
try {
3848+
const sessionService = SessionManager.init()
3849+
3850+
if (!sessionService.sessionId) {
3851+
vscode.window.showErrorMessage("No active session. Start a new task to create a session.")
3852+
break
3853+
}
3854+
3855+
const result = await sessionService.shareSession()
3856+
3857+
const shareUrl = `https://app.kilo.ai/share/${result.share_id}`
3858+
3859+
// Copy URL to clipboard and show success notification
3860+
await vscode.env.clipboard.writeText(shareUrl)
3861+
vscode.window.showInformationMessage(`Session shared! Link copied to clipboard: ${shareUrl}`)
3862+
} catch (error) {
3863+
const errorMessage = error instanceof Error ? error.message : String(error)
3864+
vscode.window.showErrorMessage(`Failed to share session: ${errorMessage}`)
3865+
}
3866+
break
3867+
}
3868+
case "shareTaskSession": {
3869+
try {
3870+
if (!message.text) {
3871+
vscode.window.showErrorMessage("Task ID is required for sharing a task session")
3872+
break
3873+
}
3874+
3875+
const taskId = message.text
3876+
const sessionService = SessionManager.init()
3877+
3878+
const sessionId = await sessionService.getSessionFromTask(taskId, provider)
3879+
3880+
const result = await sessionService.shareSession(sessionId)
3881+
3882+
const shareUrl = `https://app.kilo.ai/share/${result.share_id}`
3883+
3884+
await vscode.env.clipboard.writeText(shareUrl)
3885+
vscode.window.showInformationMessage(`Session shared! Link copied to clipboard.`)
3886+
} catch (error) {
3887+
const errorMessage = error instanceof Error ? error.message : String(error)
3888+
vscode.window.showErrorMessage(`Failed to share task session: ${errorMessage}`)
3889+
}
3890+
break
3891+
}
3892+
case "sessionFork": {
3893+
try {
3894+
if (!message.shareId) {
3895+
vscode.window.showErrorMessage("ID is required for forking a session")
3896+
break
3897+
}
3898+
3899+
const sessionService = SessionManager.init()
3900+
3901+
await sessionService.forkSession(message.shareId, true)
3902+
3903+
await provider.postStateToWebview()
3904+
3905+
vscode.window.showInformationMessage(`Session forked successfully from ${message.shareId}`)
3906+
} catch (error) {
3907+
const errorMessage = error instanceof Error ? error.message : String(error)
3908+
vscode.window.showErrorMessage(`Failed to fork session: ${errorMessage}`)
3909+
}
3910+
break
3911+
}
38453912
case "singleCompletion": {
38463913
try {
38473914
const { text, completionRequestId } = message

src/extension.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ import { flushModels, getModels } from "./api/providers/fetchers/modelCache"
5151
import { ManagedIndexer } from "./services/code-index/managed/ManagedIndexer" // kilocode_change
5252
import { kilo_initializeSessionManager } from "./shared/kilocode/cli-sessions/extension/session-manager-utils"
5353

54+
// kilocode_change start
55+
async function findKilocodeTokenFromAnyProfile(provider: ClineProvider): Promise<string | undefined> {
56+
const { apiConfiguration } = await provider.getState()
57+
if (apiConfiguration.kilocodeToken) {
58+
return apiConfiguration.kilocodeToken
59+
}
60+
61+
const profiles = await provider.providerSettingsManager.listConfig()
62+
63+
for (const profile of profiles) {
64+
try {
65+
const fullProfile = await provider.providerSettingsManager.getProfile({ name: profile.name })
66+
if (fullProfile.kilocodeToken) {
67+
return fullProfile.kilocodeToken
68+
}
69+
} catch {
70+
continue
71+
}
72+
}
73+
74+
return undefined
75+
}
76+
// kilocode_change end
77+
5478
/**
5579
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
5680
*
@@ -270,11 +294,11 @@ export async function activate(context: vscode.ExtensionContext) {
270294

271295
// kilocode_change start
272296
try {
273-
const { apiConfiguration } = await provider.getState()
297+
const kiloToken = await findKilocodeTokenFromAnyProfile(provider)
274298

275299
await kilo_initializeSessionManager({
276300
context: context,
277-
kiloToken: apiConfiguration.kilocodeToken,
301+
kiloToken,
278302
log: provider.log.bind(provider),
279303
outputChannel,
280304
provider,

src/services/kilo-session/ExtensionMessengerImpl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import type { WebviewMessage } from "../../shared/kilocode/cli-sessions/types/IE
33
import type { ExtensionMessage } from "../../shared/ExtensionMessage"
44
import type { ClineProvider } from "../../core/webview/ClineProvider"
55
import { singleCompletionHandler } from "../../utils/single-completion-handler"
6+
import { webviewMessageHandler } from "../../core/webview/webviewMessageHandler"
67

78
export class ExtensionMessengerImpl implements IExtensionMessenger {
89
constructor(private readonly provider: ClineProvider) {}
910

11+
// we can directly handle whatever is sent
1012
async sendWebviewMessage(message: WebviewMessage): Promise<void> {
11-
await this.provider.postMessageToWebview(message as unknown as ExtensionMessage)
13+
return webviewMessageHandler(this.provider, message)
1214
}
1315

1416
async requestSingleCompletion(prompt: string, timeoutMs: number): Promise<string> {

src/services/kilo-session/ExtensionPathProvider.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,20 @@ export class ExtensionPathProvider implements IPathProvider {
99

1010
constructor(context: vscode.ExtensionContext) {
1111
this.globalStoragePath = context.globalStorageUri.fsPath
12-
this.ensureDirectories()
13-
}
14-
15-
private ensureDirectories(): void {
16-
const sessionsDir = path.join(this.globalStoragePath, "sessions")
17-
const tasksDir = this.getTasksDir()
18-
const workspacesDir = path.join(sessionsDir, "workspaces")
19-
20-
for (const dir of [sessionsDir, tasksDir, workspacesDir]) {
21-
if (!existsSync(dir)) {
22-
mkdirSync(dir, { recursive: true })
23-
}
24-
}
2512
}
2613

2714
getTasksDir(): string {
28-
return path.join(this.globalStoragePath, "sessions", "tasks")
15+
return path.join(this.globalStoragePath, "tasks")
2916
}
3017

31-
getSessionFilePath(workspaceDir: string): string {
32-
const hash = createHash("sha256").update(workspaceDir).digest("hex").substring(0, 16)
33-
const workspacesDir = path.join(this.globalStoragePath, "sessions", "workspaces")
34-
const workspaceSessionDir = path.join(workspacesDir, hash)
18+
getSessionFilePath(workspaceName: string): string {
19+
const hash = createHash("sha256").update(workspaceName).digest("hex").substring(0, 16)
20+
const workspaceDir = path.join(this.globalStoragePath, "sessions", hash)
3521

36-
if (!existsSync(workspaceSessionDir)) {
37-
mkdirSync(workspaceSessionDir, { recursive: true })
22+
if (!existsSync(workspaceDir)) {
23+
mkdirSync(workspaceDir, { recursive: true })
3824
}
3925

40-
return path.join(workspaceSessionDir, "session.json")
26+
return path.join(workspaceDir, "session.json")
4127
}
4228
}

0 commit comments

Comments
 (0)