From e81255e58936d6e9c0b8c63ee1d06e1226032182 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 20 Nov 2025 00:01:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(vscode-ide-companion):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=9D=83=E9=99=90=E8=AF=B7=E6=B1=82=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移动权限请求组件到抽屉中,优化用户体验 - 为权限选项添加编号,提高可识别性 - 实现错误对象的特殊处理,提取更有意义的错误信息 - 优化工具调用错误内容的展示,提高错误信息的可读性 --- .../vscode-ide-companion/src/webview/App.scss | 20 +++ .../vscode-ide-companion/src/webview/App.tsx | 28 ++-- .../webview/components/PermissionDrawer.css | 156 ++++++++++++++++++ .../webview/components/PermissionDrawer.tsx | 112 +++++++++++++ .../webview/components/PermissionRequest.tsx | 5 +- .../components/toolcalls/shared/utils.ts | 41 ++++- 6 files changed, 342 insertions(+), 20 deletions(-) create mode 100644 packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css create mode 100644 packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx diff --git a/packages/vscode-ide-companion/src/webview/App.scss b/packages/vscode-ide-companion/src/webview/App.scss index 5ec1f226..bd3cf08e 100644 --- a/packages/vscode-ide-companion/src/webview/App.scss +++ b/packages/vscode-ide-companion/src/webview/App.scss @@ -1035,6 +1035,26 @@ button { flex: 1; } +.permission-option-number { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + font-size: 11px; + font-weight: 600; + color: var(--app-secondary-foreground); + background-color: var(--app-list-hover-background); + border-radius: 4px; + margin-right: 4px; +} + +.permission-option.selected .permission-option-number { + color: var(--app-qwen-ivory); + background-color: var(--app-qwen-orange); +} + .permission-always-badge { font-size: 12px; } diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index ceb55f3b..16a84c6e 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -8,10 +8,10 @@ import React, { useState, useEffect, useRef } from 'react'; import { useVSCode } from './hooks/useVSCode.js'; import type { Conversation } from '../storage/conversationStore.js'; import { - PermissionRequest, type PermissionOption, type ToolCall as PermissionToolCall, } from './components/PermissionRequest.js'; +import { PermissionDrawer } from './components/PermissionDrawer.js'; import { ToolCall, type ToolCallData } from './components/ToolCall.js'; import { EmptyState } from './components/EmptyState.js'; @@ -624,11 +624,7 @@ export const App: React.FC = () => { }; // Check if there are any messages or active content - const hasContent = - messages.length > 0 || - isStreaming || - toolCalls.size > 0 || - permissionRequest !== null; + const hasContent = messages.length > 0 || isStreaming || toolCalls.size > 0; return (
@@ -810,15 +806,6 @@ export const App: React.FC = () => { ))} - {/* Permission Request */} - {permissionRequest && ( - - )} - {/* Loading/Waiting Message - in message list */} {isWaitingForResponse && loadingMessage && (
@@ -1003,6 +990,17 @@ export const App: React.FC = () => {
+ + {/* Permission Drawer - Cursor style */} + {permissionRequest && ( + setPermissionRequest(null)} + /> + )} ); }; diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css new file mode 100644 index 00000000..9f1f1920 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.css @@ -0,0 +1,156 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + * + * Permission Drawer - Bottom sheet style for permission requests + */ + +/* Backdrop overlay */ +.permission-drawer-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.4); + z-index: 998; + animation: fadeIn 0.2s ease-in-out; +} + +/* Drawer container - bottom sheet style */ +.permission-drawer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + max-height: 70vh; + background-color: var(--app-primary-background); + border-top: 1px solid var(--app-primary-border-color); + border-top-left-radius: 12px; + border-top-right-radius: 12px; + z-index: 999; + display: flex; + flex-direction: column; + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.2); + animation: slideUpFromBottom 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUpFromBottom { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +/* Drawer header */ +.permission-drawer-header { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 20px 16px; + border-bottom: 1px solid var(--app-primary-border-color); + background-color: var(--app-header-background); + border-top-left-radius: 12px; + border-top-right-radius: 12px; + flex-shrink: 0; +} + +.permission-drawer-title { + font-size: 14px; + font-weight: 600; + color: var(--app-primary-foreground); + margin: 0; +} + +.permission-drawer-close { + width: 28px; + height: 28px; + padding: 0; + background: transparent; + border: none; + border-radius: 4px; + color: var(--app-secondary-foreground); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.15s, color 0.15s; +} + +.permission-drawer-close:hover { + background-color: var(--app-ghost-button-hover-background); + color: var(--app-primary-foreground); +} + +/* Drawer content */ +.permission-drawer-content { + flex: 1; + overflow-y: auto; + padding: 20px; + min-height: 0; +} + +/* Override permission card styles when in drawer */ +.permission-drawer-content .permission-request-card { + border: none; + margin: 0; + background: transparent; + box-shadow: none; +} + +.permission-drawer-content .permission-card-body { + padding: 0; +} + +/* Scrollbar for drawer content */ +.permission-drawer-content::-webkit-scrollbar { + width: 8px; +} + +.permission-drawer-content::-webkit-scrollbar-track { + background: transparent; +} + +.permission-drawer-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; +} + +.permission-drawer-content::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +/* Add a drag handle indicator at the top */ +.permission-drawer-header::before { + content: ''; + position: absolute; + top: 8px; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 4px; + background-color: var(--app-secondary-foreground); + opacity: 0.3; + border-radius: 2px; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .permission-drawer { + max-height: 85vh; + } +} + diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx new file mode 100644 index 00000000..7c14fd7c --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { useEffect } from 'react'; +import { + PermissionRequest, + type PermissionOption, + type ToolCall, +} from './PermissionRequest.js'; +import './PermissionDrawer.css'; + +interface PermissionDrawerProps { + isOpen: boolean; + options: PermissionOption[]; + toolCall: ToolCall; + onResponse: (optionId: string) => void; + onClose?: () => void; +} + +/** + * Permission drawer component - displays permission requests in a bottom sheet + * @param isOpen - Whether the drawer is open + * @param options - Permission options to display + * @param toolCall - Tool call information + * @param onResponse - Callback when user responds + * @param onClose - Optional callback when drawer closes + */ +export const PermissionDrawer: React.FC = ({ + isOpen, + options, + toolCall, + onResponse, + onClose, +}) => { + // Close drawer on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!isOpen) return; + + // Close on Escape + if (e.key === 'Escape' && onClose) { + onClose(); + return; + } + + // Quick select with number keys (1-9) + const numMatch = e.key.match(/^[1-9]$/); + if (numMatch) { + const index = parseInt(e.key, 10) - 1; + if (index < options.length) { + e.preventDefault(); + onResponse(options[index].optionId); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose, options, onResponse]); + + if (!isOpen) { + return null; + } + + return ( + <> + {/* Backdrop */} +
+ + {/* Drawer */} +
+
+

Permission Required

+ {onClose && ( + + )} +
+ +
+ +
+
+ + ); +}; diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx index 8ddd72fe..3671b974 100644 --- a/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx @@ -150,7 +150,7 @@ export const PermissionRequest: React.FC = ({
Choose an action:
{options && options.length > 0 ? ( - options.map((option) => { + options.map((option, index) => { const isSelected = selected === option.optionId; const isAllow = option.kind.includes('allow'); const isAlways = option.kind.includes('always'); @@ -171,6 +171,9 @@ export const PermissionRequest: React.FC = ({ className="permission-radio" /> + + {index + 1} + {isAlways && ( )} diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts index 2ac965c5..5a93e234 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts @@ -18,6 +18,15 @@ export const formatValue = (value: unknown): string => { if (typeof value === 'string') { return value; } + // Handle Error objects specially + if (value instanceof Error) { + return value.message || value.toString(); + } + // Handle error-like objects with message property + if (typeof value === 'object' && value !== null && 'message' in value) { + const errorObj = value as { message?: string; stack?: string }; + return errorObj.message || String(value); + } if (typeof value === 'object') { try { return JSON.stringify(value, null, 2); @@ -84,10 +93,34 @@ export const groupContent = (content?: ToolCallContent[]): GroupedContent => { // Handle error content if (contentObj.type === 'error' || 'error' in contentObj) { - const errorMsg = - formatValue(contentObj.error) || - formatValue(contentObj.text) || - 'An error occurred'; + // Try to extract meaningful error message + let errorMsg = ''; + + // Check if error is a string + if (typeof contentObj.error === 'string') { + errorMsg = contentObj.error; + } + // Check if error has a message property + else if ( + contentObj.error && + typeof contentObj.error === 'object' && + 'message' in contentObj.error + ) { + errorMsg = (contentObj.error as { message: string }).message; + } + // Try text field + else if (contentObj.text) { + errorMsg = formatValue(contentObj.text); + } + // Format the error object itself + else if (contentObj.error) { + errorMsg = formatValue(contentObj.error); + } + // Fallback + else { + errorMsg = 'An error occurred'; + } + errors.push(errorMsg); } // Handle text content