diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 6885c030..991f2c30 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -351,7 +351,7 @@ const summaryCommand: SlashCommand = { role: 'user', parts: [ { - text: '' + getProjectSummaryPrompt(), + text: getProjectSummaryPrompt(), }, ], }, @@ -397,7 +397,7 @@ const summaryCommand: SlashCommand = { --- ## Summary Metadata -**Generated**: ${new Date().toISOString()} This summary was automatically generated from the conversation history and is designed to provide comprehensive context for future development sessions.* +**Update time**: ${new Date().toISOString()} `; await fsPromises.writeFile(summaryPath, summaryContent, 'utf8'); diff --git a/packages/cli/src/ui/components/WelcomeBackDialog.tsx b/packages/cli/src/ui/components/WelcomeBackDialog.tsx new file mode 100644 index 00000000..8a6161b3 --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeBackDialog.tsx @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Box, Text } from 'ink'; +import { Colors } from '../colors.js'; +import { ProjectSummaryInfo } from '@qwen-code/qwen-code-core'; +import { + RadioButtonSelect, + RadioSelectItem, +} from './shared/RadioButtonSelect.js'; +import { useKeypress } from '../hooks/useKeypress.js'; + +interface WelcomeBackDialogProps { + welcomeBackInfo: ProjectSummaryInfo; + onSelect: (choice: 'restart' | 'continue') => void; + onClose: () => void; +} + +export function WelcomeBackDialog({ + welcomeBackInfo, + onSelect, + onClose, +}: WelcomeBackDialogProps) { + useKeypress( + (key) => { + if (key.name === 'escape') { + onClose(); + } + }, + { isActive: true }, + ); + + const options: Array> = [ + { + label: 'Start new chat session', + value: 'restart', + }, + { + label: 'Continue previous conversation', + value: 'continue', + }, + ]; + + // Extract data from welcomeBackInfo + const { + timeAgo, + goalContent, + totalTasks = 0, + doneCount = 0, + inProgressCount = 0, + pendingTasks = [], + } = welcomeBackInfo; + + return ( + + + + 👋 Welcome back! (Last updated: {timeAgo}) + + + + {/* Overall Goal Section */} + {goalContent && ( + + + 🎯 Overall Goal: + + + {goalContent} + + + )} + + {/* Current Plan Section */} + {totalTasks > 0 && ( + + + 📋 Current Plan: + + + + Progress: {doneCount}/{totalTasks} tasks completed + {inProgressCount > 0 && `, ${inProgressCount} in progress`} + + + + {pendingTasks.length > 0 && ( + + + Pending Tasks: + + {pendingTasks.map((task: string, index: number) => ( + + • {task} + + ))} + + )} + + )} + + {/* Action Selection */} + + What would you like to do? + Choose how to proceed with your session: + + + + + + + ); +} diff --git a/packages/cli/src/ui/hooks/useWelcomeBack.ts b/packages/cli/src/ui/hooks/useWelcomeBack.ts new file mode 100644 index 00000000..de1945f1 --- /dev/null +++ b/packages/cli/src/ui/hooks/useWelcomeBack.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState, useEffect, useCallback } from 'react'; +import { + getProjectSummaryInfo, + type ProjectSummaryInfo, + type Config, +} from '@qwen-code/qwen-code-core'; + +export interface WelcomeBackState { + welcomeBackInfo: ProjectSummaryInfo | null; + showWelcomeBackDialog: boolean; + welcomeBackChoice: 'restart' | 'continue' | null; +} + +export interface WelcomeBackActions { + handleWelcomeBackSelection: (choice: 'restart' | 'continue') => void; + handleWelcomeBackClose: () => void; + checkWelcomeBack: () => Promise; +} + +export function useWelcomeBack( + config: Config, + submitQuery: (query: string) => void, +): WelcomeBackState & WelcomeBackActions { + const [welcomeBackInfo, setWelcomeBackInfo] = + useState(null); + const [showWelcomeBackDialog, setShowWelcomeBackDialog] = useState(false); + const [welcomeBackChoice, setWelcomeBackChoice] = useState< + 'restart' | 'continue' | null + >(null); + + // Check for conversation history on startup + const checkWelcomeBack = useCallback(async () => { + try { + const info = await getProjectSummaryInfo(); + if (info.hasHistory) { + setWelcomeBackInfo(info); + setShowWelcomeBackDialog(true); + } + } catch (error) { + // Silently ignore errors - welcome back is not critical + console.debug('Welcome back check failed:', error); + } + }, []); + + // Handle welcome back dialog selection + const handleWelcomeBackSelection = useCallback( + (choice: 'restart' | 'continue') => { + setWelcomeBackChoice(choice); + setShowWelcomeBackDialog(false); + + if (choice === 'continue' && welcomeBackInfo?.content) { + // Load conversation history as context + const contextMessage = `Based on our previous conversation, here's the current project status: + +${welcomeBackInfo.content} + +Let's continue where we left off. What would you like to work on next?`; + + // Submit the context as the initial prompt + submitQuery(contextMessage); + } + // If choice is 'restart', just close the dialog and continue normally + }, + [welcomeBackInfo, submitQuery], + ); + + const handleWelcomeBackClose = useCallback(() => { + setWelcomeBackChoice('restart'); // Default to restart when closed + setShowWelcomeBackDialog(false); + }, []); + + // Check for welcome back on mount + useEffect(() => { + checkWelcomeBack(); + }, [checkWelcomeBack]); + + return { + // State + welcomeBackInfo, + showWelcomeBackDialog, + welcomeBackChoice, + // Actions + handleWelcomeBackSelection, + handleWelcomeBackClose, + checkWelcomeBack, + }; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f7c89c64..42f4f058 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,6 +44,7 @@ export * from './utils/textUtils.js'; export * from './utils/formatters.js'; export * from './utils/filesearch/fileSearch.js'; export * from './utils/errorParsing.js'; +export * from './utils/projectSummary.js'; // Export services export * from './services/fileDiscoveryService.js'; diff --git a/packages/core/src/utils/projectSummary.ts b/packages/core/src/utils/projectSummary.ts new file mode 100644 index 00000000..46b66001 --- /dev/null +++ b/packages/core/src/utils/projectSummary.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'fs/promises'; +import * as path from 'path'; + +export interface ProjectSummaryInfo { + hasHistory: boolean; + content?: string; + timestamp?: string; + timeAgo?: string; + goalContent?: string; + planContent?: string; + totalTasks?: number; + doneCount?: number; + inProgressCount?: number; + todoCount?: number; + pendingTasks?: string[]; +} + +/** + * Reads and parses the project summary file to extract structured information + */ +export async function getProjectSummaryInfo(): Promise { + const summaryPath = path.join(process.cwd(), '.qwen', 'PROJECT_SUMMARY.md'); + + try { + await fs.access(summaryPath); + } catch { + return { + hasHistory: false, + }; + } + + try { + const content = await fs.readFile(summaryPath, 'utf-8'); + + // Extract timestamp if available + const timestampMatch = content.match(/\*\*Update time\*\*: (.+)/); + + const timestamp = timestampMatch + ? timestampMatch[1] + : new Date().toISOString(); + + // Calculate time ago + const getTimeAgo = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays > 0) { + return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + } else if (diffHours > 0) { + return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; + } else if (diffMinutes > 0) { + return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''} ago`; + } else { + return 'just now'; + } + }; + + const timeAgo = getTimeAgo(timestamp); + fs.writeFile( + 'debug.md', + `timestamp: ${timestampMatch} \n${timestamp}\n timeAgo: ${timeAgo}\n`, + ); + + // Parse Overall Goal section + const goalSection = content.match( + /## Overall Goal\s*\n?([\s\S]*?)(?=\n## |$)/, + ); + const goalContent = goalSection ? goalSection[1].trim() : ''; + + // Parse Current Plan section + const planSection = content.match( + /## Current Plan\s*\n?([\s\S]*?)(?=\n## |$)/, + ); + const planContent = planSection ? planSection[1] : ''; + const planLines = planContent.split('\n').filter((line) => line.trim()); + const doneCount = planLines.filter((line) => + line.includes('[DONE]'), + ).length; + const inProgressCount = planLines.filter((line) => + line.includes('[IN PROGRESS]'), + ).length; + const todoCount = planLines.filter((line) => + line.includes('[TODO]'), + ).length; + const totalTasks = doneCount + inProgressCount + todoCount; + + // Extract pending tasks + const pendingTasks = planLines + .filter( + (line) => line.includes('[TODO]') || line.includes('[IN PROGRESS]'), + ) + .map((line) => line.replace(/^\d+\.\s*/, '').trim()) + .slice(0, 3); + + return { + hasHistory: true, + content, + timestamp, + timeAgo, + goalContent, + planContent, + totalTasks, + doneCount, + inProgressCount, + todoCount, + pendingTasks, + }; + } catch (_error) { + return { + hasHistory: false, + }; + } +}