Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
eee2ea4
make session manager deps mandatory
iscekic Dec 12, 2025
0aa9ad7
remove optional access and checks
iscekic Dec 12, 2025
e88c50d
fix type
iscekic Dec 12, 2025
435d450
extract git state service
iscekic Dec 12, 2025
0a682a0
remove tests, we will recreate them later
iscekic Dec 12, 2025
176c625
extract session state manager
iscekic Dec 12, 2025
d802f00
add sync queue
iscekic Dec 12, 2025
6dceea1
extract token validation service
iscekic Dec 12, 2025
8b94c1d
extract session title service
iscekic Dec 12, 2025
d0fabd9
extract session lifecycle service
iscekic Dec 12, 2025
2c97c09
extract session sync service
iscekic Dec 12, 2025
fbce5c0
finalize facade refactor
iscekic Dec 12, 2025
e6f936c
dedupe workspace dir state
iscekic Dec 12, 2025
d35a290
fix compilation inconsistencies between cli and extension
iscekic Dec 12, 2025
9776fe1
Merge branch 'main' into improve-session-manager-maintainability
iscekic Dec 12, 2025
8111bf8
avoid circular dep
iscekic Dec 12, 2025
d598603
try to invalidate token on auth errors instead of all
iscekic Dec 12, 2025
d665be7
add TrpcError
iscekic Dec 12, 2025
acf2f70
centralize config
iscekic Dec 12, 2025
284e475
slightly improve sync queue internals
iscekic Dec 12, 2025
0d04ba7
add tests
iscekic Dec 12, 2025
dc486df
add missing facade methods
iscekic Dec 12, 2025
9cd28f5
describe version
iscekic Dec 12, 2025
be49a0f
ensure queue is flushed after forcing sync
iscekic Dec 12, 2025
03bf2c7
move logs
iscekic Dec 12, 2025
c3049cb
add in-memory task->session mapping
iscekic Dec 12, 2025
db91c42
fix copilot remarks
iscekic Dec 12, 2025
3ed8fdf
use state manager methods when getting session for task
iscekic Dec 12, 2025
2b5c588
rename test file
iscekic Dec 12, 2025
c6517eb
fix failing cli tests
iscekic Dec 12, 2025
a88693e
Merge branch 'main' into improve-session-manager-maintainability
iscekic Dec 15, 2025
bcbad32
Merge branch 'main' into improve-session-manager-maintainability
iscekic Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,14 @@ export class CLI {
logs.debug("SessionManager initialized with dependencies", "CLI")

const workspace = this.options.workspace || process.cwd()
this.sessionService.setWorkspaceDirectory(workspace)
this.sessionService?.setWorkspaceDirectory(workspace)
logs.debug("SessionManager workspace directory set", "CLI", { workspace })

if (this.options.session) {
await this.sessionService.restoreSession(this.options.session)
await this.sessionService?.restoreSession(this.options.session)
} else if (this.options.fork) {
logs.info("Forking session from share ID", "CLI", { shareId: this.options.fork })
await this.sessionService.forkSession(this.options.fork)
await this.sessionService?.forkSession(this.options.fork)
}
}

