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