feat(vscode-ide-companion): 更新主应用界面和消息处理

- 重构 App.tsx,集成新增的 UI 组件
- 增强 MessageHandler,支持更多消息类型处理
- 优化 FileOperations,改进文件操作逻辑

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yiliang114
2025-11-21 01:53:46 +08:00
parent 748ad8f4dd
commit 99f93b457c
3 changed files with 754 additions and 22 deletions

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useVSCode } from './hooks/useVSCode.js';
import type { Conversation } from '../storage/conversationStore.js';
import {
@@ -13,9 +13,15 @@ import {
} from './components/PermissionRequest.js';
import { PermissionDrawer } from './components/PermissionDrawer.js';
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,
} from './components/CompletionMenu.js';
import { useCompletionTrigger } from './hooks/useCompletionTrigger.js';
interface ToolCallUpdate {
type: 'tool_call' | 'tool_call_update';
@@ -222,6 +228,121 @@ export const App: React.FC = () => {
const [activeFileName, setActiveFileName] = useState<string | null>(null);
const [isComposing, setIsComposing] = useState(false);
// Workspace files cache
const [workspaceFiles, setWorkspaceFiles] = useState<
Array<{
id: string;
label: string;
description: string;
path: string;
}>
>([]);
// File reference map: @filename -> full path
const fileReferenceMap = useRef<Map<string, string>>(new Map());
// Request workspace files on mount or when @ is first triggered
const hasRequestedFilesRef = useRef(false);
// Debounce timer for search requests
const searchTimerRef = useRef<NodeJS.Timeout | null>(null);
// Get completion items based on trigger character
const getCompletionItems = useCallback(
async (trigger: '@' | '/', query: string): Promise<CompletionItem[]> => {
if (trigger === '@') {
// Request workspace files on first @ trigger
if (!hasRequestedFilesRef.current) {
hasRequestedFilesRef.current = true;
vscode.postMessage({
type: 'getWorkspaceFiles',
data: {},
});
}
// Convert workspace files to completion items
const fileIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M9 2H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7l-5-5zm3 7V3.5L10.5 2H10v3a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V2H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1zM6 3h3v2H6V3z" />
</svg>
);
// Convert all files to items
const allItems: CompletionItem[] = workspaceFiles.map((file) => ({
id: file.id,
label: file.label,
description: file.description,
type: 'file' as const,
icon: fileIcon,
value: file.path,
}));
// If query provided, filter locally AND request from backend (debounced)
if (query && query.length >= 1) {
// Clear previous search timer
if (searchTimerRef.current) {
clearTimeout(searchTimerRef.current);
}
// Debounce backend search request (300ms)
searchTimerRef.current = setTimeout(() => {
vscode.postMessage({
type: 'getWorkspaceFiles',
data: { query },
});
}, 300);
// Filter locally for immediate feedback
const lowerQuery = query.toLowerCase();
const filtered = allItems.filter(
(item) =>
item.label.toLowerCase().includes(lowerQuery) ||
(item.description &&
item.description.toLowerCase().includes(lowerQuery)),
);
return filtered;
}
return allItems;
} else {
// Slash commands - only /login for now
const commands: CompletionItem[] = [
{
id: 'login',
label: '/login',
description: 'Login to Qwen Code',
type: 'command',
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z" />
</svg>
),
},
];
return commands.filter((cmd) =>
cmd.label.toLowerCase().includes(query.toLowerCase()),
);
}
},
[vscode, workspaceFiles],
);
// Use completion trigger hook
const completion = useCompletionTrigger(inputFieldRef, getCompletionItems);
// Don't auto-refresh completion menu when workspace files update
// This was causing flickering. User can re-type to get fresh results.
const handlePermissionRequest = React.useCallback(
(request: {
options: PermissionOption[];
@@ -243,6 +364,181 @@ export const App: React.FC = () => {
[vscode],
);
// Handle completion item selection
const handleCompletionSelect = useCallback(
(item: CompletionItem) => {
if (!inputFieldRef.current) {
return;
}
const inputElement = inputFieldRef.current;
const currentText = inputElement.textContent || '';
if (item.type === 'file') {
// Store file reference mapping
const filePath = (item.value as string) || item.label;
fileReferenceMap.current.set(item.label, filePath);
console.log('[handleCompletionSelect] Current text:', currentText);
console.log('[handleCompletionSelect] Selected file:', item.label);
// Find the @ position in current text
const atPos = currentText.lastIndexOf('@');
if (atPos !== -1) {
// Find the end of the query (could be at cursor or at next space/end)
const textAfterAt = currentText.substring(atPos + 1);
const spaceIndex = textAfterAt.search(/[\s\n]/);
const queryEnd =
spaceIndex === -1 ? currentText.length : atPos + 1 + spaceIndex;
// Replace from @ to end of query with @filename
const textBefore = currentText.substring(0, atPos);
const textAfter = currentText.substring(queryEnd);
const newText = `${textBefore}@${item.label} ${textAfter}`;
console.log('[handleCompletionSelect] New text:', newText);
// Update the input
inputElement.textContent = newText;
setInputText(newText);
// Set cursor after the inserted filename (after the space)
const newCursorPos = atPos + item.label.length + 2; // +1 for @, +1 for space
// Wait for DOM to update, then set cursor
setTimeout(() => {
const textNode = inputElement.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const selection = window.getSelection();
if (selection) {
const range = document.createRange();
try {
range.setStart(
textNode,
Math.min(newCursorPos, newText.length),
);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
} catch (e) {
console.error(
'[handleCompletionSelect] Error setting cursor:',
e,
);
// Fallback: move cursor to end
range.selectNodeContents(inputElement);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
inputElement.focus();
}, 10);
}
} else if (item.type === 'command') {
// Replace entire input with command
inputElement.textContent = item.label + ' ';
setInputText(item.label + ' ');
// Move cursor to end
setTimeout(() => {
const range = document.createRange();
const sel = window.getSelection();
if (inputElement.firstChild) {
range.setStart(inputElement.firstChild, (item.label + ' ').length);
range.collapse(true);
} else {
range.selectNodeContents(inputElement);
range.collapse(false);
}
sel?.removeAllRanges();
sel?.addRange(range);
inputElement.focus();
}, 10);
}
// Close completion
completion.closeCompletion();
},
[completion],
);
// Handle attach context button click (Cmd/Ctrl + /)
const handleAttachContextClick = useCallback(async () => {
if (inputFieldRef.current) {
// Focus the input first
inputFieldRef.current.focus();
// Insert @ at the end of current text
const currentText = inputFieldRef.current.textContent || '';
const newText = currentText ? `${currentText} @` : '@';
inputFieldRef.current.textContent = newText;
setInputText(newText);
// Move cursor to end
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(inputFieldRef.current);
range.collapse(false);
sel?.removeAllRanges();
sel?.addRange(range);
// Wait for DOM to update before getting position and opening menu
requestAnimationFrame(async () => {
if (!inputFieldRef.current) {
return;
}
// Get cursor position for menu placement
let position = { top: 0, left: 0 };
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
try {
const currentRange = selection.getRangeAt(0);
const rangeRect = currentRange.getBoundingClientRect();
if (rangeRect.top > 0 && rangeRect.left > 0) {
position = {
top: rangeRect.top,
left: rangeRect.left,
};
} else {
const inputRect = inputFieldRef.current.getBoundingClientRect();
position = { top: inputRect.top, left: inputRect.left };
}
} catch (error) {
console.error('[App] Error getting cursor position:', error);
const inputRect = inputFieldRef.current.getBoundingClientRect();
position = { top: inputRect.top, left: inputRect.left };
}
} else {
const inputRect = inputFieldRef.current.getBoundingClientRect();
position = { top: inputRect.top, left: inputRect.left };
}
// Open completion menu with @ trigger
await completion.openCompletion('@', '', position);
});
}
}, [completion]);
// Handle keyboard shortcut for attach context (Cmd/Ctrl + /)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd/Ctrl + / for attach context
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
e.preventDefault();
handleAttachContextClick();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleAttachContextClick]);
// Handle removing context attachment
const handleToolCallUpdate = React.useCallback((update: ToolCallUpdate) => {
setToolCalls((prev) => {
const newMap = new Map(prev);
@@ -467,6 +763,18 @@ export const App: React.FC = () => {
setCurrentSessionTitle('Past Conversations');
break;
case 'sessionTitleUpdated': {
// Update session title when first message is sent
const sessionId = message.data?.sessionId as string;
const title = message.data?.title as string;
if (sessionId && title) {
console.log('[App] Session title updated:', title);
setCurrentSessionId(sessionId);
setCurrentSessionTitle(title);
}
break;
}
case 'activeEditorChanged': {
// 从扩展接收当前激活编辑器的文件名
const fileName = message.data?.fileName as string | null;
@@ -474,6 +782,52 @@ export const App: React.FC = () => {
break;
}
case 'fileAttached': {
// Handle file attachment from VSCode - insert as @mention
const attachment = message.data as {
id: string;
type: string;
name: string;
value: string;
};
// Store file reference
fileReferenceMap.current.set(attachment.name, attachment.value);
// Insert @filename into input
if (inputFieldRef.current) {
const currentText = inputFieldRef.current.textContent || '';
const newText = currentText
? `${currentText} @${attachment.name} `
: `@${attachment.name} `;
inputFieldRef.current.textContent = newText;
setInputText(newText);
// Move cursor to end
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(inputFieldRef.current);
range.collapse(false);
sel?.removeAllRanges();
sel?.addRange(range);
}
break;
}
case 'workspaceFiles': {
// Handle workspace files list from VSCode
const files = message.data?.files as Array<{
id: string;
label: string;
description: string;
path: string;
}>;
if (files) {
setWorkspaceFiles(files);
}
break;
}
default:
break;
}
@@ -588,16 +942,38 @@ export const App: React.FC = () => {
setIsWaitingForResponse(true);
setLoadingMessage(getRandomLoadingMessage());
// Parse @file references from input text
const context: Array<{ type: string; name: string; value: string }> = [];
const fileRefPattern = /@([^\s]+)/g;
let match;
while ((match = fileRefPattern.exec(inputText)) !== null) {
const fileName = match[1];
const filePath = fileReferenceMap.current.get(fileName);
if (filePath) {
context.push({
type: 'file',
name: fileName,
value: filePath,
});
}
}
vscode.postMessage({
type: 'sendMessage',
data: { text: inputText },
data: {
text: inputText,
context: context.length > 0 ? context : undefined,
},
});
// Clear input field
// Clear input field and file reference map
setInputText('');
if (inputFieldRef.current) {
inputFieldRef.current.textContent = '';
}
fileReferenceMap.current.clear();
};
const handleLoadQwenSessions = () => {
@@ -911,10 +1287,12 @@ export const App: React.FC = () => {
);
})}
{/* Tool Calls */}
{Array.from(toolCalls.values()).map((toolCall) => (
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
))}
{/* Tool Calls - only show those with actual output */}
{Array.from(toolCalls.values())
.filter((toolCall) => hasToolCallOutput(toolCall))
.map((toolCall) => (
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
))}
{/* Plan Display - shows task list when available */}
{planEntries.length > 0 && <PlanDisplay entries={planEntries} />}
@@ -1006,6 +1384,8 @@ export const App: React.FC = () => {
<div className="input-form-container">
<div className="input-form-wrapper">
{/* Context Pills - Removed: now using inline @mentions in input */}
<form className="input-form" onSubmit={handleSubmit}>
<div className="input-form-background"></div>
<div className="input-banner"></div>
@@ -1031,6 +1411,10 @@ export const App: React.FC = () => {
onKeyDown={(e) => {
// 如果正在进行中文输入法输入(拼音输入),不处理回车键
if (e.key === 'Enter' && !e.shiftKey && !isComposing) {
// 如果 CompletionMenu 打开,让它处理 Enter 键(选中文件)
if (completion.isOpen) {
return;
}
e.preventDefault();
handleSubmit(e);
}
@@ -1078,6 +1462,8 @@ export const App: React.FC = () => {
</button>
)}
<div className="action-divider"></div>
{/* Spacer 将右侧按钮推到右边 */}
<div className="input-actions-spacer"></div>
<button
type="button"
className={`action-icon-button thinking-button ${thinkingEnabled ? 'active' : ''}`}
@@ -1105,6 +1491,54 @@ export const App: React.FC = () => {
type="button"
className="action-icon-button command-button"
title="Show command menu (/)"
onClick={async () => {
if (inputFieldRef.current) {
// Focus the input first to ensure cursor is in the right place
inputFieldRef.current.focus();
// Get cursor position for menu placement
const selection = window.getSelection();
let position = { top: 0, left: 0 };
// Try to get precise cursor position
if (selection && selection.rangeCount > 0) {
try {
const range = selection.getRangeAt(0);
const rangeRect = range.getBoundingClientRect();
if (rangeRect.top > 0 && rangeRect.left > 0) {
position = {
top: rangeRect.top,
left: rangeRect.left,
};
} else {
// Fallback to input element position
const inputRect =
inputFieldRef.current.getBoundingClientRect();
position = {
top: inputRect.top,
left: inputRect.left,
};
}
} catch (error) {
console.error(
'[App] Error getting cursor position:',
error,
);
const inputRect =
inputFieldRef.current.getBoundingClientRect();
position = { top: inputRect.top, left: inputRect.left };
}
} else {
// No selection, use input element position
const inputRect =
inputFieldRef.current.getBoundingClientRect();
position = { top: inputRect.top, left: inputRect.left };
}
// Open completion menu with / commands
await completion.openCompletion('/', '', position);
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -1119,7 +1553,26 @@ export const App: React.FC = () => {
></path>
</svg>
</button>
<div className="input-actions-spacer"></div>
<button
type="button"
className="action-icon-button attach-button"
title="Attach context (Cmd/Ctrl + /)"
onClick={handleAttachContextClick}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M15.621 4.379a3 3 0 0 0-4.242 0l-7 7a3 3 0 0 0 4.241 4.243h.001l.497-.5a.75.75 0 0 1 1.064 1.057l-.498.501-.002.002a4.5 4.5 0 0 1-6.364-6.364l7-7a4.5 4.5 0 0 1 6.368 6.36l-3.455 3.553A2.625 2.625 0 1 1 9.52 9.52l3.45-3.451a.75.75 0 1 1 1.061 1.06l-3.45 3.451a1.125 1.125 0 0 0 1.587 1.595l3.454-3.553a3 3 0 0 0 0-4.242Z"
clipRule="evenodd"
></path>
</svg>
</button>
<button
type="submit"
className="send-button-icon"
@@ -1153,6 +1606,16 @@ export const App: React.FC = () => {
onClose={() => setPermissionRequest(null)}
/>
)}
{/* Completion Menu for @ and / */}
{completion.isOpen && completion.items.length > 0 && (
<CompletionMenu
items={completion.items}
position={completion.position}
onSelect={handleCompletionSelect}
onClose={completion.closeCompletion}
/>
)}
</div>
);
};

View File

@@ -13,8 +13,8 @@ import { getFileName } from '../utils/webviewUtils.js';
*/
export class FileOperations {
/**
* 打开文件并可选跳转到指定行
* @param filePath 文件路径可以包含行号格式path/to/file.ts:123
* 打开文件并可选跳转到指定行和列
* @param filePath 文件路径,可以包含行号和列号格式path/to/file.ts:123 或 path/to/file.ts:123:45
*/
static async openFile(filePath?: string): Promise<void> {
try {
@@ -25,15 +25,17 @@ export class FileOperations {
console.log('[FileOperations] Opening file:', filePath);
// Parse file path and line number (format: path/to/file.ts:123)
const match = filePath.match(/^(.+?)(?::(\d+))?$/);
// Parse file path, line number, and column number
// Formats: path/to/file.ts, path/to/file.ts:123, path/to/file.ts:123:45
const match = filePath.match(/^(.+?)(?::(\d+))?(?::(\d+))?$/);
if (!match) {
console.warn('[FileOperations] Invalid file path format:', filePath);
return;
}
const [, path, lineStr] = match;
const [, path, lineStr, columnStr] = match;
const lineNumber = lineStr ? parseInt(lineStr, 10) - 1 : 0; // VS Code uses 0-based line numbers
const columnNumber = columnStr ? parseInt(columnStr, 10) - 1 : 0; // VS Code uses 0-based column numbers
// Convert to absolute path if relative
let absolutePath = path;
@@ -53,9 +55,9 @@ export class FileOperations {
preserveFocus: false,
});
// Navigate to line if specified
// Navigate to line and column if specified
if (lineStr) {
const position = new vscode.Position(lineNumber, 0);
const position = new vscode.Position(lineNumber, columnNumber);
editor.selection = new vscode.Selection(position, position);
editor.revealRange(
new vscode.Range(position, position),

View File

@@ -172,13 +172,12 @@ export class MessageHandler {
break;
case 'openDiff':
await FileOperations.openDiff(
data as {
path?: string;
oldText?: string;
newText?: string;
},
);
console.log('[MessageHandler] openDiff called with:', data);
await vscode.commands.executeCommand('qwenCode.showDiff', {
path: (data as { path?: string })?.path || '',
oldText: (data as { oldText?: string })?.oldText || '',
newText: (data as { newText?: string })?.newText || '',
});
break;
case 'openNewChatTab':
@@ -186,6 +185,18 @@ export class MessageHandler {
await vscode.commands.executeCommand('qwenCode.openNewChatTab');
break;
case 'attachFile':
await this.handleAttachFile();
break;
case 'showContextPicker':
await this.handleShowContextPicker();
break;
case 'getWorkspaceFiles':
await this.handleGetWorkspaceFiles(data?.query as string);
break;
default:
console.warn('[MessageHandler] Unknown message type:', message.type);
break;
@@ -237,6 +248,35 @@ export class MessageHandler {
return;
}
// Check if this is the first message by checking conversation messages
let isFirstMessage = false;
try {
const conversation = await this.conversationStore.getConversation(
this.currentConversationId,
);
// First message if conversation has no messages yet
isFirstMessage = !conversation || conversation.messages.length === 0;
console.log('[MessageHandler] Is first message:', isFirstMessage);
} catch (error) {
console.error('[MessageHandler] Failed to check conversation:', error);
}
// If this is the first message, generate and send session title
if (isFirstMessage) {
// Generate title from first message (max 50 characters)
const title = text.substring(0, 50) + (text.length > 50 ? '...' : '');
console.log('[MessageHandler] Generated session title:', title);
// Send title update to WebView
this.sendToWebView({
type: 'sessionTitleUpdated',
data: {
sessionId: this.currentConversationId,
title,
},
});
}
// Save user message
const userMessage: ChatMessage = {
role: 'user',
@@ -521,4 +561,231 @@ export class MessageHandler {
});
}
}
/**
* 处理附加文件请求
* 打开文件选择器将选中的文件信息发送回WebView
*/
private async handleAttachFile(): Promise<void> {
try {
const uris = await vscode.window.showOpenDialog({
canSelectMany: false,
canSelectFiles: true,
canSelectFolders: false,
openLabel: 'Attach',
});
if (uris && uris.length > 0) {
const uri = uris[0];
const fileName = getFileName(uri.fsPath);
this.sendToWebView({
type: 'fileAttached',
data: {
id: `file-${Date.now()}`,
type: 'file',
name: fileName,
value: uri.fsPath,
},
});
}
} catch (error) {
console.error('[MessageHandler] Failed to attach file:', error);
this.sendToWebView({
type: 'error',
data: { message: `Failed to attach file: ${error}` },
});
}
}
/**
* 获取工作区文件列表
* 用于在 @ 触发时显示文件补全
* 优先显示最近使用的文件(打开的标签页)
*/
private async handleGetWorkspaceFiles(query?: string): Promise<void> {
try {
const files: Array<{
id: string;
label: string;
description: string;
path: string;
}> = [];
const addedPaths = new Set<string>();
// Helper function to add a file
const addFile = (uri: vscode.Uri, isCurrentFile = false) => {
if (addedPaths.has(uri.fsPath)) {
return;
}
const fileName = getFileName(uri.fsPath);
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
const relativePath = workspaceFolder
? vscode.workspace.asRelativePath(uri, false)
: uri.fsPath;
// Filter by query if provided
if (
query &&
!fileName.toLowerCase().includes(query.toLowerCase()) &&
!relativePath.toLowerCase().includes(query.toLowerCase())
) {
return;
}
files.push({
id: isCurrentFile ? 'current-file' : uri.fsPath,
label: fileName,
description: relativePath,
path: uri.fsPath,
});
addedPaths.add(uri.fsPath);
};
// If query provided, search entire workspace
if (query) {
// Search workspace files matching the query
const uris = await vscode.workspace.findFiles(
`**/*${query}*`,
'**/node_modules/**',
50, // Allow more results for search
);
for (const uri of uris) {
addFile(uri);
}
} else {
// No query: show recently used files
// 1. Add current active file first
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor) {
addFile(activeEditor.document.uri, true);
}
// 2. Add all open tabs (recently used files)
const tabGroups = vscode.window.tabGroups.all;
for (const tabGroup of tabGroups) {
for (const tab of tabGroup.tabs) {
const input = tab.input as { uri?: vscode.Uri } | undefined;
if (input && input.uri instanceof vscode.Uri) {
addFile(input.uri);
}
}
}
// 3. If still not enough files (less than 10), add some workspace files
if (files.length < 10) {
const recentUris = await vscode.workspace.findFiles(
'**/*',
'**/node_modules/**',
20,
);
for (const uri of recentUris) {
if (files.length >= 20) {
break;
}
addFile(uri);
}
}
}
this.sendToWebView({
type: 'workspaceFiles',
data: { files },
});
} catch (error) {
console.error('[MessageHandler] Failed to get workspace files:', error);
this.sendToWebView({
type: 'error',
data: { message: `Failed to get workspace files: ${error}` },
});
}
}
/**
* 处理显示上下文选择器请求
* 显示快速选择菜单,包含文件、符号等选项
* 参考 vscode-copilot-chat 的 AttachContextAction
*/
private async handleShowContextPicker(): Promise<void> {
try {
const items: vscode.QuickPickItem[] = [];
// Add current file
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor) {
const fileName = getFileName(activeEditor.document.uri.fsPath);
items.push({
label: `$(file) ${fileName}`,
description: 'Current file',
detail: activeEditor.document.uri.fsPath,
});
}
// Add file picker option
items.push({
label: '$(file) File...',
description: 'Choose a file to attach',
});
// Add workspace files option
items.push({
label: '$(search) Search files...',
description: 'Search workspace files',
});
const selected = await vscode.window.showQuickPick(items, {
placeHolder: 'Attach context',
matchOnDescription: true,
matchOnDetail: true,
});
if (selected) {
if (selected.label.includes('Current file') && activeEditor) {
const fileName = getFileName(activeEditor.document.uri.fsPath);
this.sendToWebView({
type: 'fileAttached',
data: {
id: `file-${Date.now()}`,
type: 'file',
name: fileName,
value: activeEditor.document.uri.fsPath,
},
});
} else if (selected.label.includes('File...')) {
await this.handleAttachFile();
} else if (selected.label.includes('Search files')) {
// Open workspace file picker
const uri = await vscode.window.showOpenDialog({
defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri,
canSelectMany: false,
canSelectFiles: true,
canSelectFolders: false,
openLabel: 'Attach',
});
if (uri && uri.length > 0) {
const fileName = getFileName(uri[0].fsPath);
this.sendToWebView({
type: 'fileAttached',
data: {
id: `file-${Date.now()}`,
type: 'file',
name: fileName,
value: uri[0].fsPath,
},
});
}
}
}
} catch (error) {
console.error('[MessageHandler] Failed to show context picker:', error);
this.sendToWebView({
type: 'error',
data: { message: `Failed to show context picker: ${error}` },
});
}
}
}