From d4531f8fc29d2a2133257d306b602a6b75b47460 Mon Sep 17 00:00:00 2001 From: ahmed al mihy Date: Mon, 1 Sep 2025 09:17:08 +0300 Subject: [PATCH 01/17] Update version in package-lock.json and enhance UI styles for improved readability and aesthetics --- package-lock.json | 4 +- src/script.ts | 2 +- src/ui-styles.ts | 486 ++++++++++++++++++++++++---------------------- 3 files changed, 254 insertions(+), 238 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82a87fc..1e3d15a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-chat", - "version": "1.0.0", + "version": "1.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-chat", - "version": "1.0.0", + "version": "1.0.6", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/src/script.ts b/src/script.ts index 871f6ab..6df4f5f 100644 --- a/src/script.ts +++ b/src/script.ts @@ -2273,10 +2273,10 @@ const getScript = (isTelemetryEnabled: boolean) => `` + `; export default getScript; \ No newline at end of file diff --git a/src/services/BackupService.ts b/src/services/BackupService.ts new file mode 100644 index 0000000..011c7bf --- /dev/null +++ b/src/services/BackupService.ts @@ -0,0 +1,143 @@ +import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import * as util from 'util'; +import * as path from 'path'; +import { CommitInfo } from '../types'; + +const exec = util.promisify(cp.exec); + +export class BackupService { + private _backupRepoPath: string | undefined; + private _commits: CommitInfo[] = []; + + constructor( + private readonly _context: vscode.ExtensionContext + ) { + this._initializeBackupRepo(); + } + + public get commits(): CommitInfo[] { + return this._commits; + } + + private async _initializeBackupRepo(): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { return; } + + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { + console.error('No workspace storage available'); + return; + } + console.log('Workspace storage path:', storagePath); + this._backupRepoPath = path.join(storagePath, 'backups', '.git'); + + // Create backup git directory if it doesn't exist + try { + await vscode.workspace.fs.stat(vscode.Uri.file(this._backupRepoPath)); + } catch { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._backupRepoPath)); + + const workspacePath = workspaceFolder.uri.fsPath; + + // Initialize git repo with workspace as work-tree + await exec(`git --git-dir="${this._backupRepoPath}" --work-tree="${workspacePath}" init`); + await exec(`git --git-dir="${this._backupRepoPath}" config user.name "Claude Code Chat"`); + await exec(`git --git-dir="${this._backupRepoPath}" config user.email "claude@anthropic.com"`); + + console.log(`Initialized backup repository at: ${this._backupRepoPath}`); + } + } catch (error: any) { + console.error('Failed to initialize backup repository:', error.message); + } + } + + public async createBackupCommit(userMessage: string): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder || !this._backupRepoPath) { return; } + + const workspacePath = workspaceFolder.uri.fsPath; + const now = new Date(); + const timestamp = now.toISOString().replace(/[:.]/g, '-'); + const displayTimestamp = now.toISOString(); + const commitMessage = `Before: ${userMessage.substring(0, 50)}${userMessage.length > 50 ? '...' : ''}`; + + // Add all files using git-dir and work-tree (excludes .git automatically) + await exec(`git --git-dir="${this._backupRepoPath}" --work-tree="${workspacePath}" add -A`); + + // Check if this is the first commit (no HEAD exists yet) + let isFirstCommit = false; + try { + await exec(`git --git-dir="${this._backupRepoPath}" rev-parse HEAD`); + } catch { + isFirstCommit = true; + } + + // Check if there are changes to commit + const { stdout: status } = await exec(`git --git-dir="${this._backupRepoPath}" --work-tree="${workspacePath}" status --porcelain`); + + // Always create a checkpoint, even if no files changed + let actualMessage; + if (isFirstCommit) { + actualMessage = `Initial backup: ${userMessage.substring(0, 50)}${userMessage.length > 50 ? '...' : ''}`; + } else if (status.trim()) { + actualMessage = commitMessage; + } else { + actualMessage = `Checkpoint (no changes): ${userMessage.substring(0, 50)}${userMessage.length > 50 ? '...' : ''}`; + } + + // Create commit with --allow-empty to ensure checkpoint is always created + await exec(`git --git-dir="${this._backupRepoPath}" --work-tree="${workspacePath}" commit --allow-empty -m "${actualMessage}"`); + const { stdout: sha } = await exec(`git --git-dir="${this._backupRepoPath}" rev-parse HEAD`); + + // Store commit info + const commitInfo: CommitInfo = { + id: `commit-${timestamp}`, + sha: sha.trim(), + message: actualMessage, + timestamp: displayTimestamp + }; + + this._commits.push(commitInfo); + + console.log(`Created backup commit: ${commitInfo.sha.substring(0, 8)} - ${actualMessage}`); + return commitInfo; + } catch (error: any) { + console.error('Failed to create backup commit:', error.message); + return undefined; + } + } + + public async restoreToCommit(commitSha: string): Promise<{ success: boolean; message: string }> { + try { + const commit = this._commits.find(c => c.sha === commitSha); + if (!commit) { + return { success: false, message: 'Commit not found' }; + } + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder || !this._backupRepoPath) { + return { success: false, message: 'No workspace folder or backup repository available' }; + } + + const workspacePath = workspaceFolder.uri.fsPath; + + // Restore files directly to workspace using git checkout + await exec(`git --git-dir="${this._backupRepoPath}" --work-tree="${workspacePath}" checkout ${commitSha} -- .`); + + vscode.window.showInformationMessage(`Restored to commit: ${commit.message}`); + return { success: true, message: `Successfully restored to: ${commit.message}` }; + + } catch (error: any) { + console.error('Failed to restore commit:', error.message); + vscode.window.showErrorMessage(`Failed to restore commit: ${error.message}`); + return { success: false, message: `Failed to restore: ${error.message}` }; + } + } + + public clearCommits(): void { + this._commits = []; + } +} \ No newline at end of file diff --git a/src/services/ClaudeProcessService.ts b/src/services/ClaudeProcessService.ts new file mode 100644 index 0000000..571f830 --- /dev/null +++ b/src/services/ClaudeProcessService.ts @@ -0,0 +1,224 @@ +import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import { TokenUsage } from '../types'; + +export interface ClaudeProcessOptions { + message: string; + sessionId?: string; + selectedModel?: string; + yoloMode?: boolean; + mcpConfigPath?: string; + wslConfig?: { + enabled: boolean; + distro: string; + nodePath: string; + claudePath: string; + }; +} + +export interface JsonStreamData { + type: string; + subtype?: string; + session_id?: string; + tools?: any[]; + mcp_servers?: any[]; + message?: { + content: any[]; + usage?: TokenUsage; + }; + is_error?: boolean; + result?: string; + total_cost_usd?: number; + duration_ms?: number; + num_turns?: number; +} + +export class ClaudeProcessService { + private _currentClaudeProcess: cp.ChildProcess | undefined; + + public startClaudeProcess( + options: ClaudeProcessOptions, + onData: (data: JsonStreamData) => void, + onClose: (code: number | null, errorOutput: string) => void, + onError: (error: Error) => void + ): void { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const cwd = workspaceFolder ? workspaceFolder.uri.fsPath : process.cwd(); + + // Build command arguments with session management + const args = [ + '-p', + '--output-format', 'stream-json', '--verbose' + ]; + + if (options.yoloMode) { + // Yolo mode: skip all permissions regardless of MCP config + args.push('--dangerously-skip-permissions'); + } else if (options.mcpConfigPath) { + // Add MCP configuration for permissions + const convertedPath = this._convertToWSLPath(options.mcpConfigPath, options.wslConfig?.enabled || false); + args.push('--mcp-config', convertedPath); + args.push('--allowedTools', 'mcp__claude-code-chat-permissions__approval_prompt'); + args.push('--permission-prompt-tool', 'mcp__claude-code-chat-permissions__approval_prompt'); + } + + // Add model selection if not using default + if (options.selectedModel && options.selectedModel !== 'default') { + args.push('--model', options.selectedModel); + } + + // Add session resume if we have a current session + if (options.sessionId) { + args.push('--resume', options.sessionId); + console.log('Resuming session:', options.sessionId); + } else { + console.log('Starting new session'); + } + + console.log('Claude command args:', args); + + let claudeProcess: cp.ChildProcess; + + if (options.wslConfig?.enabled) { + // Use WSL with bash -ic for proper environment loading + console.log('Using WSL configuration:', options.wslConfig); + const wslCommand = `"${options.wslConfig.nodePath}" --no-warnings --enable-source-maps "${options.wslConfig.claudePath}" ${args.join(' ')}`; + + claudeProcess = cp.spawn('wsl', ['-d', options.wslConfig.distro, 'bash', '-ic', wslCommand], { + cwd: cwd, + stdio: ['pipe', 'pipe', 'pipe'], + env: { + ...process.env, + FORCE_COLOR: '0', + NO_COLOR: '1' + } + }); + } else { + // Use native claude command + console.log('Using native Claude command'); + // On Windows with shell:true, we need to properly quote arguments with spaces + const quotedArgs = args.map(arg => { + // Quote arguments that contain spaces + if (arg.includes(' ') && !arg.startsWith('"')) { + return `"${arg}"`; + } + return arg; + }); + claudeProcess = cp.spawn('claude', quotedArgs, { + shell: process.platform === 'win32', + cwd: cwd, + stdio: ['pipe', 'pipe', 'pipe'], + env: { + ...process.env, + FORCE_COLOR: '0', + NO_COLOR: '1' + } + }); + } + + // Store process reference for potential termination + this._currentClaudeProcess = claudeProcess; + + // Send the message to Claude's stdin + if (claudeProcess.stdin) { + claudeProcess.stdin.write(options.message + '\n'); + claudeProcess.stdin.end(); + } + + let rawOutput = ''; + let errorOutput = ''; + + if (claudeProcess.stdout) { + claudeProcess.stdout.on('data', (data) => { + rawOutput += data.toString(); + + // Process JSON stream line by line + const lines = rawOutput.split('\n'); + rawOutput = lines.pop() || ''; // Keep incomplete line for next chunk + + for (const line of lines) { + if (line.trim()) { + try { + const jsonData: JsonStreamData = JSON.parse(line.trim()); + onData(jsonData); + } catch (error) { + console.log('Failed to parse JSON line:', line, error); + } + } + } + }); + } + + if (claudeProcess.stderr) { + claudeProcess.stderr.on('data', (data) => { + errorOutput += data.toString(); + }); + } + + claudeProcess.on('close', (code) => { + console.log('Claude process closed with code:', code); + console.log('Claude stderr output:', errorOutput); + + if (!this._currentClaudeProcess) { + return; + } + + // Clear process reference + this._currentClaudeProcess = undefined; + onClose(code, errorOutput); + }); + + claudeProcess.on('error', (error) => { + console.log('Claude process error:', error.message); + + if (!this._currentClaudeProcess) { + return; + } + + // Clear process reference + this._currentClaudeProcess = undefined; + onError(error); + }); + } + + public stopClaudeProcess(): boolean { + console.log('Stop request received'); + + if (this._currentClaudeProcess) { + console.log('Terminating Claude process...'); + + // Try graceful termination first + this._currentClaudeProcess.kill('SIGTERM'); + + // Force kill after 2 seconds if still running + setTimeout(() => { + if (this._currentClaudeProcess && !this._currentClaudeProcess.killed) { + console.log('Force killing Claude process...'); + this._currentClaudeProcess.kill('SIGKILL'); + } + }, 2000); + + // Clear process reference + this._currentClaudeProcess = undefined; + + console.log('Claude process termination initiated'); + return true; + } else { + console.log('No Claude process running to stop'); + return false; + } + } + + public isProcessRunning(): boolean { + return !!this._currentClaudeProcess; + } + + private _convertToWSLPath(windowsPath: string, wslEnabled: boolean): string { + if (wslEnabled && windowsPath.match(/^[a-zA-Z]:/)) { + // Convert C:\Users\... to /mnt/c/Users/... + return windowsPath.replace(/^([a-zA-Z]):/, '/mnt/$1').toLowerCase().replace(/\\/g, '/'); + } + + return windowsPath; + } +} \ No newline at end of file diff --git a/src/services/ConversationService.ts b/src/services/ConversationService.ts new file mode 100644 index 0000000..cd4512d --- /dev/null +++ b/src/services/ConversationService.ts @@ -0,0 +1,225 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { ConversationData, ConversationIndexEntry } from '../types'; + +export class ConversationService { + private _conversationsPath: string | undefined; + private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = []; + private _conversationStartTime: string | undefined; + private _conversationIndex: ConversationIndexEntry[] = []; + + constructor( + private readonly _context: vscode.ExtensionContext + ) { + this._initializeConversations(); + this._conversationIndex = this._context.workspaceState.get('claude.conversationIndex', []); + } + + public get currentConversation(): Array<{ timestamp: string, messageType: string, data: any }> { + return this._currentConversation; + } + + public get conversationIndex(): ConversationIndexEntry[] { + return this._conversationIndex; + } + + public get conversationStartTime(): string | undefined { + return this._conversationStartTime; + } + + private async _initializeConversations(): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { return; } + + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { return; } + + this._conversationsPath = path.join(storagePath, 'conversations'); + + // Create conversations directory if it doesn't exist + try { + await vscode.workspace.fs.stat(vscode.Uri.file(this._conversationsPath)); + } catch { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._conversationsPath)); + console.log(`Created conversations directory at: ${this._conversationsPath}`); + } + } catch (error: any) { + console.error('Failed to initialize conversations directory:', error.message); + } + } + + public addMessage(message: { type: string, data: any }): void { + // Initialize conversation if this is the first message + if (this._currentConversation.length === 0) { + this._conversationStartTime = new Date().toISOString(); + } + + // Save to conversation + this._currentConversation.push({ + timestamp: new Date().toISOString(), + messageType: message.type, + data: message.data + }); + + // Persist conversation + void this._saveCurrentConversation(); + } + + private async _saveCurrentConversation(): Promise { + if (!this._conversationsPath || this._currentConversation.length === 0) { return; } + + try { + // Create filename from first user message and timestamp + const firstUserMessage = this._currentConversation.find(m => m.messageType === 'userInput'); + const firstMessage = firstUserMessage ? firstUserMessage.data : 'conversation'; + const startTime = this._conversationStartTime || new Date().toISOString(); + + // Clean and truncate first message for filename + const cleanMessage = firstMessage + .replace(/[^a-zA-Z0-9\s]/g, '') // Remove special chars + .replace(/\s+/g, '-') // Replace spaces with dashes + .substring(0, 50) // Limit length + .toLowerCase(); + + const datePrefix = startTime.substring(0, 16).replace('T', '_').replace(/:/g, '-'); + const filename = `${datePrefix}_${cleanMessage}.json`; + + const conversationData: ConversationData = { + sessionId: '', // Will be set by calling code + startTime: this._conversationStartTime, + endTime: new Date().toISOString(), + messageCount: this._currentConversation.length, + totalCost: 0, // Will be set by calling code + totalTokens: { + input: 0, // Will be set by calling code + output: 0 // Will be set by calling code + }, + messages: this._currentConversation, + filename + }; + + const filePath = path.join(this._conversationsPath, filename); + const content = new TextEncoder().encode(JSON.stringify(conversationData, null, 2)); + await vscode.workspace.fs.writeFile(vscode.Uri.file(filePath), content); + + console.log(`Saved conversation: ${filename}`, this._conversationsPath); + } catch (error: any) { + console.error('Failed to save conversation:', error.message); + } + } + + public async saveConversationWithMetadata( + sessionId: string, + totalCost: number, + totalTokensInput: number, + totalTokensOutput: number + ): Promise { + if (!this._conversationsPath || this._currentConversation.length === 0 || !sessionId) { return; } + + try { + // Create filename from first user message and timestamp + const firstUserMessage = this._currentConversation.find(m => m.messageType === 'userInput'); + const firstMessage = firstUserMessage ? firstUserMessage.data : 'conversation'; + const startTime = this._conversationStartTime || new Date().toISOString(); + + // Clean and truncate first message for filename + const cleanMessage = firstMessage + .replace(/[^a-zA-Z0-9\s]/g, '') + .replace(/\s+/g, '-') + .substring(0, 50) + .toLowerCase(); + + const datePrefix = startTime.substring(0, 16).replace('T', '_').replace(/:/g, '-'); + const filename = `${datePrefix}_${cleanMessage}.json`; + + const conversationData: ConversationData = { + sessionId, + startTime: this._conversationStartTime, + endTime: new Date().toISOString(), + messageCount: this._currentConversation.length, + totalCost, + totalTokens: { + input: totalTokensInput, + output: totalTokensOutput + }, + messages: this._currentConversation, + filename + }; + + const filePath = path.join(this._conversationsPath, filename); + const content = new TextEncoder().encode(JSON.stringify(conversationData, null, 2)); + await vscode.workspace.fs.writeFile(vscode.Uri.file(filePath), content); + + // Update conversation index + this._updateConversationIndex(filename, conversationData); + + console.log(`Saved conversation: ${filename}`, this._conversationsPath); + } catch (error: any) { + console.error('Failed to save conversation:', error.message); + } + } + + public async loadConversation(filename: string): Promise { + if (!this._conversationsPath) { return; } + + try { + const filePath = path.join(this._conversationsPath, filename); + const fileUri = vscode.Uri.file(filePath); + const content = await vscode.workspace.fs.readFile(fileUri); + const conversationData: ConversationData = JSON.parse(new TextDecoder().decode(content)); + + // Load conversation into current state + this._currentConversation = conversationData.messages || []; + this._conversationStartTime = conversationData.startTime; + + console.log(`Loaded conversation history: ${filename}`); + return conversationData; + } catch (error: any) { + console.error('Failed to load conversation history:', error.message); + return undefined; + } + } + + public getLatestConversation(): ConversationIndexEntry | undefined { + return this._conversationIndex.length > 0 ? this._conversationIndex[0] : undefined; + } + + public clearConversation(): void { + this._currentConversation = []; + this._conversationStartTime = undefined; + } + + private _updateConversationIndex(filename: string, conversationData: ConversationData): void { + // Extract first and last user messages + const userMessages = conversationData.messages.filter((m: any) => m.messageType === 'userInput'); + const firstUserMessage = userMessages.length > 0 ? userMessages[0].data : 'No user message'; + const lastUserMessage = userMessages.length > 0 ? userMessages[userMessages.length - 1].data : firstUserMessage; + + // Create or update index entry + const indexEntry: ConversationIndexEntry = { + filename: filename, + sessionId: conversationData.sessionId, + startTime: conversationData.startTime || '', + endTime: conversationData.endTime, + messageCount: conversationData.messageCount, + totalCost: conversationData.totalCost, + firstUserMessage: firstUserMessage.substring(0, 100), + lastUserMessage: lastUserMessage.substring(0, 100) + }; + + // Remove any existing entry for this session (in case of updates) + this._conversationIndex = this._conversationIndex.filter(entry => entry.filename !== conversationData.filename); + + // Add new entry at the beginning (most recent first) + this._conversationIndex.unshift(indexEntry); + + // Keep only last 50 conversations to avoid workspace state bloat + if (this._conversationIndex.length > 50) { + this._conversationIndex = this._conversationIndex.slice(0, 50); + } + + // Save to workspace state + this._context.workspaceState.update('claude.conversationIndex', this._conversationIndex); + } +} \ No newline at end of file diff --git a/src/services/FileService.ts b/src/services/FileService.ts new file mode 100644 index 0000000..c090545 --- /dev/null +++ b/src/services/FileService.ts @@ -0,0 +1,136 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { WorkspaceFile } from '../types'; + +export class FileService { + + public async getWorkspaceFiles(searchTerm?: string): Promise { + try { + // Always get all files and filter on the backend for better search results + const files = await vscode.workspace.findFiles( + '**/*', + '{**/node_modules/**,**/.git/**,**/dist/**,**/build/**,**/.next/**,**/.nuxt/**,**/target/**,**/bin/**,**/obj/**}', + 500 // Reasonable limit for filtering + ); + + let fileList: WorkspaceFile[] = files.map(file => { + const relativePath = vscode.workspace.asRelativePath(file); + return { + name: file.path.split('/').pop() || '', + path: relativePath, + fsPath: file.fsPath + }; + }); + + // Filter results based on search term + if (searchTerm && searchTerm.trim()) { + const term = searchTerm.toLowerCase(); + fileList = fileList.filter(file => { + const fileName = file.name.toLowerCase(); + const filePath = file.path.toLowerCase(); + + // Check if term matches filename or any part of the path + return fileName.includes(term) || + filePath.includes(term) || + filePath.split('/').some(segment => segment.includes(term)); + }); + } + + // Sort and limit results + fileList = fileList + .sort((a, b) => a.name.localeCompare(b.name)) + .slice(0, 50); + + return fileList; + } catch (error) { + console.error('Error getting workspace files:', error); + return []; + } + } + + public async selectImageFiles(): Promise { + try { + // Show VS Code's native file picker for images + const result = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: true, + title: 'Select image files', + filters: { + 'Images': ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp'] + } + }); + + if (result && result.length > 0) { + return result.map(uri => uri.fsPath); + } + + return []; + } catch (error) { + console.error('Error selecting image files:', error); + return []; + } + } + + public async openFileInEditor(filePath: string): Promise { + try { + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(document, vscode.ViewColumn.One); + } catch (error) { + vscode.window.showErrorMessage(`Failed to open file: ${filePath}`); + console.error('Error opening file:', error); + } + } + + public async createImageFile(imageData: string, imageType: string): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { return; } + + // Extract base64 data from data URL + const base64Data = imageData.split(',')[1]; + const buffer = Buffer.from(base64Data, 'base64'); + + // Get file extension from image type + const extension = imageType.split('/')[1] || 'png'; + + // Create unique filename with timestamp + const timestamp = Date.now(); + const imageFileName = `image_${timestamp}.${extension}`; + + // Create images folder in workspace .claude directory + const imagesDir = vscode.Uri.joinPath(workspaceFolder.uri, '.claude', 'claude-code-chat-images'); + await vscode.workspace.fs.createDirectory(imagesDir); + + // Create .gitignore to ignore all images + const gitignorePath = vscode.Uri.joinPath(imagesDir, '.gitignore'); + try { + await vscode.workspace.fs.stat(gitignorePath); + } catch { + // .gitignore doesn't exist, create it + const gitignoreContent = new TextEncoder().encode('*\n'); + await vscode.workspace.fs.writeFile(gitignorePath, gitignoreContent); + } + + // Create the image file + const imagePath = vscode.Uri.joinPath(imagesDir, imageFileName); + await vscode.workspace.fs.writeFile(imagePath, buffer); + + return imagePath.fsPath; + } catch (error) { + console.error('Error creating image file:', error); + vscode.window.showErrorMessage('Failed to create image file'); + return undefined; + } + } + + public async getClipboardText(): Promise { + try { + return await vscode.env.clipboard.readText(); + } catch (error) { + console.error('Failed to read clipboard:', error); + return ''; + } + } +} \ No newline at end of file diff --git a/src/services/PermissionService.ts b/src/services/PermissionService.ts new file mode 100644 index 0000000..29f4e9d --- /dev/null +++ b/src/services/PermissionService.ts @@ -0,0 +1,591 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { PermissionRequest, PermissionResponse, Permissions } from '../types'; + +export class PermissionService { + private _permissionRequestsPath: string | undefined; + private _permissionWatcher: vscode.FileSystemWatcher | undefined; + private _pendingPermissionResolvers: Map void> = new Map(); + private _disposables: vscode.Disposable[] = []; + + constructor( + private readonly _context: vscode.ExtensionContext + ) { + this._initializePermissions(); + } + + private async _initializePermissions(): Promise { + try { + if (this._permissionWatcher) { + this._permissionWatcher.dispose(); + this._permissionWatcher = undefined; + } + + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { return; } + + // Create permission requests directory + this._permissionRequestsPath = path.join(storagePath, 'permission-requests'); + try { + await vscode.workspace.fs.stat(vscode.Uri.file(this._permissionRequestsPath)); + } catch { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._permissionRequestsPath)); + console.log(`Created permission requests directory at: ${this._permissionRequestsPath}`); + } + + console.log("DIRECTORY-----", this._permissionRequestsPath); + + // Set up file watcher for *.request files + this._permissionWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(this._permissionRequestsPath, '*.request') + ); + + this._permissionWatcher.onDidCreate(async (uri) => { + // Only handle file scheme URIs, ignore vscode-userdata scheme + if (uri.scheme === 'file') { + await this._handlePermissionRequest(uri); + } + }); + + this._disposables.push(this._permissionWatcher); + + } catch (error: any) { + console.error('Failed to initialize permissions:', error.message); + } + } + + private async _handlePermissionRequest(requestUri: vscode.Uri): Promise { + try { + // Read the request file + const content = await vscode.workspace.fs.readFile(requestUri); + const request = JSON.parse(new TextDecoder().decode(content)); + + // Show permission dialog + const approved = await this._showPermissionDialog(request); + + // Write response file + const responseFile = requestUri.fsPath.replace('.request', '.response'); + const response: PermissionResponse = { + id: request.id, + approved: approved, + timestamp: new Date().toISOString() + }; + + const responseContent = new TextEncoder().encode(JSON.stringify(response)); + await vscode.workspace.fs.writeFile(vscode.Uri.file(responseFile), responseContent); + + // Clean up request file + await vscode.workspace.fs.delete(requestUri); + + } catch (error: any) { + console.error('Failed to handle permission request:', error.message); + } + } + + private async _showPermissionDialog(request: PermissionRequest): Promise { + // This method will need to be implemented by the calling code + // since it needs access to the webview message system + // For now, return a promise that can be resolved externally + return new Promise((resolve) => { + this._pendingPermissionResolvers.set(request.id, resolve); + }); + } + + public resolvePermissionRequest(id: string, approved: boolean): void { + if (this._pendingPermissionResolvers.has(id)) { + const resolver = this._pendingPermissionResolvers.get(id); + if (resolver) { + resolver(approved); + this._pendingPermissionResolvers.delete(id); + } + } + } + + public async saveAlwaysAllowPermission(requestId: string): Promise { + try { + // Read the original request to get tool name and input + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) {return;} + + const requestFileUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', `${requestId}.request`)); + + let requestContent: Uint8Array; + try { + requestContent = await vscode.workspace.fs.readFile(requestFileUri); + } catch { + return; // Request file doesn't exist + } + + const request = JSON.parse(new TextDecoder().decode(requestContent)); + + // Load existing workspace permissions + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: Permissions = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist yet, use default permissions + } + + // Add the new permission + const toolName = request.tool; + if (toolName === 'Bash' && request.input?.command) { + // For Bash, store the command pattern + if (!permissions.alwaysAllow[toolName]) { + permissions.alwaysAllow[toolName] = []; + } + if (Array.isArray(permissions.alwaysAllow[toolName])) { + const command = request.input.command.trim(); + const pattern = this.getCommandPattern(command); + if (!permissions.alwaysAllow[toolName].includes(pattern)) { + permissions.alwaysAllow[toolName].push(pattern); + } + } + } else { + // For other tools, allow all instances + permissions.alwaysAllow[toolName] = true; + } + + // Ensure permissions directory exists + const permissionsDir = vscode.Uri.file(path.dirname(permissionsUri.fsPath)); + try { + await vscode.workspace.fs.stat(permissionsDir); + } catch { + await vscode.workspace.fs.createDirectory(permissionsDir); + } + + // Save the permissions + const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); + await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); + + console.log(`Saved always-allow permission for ${toolName}`); + } catch (error) { + console.error('Error saving always-allow permission:', error); + } + } + + public async getPermissions(): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { + return { alwaysAllow: {} }; + } + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: Permissions = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist or can't be read, use default permissions + } + + return permissions; + } catch (error) { + console.error('Error getting permissions:', error); + return { alwaysAllow: {} }; + } + } + + public async removePermission(toolName: string, command: string | null): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) {return;} + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: Permissions = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist or can't be read, nothing to remove + return; + } + + // Remove the permission + if (command === null) { + // Remove entire tool permission + delete permissions.alwaysAllow[toolName]; + } else { + // Remove specific command from tool permissions + if (Array.isArray(permissions.alwaysAllow[toolName])) { + permissions.alwaysAllow[toolName] = permissions.alwaysAllow[toolName].filter( + (cmd: string) => cmd !== command + ); + // If no commands left, remove the tool entirely + if (permissions.alwaysAllow[toolName].length === 0) { + delete permissions.alwaysAllow[toolName]; + } + } + } + + // Save updated permissions + const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); + await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); + + console.log(`Removed permission for ${toolName}${command ? ` command: ${command}` : ''}`); + } catch (error) { + console.error('Error removing permission:', error); + } + } + + public async addPermission(toolName: string, command: string | null): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) {return;} + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: Permissions = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist, use default permissions + } + + // Add the new permission + if (command === null || command === '') { + // Allow all commands for this tool + permissions.alwaysAllow[toolName] = true; + } else { + // Add specific command pattern + if (!permissions.alwaysAllow[toolName]) { + permissions.alwaysAllow[toolName] = []; + } + + // Convert to array if it's currently set to true + if (permissions.alwaysAllow[toolName] === true) { + permissions.alwaysAllow[toolName] = []; + } + + if (Array.isArray(permissions.alwaysAllow[toolName])) { + // For Bash commands, convert to pattern using existing logic + let commandToAdd = command; + if (toolName === 'Bash') { + commandToAdd = this.getCommandPattern(command); + } + + // Add if not already present + if (!permissions.alwaysAllow[toolName].includes(commandToAdd)) { + permissions.alwaysAllow[toolName].push(commandToAdd); + } + } + } + + // Ensure permissions directory exists + const permissionsDir = vscode.Uri.file(path.dirname(permissionsUri.fsPath)); + try { + await vscode.workspace.fs.stat(permissionsDir); + } catch { + await vscode.workspace.fs.createDirectory(permissionsDir); + } + + // Save updated permissions + const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); + await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); + + console.log(`Added permission for ${toolName}${command ? ` command: ${command}` : ' (all commands)'}`); + } catch (error) { + console.error('Error adding permission:', error); + } + } + + public getCommandPattern(command: string): string { + const parts = command.trim().split(/\s+/); + if (parts.length === 0) {return command;} + + const baseCmd = parts[0]; + const subCmd = parts.length > 1 ? parts[1] : ''; + + // Common patterns that should use wildcards + const patterns = [ + // Package managers + ['npm', 'install', 'npm install *'], + ['npm', 'i', 'npm i *'], + ['npm', 'add', 'npm add *'], + ['npm', 'remove', 'npm remove *'], + ['npm', 'uninstall', 'npm uninstall *'], + ['npm', 'update', 'npm update *'], + ['npm', 'run', 'npm run *'], + ['yarn', 'add', 'yarn add *'], + ['yarn', 'remove', 'yarn remove *'], + ['yarn', 'install', 'yarn install *'], + ['pnpm', 'install', 'pnpm install *'], + ['pnpm', 'add', 'pnpm add *'], + ['pnpm', 'remove', 'pnpm remove *'], + + // Git commands + ['git', 'add', 'git add *'], + ['git', 'commit', 'git commit *'], + ['git', 'push', 'git push *'], + ['git', 'pull', 'git pull *'], + ['git', 'checkout', 'git checkout *'], + ['git', 'branch', 'git branch *'], + ['git', 'merge', 'git merge *'], + ['git', 'clone', 'git clone *'], + ['git', 'reset', 'git reset *'], + ['git', 'rebase', 'git rebase *'], + ['git', 'tag', 'git tag *'], + + // Docker commands + ['docker', 'run', 'docker run *'], + ['docker', 'build', 'docker build *'], + ['docker', 'exec', 'docker exec *'], + ['docker', 'logs', 'docker logs *'], + ['docker', 'stop', 'docker stop *'], + ['docker', 'start', 'docker start *'], + ['docker', 'rm', 'docker rm *'], + ['docker', 'rmi', 'docker rmi *'], + ['docker', 'pull', 'docker pull *'], + ['docker', 'push', 'docker push *'], + + // Build tools + ['make', '', 'make *'], + ['cargo', 'build', 'cargo build *'], + ['cargo', 'run', 'cargo run *'], + ['cargo', 'test', 'cargo test *'], + ['cargo', 'install', 'cargo install *'], + ['mvn', 'compile', 'mvn compile *'], + ['mvn', 'test', 'mvn test *'], + ['mvn', 'package', 'mvn package *'], + ['gradle', 'build', 'gradle build *'], + ['gradle', 'test', 'gradle test *'], + + // System commands + ['curl', '', 'curl *'], + ['wget', '', 'wget *'], + ['ssh', '', 'ssh *'], + ['scp', '', 'scp *'], + ['rsync', '', 'rsync *'], + ['tar', '', 'tar *'], + ['zip', '', 'zip *'], + ['unzip', '', 'unzip *'], + + // Development tools + ['node', '', 'node *'], + ['python', '', 'python *'], + ['python3', '', 'python3 *'], + ['pip', 'install', 'pip install *'], + ['pip3', 'install', 'pip3 install *'], + ['composer', 'install', 'composer install *'], + ['composer', 'require', 'composer require *'], + ['bundle', 'install', 'bundle install *'], + ['gem', 'install', 'gem install *'], + ]; + + // Find matching pattern + for (const [cmd, sub, pattern] of patterns) { + if (baseCmd === cmd && (sub === '' || subCmd === sub)) { + return pattern; + } + } + + // Default: return exact command + return command; + } + + public async initializeMCPConfig(): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { return; } + + // Create MCP config directory + const mcpConfigDir = path.join(storagePath, 'mcp'); + try { + await vscode.workspace.fs.stat(vscode.Uri.file(mcpConfigDir)); + } catch { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(mcpConfigDir)); + console.log(`Created MCP config directory at: ${mcpConfigDir}`); + } + + // Create or update mcp-servers.json with permissions server, preserving existing servers + const mcpConfigPath = path.join(mcpConfigDir, 'mcp-servers.json'); + const mcpPermissionsPath = this.convertToWSLPath(path.join(this._context.extensionUri.fsPath, 'mcp-permissions.js')); + const permissionRequestsPath = this.convertToWSLPath(path.join(storagePath, 'permission-requests')); + + // Load existing config or create new one + let mcpConfig: any = { mcpServers: {} }; + const mcpConfigUri = vscode.Uri.file(mcpConfigPath); + + try { + const existingContent = await vscode.workspace.fs.readFile(mcpConfigUri); + mcpConfig = JSON.parse(new TextDecoder().decode(existingContent)); + console.log('Loaded existing MCP config, preserving user servers'); + } catch { + console.log('No existing MCP config found, creating new one'); + } + + // Ensure mcpServers exists + if (!mcpConfig.mcpServers) { + mcpConfig.mcpServers = {}; + } + + // Add or update the permissions server entry + mcpConfig.mcpServers['claude-code-chat-permissions'] = { + command: 'node', + args: [mcpPermissionsPath], + env: { + CLAUDE_PERMISSIONS_PATH: permissionRequestsPath + } + }; + + const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2)); + await vscode.workspace.fs.writeFile(mcpConfigUri, configContent); + + console.log(`Updated MCP config at: ${mcpConfigPath}`); + } catch (error: any) { + console.error('Failed to initialize MCP config:', error.message); + } + } + + public getMCPConfigPath(): string | undefined { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { return undefined; } + + const configPath = path.join(storagePath, 'mcp', 'mcp-servers.json'); + return configPath; + } + + public async loadMCPServers(): Promise<{ [name: string]: any }> { + try { + const mcpConfigPath = this.getMCPConfigPath(); + if (!mcpConfigPath) { + return {}; + } + + const mcpConfigUri = vscode.Uri.file(mcpConfigPath); + let mcpConfig: any = { mcpServers: {} }; + + try { + const content = await vscode.workspace.fs.readFile(mcpConfigUri); + mcpConfig = JSON.parse(new TextDecoder().decode(content)); + } catch (error) { + console.log('MCP config file not found or error reading:', error); + return {}; + } + + // Filter out internal servers before returning + const filteredServers = Object.fromEntries( + Object.entries(mcpConfig.mcpServers || {}).filter(([name]) => name !== 'claude-code-chat-permissions') + ); + return filteredServers; + } catch (error) { + console.error('Error loading MCP servers:', error); + return {}; + } + } + + public async saveMCPServer(name: string, config: any): Promise<{ success: boolean; error?: string }> { + try { + const mcpConfigPath = this.getMCPConfigPath(); + if (!mcpConfigPath) { + return { success: false, error: 'Storage path not available' }; + } + + const mcpConfigUri = vscode.Uri.file(mcpConfigPath); + let mcpConfig: any = { mcpServers: {} }; + + // Load existing config + try { + const content = await vscode.workspace.fs.readFile(mcpConfigUri); + mcpConfig = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist, use default structure + } + + // Ensure mcpServers exists + if (!mcpConfig.mcpServers) { + mcpConfig.mcpServers = {}; + } + + // Add/update the server + mcpConfig.mcpServers[name] = config; + + // Ensure directory exists + const mcpDir = vscode.Uri.file(path.dirname(mcpConfigPath)); + try { + await vscode.workspace.fs.stat(mcpDir); + } catch { + await vscode.workspace.fs.createDirectory(mcpDir); + } + + // Save the config + const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2)); + await vscode.workspace.fs.writeFile(mcpConfigUri, configContent); + + console.log(`Saved MCP server: ${name}`); + return { success: true }; + } catch (error) { + console.error('Error saving MCP server:', error); + return { success: false, error: 'Failed to save MCP server' }; + } + } + + public async deleteMCPServer(name: string): Promise<{ success: boolean; error?: string }> { + try { + const mcpConfigPath = this.getMCPConfigPath(); + if (!mcpConfigPath) { + return { success: false, error: 'Storage path not available' }; + } + + const mcpConfigUri = vscode.Uri.file(mcpConfigPath); + let mcpConfig: any = { mcpServers: {} }; + + // Load existing config + try { + const content = await vscode.workspace.fs.readFile(mcpConfigUri); + mcpConfig = JSON.parse(new TextDecoder().decode(content)); + } catch { + return { success: false, error: 'MCP config file not found' }; + } + + // Delete the server + if (mcpConfig.mcpServers && mcpConfig.mcpServers[name]) { + delete mcpConfig.mcpServers[name]; + + // Save the updated config + const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2)); + await vscode.workspace.fs.writeFile(mcpConfigUri, configContent); + + console.log(`Deleted MCP server: ${name}`); + return { success: true }; + } else { + return { success: false, error: `Server '${name}' not found` }; + } + } catch (error) { + console.error('Error deleting MCP server:', error); + return { success: false, error: 'Failed to delete MCP server' }; + } + } + + private convertToWSLPath(windowsPath: string): string { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const wslEnabled = config.get('wsl.enabled', false); + + if (wslEnabled && windowsPath.match(/^[a-zA-Z]:/)) { + // Convert C:\Users\... to /mnt/c/Users/... + return windowsPath.replace(/^([a-zA-Z]):/, '/mnt/$1').toLowerCase().replace(/\\/g, '/'); + } + + return windowsPath; + } + + public dispose(): void { + if (this._permissionWatcher) { + this._permissionWatcher.dispose(); + this._permissionWatcher = undefined; + } + + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/services/SettingsService.ts b/src/services/SettingsService.ts new file mode 100644 index 0000000..bf895e1 --- /dev/null +++ b/src/services/SettingsService.ts @@ -0,0 +1,218 @@ +import * as vscode from 'vscode'; +import { ClaudeSettings, PlatformInfo } from '../types'; + +export class SettingsService { + private _selectedModel: string = 'default'; + + constructor( + private readonly _context: vscode.ExtensionContext + ) { + // Load saved model preference + this._selectedModel = this._context.workspaceState.get('claude.selectedModel', 'default'); + } + + public get selectedModel(): string { + return this._selectedModel; + } + + public getCurrentSettings(): ClaudeSettings { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + return { + 'thinking.intensity': config.get('thinking.intensity', 'think'), + 'wsl.enabled': config.get('wsl.enabled', false), + 'wsl.distro': config.get('wsl.distro', 'Ubuntu'), + 'wsl.nodePath': config.get('wsl.nodePath', '/usr/bin/node'), + 'wsl.claudePath': config.get('wsl.claudePath', '/usr/local/bin/claude'), + 'permissions.yoloMode': config.get('permissions.yoloMode', false) + }; + } + + public async updateSettings(settings: { [key: string]: any }): Promise { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + + try { + for (const [key, value] of Object.entries(settings)) { + if (key === 'permissions.yoloMode') { + // YOLO mode is workspace-specific + await config.update(key, value, vscode.ConfigurationTarget.Workspace); + } else { + // Other settings are global (user-wide) + await config.update(key, value, vscode.ConfigurationTarget.Global); + } + } + + console.log('Settings updated:', settings); + } catch (error) { + console.error('Failed to update settings:', error); + vscode.window.showErrorMessage('Failed to update settings'); + } + } + + public setSelectedModel(model: string): boolean { + // Validate model name to prevent issues mentioned in the GitHub issue + const validModels = ['opus', 'sonnet', 'default']; + if (validModels.includes(model)) { + this._selectedModel = model; + console.log('Model selected:', model); + + // Store the model preference in workspace state + this._context.workspaceState.update('claude.selectedModel', model); + + // Show confirmation + vscode.window.showInformationMessage(`Claude model switched to: ${model.charAt(0).toUpperCase() + model.slice(1)}`); + return true; + } else { + console.error('Invalid model selected:', model); + vscode.window.showErrorMessage(`Invalid model: ${model}. Please select Opus, Sonnet, or Default.`); + return false; + } + } + + public async enableYoloMode(): Promise { + try { + // Update VS Code configuration to enable YOLO mode + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + + // Clear any global setting and set workspace setting + await config.update('permissions.yoloMode', true, vscode.ConfigurationTarget.Workspace); + + console.log('YOLO Mode enabled - all future permissions will be skipped'); + } catch (error) { + console.error('Error enabling YOLO mode:', error); + } + } + + public dismissWSLAlert(): void { + this._context.globalState.update('wslAlertDismissed', true); + } + + public getPlatformInfo(): PlatformInfo { + const platform = process.platform; + const dismissed = this._context.globalState.get('wslAlertDismissed', false); + + // Get WSL configuration + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const wslEnabled = config.get('wsl.enabled', false); + + return { + platform: platform, + isWindows: platform === 'win32', + wslAlertDismissed: dismissed, + wslEnabled: wslEnabled + }; + } + + public openModelTerminal(currentSessionId?: string): void { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const wslEnabled = config.get('wsl.enabled', false); + const wslDistro = config.get('wsl.distro', 'Ubuntu'); + const nodePath = config.get('wsl.nodePath', '/usr/bin/node'); + const claudePath = config.get('wsl.claudePath', '/usr/local/bin/claude'); + + // Build command arguments + const args = ['/model']; + + // Add session resume if we have a current session + if (currentSessionId) { + args.push('--resume', currentSessionId); + } + + // Create terminal with the claude /model command + const terminal = vscode.window.createTerminal('Claude Model Selection'); + if (wslEnabled) { + terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`); + } else { + terminal.sendText(`claude ${args.join(' ')}`); + } + terminal.show(); + + // Show info message + vscode.window.showInformationMessage( + 'Check the terminal to update your default model configuration. Come back to this chat here after making changes.', + 'OK' + ); + } + + public executeSlashCommand(command: string, currentSessionId?: string): void { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const wslEnabled = config.get('wsl.enabled', false); + const wslDistro = config.get('wsl.distro', 'Ubuntu'); + const nodePath = config.get('wsl.nodePath', '/usr/bin/node'); + const claudePath = config.get('wsl.claudePath', '/usr/local/bin/claude'); + + // Build command arguments + const args = [`/${command}`]; + + // Add session resume if we have a current session + if (currentSessionId) { + args.push('--resume', currentSessionId); + } + + // Create terminal with the claude command + const terminal = vscode.window.createTerminal(`Claude /${command}`); + if (wslEnabled) { + terminal.sendText(`wsl -d ${wslDistro} ${nodePath} --no-warnings --enable-source-maps ${claudePath} ${args.join(' ')}`); + } else { + terminal.sendText(`claude ${args.join(' ')}`); + } + terminal.show(); + + // Show info message + vscode.window.showInformationMessage( + `Executing /${command} command in terminal. Check the terminal output and return when ready.`, + 'OK' + ); + } + + public async saveCustomSnippet(snippet: any): Promise { + try { + const customSnippets = this._context.globalState.get<{ [key: string]: any }>('customPromptSnippets', {}); + customSnippets[snippet.id] = snippet; + + await this._context.globalState.update('customPromptSnippets', customSnippets); + console.log('Saved custom snippet:', snippet.name); + } catch (error) { + console.error('Error saving custom snippet:', error); + throw new Error('Failed to save custom snippet'); + } + } + + public async deleteCustomSnippet(snippetId: string): Promise { + try { + const customSnippets = this._context.globalState.get<{ [key: string]: any }>('customPromptSnippets', {}); + + if (customSnippets[snippetId]) { + delete customSnippets[snippetId]; + await this._context.globalState.update('customPromptSnippets', customSnippets); + console.log('Deleted custom snippet:', snippetId); + } else { + throw new Error('Snippet not found'); + } + } catch (error) { + console.error('Error deleting custom snippet:', error); + throw new Error('Failed to delete custom snippet'); + } + } + + public getCustomSnippets(): { [key: string]: any } { + return this._context.globalState.get<{ [key: string]: any }>('customPromptSnippets', {}); + } + + public isPermissionSystemEnabled(): boolean { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const yoloMode = config.get('permissions.yoloMode', false); + return !yoloMode; + } + + public convertToWSLPath(windowsPath: string): string { + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const wslEnabled = config.get('wsl.enabled', false); + + if (wslEnabled && windowsPath.match(/^[a-zA-Z]:/)) { + // Convert C:\Users\... to /mnt/c/Users/... + return windowsPath.replace(/^([a-zA-Z]):/, '/mnt/$1').toLowerCase().replace(/\\/g, '/'); + } + + return windowsPath; + } +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..0bd9268 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,109 @@ +import * as vscode from 'vscode'; + +export interface ConversationData { + sessionId: string; + startTime: string | undefined; + endTime: string; + messageCount: number; + totalCost: number; + totalTokens: { + input: number; + output: number; + }; + messages: Array<{ timestamp: string, messageType: string, data: any }>; + filename: string; +} + +export interface ConversationIndexEntry { + filename: string; + sessionId: string; + startTime: string; + endTime: string; + messageCount: number; + totalCost: number; + firstUserMessage: string; + lastUserMessage: string; +} + +export interface CommitInfo { + id: string; + sha: string; + message: string; + timestamp: string; +} + +export interface TokenUsage { + input_tokens?: number; + output_tokens?: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; +} + +export interface WebviewMessage { + type: string; + data?: any; + text?: string; + planMode?: boolean; + thinkingMode?: boolean; + model?: string; + command?: string; + filename?: string; + settings?: { [key: string]: any }; + searchTerm?: string; + filePath?: string; + imageData?: string; + imageType?: string; + id?: string; + approved?: boolean; + alwaysAllow?: boolean; + toolName?: string; + name?: string; + config?: any; + snippetId?: string; + snippet?: any; +} + +export interface PermissionRequest { + id: string; + tool: string; + input: any; + timestamp: string; +} + +export interface PermissionResponse { + id: string; + approved: boolean; + timestamp: string; +} + +export interface Permissions { + alwaysAllow: { [toolName: string]: boolean | string[] }; +} + +export interface MCPServerConfig { + command: string; + args: string[]; + env?: { [key: string]: string }; +} + +export interface WorkspaceFile { + name: string; + path: string; + fsPath: string; +} + +export interface ClaudeSettings { + 'thinking.intensity': string; + 'wsl.enabled': boolean; + 'wsl.distro': string; + 'wsl.nodePath': string; + 'wsl.claudePath': string; + 'permissions.yoloMode': boolean; +} + +export interface PlatformInfo { + platform: string; + isWindows: boolean; + wslAlertDismissed: boolean; + wslEnabled: boolean; +} \ No newline at end of file diff --git a/src/ui-styles.ts b/src/ui-styles.ts index cc7bc25..45f0034 100644 --- a/src/ui-styles.ts +++ b/src/ui-styles.ts @@ -3331,6 +3331,6 @@ const styles = ` overflow: hidden; text-overflow: ellipsis; } -` +`; -export default styles \ No newline at end of file +export default styles; \ No newline at end of file diff --git a/src/ui.ts b/src/ui.ts index c5fd526..65b268f 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,5 +1,5 @@ import getScript from './script'; -import styles from './ui-styles' +import styles from './ui-styles'; const getHtml = (isTelemetryEnabled: boolean) => ` From 17a6d346f8f247e0c5a4ea3cc72c0ec1e7b7f188 Mon Sep 17 00:00:00 2001 From: ahmed al mihy Date: Tue, 2 Sep 2025 01:31:10 +0300 Subject: [PATCH 14/17] Refactor code structure for improved readability and maintainability --- src/styles/animation-styles.ts | 414 ++++ src/styles/base-styles.ts | 67 + src/styles/chat-styles.ts | 522 +++++ src/styles/index.ts | 22 + src/styles/input-styles.ts | 311 +++ src/styles/mcp-styles.ts | 298 +++ src/styles/modal-styles.ts | 721 +++++++ src/styles/permission-styles.ts | 623 ++++++ src/styles/tool-styles.ts | 76 + src/ui-styles.ts | 3337 +------------------------------ 10 files changed, 3056 insertions(+), 3335 deletions(-) create mode 100644 src/styles/animation-styles.ts create mode 100644 src/styles/base-styles.ts create mode 100644 src/styles/chat-styles.ts create mode 100644 src/styles/index.ts create mode 100644 src/styles/input-styles.ts create mode 100644 src/styles/mcp-styles.ts create mode 100644 src/styles/modal-styles.ts create mode 100644 src/styles/permission-styles.ts create mode 100644 src/styles/tool-styles.ts diff --git a/src/styles/animation-styles.ts b/src/styles/animation-styles.ts new file mode 100644 index 0000000..e52f6ee --- /dev/null +++ b/src/styles/animation-styles.ts @@ -0,0 +1,414 @@ +export const animationStyles = ` + @keyframes pulse { + 0%, 100% { + opacity: 0.6; + transform: scale(1); + box-shadow: 0 0 4px rgba(0, 122, 204, 0.4); + } + 50% { + opacity: 1; + transform: scale(1.3); + box-shadow: 0 0 8px rgba(0, 122, 204, 0.6); + } + } + + @keyframes bounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } + } + + @keyframes slideUp { + 0% { + opacity: 0; + transform: translateY(10px); + } + 100% { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes slideDown { + 0% { + opacity: 0; + max-height: 0; + padding: 0; + } + 100% { + opacity: 1; + max-height: 500px; + padding: initial; + } + } + + /* Tool loading animation */ + .tool-loading { + padding: 16px 12px; + display: flex; + align-items: center; + gap: 12px; + background-color: var(--vscode-panel-background); + border-top: 1px solid var(--vscode-panel-border); + } + + .loading-spinner { + display: flex; + gap: 4px; + } + + .loading-ball { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--vscode-button-background); + animation: bounce 1.4s ease-in-out infinite both; + } + + .loading-ball:nth-child(1) { animation-delay: -0.32s; } + .loading-ball:nth-child(2) { animation-delay: -0.16s; } + .loading-ball:nth-child(3) { animation-delay: 0s; } + + .loading-text { + font-size: 12px; + color: var(--vscode-descriptionForeground); + font-style: italic; + } + + /* Tool completion indicator */ + .tool-completion { + padding: 8px 12px; + display: flex; + align-items: center; + gap: 6px; + background-color: rgba(76, 175, 80, 0.1); + border-top: 1px solid rgba(76, 175, 80, 0.2); + font-size: 12px; + } + + .completion-icon { + color: #4caf50; + font-weight: bold; + } + + .completion-text { + color: var(--vscode-foreground); + opacity: 0.8; + } + + .status { + padding: 12px 16px 6px 16px; + background: transparent; + color: #808080; + font-size: 12px; + border: none; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 400; + } + + .status-left { + display: flex; + align-items: center; + gap: 16px; + } + + .status-right { + display: flex; + align-items: center; + gap: 6px; + } + + .status-indicator { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + } + + .status.ready .status-indicator { + background-color: rgba(255, 255, 255, 0.3); + box-shadow: none; + } + + .status.processing .status-indicator { + background-color: var(--vscode-charts-blue); + box-shadow: 0 0 4px rgba(0, 122, 204, 0.4); + animation: pulse 1.2s ease-in-out infinite; + } + + .status.error .status-indicator { + background-color: #ff453a; + box-shadow: 0 0 6px rgba(255, 69, 58, 0.5); + } + + .status-text { + flex: 1; + } + + pre { + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; + } + + .session-badge { + margin-left: 16px; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 4px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; + transition: background-color 0.2s, transform 0.1s; + } + + .session-badge:hover { + background-color: var(--vscode-button-hoverBackground); + transform: scale(1.02); + } + + .session-icon { + font-size: 10px; + } + + .session-label { + opacity: 0.8; + font-size: 10px; + } + + .session-status { + font-size: 12px; + color: var(--vscode-descriptionForeground); + padding: 2px 6px; + border-radius: 4px; + background-color: var(--vscode-badge-background); + border: 1px solid var(--vscode-panel-border); + } + + .session-status.active { + color: var(--vscode-terminal-ansiGreen); + background-color: rgba(0, 210, 106, 0.1); + border-color: var(--vscode-terminal-ansiGreen); + } + + .workspace-info { + margin-left: 12px; + padding: 2px 8px; + background-color: rgba(255, 255, 255, 0.05); + border-radius: 4px; + font-size: 11px; + color: var(--vscode-descriptionForeground); + border: 1px solid rgba(255, 255, 255, 0.08); + } + + .workspace-icon { + margin-right: 4px; + font-size: 10px; + } + + .user-copy-btn { + position: absolute; + top: 8px; + right: 8px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + color: var(--vscode-foreground); + cursor: pointer; + padding: 4px; + opacity: 0; + transition: opacity 0.2s ease, background-color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + } + + .user-copy-btn:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); + } + + .message.user:hover .user-copy-btn { + opacity: 1; + } + + /* Markdown content styles */ + .message h1, .message h2, .message h3, .message h4 { + margin: 0.8em 0 0.4em 0; + font-weight: 600; + line-height: 1.3; + } + + .message h1 { + font-size: 1.5em; + border-bottom: 2px solid var(--vscode-panel-border); + padding-bottom: 0.3em; + } + + .message h2 { + font-size: 1.3em; + border-bottom: 1px solid var(--vscode-panel-border); + padding-bottom: 0.2em; + } + + .message h3 { + font-size: 1.1em; + } + + .message h4 { + font-size: 1.05em; + } + + .message strong { + font-weight: 600; + color: var(--vscode-terminal-ansiBrightWhite); + } + + .message em { + font-style: italic; + } + + .message ul, .message ol { + margin: 0.6em 0; + padding-left: 1.5em; + } + + .message li { + margin: 0.3em 0; + line-height: 1.4; + } + + .message ul li { + list-style-type: disc; + } + + .message ol li { + list-style-type: decimal; + } + + .message p { + margin: 0; + line-height: 1.5; + } + + .message p:first-child { + margin-top: 0; + } + + .message p:last-child { + margin-bottom: 0; + } + + .message br { + line-height: 1.2; + } + + .restore-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px + } + + .restore-btn { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + } + + .restore-btn.dark { + background-color: #2d2d30; + color: #999999; + } + + .restore-btn:hover { + background-color: var(--vscode-button-hoverBackground); + } + + .restore-btn.dark:hover { + background-color: #3e3e42; + } + + .restore-date { + font-size: 10px; + color: var(--vscode-descriptionForeground); + opacity: 0.8; + } + + .conversation-history { + position: absolute; + top: 60px; + left: 0; + right: 0; + bottom: 60px; + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-widget-border); + z-index: 1000; + } + + .conversation-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--vscode-widget-border); + } + + .conversation-header h3 { + margin: 0; + font-size: 16px; + } + + .conversation-list { + padding: 8px; + overflow-y: auto; + height: calc(100% - 60px); + } + + .conversation-item { + padding: 12px; + margin: 4px 0; + border: 1px solid var(--vscode-widget-border); + border-radius: 6px; + cursor: pointer; + background-color: var(--vscode-list-inactiveSelectionBackground); + } + + .conversation-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .conversation-title { + font-weight: 500; + margin-bottom: 4px; + } + + .conversation-meta { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-bottom: 4px; + } + + .conversation-preview { + font-size: 11px; + color: var(--vscode-descriptionForeground); + opacity: 0.8; + } +`; \ No newline at end of file diff --git a/src/styles/base-styles.ts b/src/styles/base-styles.ts new file mode 100644 index 0000000..1e692e2 --- /dev/null +++ b/src/styles/base-styles.ts @@ -0,0 +1,67 @@ +export const baseStyles = ` + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: #1a1a1a; + color: #cccccc; + margin: 0; + padding: 0; + height: 100vh; + display: flex; + flex-direction: column; + } + + .header { + padding: 8px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + background-color: #1a1a1a; + display: flex; + justify-content: space-between; + align-items: center; + } + + .header h2 { + margin: 0; + font-size: 12px; + font-weight: 400; + color: #808080; + letter-spacing: 0; + } + + .controls { + display: flex; + gap: 6px; + align-items: center; + } + + .btn { + background-color: transparent; + color: #808080; + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 400; + transition: all 0.15s ease; + display: flex; + align-items: center; + gap: 4px; + } + + .btn:hover { + background-color: rgba(255, 255, 255, 0.05); + color: #cccccc; + border-color: rgba(255, 255, 255, 0.12); + } + + .btn.outlined { + background-color: transparent; + color: var(--vscode-foreground); + border-color: var(--vscode-panel-border); + } + + .btn.outlined:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } +`; \ No newline at end of file diff --git a/src/styles/chat-styles.ts b/src/styles/chat-styles.ts new file mode 100644 index 0000000..64339f3 --- /dev/null +++ b/src/styles/chat-styles.ts @@ -0,0 +1,522 @@ +export const chatStyles = ` + .chat-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .messages { + flex: 1; + padding: 12px 16px; + overflow-y: auto; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace; + font-size: 12px; + line-height: 1.6; + } + + .message { + margin-bottom: 12px; + padding: 12px; + border-radius: 6px; + } + + .message.user { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + color: #e0e0e0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + position: relative; + overflow: hidden; + background-color: #242424; + padding: 14px; + } + + .message.user::before { + display: none; + } + + .message.claude { + border: none; + border-radius: 0; + color: #d4d4d4; + position: relative; + overflow: hidden; + background-color: transparent; + padding: 4px 0; + font-size: 13px; + line-height: 1.5; + font-weight: 400; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + } + + .message.claude::before { + display: none; + } + + .message.error { + border: 1px solid rgba(231, 76, 60, 0.3); + border-radius: 8px; + color: var(--vscode-editor-foreground); + position: relative; + overflow: hidden; + } + + .message.error::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: linear-gradient(180deg, #e74c3c 0%, #c0392b 100%); + } + + .message.system { + background-color: var(--vscode-panel-background); + color: var(--vscode-descriptionForeground); + font-style: italic; + } + + .message.tool { + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 6px; + color: #999999; + position: relative; + overflow: hidden; + background-color: #212121; + padding: 12px; + font-size: 11px; + margin-bottom: 8px; + } + + .message.tool::before { + display: none; + } + + .message.tool-result { + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 6px; + color: #cccccc; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace; + white-space: pre-wrap; + position: relative; + overflow: hidden; + background-color: #212121; + padding: 12px; + font-size: 12px; + margin-bottom: 8px; + } + + .message.tool-result::before { + display: none; + } + + .message.thinking { + border: none; + border-radius: 0; + color: #999999; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-style: italic; + opacity: 0.8; + position: relative; + overflow: hidden; + background-color: transparent; + padding: 4px 0; + font-size: 13px; + } + + .message.thinking::before { + content: 'thinking... '; + color: #808080; + font-style: normal; + font-weight: 400; + } + + .message.thinking::before { + display: none; + } + + .message-header { + display: none; + } + + .message.user .message-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + opacity: 0; + transition: opacity 0.2s ease; + } + + .message.user:hover .message-header { + opacity: 1; + } + + .message.claude .message-header { + display: none; + } + + .copy-btn { + background: transparent; + border: none; + color: var(--vscode-descriptionForeground); + cursor: pointer; + padding: 2px; + border-radius: 3px; + opacity: 0; + transition: opacity 0.2s ease; + margin-left: auto; + display: flex; + align-items: center; + justify-content: center; + } + + .message:hover .copy-btn { + opacity: 0.7; + } + + .copy-btn:hover { + opacity: 1; + background-color: var(--vscode-list-hoverBackground); + } + + .message-icon { + width: 16px; + height: 16px; + border-radius: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + color: #808080; + font-weight: 400; + flex-shrink: 0; + margin-left: 0; + } + + .message-icon.user { + background: rgba(255, 255, 255, 0.06); + } + + .message-icon.claude { + background: transparent; + } + + .message-icon.system { + background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%); + } + + .message-icon.error { + background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); + } + + .message-label { + font-weight: 400; + font-size: 11px; + opacity: 0.5; + text-transform: none; + letter-spacing: 0; + color: #606060; + } + + .message-content { + padding-left: 6px; + } + + /* Code blocks generated by markdown parser only */ + .message-content pre.code-block { + background-color: #242424; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + padding: 12px; + margin: 8px 0; + overflow-x: auto; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace; + font-size: 12px; + line-height: 1.5; + white-space: pre; + } + + .message-content pre.code-block code { + background: none; + border: none; + padding: 0; + color: var(--vscode-editor-foreground); + } + + .code-line { + white-space: pre-wrap; + word-break: break-word; + } + + /* Code block container and header */ + .code-block-container { + margin: 8px 0; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + background-color: #242424; + overflow: hidden; + } + + .code-block-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 6px; + background-color: var(--vscode-editor-background); + border-bottom: 1px solid var(--vscode-panel-border); + font-size: 10px; + } + + .code-block-language { + color: var(--vscode-descriptionForeground); + font-family: var(--vscode-editor-font-family); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .code-copy-btn { + background: none; + border: none; + color: var(--vscode-descriptionForeground); + cursor: pointer; + padding: 4px; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + opacity: 0.7; + } + + .code-copy-btn:hover { + background-color: var(--vscode-list-hoverBackground); + opacity: 1; + } + + .code-block-container .code-block { + margin: 0; + border: none; + border-radius: 0; + background: none; + } + + /* Inline code */ + .message-content code { + background-color: rgba(255, 255, 255, 0.06); + border: none; + border-radius: 3px; + padding: 2px 4px; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace; + font-size: 0.9em; + color: #cccccc; + } + + .priority-badge { + display: inline-block; + padding: 2px 6px; + border-radius: 12px; + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: 6px; + } + + .priority-badge.high { + background: rgba(231, 76, 60, 0.15); + color: #e74c3c; + border: 1px solid rgba(231, 76, 60, 0.3); + } + + .priority-badge.medium { + background: rgba(243, 156, 18, 0.15); + color: #f39c12; + border: 1px solid rgba(243, 156, 18, 0.3); + } + + .priority-badge.low { + background: rgba(149, 165, 166, 0.15); + color: #95a5a6; + border: 1px solid rgba(149, 165, 166, 0.3); + } + + /* Diff display styles for Edit tool */ + .diff-container { + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + overflow: hidden; + } + + .diff-header { + background-color: var(--vscode-panel-background); + padding: 6px 12px; + font-size: 11px; + font-weight: 600; + color: var(--vscode-foreground); + border-bottom: 1px solid var(--vscode-panel-border); + } + + .diff-removed, + .diff-added { + font-family: var(--vscode-editor-font-family); + font-size: 12px; + line-height: 1.4; + } + + .diff-line { + padding: 2px 12px; + white-space: pre-wrap; + word-break: break-word; + } + + .diff-line.removed { + background-color: rgba(244, 67, 54, 0.1); + border-left: 3px solid rgba(244, 67, 54, 0.6); + color: var(--vscode-foreground); + } + + .diff-line.added { + background-color: rgba(76, 175, 80, 0.1); + border-left: 3px solid rgba(76, 175, 80, 0.6); + color: var(--vscode-foreground); + } + + .diff-line.removed::before { + content: ''; + color: rgba(244, 67, 54, 0.8); + font-weight: 600; + margin-right: 8px; + } + + .diff-line.added::before { + content: ''; + color: rgba(76, 175, 80, 0.8); + font-weight: 600; + margin-right: 8px; + } + + .diff-expand-container { + padding: 8px 12px; + text-align: center; + border-top: 1px solid var(--vscode-panel-border); + background-color: var(--vscode-editor-background); + } + + .diff-expand-btn { + background: linear-gradient(135deg, rgba(64, 165, 255, 0.15) 0%, rgba(64, 165, 255, 0.1) 100%); + border: 1px solid rgba(64, 165, 255, 0.3); + color: #40a5ff; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 500; + transition: all 0.2s ease; + } + + .diff-expand-btn:hover { + background: linear-gradient(135deg, rgba(64, 165, 255, 0.25) 0%, rgba(64, 165, 255, 0.15) 100%); + border-color: rgba(64, 165, 255, 0.5); + } + + .diff-expand-btn:active { + transform: translateY(1px); + } + + /* MultiEdit specific styles */ + .single-edit { + margin-bottom: 12px; + } + + .edit-number { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.15); + color: var(--vscode-descriptionForeground); + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + margin-top: 6px; + display: inline-block; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .diff-edit-separator { + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + margin: 12px 0; + } + + /* File path display styles */ + .diff-file-path { + padding: 8px 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; + } + + .diff-file-path:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .diff-file-path:active { + transform: translateY(1px); + } + + .file-path-short, + .file-path-truncated { + font-family: var(--vscode-editor-font-family); + color: var(--vscode-foreground); + font-weight: 500; + } + + .file-path-truncated { + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + transition: all 0.2s ease; + padding: 2px 4px; + border-radius: 3px; + } + + .file-path-truncated .file-icon { + font-size: 14px; + opacity: 0.7; + transition: opacity 0.2s ease; + } + + .file-path-truncated:hover { + color: var(--vscode-textLink-foreground); + background-color: var(--vscode-list-hoverBackground); + } + + .file-path-truncated:hover .file-icon { + opacity: 1; + } + + .file-path-truncated:active { + transform: translateY(1px); + } + + .expand-btn { + background: linear-gradient(135deg, rgba(64, 165, 255, 0.15) 0%, rgba(64, 165, 255, 0.1) 100%); + border: 1px solid rgba(64, 165, 255, 0.3); + color: #40a5ff; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 500; + margin-left: 6px; + display: inline-block; + } +`; \ No newline at end of file diff --git a/src/styles/index.ts b/src/styles/index.ts new file mode 100644 index 0000000..aaef7f2 --- /dev/null +++ b/src/styles/index.ts @@ -0,0 +1,22 @@ +import { baseStyles } from './base-styles'; +import { permissionStyles } from './permission-styles'; +import { chatStyles } from './chat-styles'; +import { toolStyles } from './tool-styles'; +import { inputStyles } from './input-styles'; +import { modalStyles } from './modal-styles'; +import { mcpStyles } from './mcp-styles'; +import { animationStyles } from './animation-styles'; + +export const combinedStyles = ` +`; + +export default combinedStyles; \ No newline at end of file diff --git a/src/styles/input-styles.ts b/src/styles/input-styles.ts new file mode 100644 index 0000000..83dc825 --- /dev/null +++ b/src/styles/input-styles.ts @@ -0,0 +1,311 @@ +export const inputStyles = ` + .input-container { + padding: 0; + border-top: 1px solid rgba(255, 255, 255, 0.06); + background-color: #1a1a1a; + display: flex; + flex-direction: column; + position: relative; + } + + .input-modes { + display: flex; + gap: 16px; + align-items: center; + padding-bottom: 5px; + font-size: 9.5px; + } + + .mode-toggle { + display: flex; + align-items: center; + gap: 6px; + color: var(--vscode-foreground); + opacity: 0.8; + transition: opacity 0.2s ease; + font-size: 12px; + } + + .mode-toggle span { + cursor: pointer; + transition: opacity 0.2s ease; + } + + .mode-toggle span:hover { + opacity: 1; + } + + .mode-toggle:hover { + opacity: 1; + } + + .mode-switch { + position: relative; + width: 22px; + height: 12px; + background-color: var(--vscode-panel-border); + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; + } + + .mode-switch.active { + background-color: var(--vscode-button-background); + } + + .mode-switch::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 8px; + height: 8px; + background-color: var(--vscode-foreground); + border-radius: 50%; + transition: transform 0.2s ease; + } + + .mode-switch.active::after { + transform: translateX(8px); + background-color: var(--vscode-button-foreground); + } + + .textarea-container { + display: flex; + gap: 10px; + align-items: flex-end; + padding: 6px 16px 12px 16px; + } + + .textarea-wrapper { + flex: 1; + background-color: #242424; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + overflow: hidden; + } + + .textarea-wrapper:focus-within { + border-color: rgba(255, 255, 255, 0.12); + background-color: #282828; + } + + .input-field { + width: 100%; + box-sizing: border-box; + background-color: transparent; + color: #cccccc; + border: none; + padding: 10px; + outline: none; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 13px; + min-height: 60px; + line-height: 1.5; + overflow-y: hidden; + resize: none; + } + + .input-field:focus { + border: none; + outline: none; + } + + .input-field::placeholder { + color: #606060; + border: none; + outline: none; + } + + .input-controls { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + padding: 4px 6px; + border-top: 1px solid rgba(255, 255, 255, 0.03); + background-color: transparent; + } + + .left-controls { + display: flex; + align-items: center; + gap: 8px; + } + + .model-selector { + background-color: transparent; + color: #606060; + border: none; + padding: 3px 6px; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + font-weight: 400; + transition: all 0.15s ease; + opacity: 0.8; + display: flex; + align-items: center; + gap: 3px; + } + + .model-selector:hover { + background-color: rgba(255, 255, 255, 0.04); + color: #999999; + opacity: 1; + } + + .tools-btn { + background-color: rgba(128, 128, 128, 0.15); + color: var(--vscode-foreground); + border: none; + padding: 3px 7px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 500; + transition: all 0.2s ease; + opacity: 0.9; + display: flex; + align-items: center; + gap: 4px; + } + + .tools-btn:hover { + background-color: rgba(128, 128, 128, 0.25); + opacity: 1; + } + + .slash-btn, + .at-btn { + background-color: transparent; + color: #606060; + border: none; + padding: 3px 5px; + border-radius: 3px; + cursor: pointer; + font-size: 11px; + font-weight: 400; + transition: all 0.15s ease; + opacity: 0.8; + } + + .slash-btn:hover, + .at-btn:hover { + background-color: rgba(255, 255, 255, 0.04); + color: #999999; + opacity: 1; + } + + .image-btn { + background-color: transparent; + color: #606060; + border: none; + padding: 3px; + border-radius: 3px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + transition: all 0.15s ease; + opacity: 0.8; + } + + .image-btn:hover { + background-color: rgba(255, 255, 255, 0.04); + color: #999999; + opacity: 1; + } + + .send-btn { + background-color: rgba(255, 255, 255, 0.06); + color: #999999; + border: none; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 400; + transition: all 0.15s ease; + } + + .send-btn div { + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + } + + .send-btn span { + line-height: 1; + } + + .send-btn:hover { + background-color: rgba(255, 255, 255, 0.08); + color: #cccccc; + } + + .send-btn:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + .send-btn.stop-mode { + background-color: rgba(255, 99, 71, 0.1); + color: #ff6347; + border: 1px solid rgba(255, 99, 71, 0.2); + } + + .send-btn.stop-mode:hover { + background-color: rgba(255, 99, 71, 0.2); + color: #ff6347; + } + + .expand-btn { + background: linear-gradient(135deg, rgba(64, 165, 255, 0.15) 0%, rgba(64, 165, 255, 0.1) 100%); + border: 1px solid rgba(64, 165, 255, 0.3); + color: #40a5ff; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 500; + margin-left: 6px; + display: inline-block; + transition: all 0.2s ease; + } + + .expand-btn:hover { + background: linear-gradient(135deg, rgba(64, 165, 255, 0.25) 0%, rgba(64, 165, 255, 0.15) 100%); + border-color: rgba(64, 165, 255, 0.5); + transform: translateY(-1px); + } + + .expanded-content { + margin-top: 8px; + padding: 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + position: relative; + } + + .expanded-content::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: linear-gradient(180deg, #40a5ff 0%, #0078d4 100%); + border-radius: 0 0 0 6px; + } + + .expanded-content pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; + } +`; \ No newline at end of file diff --git a/src/styles/mcp-styles.ts b/src/styles/mcp-styles.ts new file mode 100644 index 0000000..f8d793d --- /dev/null +++ b/src/styles/mcp-styles.ts @@ -0,0 +1,298 @@ +export const mcpStyles = ` + /* MCP Modal content area improvements */ + #mcpModal * { + box-sizing: border-box; + } + + #mcpModal .tools-list { + padding: 24px; + max-height: calc(80vh - 120px); + overflow-y: auto; + width: 100%; + } + + #mcpModal .mcp-servers-list { + padding: 0; + } + + #mcpModal .mcp-add-server { + padding: 0; + } + + #mcpModal .mcp-add-form { + padding: 12px; + } + + /* MCP Servers styles */ + .mcp-servers-list { + padding: 4px; + } + + .mcp-server-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border: 1px solid var(--vscode-panel-border); + border-radius: 8px; + margin-bottom: 16px; + background-color: var(--vscode-editor-background); + transition: all 0.2s ease; + } + + .mcp-server-item:hover { + border-color: var(--vscode-focusBorder); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + .server-info { + flex: 1; + } + + .server-name { + font-weight: 600; + font-size: 16px; + color: var(--vscode-foreground); + margin-bottom: 8px; + } + + .server-type { + display: inline-block; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; + margin-bottom: 8px; + } + + .server-config { + font-size: 13px; + color: var(--vscode-descriptionForeground); + opacity: 0.9; + line-height: 1.4; + } + + .server-delete-btn { + padding: 8px 16px; + font-size: 13px; + color: var(--vscode-errorForeground); + border-color: var(--vscode-errorForeground); + min-width: 80px; + justify-content: center; + } + + .server-delete-btn:hover { + background-color: var(--vscode-inputValidation-errorBackground); + border-color: var(--vscode-errorForeground); + } + + .server-actions { + display: flex; + gap: 8px; + align-items: center; + flex-shrink: 0; + } + + .server-edit-btn { + padding: 8px 16px; + font-size: 13px; + color: var(--vscode-foreground); + border-color: var(--vscode-panel-border); + min-width: 80px; + transition: all 0.2s ease; + justify-content: center; + } + + .server-edit-btn:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .mcp-add-server { + text-align: center; + margin-bottom: 24px; + padding: 0 4px; + } + + .mcp-add-form { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 8px; + padding: 24px; + margin-top: 20px; + box-sizing: border-box; + width: 100%; + } + + .form-group { + margin-bottom: 20px; + box-sizing: border-box; + width: 100%; + } + + .form-group label { + display: block; + margin-bottom: 6px; + font-weight: 500; + font-size: 13px; + color: var(--vscode-foreground); + } + + .form-group input, + .form-group select, + .form-group textarea { + width: 100%; + max-width: 100%; + padding: 8px 12px; + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-size: 13px; + font-family: var(--vscode-font-family); + box-sizing: border-box; + resize: vertical; + } + + .form-group input:focus, + .form-group select:focus, + .form-group textarea:focus { + outline: none; + border-color: var(--vscode-focusBorder); + box-shadow: 0 0 0 1px var(--vscode-focusBorder); + } + + .form-group textarea { + resize: vertical; + min-height: 60px; + } + + .form-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 20px; + } + + .no-servers { + text-align: center; + color: var(--vscode-descriptionForeground); + font-style: italic; + padding: 40px 20px; + } + + /* Popular MCP Servers */ + .mcp-popular-servers { + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid var(--vscode-panel-border); + } + + .mcp-popular-servers h4 { + margin: 0 0 16px 0; + font-size: 14px; + font-weight: 600; + color: var(--vscode-foreground); + opacity: 0.9; + } + + .popular-servers-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; + } + + .popular-server-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + } + + .popular-server-item:hover { + border-color: var(--vscode-focusBorder); + background-color: var(--vscode-list-hoverBackground); + transform: translateY(-1px); + } + + .popular-server-icon { + font-size: 24px; + flex-shrink: 0; + } + + .popular-server-info { + flex: 1; + min-width: 0; + } + + .popular-server-name { + font-weight: 600; + font-size: 13px; + color: var(--vscode-foreground); + margin-bottom: 2px; + } + + .popular-server-desc { + font-size: 11px; + color: var(--vscode-descriptionForeground); + opacity: 0.8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .yolo-warning { + font-size: 12px; + color: var(--vscode-foreground); + text-align: center; + font-weight: 500; + background-color: rgba(255, 99, 71, 0.08); + border: 1px solid rgba(255, 99, 71, 0.2); + padding: 8px 12px; + margin: 4px 4px; + border-radius: 4px; + animation: slideDown 0.3s ease; + } + + .yolo-suggestion { + margin-top: 12px; + padding: 12px; + background-color: rgba(0, 122, 204, 0.1); + border: 1px solid rgba(0, 122, 204, 0.3); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .yolo-suggestion-text { + font-size: 12px; + color: var(--vscode-foreground); + flex-grow: 1; + } + + .yolo-suggestion-btn { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 4px; + padding: 6px 12px; + font-size: 11px; + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: 500; + flex-shrink: 0; + } + + .yolo-suggestion-btn:hover { + background-color: var(--vscode-button-hoverBackground); + } +`; \ No newline at end of file diff --git a/src/styles/modal-styles.ts b/src/styles/modal-styles.ts new file mode 100644 index 0000000..23c6e89 --- /dev/null +++ b/src/styles/modal-styles.ts @@ -0,0 +1,721 @@ +export const modalStyles = ` + .file-picker-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + } + + .file-picker-content { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + width: 400px; + max-height: 500px; + display: flex; + flex-direction: column; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + .file-picker-header { + padding: 12px; + border-bottom: 1px solid var(--vscode-panel-border); + display: flex; + flex-direction: column; + gap: 8px; + } + + .file-picker-header span { + font-weight: 500; + color: var(--vscode-foreground); + } + + .file-search-input { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + padding: 6px 8px; + border-radius: 3px; + outline: none; + font-size: 13px; + } + + .file-search-input:focus { + border-color: var(--vscode-focusBorder); + } + + .file-list { + max-height: 400px; + overflow-y: auto; + padding: 4px; + } + + .file-item { + display: flex; + align-items: center; + padding: 8px 12px; + cursor: pointer; + border-radius: 3px; + font-size: 13px; + gap: 8px; + } + + .file-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .file-item.selected { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + } + + .file-icon { + font-size: 16px; + flex-shrink: 0; + } + + .file-info { + flex: 1; + display: flex; + flex-direction: column; + } + + .file-name { + font-weight: 500; + } + + .file-path { + font-size: 11px; + color: var(--vscode-descriptionForeground); + } + + .file-thumbnail { + width: 32px; + height: 32px; + border-radius: 4px; + overflow: hidden; + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .thumbnail-img { + max-width: 100%; + max-height: 100%; + object-fit: cover; + } + + .tools-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + } + + .tools-modal-content { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 8px; + width: 700px; + max-width: 90vw; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + overflow: hidden; + } + + .tools-modal-header { + padding: 16px 20px; + border-bottom: 1px solid var(--vscode-panel-border); + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + } + + .tools-modal-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + } + + .tools-modal-header span { + font-weight: 600; + font-size: 14px; + color: var(--vscode-foreground); + } + + .tools-close-btn { + background: none; + border: none; + color: var(--vscode-foreground); + cursor: pointer; + font-size: 16px; + padding: 4px; + } + + .tools-beta-warning { + padding: 12px 16px; + background-color: var(--vscode-notifications-warningBackground); + color: var(--vscode-notifications-warningForeground); + font-size: 12px; + border-bottom: 1px solid var(--vscode-panel-border); + } + + .tools-list { + padding: 20px; + max-height: 400px; + overflow-y: auto; + } + + .tool-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px 0; + cursor: pointer; + border-radius: 6px; + transition: background-color 0.2s ease; + } + + .tool-item:last-child { + border-bottom: none; + } + + .tool-item:hover { + background-color: var(--vscode-list-hoverBackground); + padding: 16px 12px; + margin: 0 -12px; + } + + .tool-item input[type="checkbox"], + .tool-item input[type="radio"] { + margin: 0; + margin-top: 2px; + flex-shrink: 0; + } + + .tool-item label { + color: var(--vscode-foreground); + font-size: 13px; + cursor: pointer; + flex: 1; + line-height: 1.4; + } + + .tool-item input[type="checkbox"]:disabled + label { + opacity: 0.7; + } + + /* Model selection specific styles */ + .model-explanatory-text { + padding: 20px; + padding-bottom: 0px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + line-height: 1.4; + } + + .model-title { + font-weight: 600; + margin-bottom: 4px; + } + + .model-description { + font-size: 11px; + color: var(--vscode-descriptionForeground); + line-height: 1.3; + } + + .model-option-content { + flex: 1; + } + + .default-model-layout { + cursor: pointer; + display: flex; + align-items: flex-start; + justify-content: space-between; + width: 100%; + } + + .configure-button { + margin-left: 12px; + flex-shrink: 0; + align-self: flex-start; + } + + /* Thinking intensity slider */ + .thinking-slider-container { + position: relative; + padding: 0px 16px; + margin: 12px 0; + } + + .thinking-slider { + width: 100%; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: var(--vscode-panel-border); + outline: none !important; + border: none; + cursor: pointer; + border-radius: 2px; + } + + .thinking-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + background: var(--vscode-foreground); + cursor: pointer; + border-radius: 50%; + transition: transform 0.2s ease; + } + + .thinking-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); + } + + .thinking-slider::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--vscode-foreground); + cursor: pointer; + border-radius: 50%; + border: none; + transition: transform 0.2s ease; + } + + .thinking-slider::-moz-range-thumb:hover { + transform: scale(1.2); + } + + .slider-labels { + display: flex; + justify-content: space-between; + margin-top: 12px; + padding: 0 8px; + } + + .slider-label { + font-size: 12px; + color: var(--vscode-descriptionForeground); + opacity: 0.7; + transition: all 0.2s ease; + text-align: center; + width: 100px; + cursor: pointer; + } + + .slider-label:hover { + opacity: 1; + color: var(--vscode-foreground); + } + + .slider-label.active { + opacity: 1; + color: var(--vscode-foreground); + font-weight: 500; + } + + .slider-label:first-child { + margin-left: -50px; + } + + .slider-label:last-child { + margin-right: -50px; + } + + .settings-group { + padding-bottom: 20px; + margin-bottom: 40px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + + .settings-group h3 { + margin: 0 0 12px 0; + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); + } + + /* Thinking intensity modal */ + .thinking-modal-description { + padding: 0px 20px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + line-height: 1.5; + text-align: center; + margin: 20px; + margin-bottom: 0px; + } + + .thinking-modal-actions { + padding-top: 20px; + text-align: right; + border-top: 1px solid var(--vscode-widget-border); + } + + .confirm-btn { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: 1px solid var(--vscode-panel-border); + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 400; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 5px; + } + + .confirm-btn:hover { + background-color: var(--vscode-button-background); + border-color: var(--vscode-focusBorder); + } + + /* Slash commands modal */ + .slash-commands-search { + padding: 16px 20px; + border-bottom: 1px solid var(--vscode-panel-border); + position: sticky; + top: 0; + background-color: var(--vscode-editor-background); + z-index: 10; + } + + .search-input-wrapper { + display: flex; + align-items: center; + border: 1px solid var(--vscode-input-border); + border-radius: 6px; + background-color: var(--vscode-input-background); + transition: all 0.2s ease; + position: relative; + } + + .search-input-wrapper:focus-within { + border-color: var(--vscode-focusBorder); + box-shadow: 0 0 0 1px var(--vscode-focusBorder); + } + + .search-prefix { + display: flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + font-size: 13px; + font-weight: 600; + border-radius: 4px 0 0 4px; + border-right: 1px solid var(--vscode-input-border); + } + + .slash-commands-search input { + flex: 1; + padding: 8px 12px; + border: none !important; + background: transparent; + color: var(--vscode-input-foreground); + font-size: 13px; + outline: none !important; + box-shadow: none !important; + } + + .slash-commands-search input:focus { + border: none !important; + outline: none !important; + box-shadow: none !important; + } + + .slash-commands-search input::placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .command-input-wrapper { + display: flex; + align-items: center; + border: 1px solid var(--vscode-input-border); + border-radius: 6px; + background-color: var(--vscode-input-background); + transition: all 0.2s ease; + width: 100%; + position: relative; + } + + .command-input-wrapper:focus-within { + border-color: var(--vscode-focusBorder); + box-shadow: 0 0 0 1px var(--vscode-focusBorder); + } + + .command-prefix { + display: flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + font-size: 12px; + font-weight: 600; + border-radius: 4px 0 0 4px; + border-right: 1px solid var(--vscode-input-border); + } + + .slash-commands-section { + margin-bottom: 32px; + } + + .slash-commands-section:last-child { + margin-bottom: 16px; + } + + .slash-commands-section h3 { + margin: 16px 20px 12px 20px; + font-size: 14px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .slash-commands-info { + padding: 12px 20px; + background-color: rgba(255, 149, 0, 0.1); + border: 1px solid rgba(255, 149, 0, 0.2); + border-radius: 4px; + margin: 0 20px 16px 20px; + } + + .slash-commands-info p { + margin: 0; + font-size: 11px; + color: var(--vscode-descriptionForeground); + text-align: center; + opacity: 0.9; + } + + .prompt-snippet-item { + border-left: 2px solid var(--vscode-charts-blue); + background-color: rgba(0, 122, 204, 0.03); + } + + .prompt-snippet-item:hover { + background-color: rgba(0, 122, 204, 0.08); + } + + .add-snippet-item { + border-left: 2px solid var(--vscode-charts-green); + background-color: rgba(0, 200, 83, 0.03); + border-style: dashed; + } + + .add-snippet-item:hover { + background-color: rgba(0, 200, 83, 0.08); + border-style: solid; + } + + .add-snippet-form { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 16px; + margin: 8px 0; + animation: slideDown 0.2s ease; + } + + .add-snippet-form .form-group { + margin-bottom: 12px; + } + + .add-snippet-form label { + display: block; + margin-bottom: 4px; + font-weight: 500; + font-size: 12px; + color: var(--vscode-foreground); + } + + .add-snippet-form textarea { + width: 100%; + padding: 6px 8px; + border: 1px solid var(--vscode-input-border); + border-radius: 3px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-size: 12px; + font-family: var(--vscode-font-family); + box-sizing: border-box; + } + + .add-snippet-form .command-input-wrapper input { + flex: 1; + padding: 6px 8px; + border: none !important; + background: transparent; + color: var(--vscode-input-foreground); + font-size: 12px; + font-family: var(--vscode-font-family); + outline: none !important; + box-shadow: none !important; + } + + .add-snippet-form .command-input-wrapper input:focus { + border: none !important; + outline: none !important; + box-shadow: none !important; + } + + .add-snippet-form textarea:focus { + outline: none; + border-color: var(--vscode-focusBorder); + } + + .add-snippet-form input::placeholder, + .add-snippet-form textarea::placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .add-snippet-form textarea { + resize: vertical; + min-height: 60px; + } + + .add-snippet-form .form-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 12px; + } + + .custom-snippet-item { + position: relative; + } + + .snippet-actions { + display: flex; + align-items: center; + opacity: 0; + transition: opacity 0.2s ease; + margin-left: 8px; + } + + .custom-snippet-item:hover .snippet-actions { + opacity: 1; + } + + .snippet-delete-btn { + background: none; + border: none; + color: var(--vscode-descriptionForeground); + cursor: pointer; + padding: 4px; + border-radius: 3px; + font-size: 12px; + transition: all 0.2s ease; + opacity: 0.7; + } + + .snippet-delete-btn:hover { + background-color: rgba(231, 76, 60, 0.1); + color: var(--vscode-errorForeground); + opacity: 1; + } + + .slash-commands-list { + display: grid; + gap: 6px; + padding: 0 20px; + } + + .slash-command-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; + border: 1px solid transparent; + background-color: transparent; + } + + .slash-command-item:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-list-hoverBackground); + } + + .slash-command-icon { + font-size: 16px; + min-width: 20px; + text-align: center; + opacity: 0.8; + } + + .slash-command-content { + flex: 1; + } + + .slash-command-title { + font-size: 13px; + font-weight: 500; + color: var(--vscode-foreground); + margin-bottom: 2px; + } + + .slash-command-description { + font-size: 11px; + color: var(--vscode-descriptionForeground); + opacity: 0.7; + line-height: 1.3; + } + + /* Quick command input */ + .custom-command-item { + cursor: default; + } + + .custom-command-item .command-input-wrapper { + margin-top: 4px; + max-width: 200px; + } + + .secondary-button { + background-color: var(--vscode-button-secondaryBackground, rgba(128, 128, 128, 0.2)); + color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); + border: 1px solid var(--vscode-panel-border); + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 500; + transition: all 0.2s ease; + white-space: nowrap; + } + + .secondary-button:hover { + background-color: var(--vscode-button-secondaryHoverBackground, rgba(128, 128, 128, 0.3)); + border-color: var(--vscode-focusBorder); + } +`; \ No newline at end of file diff --git a/src/styles/permission-styles.ts b/src/styles/permission-styles.ts new file mode 100644 index 0000000..7f3426a --- /dev/null +++ b/src/styles/permission-styles.ts @@ -0,0 +1,623 @@ +export const permissionStyles = ` + /* Permission Request */ + .permission-request { + margin: 8px 16px; + background-color: #242424; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + padding: 12px; + animation: slideUp 0.2s ease; + } + + .permission-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; + font-weight: 400; + color: #cccccc; + font-size: 12px; + } + + .permission-header .icon { + font-size: 12px; + opacity: 0.6; + } + + .permission-menu { + position: relative; + margin-left: auto; + } + + .permission-menu-btn { + background: none; + border: none; + color: var(--vscode-descriptionForeground); + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + font-size: 16px; + font-weight: bold; + transition: all 0.2s ease; + line-height: 1; + } + + .permission-menu-btn:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-foreground); + } + + .permission-menu-dropdown { + position: absolute; + top: 100%; + right: 0; + background-color: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + min-width: 220px; + padding: 4px 0; + margin-top: 4px; + } + + .permission-menu-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 16px; + background: none; + border: none; + width: 100%; + text-align: left; + cursor: pointer; + color: var(--vscode-foreground); + transition: background-color 0.2s ease; + } + + .permission-menu-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .permission-menu-item .menu-icon { + font-size: 16px; + margin-top: 1px; + flex-shrink: 0; + } + + .permission-menu-item .menu-content { + display: flex; + flex-direction: column; + gap: 2px; + } + + .permission-menu-item .menu-title { + font-weight: 500; + font-size: 13px; + line-height: 1.2; + } + + .permission-menu-item .menu-subtitle { + font-size: 11px; + color: var(--vscode-descriptionForeground); + opacity: 0.8; + line-height: 1.2; + } + + .permission-content { + font-size: 11px; + line-height: 1.4; + color: #808080; + } + + .permission-tool { + font-family: var(--vscode-editor-font-family); + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + padding: 8px 10px; + margin: 8px 0; + font-size: 12px; + color: var(--vscode-editor-foreground); + } + + .permission-buttons { + margin-top: 2px; + display: flex; + gap: 8px; + justify-content: flex-end; + flex-wrap: wrap; + } + + .permission-buttons .btn { + font-size: 12px; + padding: 6px 12px; + min-width: 70px; + text-align: center; + display: inline-flex; + align-items: center; + justify-content: center; + height: 28px; + border-radius: 4px; + border: 1px solid; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + box-sizing: border-box; + } + + .permission-buttons .btn.allow { + background-color: rgba(255, 255, 255, 0.08); + color: #cccccc; + border: 1px solid rgba(255, 255, 255, 0.12); + } + + .permission-buttons .btn.allow:hover { + background-color: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.16); + } + + .permission-buttons .btn.deny { + background-color: transparent; + color: #808080; + border: 1px solid rgba(255, 255, 255, 0.08); + } + + .permission-buttons .btn.deny:hover { + background-color: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.12); + color: #999999; + } + + .permission-buttons .btn.always-allow { + background-color: rgba(0, 122, 204, 0.1); + color: var(--vscode-charts-blue); + border-color: rgba(0, 122, 204, 0.3); + font-weight: 500; + min-width: auto; + padding: 6px 14px; + height: 28px; + } + + .permission-buttons .btn.always-allow:hover { + background-color: rgba(0, 122, 204, 0.2); + border-color: rgba(0, 122, 204, 0.5); + transform: translateY(-1px); + } + + .permission-buttons .btn.always-allow code { + background-color: rgba(0, 0, 0, 0.2); + padding: 2px 4px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + font-size: 11px; + color: var(--vscode-editor-foreground); + margin-left: 4px; + display: inline; + line-height: 1; + vertical-align: baseline; + } + + .permission-decision { + font-size: 13px; + font-weight: 600; + padding: 8px 12px; + text-align: center; + border-radius: 4px; + margin-top: 8px; + } + + .permission-decision.allowed { + background-color: rgba(0, 122, 204, 0.15); + color: var(--vscode-charts-blue); + border: 1px solid rgba(0, 122, 204, 0.3); + } + + .permission-decision.denied { + background-color: rgba(231, 76, 60, 0.15); + color: #e74c3c; + border: 1px solid rgba(231, 76, 60, 0.3); + } + + .permission-decided { + opacity: 0.7; + pointer-events: none; + } + + .permission-decided .permission-buttons { + display: none; + } + + .permission-decided.allowed { + border-color: var(--vscode-inputValidation-infoBackground); + background-color: rgba(0, 122, 204, 0.1); + } + + .permission-decided.denied { + border-color: var(--vscode-inputValidation-errorBorder); + background-color: var(--vscode-inputValidation-errorBackground); + } + + /* Permissions Management */ + .permissions-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background-color: var(--vscode-input-background); + margin-top: 8px; + } + + .permission-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 6px; + padding-right: 6px; + border-bottom: 1px solid var(--vscode-panel-border); + transition: background-color 0.2s ease; + min-height: 32px; + } + + .permission-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .permission-item:last-child { + border-bottom: none; + } + + .permission-info { + display: flex; + align-items: center; + gap: 8px; + flex-grow: 1; + min-width: 0; + } + + .permission-command { + font-size: 12px; + color: var(--vscode-foreground); + flex-grow: 1; + } + + .permission-command code { + background-color: var(--vscode-textCodeBlock-background); + padding: 3px 6px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + color: var(--vscode-textLink-foreground); + font-size: 11px; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-desc { + color: var(--vscode-descriptionForeground); + font-size: 11px; + font-style: italic; + flex-grow: 1; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-remove-btn { + background-color: transparent; + color: var(--vscode-descriptionForeground); + border: none; + padding: 4px 8px; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + transition: all 0.2s ease; + font-weight: 500; + flex-shrink: 0; + opacity: 0.7; + } + + .permission-remove-btn:hover { + background-color: rgba(231, 76, 60, 0.1); + color: var(--vscode-errorForeground); + opacity: 1; + } + + .permissions-empty { + padding: 16px; + text-align: center; + color: var(--vscode-descriptionForeground); + font-style: italic; + font-size: 13px; + } + + .permissions-empty::before { + display: none; + } + + /* Add Permission Form */ + .permissions-add-section { + margin-top: 6px; + } + + .permissions-show-add-btn { + background-color: transparent; + color: var(--vscode-descriptionForeground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 6px 8px; + font-size: 11px; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 400; + opacity: 0.7; + } + + .permissions-show-add-btn:hover { + background-color: var(--vscode-list-hoverBackground); + opacity: 1; + } + + .permissions-add-form { + margin-top: 8px; + padding: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background-color: var(--vscode-input-background); + animation: slideDown 0.2s ease; + } + + .permissions-form-row { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 8px; + } + + .permissions-tool-select { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 4px 8px; + font-size: 12px; + min-width: 100px; + height: 24px; + flex-shrink: 0; + } + + .permissions-command-input { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 4px 8px; + font-size: 12px; + flex-grow: 1; + height: 24px; + font-family: var(--vscode-editor-font-family); + } + + .permissions-command-input::placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .permissions-add-btn { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 3px; + padding: 4px 12px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s ease; + height: 24px; + font-weight: 500; + flex-shrink: 0; + } + + .permissions-add-btn:hover { + background-color: var(--vscode-button-hoverBackground); + } + + .permissions-add-btn:disabled { + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + cursor: not-allowed; + opacity: 0.5; + } + + .permissions-cancel-btn { + background-color: transparent; + color: var(--vscode-foreground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 4px 12px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; + height: 24px; + font-weight: 500; + } + + .permissions-cancel-btn:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .permissions-form-hint { + font-size: 11px; + color: var(--vscode-descriptionForeground); + font-style: italic; + line-height: 1.3; + } + + .yolo-mode-section { + display: flex; + align-items: center; + gap: 6px; + margin-top: 12px; + opacity: 1; + transition: opacity 0.2s ease; + } + + .yolo-mode-section:hover { + opacity: 1; + } + + .yolo-mode-section input[type="checkbox"] { + transform: scale(0.9); + margin: 0; + } + + .yolo-mode-section label { + font-size: 12px; + color: var(--vscode-descriptionForeground); + cursor: pointer; + font-weight: 400; + } + + /* Minimal Permission Request */ + .permission-request-minimal { + margin: 3px 16px; + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(100, 149, 237, 0.4); + border-radius: 4px; + padding: 6px 10px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + animation: slideUp 0.15s ease; + } + + .permission-minimal-content { + display: flex; + flex-direction: column; + gap: 6px; + } + + .permission-minimal-buttons { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 6px; + align-self: flex-end; + width: 100%; + } + + .btn-minimal { + font-size: 11px; + padding: 4px 8px; + border-radius: 3px; + border: 1px solid; + cursor: pointer; + transition: all 0.15s ease; + background: transparent; + font-weight: 400; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .btn-minimal.deny { + color: var(--vscode-descriptionForeground); + border-color: rgba(255, 255, 255, 0.08); + } + + .btn-minimal.deny:hover { + background-color: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.12); + } + + .btn-minimal.allow { + color: var(--vscode-foreground); + border-color: rgba(255, 255, 255, 0.12); + background-color: rgba(255, 255, 255, 0.05); + } + + .btn-minimal.allow:hover { + background-color: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.16); + } + + .allow-button-group { + position: relative; + display: flex; + align-items: center; + } + + .allow-dropdown-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.12); + border-left: none; + border-radius: 0 3px 3px 0; + padding: 4px 3px; + cursor: pointer; + color: var(--vscode-descriptionForeground); + height: 22px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; + margin-left: -1px; + } + + .allow-dropdown-btn:hover { + background-color: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.16); + color: var(--vscode-foreground); + } + + .allow-button-group .btn-minimal.allow { + border-radius: 3px 0 0 3px; + } + + .always-allow-dropdown { + position: absolute; + bottom: 100%; + right: 0; + background-color: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); + border-radius: 3px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + z-index: 1000; + margin-bottom: 2px; + } + + .always-allow-option { + display: block; + width: 100%; + padding: 6px 10px; + background: none; + border: none; + text-align: left; + cursor: pointer; + color: var(--vscode-foreground); + font-size: 11px; + transition: background-color 0.15s ease; + white-space: nowrap; + } + + .always-allow-option:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .permission-minimal-decision { + font-size: 11px; + font-weight: 500; + padding: 4px 8px; + border-radius: 3px; + } + + .permission-minimal-decision.allowed { + color: var(--vscode-charts-blue); + background-color: rgba(0, 122, 204, 0.1); + } + + .permission-minimal-decision.denied { + color: #e74c3c; + background-color: rgba(231, 76, 60, 0.1); + } +`; \ No newline at end of file diff --git a/src/styles/tool-styles.ts b/src/styles/tool-styles.ts new file mode 100644 index 0000000..1ae8b0c --- /dev/null +++ b/src/styles/tool-styles.ts @@ -0,0 +1,76 @@ +export const toolStyles = ` + .tool-icon { + width: 14px; + height: 14px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + color: #999999; + font-weight: 400; + flex-shrink: 0; + margin-left: 0; + } + + .tool-info { + font-weight: 400; + font-size: 11px; + color: #999999; + opacity: 0.8; + cursor: default; /* Ensure tool name is not clickable */ + } + + .clickable-filename { + cursor: pointer; + color: var(--vscode-textLink-foreground); + text-decoration: underline; + text-decoration-color: transparent; + transition: all 0.2s ease; + border-radius: 2px; + padding: 1px 2px; + } + + .clickable-filename:hover { + color: var(--vscode-textLink-activeForeground); + text-decoration-color: currentColor; + background-color: var(--vscode-textLink-foreground); + color: var(--vscode-editor-background); + } + + .tool-input { + padding: 0; + font-family: var(--vscode-editor-font-family); + font-size: 12px; + line-height: 1.4; + white-space: pre-line; + } + + .tool-input-label { + color: var(--vscode-descriptionForeground); + font-size: 11px; + font-weight: 500; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .tool-input-content { + color: var(--vscode-editor-foreground); + opacity: 0.95; + } + + /* Todo list styling */ + .tool-input-content span { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 12px; + } + + /* Completed todo strikethrough positioning */ + .tool-input-content span[style*="line-through"] { + text-decoration-thickness: 1px; + text-underline-offset: 0.2em; + text-decoration-skip-ink: none; + } +`; \ No newline at end of file diff --git a/src/ui-styles.ts b/src/ui-styles.ts index 45f0034..18dd992 100644 --- a/src/ui-styles.ts +++ b/src/ui-styles.ts @@ -1,3336 +1,3 @@ -const styles = ` -`; - -export default styles; \ No newline at end of file +export default combinedStyles; \ No newline at end of file From 4173da2d8f73ed51922df1d0c61a735e6d8e3753 Mon Sep 17 00:00:00 2001 From: ahmed al mihy Date: Tue, 2 Sep 2025 01:53:05 +0300 Subject: [PATCH 15/17] Add WSL alert and Yolo mode warning to UI components - Introduced a new file `ui-notifications.ts` to encapsulate HTML for WSL alert and Yolo mode warning. - Removed inline HTML for WSL alert and Yolo mode from `ui.ts` and replaced it with a call to `getNotificationsHtml()` from the new component. --- .claude/settings.local.json | 3 +- src/styles/input-styles.ts | 12 + src/ui-components/index.ts | 20 + src/ui-components/ui-chat.ts | 5 + src/ui-components/ui-core.ts | 28 + src/ui-components/ui-header.ts | 17 + src/ui-components/ui-input.ts | 80 +++ src/ui-components/ui-modals.ts | 562 ++++++++++++++++++++ src/ui-components/ui-notifications.ts | 20 + src/ui.ts | 718 +------------------------- 10 files changed, 747 insertions(+), 718 deletions(-) create mode 100644 src/ui-components/index.ts create mode 100644 src/ui-components/ui-chat.ts create mode 100644 src/ui-components/ui-core.ts create mode 100644 src/ui-components/ui-header.ts create mode 100644 src/ui-components/ui-input.ts create mode 100644 src/ui-components/ui-modals.ts create mode 100644 src/ui-components/ui-notifications.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c36b7a7..3657de0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,8 @@ "Bash(npm run lint)", "Bash(cat:*)", "WebSearch", - "Bash(npm run lint:*)" + "Bash(npm run lint:*)", + "Bash(mkdir:*)" ], "deny": [] }, diff --git a/src/styles/input-styles.ts b/src/styles/input-styles.ts index 83dc825..481b5ad 100644 --- a/src/styles/input-styles.ts +++ b/src/styles/input-styles.ts @@ -125,12 +125,24 @@ export const inputStyles = ` padding: 4px 6px; border-top: 1px solid rgba(255, 255, 255, 0.03); background-color: transparent; + flex-wrap: nowrap; + min-height: 32px; } .left-controls { display: flex; align-items: center; gap: 8px; + flex-wrap: nowrap; + flex-shrink: 0; + } + + .right-controls { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: nowrap; + flex-shrink: 0; } .model-selector { diff --git a/src/ui-components/index.ts b/src/ui-components/index.ts new file mode 100644 index 0000000..cd03fec --- /dev/null +++ b/src/ui-components/index.ts @@ -0,0 +1,20 @@ +import { getCoreTemplate } from './ui-core'; +import { getHeaderHtml } from './ui-header'; +import { getChatContainerHtml } from './ui-chat'; +import { getInputControlsHtml } from './ui-input'; +import { getNotificationsHtml } from './ui-notifications'; +import { getModalsHtml } from './ui-modals'; + +export const getHtml = (isTelemetryEnabled: boolean) => { + const bodyContent = ` + ${getHeaderHtml()} + ${getChatContainerHtml()} + ${getNotificationsHtml()} + ${getInputControlsHtml()} + ${getModalsHtml()} + `; + + return getCoreTemplate(isTelemetryEnabled, bodyContent); +}; + +export default getHtml; \ No newline at end of file diff --git a/src/ui-components/ui-chat.ts b/src/ui-components/ui-chat.ts new file mode 100644 index 0000000..d42ee5c --- /dev/null +++ b/src/ui-components/ui-chat.ts @@ -0,0 +1,5 @@ +export const getChatContainerHtml = () => ` +
+
+
+`; \ No newline at end of file diff --git a/src/ui-components/ui-core.ts b/src/ui-components/ui-core.ts new file mode 100644 index 0000000..339fc85 --- /dev/null +++ b/src/ui-components/ui-core.ts @@ -0,0 +1,28 @@ +import getScript from '../script'; +import styles from '../ui-styles'; + +export const getCoreTemplate = (isTelemetryEnabled: boolean, bodyContent: string) => ` + + + + + Claude Code Chat + ${styles} + + + ${bodyContent} + + ${getScript(isTelemetryEnabled)} + + + ${isTelemetryEnabled ? '' : ''} + +`; \ No newline at end of file diff --git a/src/ui-components/ui-header.ts b/src/ui-components/ui-header.ts new file mode 100644 index 0000000..b639a4f --- /dev/null +++ b/src/ui-components/ui-header.ts @@ -0,0 +1,17 @@ +export const getHeaderHtml = () => ` +
+
+

Claude Code Chat

+ +
+
+ + + + +
+
+`; \ No newline at end of file diff --git a/src/ui-components/ui-input.ts b/src/ui-components/ui-input.ts new file mode 100644 index 0000000..fb06e34 --- /dev/null +++ b/src/ui-components/ui-input.ts @@ -0,0 +1,80 @@ +export const getInputControlsHtml = () => ` +
+
+
+
+ Plan First +
+
+
+ Thinking Mode +
+
+
+
+
+
Initializing...
+
+
+
+
+ +
+
+ + +
+
+ + + + +
+
+
+
+
+`; \ No newline at end of file diff --git a/src/ui-components/ui-modals.ts b/src/ui-components/ui-modals.ts new file mode 100644 index 0000000..1d989e3 --- /dev/null +++ b/src/ui-components/ui-modals.ts @@ -0,0 +1,562 @@ +export const getModalsHtml = () => ` + + + + + + + + + + + + + + + + +