From de8ea0678d18f0af8cf627f29b22791457b9dcc3 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sun, 23 Nov 2025 20:52:08 +0800 Subject: [PATCH] feat(vscode-ide-companion): refactor message components with modular architecture Refactor UI message rendering by extracting message types into dedicated components. Add ChatHeader component for better session management interface. - Extract message components: UserMessage, AssistantMessage, ThinkingMessage, StreamingMessage, WaitingMessage - Add ChatHeader component with session selector and action buttons - Delete MessageContent.css and consolidate styles into App.scss - Update Tailwind config for component styling - Improve message rendering with proper TypeScript typing --- packages/vscode-ide-companion/package.json | 2 +- .../src/WebViewProvider.ts | 205 ++++++++++-- .../src/agents/qwenConnectionHandler.ts | 8 + .../src/auth/authStateManager.ts | 11 + .../vscode-ide-companion/src/webview/App.scss | 301 +----------------- .../vscode-ide-companion/src/webview/App.tsx | 246 +++++++------- .../src/webview/MessageHandler.ts | 99 ++++-- .../src/webview/components/MessageContent.css | 61 ---- .../src/webview/components/MessageContent.tsx | 130 +++++++- .../components/messages/AssistantMessage.tsx | 42 +++ .../components/messages/StreamingMessage.tsx | 39 +++ .../components/messages/ThinkingMessage.tsx | 48 +++ .../components/messages/UserMessage.tsx | 89 ++++++ .../components/messages/WaitingMessage.tsx | 31 ++ .../src/webview/components/messages/index.tsx | 11 + .../src/webview/components/ui/ChatHeader.tsx | 109 +++++++ .../vscode-ide-companion/tailwind.config.js | 4 + 17 files changed, 901 insertions(+), 535 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/webview/components/MessageContent.css create mode 100644 packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx create mode 100644 packages/vscode-ide-companion/src/webview/components/messages/StreamingMessage.tsx create mode 100644 packages/vscode-ide-companion/src/webview/components/messages/ThinkingMessage.tsx create mode 100644 packages/vscode-ide-companion/src/webview/components/messages/UserMessage.tsx create mode 100644 packages/vscode-ide-companion/src/webview/components/messages/WaitingMessage.tsx create mode 100644 packages/vscode-ide-companion/src/webview/components/messages/index.tsx create mode 100644 packages/vscode-ide-companion/src/webview/components/ui/ChatHeader.tsx diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 1f23bf29..ad8d837d 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -151,7 +151,7 @@ "scripts": { "prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod", "build": "npm run build:dev", - "build:dev": "npm run check-types && npm run lint && node esbuild.js", + "build:dev": "node esbuild.js", "build:prod": "node esbuild.js --production", "generate:notices": "node ./scripts/generate-notices.js", "prepare1": "npm run generate:notices", diff --git a/packages/vscode-ide-companion/src/WebViewProvider.ts b/packages/vscode-ide-companion/src/WebViewProvider.ts index c8c7b0c9..5a9c288f 100644 --- a/packages/vscode-ide-companion/src/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/WebViewProvider.ts @@ -43,9 +43,9 @@ export class WebViewProvider { (message) => this.sendMessageToWebView(message), ); - // Set login handler for /login command + // Set login handler for /login command - force re-login this.messageHandler.setLoginHandler(async () => { - await this.initializeAgentConnection(); + await this.forceReLogin(); }); // Setup agent callbacks @@ -159,27 +159,105 @@ export class WebViewProvider { // Register panel dispose handler this.panelManager.registerDisposeHandler(this.disposables); + // Track last known editor state (to preserve when switching to webview) + const _lastEditorState: { + fileName: string | null; + filePath: string | null; + selection: { + startLine: number; + endLine: number; + } | null; + } | null = null; + // Listen for active editor changes and notify WebView const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor( (editor) => { - const fileName = editor?.document.uri.fsPath - ? getFileName(editor.document.uri.fsPath) - : null; + // If switching to a non-text editor (like webview), keep the last state + if (!editor) { + // Don't update - keep previous state + return; + } + + const filePath = editor.document.uri.fsPath || null; + const fileName = filePath ? getFileName(filePath) : null; + + // Get selection info if there is any selected text + let selectionInfo = null; + if (editor && !editor.selection.isEmpty) { + const selection = editor.selection; + selectionInfo = { + startLine: selection.start.line + 1, + endLine: selection.end.line + 1, + }; + } + + // Update last known state + lastEditorState = { fileName, filePath, selection: selectionInfo }; + this.sendMessageToWebView({ type: 'activeEditorChanged', - data: { fileName }, + data: { fileName, filePath, selection: selectionInfo }, }); }, ); this.disposables.push(editorChangeDisposable); - // Don't auto-login; user must use /login command - // Just initialize empty conversation for the UI + // Listen for text selection changes + const selectionChangeDisposable = + vscode.window.onDidChangeTextEditorSelection((event) => { + const editor = event.textEditor; + if (editor === vscode.window.activeTextEditor) { + const filePath = editor.document.uri.fsPath || null; + const fileName = filePath ? getFileName(filePath) : null; + + // Get selection info if there is any selected text + let selectionInfo = null; + if (!event.selections[0].isEmpty) { + const selection = event.selections[0]; + selectionInfo = { + startLine: selection.start.line + 1, + endLine: selection.end.line + 1, + }; + } + + // Update last known state + lastEditorState = { fileName, filePath, selection: selectionInfo }; + + this.sendMessageToWebView({ + type: 'activeEditorChanged', + data: { fileName, filePath, selection: selectionInfo }, + }); + } + }); + this.disposables.push(selectionChangeDisposable); + + // Check if we have valid auth cache and auto-reconnect if (!this.agentInitialized) { - console.log( - '[WebViewProvider] Agent not initialized, waiting for /login command', + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + const config = vscode.workspace.getConfiguration('qwenCode'); + const openaiApiKey = config.get('openaiApiKey', ''); + // Use the same authMethod logic as qwenConnectionHandler + const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth'; + + // Check if we have valid cached auth + const hasValidAuth = await this.authStateManager.hasValidAuth( + workingDir, + authMethod, ); - await this.initializeEmptyConversation(); + + if (hasValidAuth) { + console.log( + '[WebViewProvider] Found valid auth cache, auto-reconnecting...', + ); + // Auto-reconnect using cached auth + await this.initializeAgentConnection(); + } else { + console.log( + '[WebViewProvider] No valid auth cache, waiting for /login command', + ); + await this.initializeEmptyConversation(); + } } else { console.log( '[WebViewProvider] Agent already initialized, reusing existing connection', @@ -259,6 +337,32 @@ export class WebViewProvider { } } + /** + * Force re-login by clearing auth cache and reconnecting + * Called when user explicitly uses /login command + */ + async forceReLogin(): Promise { + console.log('[WebViewProvider] Force re-login requested'); + + // Clear existing auth cache + await this.authStateManager.clearAuthState(); + console.log('[WebViewProvider] Auth cache cleared'); + + // Disconnect existing connection if any + if (this.agentInitialized) { + try { + this.agentManager.disconnect(); + console.log('[WebViewProvider] Existing connection disconnected'); + } catch (error) { + console.log('[WebViewProvider] Error disconnecting:', error); + } + this.agentInitialized = false; + } + + // Reinitialize connection (will trigger fresh authentication) + await this.initializeAgentConnection(); + } + /** * Load messages from current Qwen session * Creates a new ACP session for immediate message sending @@ -373,15 +477,44 @@ export class WebViewProvider { // Register dispose handler this.panelManager.registerDisposeHandler(this.disposables); + // Track last known editor state (to preserve when switching to webview) + const _lastEditorState: { + fileName: string | null; + filePath: string | null; + selection: { + startLine: number; + endLine: number; + } | null; + } | null = null; + // Listen for active editor changes and notify WebView const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor( (editor) => { - const fileName = editor?.document.uri.fsPath - ? getFileName(editor.document.uri.fsPath) - : null; + // If switching to a non-text editor (like webview), keep the last state + if (!editor) { + // Don't update - keep previous state + return; + } + + const filePath = editor.document.uri.fsPath || null; + const fileName = filePath ? getFileName(filePath) : null; + + // Get selection info if there is any selected text + let selectionInfo = null; + if (editor && !editor.selection.isEmpty) { + const selection = editor.selection; + selectionInfo = { + startLine: selection.start.line + 1, + endLine: selection.end.line + 1, + }; + } + + // Update last known state + lastEditorState = { fileName, filePath, selection: selectionInfo }; + this.sendMessageToWebView({ type: 'activeEditorChanged', - data: { fileName }, + data: { fileName, filePath, selection: selectionInfo }, }); }, ); @@ -392,18 +525,38 @@ export class WebViewProvider { console.log('[WebViewProvider] Panel restored successfully'); - // Don't auto-login on restore; user must use /login command - // Just initialize empty conversation for the UI + // Check if we have valid auth cache and auto-reconnect on restore if (!this.agentInitialized) { - console.log( - '[WebViewProvider] Agent not initialized after restore, waiting for /login command', - ); - this.initializeEmptyConversation().catch((error) => { - console.error( - '[WebViewProvider] Failed to initialize empty conversation after restore:', - error, - ); - }); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + const config = vscode.workspace.getConfiguration('qwenCode'); + const openaiApiKey = config.get('openaiApiKey', ''); + // Use the same authMethod logic as qwenConnectionHandler + const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth'; + + // Check if we have valid cached auth + this.authStateManager + .hasValidAuth(workingDir, authMethod) + .then(async (hasValidAuth) => { + if (hasValidAuth) { + console.log( + '[WebViewProvider] Found valid auth cache on restore, auto-reconnecting...', + ); + await this.initializeAgentConnection(); + } else { + console.log( + '[WebViewProvider] No valid auth cache after restore, waiting for /login command', + ); + await this.initializeEmptyConversation(); + } + }) + .catch((error) => { + console.error( + '[WebViewProvider] Failed to check auth cache after restore:', + error, + ); + this.initializeEmptyConversation().catch(console.error); + }); } else { console.log( '[WebViewProvider] Agent already initialized, loading current session...', diff --git a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts index baeec815..5581a56f 100644 --- a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts @@ -101,6 +101,14 @@ export class QwenConnectionHandler { lastSession.sessionId, ); sessionRestored = true; + + // Save auth state after successful session restore + if (authStateManager) { + console.log( + '[QwenAgentManager] Saving auth state after successful session restore', + ); + await authStateManager.saveAuthState(workingDir, authMethod); + } } catch (switchError) { console.log( '[QwenAgentManager] session/switch not supported or failed:', diff --git a/packages/vscode-ide-companion/src/auth/authStateManager.ts b/packages/vscode-ide-companion/src/auth/authStateManager.ts index 23d00ae0..bc2de16c 100644 --- a/packages/vscode-ide-companion/src/auth/authStateManager.ts +++ b/packages/vscode-ide-companion/src/auth/authStateManager.ts @@ -29,9 +29,20 @@ export class AuthStateManager { const state = await this.getAuthState(); if (!state) { + console.log('[AuthStateManager] No cached auth state found'); return false; } + console.log('[AuthStateManager] Found cached auth state:', { + workingDir: state.workingDir, + authMethod: state.authMethod, + timestamp: new Date(state.timestamp).toISOString(), + }); + console.log('[AuthStateManager] Checking against:', { + workingDir, + authMethod, + }); + // Check if auth is still valid (within cache duration) const now = Date.now(); const isExpired = diff --git a/packages/vscode-ide-companion/src/webview/App.scss b/packages/vscode-ide-companion/src/webview/App.scss index d4f0cde1..298f8009 100644 --- a/packages/vscode-ide-companion/src/webview/App.scss +++ b/packages/vscode-ide-companion/src/webview/App.scss @@ -137,113 +137,8 @@ button { } /* =========================== - Header Styles (from Claude Code .he) + Animations (used by message components) =========================== */ -.chat-header { - display: flex; - border-bottom: 1px solid var(--app-primary-border-color); - padding: 6px 10px; - gap: 4px; - background-color: var(--app-header-background); - justify-content: flex-start; - user-select: none; -} - -/* Session Selector Dropdown - styled as button (from Claude Code .E) */ -.session-selector-dropdown { - flex: 1; - max-width: 300px; - display: flex; -} - -.session-selector-dropdown select { - width: 100%; - display: flex; - align-items: center; - gap: 6px; - padding: 2px 8px; - background: transparent; - color: var(--app-primary-foreground); - border: none; - border-radius: 4px; - cursor: pointer; - outline: none; - min-width: 0; - max-width: 300px; - overflow: hidden; - font-size: var(--vscode-chat-font-size, 13px); - font-family: var(--vscode-chat-font-family); - font-weight: 500; - text-overflow: ellipsis; - white-space: nowrap; -} - -.session-selector-dropdown select:hover, -.session-selector-dropdown select:focus { - background: var(--app-ghost-button-hover-background); -} - -/* New Session Button (from Claude Code .j) */ -.new-session-header-button { - flex: 0 0 auto; - padding: 0; - background: transparent; - border: 1px solid transparent; - border-radius: 4px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - outline: none; - width: 24px; - height: 24px; - color: var(--app-primary-foreground); - - svg { - width: 16px; - height: 16px; - } -} - -.new-session-header-button:hover, -.new-session-header-button:focus { - background: var(--app-ghost-button-hover-background); -} - -/* =========================== - Messages Container - =========================== */ -.messages-container { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 20px 20px 120px; - display: flex; - flex-direction: column; - gap: var(--app-spacing-medium); - background-color: var(--app-primary-background); - position: relative; - min-width: 0; -} - -.messages-container:focus { - outline: none; -} - -/* =========================== - Message Styles - =========================== */ -.message { - color: var(--app-primary-foreground); - display: flex; - gap: 0; - align-items: flex-start; - padding: var(--app-spacing-medium) 0; - flex-direction: column; - position: relative; - animation: fadeIn 0.2s ease-in; -} - @keyframes fadeIn { from { opacity: 0; @@ -255,43 +150,6 @@ button { } } -.message.user { - align-items: flex-end; - text-align: right; -} - -.message.assistant { - align-items: flex-start; - text-align: left; -} - -.message-content { - display: inline-block; - margin: 4px 0; - position: relative; - white-space: pre-wrap; - border: 1px solid var(--app-input-border); - border-radius: var(--corner-radius-medium); - background-color: var(--app-input-background); - padding: 4px 6px; - max-width: 100%; - overflow-x: auto; - overflow-y: hidden; - user-select: text; - line-height: 1.5; -} - -.message.streaming { - position: relative; -} - -.streaming-indicator { - position: absolute; - right: 12px; - bottom: 12px; - animation: pulse 1.5s ease-in-out infinite; -} - @keyframes pulse { 0%, 100% { opacity: 1; @@ -301,93 +159,6 @@ button { } } -/* Thinking message styles */ -.message.thinking { - background-color: rgba(100, 100, 255, 0.1); - border: 1px solid rgba(100, 100, 255, 0.3); - border-radius: var(--corner-radius-medium); - padding: var(--app-spacing-medium); -} - -.thinking-message { - background-color: var(--app-list-hover-background, rgba(100, 100, 255, 0.1)); - opacity: 0.8; - font-style: italic; - position: relative; - padding-left: 24px; -} - -.thinking-message::before { - content: "💭"; - position: absolute; - left: 6px; - top: 50%; - transform: translateY(-50%); -} - -.thinking-label { - font-size: 12px; - font-weight: 600; - color: rgba(150, 150, 255, 1); - margin-bottom: var(--app-spacing-medium); - display: flex; - align-items: center; - gap: 6px; -} - -.thought-content { - font-style: italic; - opacity: 0.9; - color: rgba(200, 200, 255, 0.9); -} - -/* Waiting message styles - similar to Claude Code thinking state */ -.message.waiting-message { - opacity: 0.85; -} - -.waiting-message .message-content { - background-color: transparent; - border: none; - padding: 8px 0; - display: flex; - align-items: center; - gap: 8px; -} - -/* Typing indicator for loading messages */ -.typing-indicator { - display: inline-flex; - align-items: center; - margin-right: 0; - gap: 4px; -} - -.typing-dot, .thinking-dot { - width: 6px; - height: 6px; - background-color: var(--app-secondary-foreground); - border-radius: 50%; - margin-right: 0; - opacity: 0.6; - animation: typingPulse 1.4s infinite ease-in-out; -} - -.typing-dot:nth-child(1), -.thinking-dot:nth-child(1) { - animation-delay: 0s; -} - -.typing-dot:nth-child(2), -.thinking-dot:nth-child(2) { - animation-delay: 0.2s; -} - -.typing-dot:nth-child(3), -.thinking-dot:nth-child(3) { - animation-delay: 0.4s; -} - @keyframes typingPulse { 0%, 60%, 100% { transform: scale(0.7); @@ -399,32 +170,6 @@ button { } } -.loading-text { - opacity: 0.7; - font-style: italic; - color: var(--app-secondary-foreground); -} - -/* =========================== - Scrollbar Styling - =========================== */ -.messages-container::-webkit-scrollbar { - width: 8px; -} - -.messages-container::-webkit-scrollbar-track { - background: transparent; -} - -.messages-container::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: var(--corner-radius-small); -} - -.messages-container::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); -} - /* =========================== Input Form Styles =========================== */ @@ -488,50 +233,6 @@ button { cursor: not-allowed; } -/* =========================== - Claude Code Style Header Buttons - =========================== */ -.header-conversations-button { - flex: 0 0 auto; - padding: 4px 8px; - background: transparent; - border: 1px solid transparent; - border-radius: var(--corner-radius-small); - color: var(--app-primary-foreground); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - outline: none; - font-size: var(--vscode-chat-font-size, 13px); - font-weight: 500; - transition: background-color 0.2s; -} - -.header-conversations-button:hover, -.header-conversations-button:focus { - background-color: var(--app-ghost-button-hover-background); -} - -.button-content { - display: flex; - align-items: center; - gap: 4px; -} - -.button-text { - font-size: var(--vscode-chat-font-size, 13px); -} - -.dropdown-icon { - width: 14px; - height: 14px; -} - -.header-spacer { - flex: 1; -} - /* =========================== Claude Code Style Input Form (.Me > .u) =========================== */ diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 705c269c..2790019d 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -16,7 +16,6 @@ import { ToolCall, type ToolCallData } from './components/ToolCall.js'; import { hasToolCallOutput } from './components/toolcalls/shared/utils.js'; import { EmptyState } from './components/EmptyState.js'; import { PlanDisplay, type PlanEntry } from './components/PlanDisplay.js'; -import { MessageContent } from './components/MessageContent.js'; import { CompletionMenu, type CompletionItem, @@ -24,6 +23,14 @@ import { import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { SaveSessionDialog } from './components/SaveSessionDialog.js'; import { InfoBanner } from './components/InfoBanner.js'; +import { ChatHeader } from './components/ui/ChatHeader.js'; +import { + UserMessage, + AssistantMessage, + ThinkingMessage, + StreamingMessage, + WaitingMessage, +} from './components/messages/index.js'; interface ToolCallUpdate { type: 'tool_call' | 'tool_call_update'; @@ -54,6 +61,12 @@ interface TextMessage { role: 'user' | 'assistant' | 'thinking'; content: string; timestamp: number; + fileContext?: { + fileName: string; + filePath: string; + startLine?: number; + endLine?: number; + }; } // Loading messages from Claude Code CLI @@ -228,6 +241,11 @@ export const App: React.FC = () => { const [editMode, setEditMode] = useState('ask'); const [thinkingEnabled, setThinkingEnabled] = useState(false); const [activeFileName, setActiveFileName] = useState(null); + const [activeFilePath, setActiveFilePath] = useState(null); + const [activeSelection, setActiveSelection] = useState<{ + startLine: number; + endLine: number; + } | null>(null); const [isComposing, setIsComposing] = useState(false); const [showSaveDialog, setShowSaveDialog] = useState(false); const [savedSessionTags, setSavedSessionTags] = useState([]); @@ -842,9 +860,16 @@ export const App: React.FC = () => { } case 'activeEditorChanged': { - // 从扩展接收当前激活编辑器的文件名 + // 从扩展接收当前激活编辑器的文件名和选中的行号 const fileName = message.data?.fileName as string | null; + const filePath = message.data?.filePath as string | null; + const selection = message.data?.selection as { + startLine: number; + endLine: number; + } | null; setActiveFileName(fileName); + setActiveFilePath(filePath); + setActiveSelection(selection); break; } @@ -1035,7 +1060,13 @@ export const App: React.FC = () => { setLoadingMessage(getRandomLoadingMessage()); // Parse @file references from input text - const context: Array<{ type: string; name: string; value: string }> = []; + const context: Array<{ + type: string; + name: string; + value: string; + startLine?: number; + endLine?: number; + }> = []; const fileRefPattern = /@([^\s]+)/g; let match; @@ -1052,11 +1083,43 @@ export const App: React.FC = () => { } } + // Add active file selection context if present + if (activeFilePath) { + const fileName = activeFileName || 'current file'; + context.push({ + type: 'file', + name: fileName, + value: activeFilePath, + startLine: activeSelection?.startLine, + endLine: activeSelection?.endLine, + }); + } + + // Build file context for the message + let fileContextForMessage: + | { + fileName: string; + filePath: string; + startLine?: number; + endLine?: number; + } + | undefined; + + if (activeFilePath && activeFileName) { + fileContextForMessage = { + fileName: activeFileName, + filePath: activeFilePath, + startLine: activeSelection?.startLine, + endLine: activeSelection?.endLine, + }; + } + vscode.postMessage({ type: 'sendMessage', data: { text: inputText, context: context.length > 0 ? context : undefined, + fileContext: fileContextForMessage, }, }); @@ -1298,104 +1361,59 @@ export const App: React.FC = () => { )} -
- -
- - -
+ setShowSaveDialog(true)} + onNewSession={handleNewQwenSession} + /> -
+
{!hasContent ? ( ) : ( <> {messages.map((msg, index) => { - // Special styling for thinking messages (Claude Code style) - const messageClass = - msg.role === 'thinking' - ? 'message assistant thinking-message' - : `message ${msg.role}`; + const handleFileClick = (path: string) => { + vscode.postMessage({ + type: 'openFile', + data: { path }, + }); + }; + + if (msg.role === 'thinking') { + return ( + + ); + } + + if (msg.role === 'user') { + return ( + + ); + } return ( -
-
- {msg.role === 'thinking' && ( - - - - - - )} - { - vscode.postMessage({ - type: 'openFile', - data: { path }, - }); - }} - /> -
-
- {new Date(msg.timestamp).toLocaleTimeString()} -
-
+ ); })} @@ -1411,16 +1429,7 @@ export const App: React.FC = () => { {/* Loading/Waiting Message - in message list */} {isWaitingForResponse && loadingMessage && ( -
-
- - - - - - {loadingMessage} -
-
+ )} {/* Not Logged In Message with Login Button - COMMENTED OUT */} @@ -1442,20 +1451,15 @@ export const App: React.FC = () => { )} */} {isStreaming && currentStreamContent && ( -
-
- { - vscode.postMessage({ - type: 'openFile', - data: { path }, - }); - }} - /> -
-
-
+ { + vscode.postMessage({ + type: 'openFile', + data: { path }, + }); + }} + /> )}
@@ -1527,7 +1531,7 @@ export const App: React.FC = () => { )}
diff --git a/packages/vscode-ide-companion/src/webview/MessageHandler.ts b/packages/vscode-ide-companion/src/webview/MessageHandler.ts index dc1a56bb..9639e30a 100644 --- a/packages/vscode-ide-companion/src/webview/MessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/MessageHandler.ts @@ -108,7 +108,26 @@ export class MessageHandler { switch (message.type) { case 'sendMessage': - await this.handleSendMessage((data?.text as string) || ''); + await this.handleSendMessage( + (data?.text as string) || '', + data?.context as + | Array<{ + type: string; + name: string; + value: string; + startLine?: number; + endLine?: number; + }> + | undefined, + data?.fileContext as + | { + fileName: string; + filePath: string; + startLine?: number; + endLine?: number; + } + | undefined, + ); break; case 'permissionResponse': @@ -142,14 +161,24 @@ export class MessageHandler { break; case 'getActiveEditor': { - // 发送当前激活编辑器的文件名给 WebView + // 发送当前激活编辑器的文件名和选中的行号给 WebView const editor = vscode.window.activeTextEditor; - const fileName = editor?.document.uri.fsPath - ? getFileName(editor.document.uri.fsPath) - : null; + const filePath = editor?.document.uri.fsPath || null; + const fileName = filePath ? getFileName(filePath) : null; + + // Get selection info if there is any selected text + let selectionInfo = null; + if (editor && !editor.selection.isEmpty) { + const selection = editor.selection; + selectionInfo = { + startLine: selection.start.line + 1, // VSCode is 0-indexed, display as 1-indexed + endLine: selection.end.line + 1, + }; + } + this.sendToWebView({ type: 'activeEditorChanged', - data: { fileName }, + data: { fileName, filePath, selection: selectionInfo }, }); break; } @@ -235,8 +264,42 @@ export class MessageHandler { /** * 处理发送消息请求 */ - private async handleSendMessage(text: string): Promise { + private async handleSendMessage( + text: string, + context?: Array<{ + type: string; + name: string; + value: string; + startLine?: number; + endLine?: number; + }>, + fileContext?: { + fileName: string; + filePath: string; + startLine?: number; + endLine?: number; + }, + ): Promise { console.log('[MessageHandler] handleSendMessage called with:', text); + console.log('[MessageHandler] Context:', context); + console.log('[MessageHandler] FileContext:', fileContext); + + // Format message with file context if present + let formattedText = text; + if (context && context.length > 0) { + const contextParts = context + .map((ctx) => { + if (ctx.startLine && ctx.endLine) { + // Include line numbers in the file reference + return `${ctx.value}#${ctx.startLine}${ctx.startLine !== ctx.endLine ? `-${ctx.endLine}` : ''}`; + } + return ctx.value; + }) + .join('\n'); + + // Prepend context to the message + formattedText = `${contextParts}\n\n${text}`; + } // Ensure we have an active conversation - create one if needed if (!this.currentConversationId) { @@ -306,7 +369,7 @@ export class MessageHandler { }); } - // Save user message + // Save user message (save original text, not formatted) const userMessage: ChatMessage = { role: 'user', content: text, @@ -319,10 +382,10 @@ export class MessageHandler { ); console.log('[MessageHandler] User message saved to store'); - // Send to WebView + // Send to WebView (show original text with file context) this.sendToWebView({ type: 'message', - data: userMessage, + data: { ...userMessage, fileContext }, }); console.log('[MessageHandler] User message sent to webview'); @@ -332,8 +395,8 @@ export class MessageHandler { '[MessageHandler] Agent is not connected, skipping AI response', ); - // Save pending message for auto-retry after login - this.pendingMessage = text; + // Save pending message for auto-retry after login (save formatted text for AI) + this.pendingMessage = formattedText; console.log( '[MessageHandler] Saved pending message for retry after login', ); @@ -361,7 +424,7 @@ export class MessageHandler { return; } - // Send to agent + // Send to agent (use formatted text with file context) try { // Reset stream content this.resetStreamContent(); @@ -373,8 +436,8 @@ export class MessageHandler { }); console.log('[MessageHandler] Stream start sent'); - console.log('[MessageHandler] Sending to agent manager...'); - await this.agentManager.sendMessage(text); + console.log('[MessageHandler] Sending to agent manager:', formattedText); + await this.agentManager.sendMessage(formattedText); console.log('[MessageHandler] Agent manager send complete'); // Stream is complete - save assistant message @@ -1038,9 +1101,9 @@ export class MessageHandler { console.log('[MessageHandler] Login completed successfully'); // Show success notification - vscode.window.showInformationMessage( - 'Successfully logged in to Qwen Code!', - ); + // vscode.window.showInformationMessage( + // 'Successfully logged in to Qwen Code!', + // ); // Auto-resend pending message if exists if (this.pendingMessage) { diff --git a/packages/vscode-ide-companion/src/webview/components/MessageContent.css b/packages/vscode-ide-companion/src/webview/components/MessageContent.css deleted file mode 100644 index 265adf2c..00000000 --- a/packages/vscode-ide-companion/src/webview/components/MessageContent.css +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - * - * MessageContent styles - */ - -/* Code block styles */ -.message-code-block { - background-color: var(--app-code-background, rgba(0, 0, 0, 0.05)); - border: 1px solid var(--app-primary-border-color); - border-radius: var(--corner-radius-small, 4px); - padding: 12px; - margin: 8px 0; - overflow-x: auto; - font-family: var(--app-monospace-font-family, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace); - font-size: 13px; - line-height: 1.5; -} - -.message-code-block code { - background: none; - padding: 0; - font-family: inherit; - color: var(--app-primary-foreground); -} - -/* Inline code styles */ -.message-inline-code { - background-color: var(--app-code-background, rgba(0, 0, 0, 0.05)); - border: 1px solid var(--app-primary-border-color); - border-radius: 3px; - padding: 2px 6px; - font-family: var(--app-monospace-font-family, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace); - font-size: 0.9em; - color: var(--app-primary-foreground); - white-space: nowrap; -} - -/* File path link styles */ -.message-file-path { - background: none; - border: none; - padding: 0; - font-family: var(--app-monospace-font-family, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace); - font-size: 0.95em; - color: var(--app-link-foreground, #007ACC); - text-decoration: underline; - cursor: pointer; - transition: color 0.15s; -} - -.message-file-path:hover { - color: var(--app-link-active-foreground, #005A9E); - text-decoration: underline; -} - -.message-file-path:active { - color: var(--app-link-active-foreground, #005A9E); -} diff --git a/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx b/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx index 38b0ea54..dad6d16e 100644 --- a/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx +++ b/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx @@ -7,7 +7,6 @@ */ import type React from 'react'; -import './MessageContent.css'; interface MessageContentProps { content: string; @@ -19,6 +18,9 @@ interface MessageContentProps { */ const FILE_PATH_REGEX = /([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi; +// Match file paths with optional line numbers like: path/file.ts#7-14 or path/file.ts#7 +const FILE_PATH_WITH_LINES_REGEX = + /([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi; const CODE_BLOCK_REGEX = /```(\w+)?\n([\s\S]*?)```/g; const INLINE_CODE_REGEX = /`([^`]+)`/g; @@ -51,10 +53,31 @@ export const MessageContent: React.FC = ({ matchIndex++; } - // Add code block + // Add code block with Tailwind CSS parts.push( -
-          {code}
+        
+          
+            {code}
+          
         
, ); matchIndex++; @@ -107,9 +130,19 @@ export const MessageContent: React.FC = ({ matchIndex++; } - // Add inline code + // Add inline code with Tailwind CSS parts.push( - + {code} , ); @@ -134,22 +167,99 @@ export const MessageContent: React.FC = ({ let lastIndex = 0; let matchIndex = startIndex; - const filePathMatches = Array.from(text.matchAll(FILE_PATH_REGEX)); + // First, try to match file paths with line numbers + const filePathWithLinesMatches = Array.from( + text.matchAll(FILE_PATH_WITH_LINES_REGEX), + ); + const processedRanges: Array<{ start: number; end: number }> = []; - filePathMatches.forEach((match) => { + filePathWithLinesMatches.forEach((match) => { const fullMatch = match[0]; const startIdx = match.index!; + const filePath = fullMatch.split('#')[0]; // Get path without line numbers + const startLine = match[4]; // Capture group 4 is the start line + const endLine = match[5]; // Capture group 5 is the end line (optional) + + processedRanges.push({ + start: startIdx, + end: startIdx + fullMatch.length, + }); // Add text before file path if (startIdx > lastIndex) { parts.push(text.slice(lastIndex, startIdx)); } - // Add file path link + // Display text with line numbers + const displayText = endLine + ? `${filePath}#${startLine}-${endLine}` + : `${filePath}#${startLine}`; + + // Add file path link with line numbers parts.push( , + ); + + matchIndex++; + lastIndex = startIdx + fullMatch.length; + }); + + // Now match regular file paths (without line numbers) that weren't already matched + const filePathMatches = Array.from(text.matchAll(FILE_PATH_REGEX)); + + filePathMatches.forEach((match) => { + const fullMatch = match[0]; + const startIdx = match.index!; + + // Skip if this range was already processed as a path with line numbers + const isProcessed = processedRanges.some( + (range) => startIdx >= range.start && startIdx < range.end, + ); + if (isProcessed) { + return; + } + + // Add text before file path + if (startIdx > lastIndex) { + parts.push(text.slice(lastIndex, startIdx)); + } + + // Add file path link with Tailwind CSS + parts.push( + + + {/* Spacer */} +
+ + {/* Save Session Button */} + {/* */} + + {/* New Session Button */} + +
+); diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js index b5e21b47..b973f78b 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -3,7 +3,11 @@ module.exports = { content: [ // 渐进式采用策略:只扫描新创建的Tailwind组件 + './src/webview/App.tsx', './src/webview/components/ui/**/*.{js,jsx,ts,tsx}', + './src/webview/components/messages/**/*.{js,jsx,ts,tsx}', + './src/webview/components/MessageContent.tsx', + './src/webview/components/InfoBanner.tsx', // 当需要在更多组件中使用Tailwind时,可以逐步添加路径 // "./src/webview/components/NewComponent/**/*.{js,jsx,ts,tsx}", // "./src/webview/pages/**/*.{js,jsx,ts,tsx}",