From 4f8534b223b0e0314585d409d0ce50284602eab0 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Thu, 19 Mar 2026 00:27:24 -0400 Subject: [PATCH] feat(cli): integrate TrajectoryProvider into /resume UI --- packages/cli/src/core/initializer.ts | 27 +++ packages/cli/src/ui/AppContainer.tsx | 13 ++ .../cli/src/ui/commands/agentsCommand.test.ts | 82 +++++++-- packages/cli/src/ui/commands/chatCommand.ts | 152 +++++++++++++++-- .../cli/src/ui/components/SessionBrowser.tsx | 155 +++++++++++++++++- .../cli/src/ui/hooks/useSessionBrowser.ts | 59 +++++-- 6 files changed, 441 insertions(+), 47 deletions(-) diff --git a/packages/cli/src/core/initializer.ts b/packages/cli/src/core/initializer.ts index f27e9a9511..0878d842c9 100644 --- a/packages/cli/src/core/initializer.ts +++ b/packages/cli/src/core/initializer.ts @@ -19,12 +19,16 @@ import { performInitialAuth } from './auth.js'; import { validateTheme } from './theme.js'; import type { AccountSuspensionInfo } from '../ui/contexts/UIStateContext.js'; +import { pathToFileURL } from 'node:url'; +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + export interface InitializationResult { authError: string | null; accountSuspensionInfo: AccountSuspensionInfo | null; themeError: string | null; shouldOpenAuthDialog: boolean; geminiMdFileCount: number; + recentExternalSession?: { prefix: string; id: string; displayName?: string }; } /** @@ -60,11 +64,34 @@ export async function initializeApp( logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START)); } + // Check for recent external sessions to prompt for resume + let recentExternalSession: + | { prefix: string; id: string; displayName?: string } + | undefined; + if (config.getEnableExtensionReloading() !== false) { + /* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any */ + const extensionLoader = (config as any)._extensionLoader; + if (extensionLoader) { + const workspaceUri = pathToFileURL(process.cwd()).toString(); + const recent = + await extensionLoader.getRecentExternalSession(workspaceUri); + if (recent) { + recentExternalSession = { + prefix: recent.prefix, + id: recent.id, + displayName: recent.displayName, + }; + } + } + /* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any */ + } + return { authError, accountSuspensionInfo, themeError, shouldOpenAuthDialog, geminiMdFileCount: config.getGeminiMdFileCount(), + recentExternalSession, }; } diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index b2402f9fe9..200d53c3d8 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -469,6 +469,19 @@ export const AppContainer = (props: AppContainerProps) => { generateSummary(config).catch((e) => { debugLogger.warn('Background summary generation failed:', e); }); + + // Show handoff prompt for recent external sessions + if (initializationResult.recentExternalSession) { + const { prefix, id, displayName } = + initializationResult.recentExternalSession; + historyManager.addItem( + { + type: MessageType.INFO, + text: `🛸 You have a recent session in ${displayName || 'an external tool'}. Type "/resume ${prefix}${id}" to bring it into the terminal.`, + }, + Date.now(), + ); + } })(); registerCleanup(async () => { // Turn off mouse scroll. diff --git a/packages/cli/src/ui/commands/agentsCommand.test.ts b/packages/cli/src/ui/commands/agentsCommand.test.ts index 5e6cc36efa..decd24235e 100644 --- a/packages/cli/src/ui/commands/agentsCommand.test.ts +++ b/packages/cli/src/ui/commands/agentsCommand.test.ts @@ -8,10 +8,14 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { agentsCommand } from './agentsCommand.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import type { Config } from '@google/gemini-cli-core'; +import { Storage } from '@google/gemini-cli-core'; import type { LoadedSettings } from '../../config/settings.js'; import { MessageType } from '../types.js'; import { enableAgent, disableAgent } from '../../utils/agentSettings.js'; import { renderAgentActionFeedback } from '../../utils/agentUtils.js'; +import * as fs from 'node:fs/promises'; + +vi.mock('node:fs/promises'); vi.mock('../../utils/agentSettings.js', () => ({ enableAgent: vi.fn(), @@ -105,40 +109,34 @@ describe('agentsCommand', () => { ); }); - it('should reload the agent registry when reload subcommand is called', async () => { + it('should reload the agent registry when refresh subcommand is called', async () => { const reloadSpy = vi.fn().mockResolvedValue(undefined); mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ reload: reloadSpy, }); - const reloadCommand = agentsCommand.subCommands?.find( - (cmd) => cmd.name === 'reload', + const refreshCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'refresh', ); - expect(reloadCommand).toBeDefined(); + expect(refreshCommand).toBeDefined(); - const result = await reloadCommand!.action!(mockContext, ''); + const result = await refreshCommand!.action!(mockContext, ''); expect(reloadSpy).toHaveBeenCalled(); - expect(mockContext.ui.addItem).toHaveBeenCalledWith( - expect.objectContaining({ - type: MessageType.INFO, - text: 'Reloading agent registry...', - }), - ); expect(result).toEqual({ type: 'message', messageType: 'info', - content: 'Agents reloaded successfully', + content: 'Agents refreshed successfully.', }); }); - it('should show an error if agent registry is not available during reload', async () => { + it('should show an error if agent registry is not available during refresh', async () => { mockConfig.getAgentRegistry = vi.fn().mockReturnValue(undefined); - const reloadCommand = agentsCommand.subCommands?.find( - (cmd) => cmd.name === 'reload', + const refreshCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'refresh', ); - const result = await reloadCommand!.action!(mockContext, ''); + const result = await refreshCommand!.action!(mockContext, ''); expect(result).toEqual({ type: 'message', @@ -462,4 +460,56 @@ describe('agentsCommand', () => { expect(completions).toEqual(['agent1', 'agent2']); }); }); + + describe('import sub-command', () => { + it('should import an agent with correct tool mapping', async () => { + vi.mocked(fs.readFile).mockResolvedValue(`--- +name: test-claude-agent +tools: Read, Glob, Grep, Bash +model: sonnet +--- +System prompt content`); + + vi.mocked(fs.mkdir).mockResolvedValue(undefined); + vi.mocked(fs.writeFile).mockResolvedValue(undefined); + vi.spyOn(Storage, 'getUserAgentsDir').mockReturnValue('/mock/agents'); + + const importCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'import', + ); + expect(importCommand).toBeDefined(); + + const result = await importCommand!.action!( + mockContext, + '/path/to/claude.md', + ); + + expect(fs.readFile).toHaveBeenCalledWith('/path/to/claude.md', 'utf-8'); + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/test-claude-agent\.md$/), + expect.stringContaining('tools:\n - read_file\n - run_command\n'), + 'utf-8', + ); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining( + "Successfully imported agent 'test-claude-agent'", + ), + }); + }); + + it('should show error if no file path provided', async () => { + const importCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'import', + ); + const result = await importCommand!.action!(mockContext, ' '); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Usage: /agents import ', + }); + }); + }); }); diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 8b38204aa2..0783e51ee7 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -8,6 +8,7 @@ import * as fsPromises from 'node:fs/promises'; import React from 'react'; import { Text } from 'ink'; import { theme } from '../semantic-colors.js'; +import type { Content, Part } from '@google/genai'; import type { CommandContext, SlashCommand, @@ -18,6 +19,7 @@ import { decodeTagName, type MessageActionReturn, INITIAL_HISTORY_LENGTH, + type ConversationRecord, } from '@google/gemini-cli-core'; import path from 'node:path'; import type { @@ -174,8 +176,119 @@ const resumeCheckpointCommand: SlashCommand = { const { logger, config } = context.services; await logger.initialize(); - const checkpoint = await logger.loadCheckpoint(tag); - const conversation = checkpoint.history; + + let conversation: readonly Content[] = []; + let authType: string | undefined; + + const loadExternalTrajectory = async ( + prefix: string, + id: string, + ): Promise => { + let record: ConversationRecord | null = null; + if (config && config.getEnableExtensionReloading() !== false) { + /* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ + const extensions = (config as any)._extensionLoader?.getExtensions + ? (config as any)._extensionLoader.getExtensions() + : []; + for (const extension of extensions) { + if ( + extension.trajectoryProviderModule && + extension.trajectoryProviderModule.prefix === prefix + ) { + try { + record = await extension.trajectoryProviderModule.loadSession(id); + if (record) break; + } catch (_err) { + // Ignore failure + } + } + } + /* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ + } + if (!record) return null; + + const conv: Content[] = []; + // Add a dummy system message so slice(INITIAL_HISTORY_LENGTH) works correctly + conv.push({ role: 'user', parts: [{ text: '' }] }); + + for (const m of record.messages) { + if (m.type === 'user') { + const parts = Array.isArray(m.content) + ? m.content.map((c: string | Part) => + typeof c === 'string' ? { text: c } : { text: c.text || '' }, + ) + : [{ text: '' }]; + conv.push({ role: 'user', parts }); + } else if (m.type === 'gemini') { + // 1. Model turn: Text + Function Calls + const modelParts: Part[] = []; + if (Array.isArray(m.content)) { + m.content.forEach((c: string | Part) => { + modelParts.push( + typeof c === 'string' ? { text: c } : { text: c.text || '' }, + ); + }); + } + + if (m.toolCalls) { + for (const tc of m.toolCalls) { + modelParts.push({ + functionCall: { + name: tc.name, + args: tc.args, + }, + }); + } + } + conv.push({ role: 'model', parts: modelParts }); + + // 2. User turn: Function Responses (if any) + if ( + m.toolCalls && + m.toolCalls.some((tc: { result?: unknown }) => tc.result) + ) { + const responseParts: Part[] = []; + for (const tc of m.toolCalls) { + if (tc.result) { + responseParts.push({ + functionResponse: { + name: tc.name, + response: { result: tc.result }, + id: tc.id, + }, + }); + } + } + if (responseParts.length > 0) { + conv.push({ role: 'user', parts: responseParts }); + } + } + } + } + return conv; + }; + + // Check if tag format is prefix:id + const prefixMatch = tag.match(/^([a-zA-Z0-9_]+):(.*)$/); + if (prefixMatch) { + const prefix = prefixMatch[1] + ':'; + const id = prefixMatch[2]; + const externalConv = await loadExternalTrajectory(prefix, id); + if (!externalConv) { + return { + type: 'message', + messageType: 'error', + content: `No external session found with prefix ${prefix} and id: ${id}`, + }; + } + conversation = externalConv; + } else { + const checkpoint = await logger.loadCheckpoint(tag); + if (checkpoint.history.length > 0) { + conversation = checkpoint.history; + authType = checkpoint.authType; + } + } if (conversation.length === 0) { return { @@ -186,15 +299,11 @@ const resumeCheckpointCommand: SlashCommand = { } const currentAuthType = config?.getContentGeneratorConfig()?.authType; - if ( - checkpoint.authType && - currentAuthType && - checkpoint.authType !== currentAuthType - ) { + if (authType && currentAuthType && authType !== currentAuthType) { return { type: 'message', messageType: 'error', - content: `Cannot resume chat. It was saved with a different authentication method (${checkpoint.authType}) than the current one (${currentAuthType}).`, + content: `Cannot resume chat. It was saved with a different authentication method (${authType}) than the current one (${currentAuthType}).`, }; } @@ -206,11 +315,34 @@ const resumeCheckpointCommand: SlashCommand = { const uiHistory: HistoryItemWithoutId[] = []; for (const item of conversation.slice(INITIAL_HISTORY_LENGTH)) { - const text = - item.parts + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const parts = item.parts as Array<{ + text?: string; + functionCall?: { name: string }; + functionResponse?: { name: string }; + }>; + let text = + parts ?.filter((m) => !!m.text) .map((m) => m.text) .join('') || ''; + + const toolCalls = parts?.filter((p) => !!p.functionCall); + if (toolCalls?.length) { + const calls = toolCalls + .map((tc) => `[Tool Call: ${tc.functionCall?.name}]`) + .join('\n'); + text = text ? `${text}\n${calls}` : calls; + } + + const toolResponses = parts?.filter((p) => !!p.functionResponse); + if (toolResponses?.length) { + const responses = toolResponses + .map((tr) => `[Tool Result: ${tr.functionResponse?.name}]`) + .join('\n'); + text = text ? `${text}\n${responses}` : responses; + } + if (!text) { continue; } diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx index ac9b2c2b00..5fc0e68c18 100644 --- a/packages/cli/src/ui/components/SessionBrowser.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.tsx @@ -12,12 +12,16 @@ import { Colors } from '../colors.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useKeypress } from '../hooks/useKeypress.js'; import path from 'node:path'; -import type { Config } from '@google/gemini-cli-core'; +import { pathToFileURL } from 'node:url'; +import { type Config } from '@google/gemini-cli-core'; import type { SessionInfo } from '../../utils/sessionUtils.js'; import { formatRelativeTime, getSessionFiles, } from '../../utils/sessionUtils.js'; +import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js'; +import { TabHeader, type Tab } from './shared/TabHeader.js'; +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return */ /** * Props for the main SessionBrowser component. @@ -41,6 +45,8 @@ export interface SessionBrowserState { // Data state /** All loaded sessions */ sessions: SessionInfo[]; + /** Extension tabs */ + extensionTabs: Array<{ name: string; sessions: SessionInfo[] }>; /** Sessions after filtering and sorting */ filteredAndSortedSessions: SessionInfo[]; @@ -55,6 +61,8 @@ export interface SessionBrowserState { scrollOffset: number; /** Terminal width for layout calculations */ terminalWidth: number; + /** Current active tab (0: CLI, 1+: External) */ + activeTab: number; // Search state /** Current search query string */ @@ -83,6 +91,10 @@ export interface SessionBrowserState { // State setters /** Update sessions array */ setSessions: React.Dispatch>; + /** Update extensionTabs array */ + setExtensionTabs: React.Dispatch< + React.SetStateAction> + >; /** Update loading state */ setLoading: React.Dispatch>; /** Update error state */ @@ -91,6 +103,8 @@ export interface SessionBrowserState { setActiveIndex: React.Dispatch>; /** Update scroll offset */ setScrollOffset: React.Dispatch>; + /** Update active tab */ + setActiveTab: (index: number) => void; /** Update search query */ setSearchQuery: React.Dispatch>; /** Update search mode state */ @@ -352,6 +366,9 @@ export const useSessionBrowserState = ( ): SessionBrowserState => { const { columns: terminalWidth } = useTerminalSize(); const [sessions, setSessions] = useState(initialSessions); + const [extensionTabs, setExtensionTabs] = useState< + Array<{ name: string; sessions: SessionInfo[] }> + >([]); const [loading, setLoading] = useState(initialLoading); const [error, setError] = useState(initialError); const [activeIndex, setActiveIndex] = useState(0); @@ -365,10 +382,20 @@ export const useSessionBrowserState = ( const [hasLoadedFullContent, setHasLoadedFullContent] = useState(false); const loadingFullContentRef = useRef(false); + const [activeTab, setActiveTabInternal] = useState(0); + + const setActiveTab = useCallback((index: number) => { + setActiveTabInternal(index); + setActiveIndex(0); + setScrollOffset(0); + }, []); + const filteredAndSortedSessions = useMemo(() => { - const filtered = filterSessions(sessions, searchQuery); + const currentTabSessions = + activeTab === 0 ? sessions : extensionTabs[activeTab - 1]?.sessions || []; + const filtered = filterSessions(currentTabSessions, searchQuery); return sortSessions(filtered, sortOrder, sortReverse); - }, [sessions, searchQuery, sortOrder, sortReverse]); + }, [sessions, extensionTabs, activeTab, searchQuery, sortOrder, sortReverse]); // Reset full content flag when search is cleared useEffect(() => { @@ -386,6 +413,8 @@ export const useSessionBrowserState = ( const state: SessionBrowserState = { sessions, setSessions, + extensionTabs, + setExtensionTabs, loading, setLoading, error, @@ -394,6 +423,8 @@ export const useSessionBrowserState = ( setActiveIndex, scrollOffset, setScrollOffset, + activeTab, + setActiveTab, searchQuery, setSearchQuery, isSearchMode, @@ -415,12 +446,43 @@ export const useSessionBrowserState = ( return state; }; +/** + * Converts an external session info to CLI SessionInfo format. + */ +type ExternalSessionInfo = { + id: string; + mtime: string; + name?: string; + displayName?: string; + messageCount?: number; + prefix?: string; +}; + +function convertExternalToSessionInfo( + ext: ExternalSessionInfo, + index: number, +): SessionInfo { + return { + id: ext.prefix ? `${ext.prefix}${ext.id}` : ext.id, + file: ext.id, + fileName: ext.id + '.ext', + startTime: ext.mtime, + lastUpdated: ext.mtime, + messageCount: ext.messageCount || 0, + displayName: ext.displayName || ext.name || 'External Session', + firstUserMessage: ext.displayName || ext.name || '', + isCurrentSession: false, + index, + }; +} + /** * Hook to load sessions on mount. */ const useLoadSessions = (config: Config, state: SessionBrowserState) => { const { setSessions, + setExtensionTabs, setLoading, setError, isSearchMode, @@ -432,11 +494,58 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => { const loadSessions = async () => { try { const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats'); - const sessionData = await getSessionFiles( - chatsDir, - config.getSessionId(), - ); + const workspaceUri = pathToFileURL(process.cwd()).toString(); + + const externalTabs: Array<{ name: string; sessions: SessionInfo[] }> = + []; + if (config.getEnableExtensionReloading() !== false) { + /* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any */ + const extensions = (config as any)._extensionLoader?.getExtensions + ? (config as any)._extensionLoader.getExtensions() + : []; + for (const extension of extensions) { + if (extension.trajectoryProviderModule) { + try { + const sessions = + await extension.trajectoryProviderModule.listSessions( + workspaceUri, + ); + const normalizedExt = sessions + .map((ext: any) => ({ + ...ext, + prefix: extension.trajectoryProviderModule.prefix, + })) + .sort( + (a: any, b: any) => + new Date(a.mtime).getTime() - new Date(b.mtime).getTime(), + ) + .map((ext: any, i: number) => + convertExternalToSessionInfo(ext, i + 1), + ); + + if (normalizedExt.length > 0) { + externalTabs.push({ + name: + extension.trajectoryProviderModule.displayName || + extension.name, + sessions: normalizedExt, + }); + } + } catch (_e) { + // Ignore loader errors + } + } + } + /* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any */ + } + + const [sessionData] = await Promise.all([ + getSessionFiles(chatsDir, config.getSessionId()), + ]); + setSessions(sessionData); + setExtensionTabs(externalTabs); + setLoading(false); } catch (err) { setError( @@ -448,7 +557,7 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises loadSessions(); - }, [config, setSessions, setLoading, setError]); + }, [config, setSessions, setExtensionTabs, setLoading, setError]); useEffect(() => { const loadFullContent = async () => { @@ -610,6 +719,9 @@ export const useSessionBrowserInput = ( } // Delete session control. else if (key.sequence === 'x' || key.sequence === 'X') { + // Only allow deleting CLI sessions for now + if (state.activeTab !== 0) return true; + const selectedSession = state.filteredAndSortedSessions[state.activeIndex]; if (selectedSession && !selectedSession.isCurrentSession) { @@ -684,6 +796,14 @@ export function SessionBrowserView({ }: { state: SessionBrowserState; }): React.JSX.Element { + const tabs: Tab[] = [ + { key: 'cli', header: 'Gemini CLI' }, + ...state.extensionTabs.map((ext, i) => ({ + key: `ext-${i}`, + header: ext.name, + })), + ]; + if (state.loading) { return ; } @@ -692,11 +812,20 @@ export function SessionBrowserView({ return ; } - if (state.sessions.length === 0) { + if (state.sessions.length === 0 && state.extensionTabs.length === 0) { return ; } + return ( + + + + {state.isSearchMode && } @@ -721,6 +850,14 @@ export function SessionBrowser({ useLoadSessions(config, state); const moveSelection = useMoveSelection(state); const cycleSortOrder = useCycleSortOrder(state); + + useTabbedNavigation({ + tabCount: state.extensionTabs.length + 1, + isActive: !state.isSearchMode, + wrapAround: true, + onTabChange: (index) => state.setActiveTab(index), + }); + useSessionBrowserInput( state, moveSelection, diff --git a/packages/cli/src/ui/hooks/useSessionBrowser.ts b/packages/cli/src/ui/hooks/useSessionBrowser.ts index 9a34f68e0b..e36eae4b7c 100644 --- a/packages/cli/src/ui/hooks/useSessionBrowser.ts +++ b/packages/cli/src/ui/hooks/useSessionBrowser.ts @@ -21,6 +21,7 @@ import { type SessionInfo, } from '../../utils/sessionUtils.js'; import type { Part } from '@google/genai'; +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export { convertSessionToHistoryFormats }; @@ -51,20 +52,54 @@ export const useSessionBrowser = ( handleResumeSession: useCallback( async (session: SessionInfo) => { try { - const chatsDir = path.join( - config.storage.getProjectTempDir(), - 'chats', - ); + let conversation: ConversationRecord; + let filePath: string; - const fileName = session.fileName; + if (session.fileName.endsWith('.ext')) { + // External session + let externalConv: ConversationRecord | null = null; + if (config.getEnableExtensionReloading() !== false) { + /* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any */ + const extensions = (config as any)._extensionLoader?.getExtensions + ? (config as any)._extensionLoader.getExtensions() + : []; + for (const extension of extensions) { + if (extension.trajectoryProviderModule) { + const prefix = + extension.trajectoryProviderModule.prefix || ''; + if (session.id.startsWith(prefix)) { + const originalId = prefix + ? session.id.slice(prefix.length) + : session.id; + externalConv = + await extension.trajectoryProviderModule.loadSession( + originalId, + ); + if (externalConv) break; + } + } + } + /* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any */ + } - const originalFilePath = path.join(chatsDir, fileName); + if (!externalConv) { + throw new Error(`Could not load external session ${session.id}`); + } + conversation = externalConv; + filePath = session.id + '.ext'; + } else { + // Regular CLI session + const chatsDir = path.join( + config.storage.getProjectTempDir(), + 'chats', + ); + const fileName = session.fileName; + filePath = path.join(chatsDir, fileName); - // Load up the conversation. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const conversation: ConversationRecord = JSON.parse( - await fs.readFile(originalFilePath, 'utf8'), - ); + // Load up the conversation. + + conversation = JSON.parse(await fs.readFile(filePath, 'utf8')); + } // Use the old session's ID to continue it. const existingSessionId = conversation.sessionId; @@ -73,7 +108,7 @@ export const useSessionBrowser = ( const resumedSessionData = { conversation, - filePath: originalFilePath, + filePath, }; // We've loaded it; tell the UI about it.