Expand Down
8 changes: 7 additions & 1 deletion cli/src/commands/__tests__/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ vi.mock("simple-git", () => ({

describe("sessionCommand", () => {
let mockContext: CommandContext
let mockSessionManager: Partial<SessionManager>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mockSessionManager: any // Use any to allow mocking the sessionId getter
let mockSessionClient: Partial<SessionClient>

beforeEach(() => {
Expand All @@ -54,13 +55,18 @@ describe("sessionCommand", () => {
limit: 20,
offset: 0,
}),
delete: vi.fn().mockResolvedValue({ success: true }),
}

// Create a mock session manager instance with sessionClient property
mockSessionManager = {
sessionId: null,
restoreSession: vi.fn().mockResolvedValue(undefined),
sessionClient: mockSessionClient as SessionClient,
// Add facade methods that delegate to sessionClient
listSessions: vi.fn().mockImplementation((input) => mockSessionClient.list?.(input)),
searchSessions: vi.fn().mockImplementation((input) => mockSessionClient.search?.(input)),
deleteSession: vi.fn().mockImplementation((input) => mockSessionClient.delete?.(input)),
}

// Mock SessionManager.init to return our mock instance
Expand Down
32 changes: 16 additions & 16 deletions cli/src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async function showSessionId(context: CommandContext): Promise<void> {
const { addMessage } = context

const sessionService = SessionManager.init()
const sessionId = sessionService.sessionId
const sessionId = sessionService?.sessionId

if (!sessionId) {
addMessage({
Expand All @@ -38,10 +38,9 @@ async function showSessionId(context: CommandContext): Promise<void> {
async function listSessions(context: CommandContext): Promise<void> {
const { addMessage } = context
const sessionService = SessionManager.init()
const sessionClient = sessionService.sessionClient

try {
const result = await sessionClient?.list({ limit: 50 })
const result = await sessionService?.listSessions({ limit: 50 })
if (!result || result.cliSessions.length === 0) {
addMessage({
...generateMessage(),
Expand All @@ -56,7 +55,7 @@ async function listSessions(context: CommandContext): Promise<void> {
// Format and display sessions
let content = `**Available Sessions:**\n\n`
cliSessions.forEach((session, index) => {
const isActive = session.session_id === sessionService.sessionId ? " * [Active]" : ""
const isActive = session.session_id === sessionService?.sessionId ? " * [Active]" : ""
const title = session.title || "Untitled"
const createdTime = formatRelativeTime(new Date(session.created_at).getTime())

Expand Down Expand Up @@ -118,7 +117,7 @@ async function selectSession(context: CommandContext, sessionId: string): Promis
])

await refreshTerminal()
await sessionService.restoreSession(sessionId, true)
await sessionService?.restoreSession(sessionId, true)

// Success message is handled by restoreSession via extension messages
} catch (error) {
Expand All @@ -136,7 +135,6 @@ async function selectSession(context: CommandContext, sessionId: string): Promis
async function searchSessions(context: CommandContext, query: string): Promise<void> {
const { addMessage } = context
const sessionService = SessionManager.init()
const sessionClient = sessionService.sessionClient

if (!query) {
addMessage({
Expand All @@ -148,7 +146,7 @@ async function searchSessions(context: CommandContext, query: string): Promise<v
}

try {
const result = await sessionClient?.search({ search_string: query, limit: 20 })
const result = await sessionService?.searchSessions({ search_string: query, limit: 20 })

if (!result || result.results.length === 0) {
addMessage({
Expand All @@ -163,7 +161,7 @@ async function searchSessions(context: CommandContext, query: string): Promise<v

let content = `**Search Results** (${results.length} of ${total}):\n\n`
results.forEach((session, index) => {
const isActive = session.session_id === sessionService.sessionId ? " * [Active]" : ""
const isActive = session.session_id === sessionService?.sessionId ? " * [Active]" : ""
const title = session.title || "Untitled"
const createdTime = formatRelativeTime(new Date(session.created_at).getTime())

Expand Down Expand Up @@ -194,7 +192,11 @@ async function shareSession(context: CommandContext): Promise<void> {
const sessionService = SessionManager.init()

try {
const result = await sessionService.shareSession()
const result = await sessionService?.shareSession()

if (!result) {
throw new Error("SessionManager not initialized")
}

addMessage({
...generateMessage(),
Expand Down Expand Up @@ -246,7 +248,7 @@ async function forkSession(context: CommandContext, id: string): Promise<void> {

await refreshTerminal()

await sessionService.forkSession(id, true)
await sessionService?.forkSession(id, true)

// Success message handled by restoreSession via extension messages
} catch (error) {
Expand All @@ -264,7 +266,6 @@ async function forkSession(context: CommandContext, id: string): Promise<void> {
async function deleteSession(context: CommandContext, sessionId: string): Promise<void> {
const { addMessage } = context
const sessionService = SessionManager.init()
const sessionClient = sessionService.sessionClient

if (!sessionId) {
addMessage({
Expand All @@ -276,11 +277,11 @@ async function deleteSession(context: CommandContext, sessionId: string): Promis
}

try {
if (!sessionClient) {
if (!sessionService) {
throw new Error("SessionManager used before initialization")
}

await sessionClient.delete({ session_id: sessionId })
await sessionService.deleteSession({ session_id: sessionId })

addMessage({
...generateMessage(),
Expand Down Expand Up @@ -313,7 +314,7 @@ async function renameSession(context: CommandContext, newName: string): Promise<
}

try {
if (!sessionService.sessionId) {
if (!sessionService?.sessionId) {
throw new Error("No active session to rename")
}

Expand All @@ -338,7 +339,6 @@ async function renameSession(context: CommandContext, newName: string): Promise<
*/
async function sessionIdAutocompleteProvider(context: ArgumentProviderContext): Promise<ArgumentSuggestion[]> {
const sessionService = SessionManager.init()
const sessionClient = sessionService.sessionClient

// Extract prefix from user input
const prefix = context.partialInput.trim()
Expand All @@ -349,7 +349,7 @@ async function sessionIdAutocompleteProvider(context: ArgumentProviderContext):
}

try {
const response = await sessionClient?.search({ search_string: prefix, limit: 20 })
const response = await sessionService?.searchSessions({ search_string: prefix, limit: 20 })

if (!response) {
return []
Expand Down
10 changes: 5 additions & 5 deletions cli/src/state/atoms/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,17 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension
if (message.type === "apiMessagesSaved" && message.payload) {
const [taskId, filePath] = message.payload as [string, string]

SessionManager.init().handleFileUpdate(taskId, "apiConversationHistoryPath", filePath)
SessionManager.init()?.handleFileUpdate(taskId, "apiConversationHistoryPath", filePath)
} else if (message.type === "taskMessagesSaved" && message.payload) {
const [taskId, filePath] = message.payload as [string, string]

SessionManager.init().handleFileUpdate(taskId, "uiMessagesPath", filePath)
SessionManager.init()?.handleFileUpdate(taskId, "uiMessagesPath", filePath)
} else if (message.type === "taskMetadataSaved" && message.payload) {
const [taskId, filePath] = message.payload as [string, string]

SessionManager.init().handleFileUpdate(taskId, "taskMetadataPath", filePath)
SessionManager.init()?.handleFileUpdate(taskId, "taskMetadataPath", filePath)
} else if (message.type === "currentCheckpointUpdated") {
SessionManager.init().doSync()
SessionManager.init()?.doSync()
}

// Handle different message types
Expand Down Expand Up @@ -604,7 +604,7 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension

set(ciCompletionDetectedAtom, true)

SessionManager.init().doSync(true)
SessionManager.init()?.doSync(true)
}
}
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/kilocode/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export class AgentManagerProvider implements vscode.Disposable {
break
case "agentManager.sessionShare":
SessionManager.init()
.shareSession(message.sessionId as string)
?.shareSession(message.sessionId as string)
.then((result) => {
const shareUrl = `https://app.kilo.ai/share/${result.share_id}`

Expand Down
35 changes: 24 additions & 11 deletions src/core/kilocode/agent-manager/RemoteSessionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,40 @@ export interface RemoteSessionServiceOptions {
outputChannel: vscode.OutputChannel
}

/**
* Service for fetching remote session data from the cloud.
* Uses SessionManager facade methods for all session operations.
*/
export class RemoteSessionService {
private outputChannel: vscode.OutputChannel

constructor(options: RemoteSessionServiceOptions) {
this.outputChannel = options.outputChannel
}

/**
* Fetches a list of remote sessions from the cloud.
* @returns Array of remote sessions, or empty array if SessionManager is not initialized
*/
async fetchRemoteSessions(): Promise<RemoteSession[]> {
const sessionClient = this.getSessionClient()
if (!sessionClient) {
const sessionManager = this.getSessionManager()
if (!sessionManager) {
return []
}

const response = await sessionClient.list({ limit: REMOTE_SESSIONS_FETCH_LIMIT })
const response = await sessionManager.listSessions({ limit: REMOTE_SESSIONS_FETCH_LIMIT })
const remoteSessions: RemoteSession[] = response.cliSessions

this.log(`Fetched ${remoteSessions.length} remote sessions`)

return remoteSessions
}

/**
* Fetches messages for a specific session.
* @param sessionId - The session ID to fetch messages for
* @returns Array of messages, or null if not available
*/
async fetchSessionMessages(sessionId: string): Promise<ClineMessage[] | null> {
const blobUrl = await this.getSessionMessageBlobUrl(sessionId)
if (!blobUrl) {
Expand All @@ -41,14 +54,14 @@ export class RemoteSessionService {
}

private async getSessionMessageBlobUrl(sessionId: string): Promise<string | null> {
const sessionClient = this.getSessionClient()
if (!sessionClient) {
const sessionManager = this.getSessionManager()
if (!sessionManager) {
return null
}

this.log(`Fetching messages for session: ${sessionId}`)

const session = await sessionClient.get({
const session = await sessionManager.getSession({
session_id: sessionId,
include_blob_urls: true,
})
Expand All @@ -67,13 +80,13 @@ export class RemoteSessionService {
return messages.filter((message) => message.say !== "checkpoint_saved")
}

private getSessionClient() {
const sessionClient = SessionManager.init()?.sessionClient
if (!sessionClient) {
this.log("SessionClient not available")
private getSessionManager(): SessionManager | null {
const sessionManager = SessionManager.init()
if (!sessionManager) {
this.log("SessionManager not available")
return null
}
return sessionClient
return sessionManager
}

private log(message: string): void {
Expand Down
12 changes: 6 additions & 6 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class ClineProvider
const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId)
const onTaskCompleted = (taskId: string, tokenUsage: any, toolUsage: any) => {
kilo_execIfExtension(() => {
SessionManager.init().doSync(true)
SessionManager.init()?.doSync(true)
})

return this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage)
Expand Down Expand Up @@ -1153,17 +1153,17 @@ ${prompt}
if (message.type === "apiMessagesSaved" && message.payload) {
const [taskId, filePath] = message.payload as [string, string]

SessionManager.init().handleFileUpdate(taskId, "apiConversationHistoryPath", filePath)
SessionManager.init()?.handleFileUpdate(taskId, "apiConversationHistoryPath", filePath)
} else if (message.type === "taskMessagesSaved" && message.payload) {
const [taskId, filePath] = message.payload as [string, string]

SessionManager.init().handleFileUpdate(taskId, "uiMessagesPath", filePath)
SessionManager.init()?.handleFileUpdate(taskId, "uiMessagesPath", filePath)
} else if (message.type === "taskMetadataSaved" && message.payload) {
const [taskId, filePath] = message.payload as [string, string]

SessionManager.init().handleFileUpdate(taskId, "taskMetadataPath", filePath)
SessionManager.init()?.handleFileUpdate(taskId, "taskMetadataPath", filePath)
} else if (message.type === "currentCheckpointUpdated") {
SessionManager.init().doSync()
SessionManager.init()?.doSync()
}
})

Expand Down Expand Up @@ -1946,7 +1946,7 @@ ${prompt}

await kilo_execIfExtension(() => {
if (this.currentWorkspacePath) {
SessionManager.init().setWorkspaceDirectory(this.currentWorkspacePath)
SessionManager.init()?.setWorkspaceDirectory(this.currentWorkspacePath)
}
})

Expand Down
Loading