mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion): 优化权限请求组件并添加错误处理功能
- 移动权限请求组件到抽屉中,优化用户体验 - 为权限选项添加编号,提高可识别性 - 实现错误对象的特殊处理,提取更有意义的错误信息 - 优化工具调用错误内容的展示,提高错误信息的可读性
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="chat-container">
|
||||
@@ -810,15 +806,6 @@ export const App: React.FC = () => {
|
||||
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
|
||||
))}
|
||||
|
||||
{/* Permission Request */}
|
||||
{permissionRequest && (
|
||||
<PermissionRequest
|
||||
options={permissionRequest.options}
|
||||
toolCall={permissionRequest.toolCall}
|
||||
onResponse={handlePermissionResponse}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Loading/Waiting Message - in message list */}
|
||||
{isWaitingForResponse && loadingMessage && (
|
||||
<div className="message assistant waiting-message">
|
||||
@@ -1003,6 +990,17 @@ export const App: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Permission Drawer - Cursor style */}
|
||||
{permissionRequest && (
|
||||
<PermissionDrawer
|
||||
isOpen={!!permissionRequest}
|
||||
options={permissionRequest.options}
|
||||
toolCall={permissionRequest.toolCall}
|
||||
onResponse={handlePermissionResponse}
|
||||
onClose={() => setPermissionRequest(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PermissionDrawerProps> = ({
|
||||
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 */}
|
||||
<div className="permission-drawer-backdrop" onClick={onClose} />
|
||||
|
||||
{/* Drawer */}
|
||||
<div className="permission-drawer">
|
||||
<div className="permission-drawer-header">
|
||||
<h3 className="permission-drawer-title">Permission Required</h3>
|
||||
{onClose && (
|
||||
<button
|
||||
className="permission-drawer-close"
|
||||
onClick={onClose}
|
||||
aria-label="Close drawer"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 2L14 14M2 14L14 2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="permission-drawer-content">
|
||||
<PermissionRequest
|
||||
options={options}
|
||||
toolCall={toolCall}
|
||||
onResponse={onResponse}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -150,7 +150,7 @@ export const PermissionRequest: React.FC<PermissionRequestProps> = ({
|
||||
<div className="permission-options-label">Choose an action:</div>
|
||||
<div className="permission-options-list">
|
||||
{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<PermissionRequestProps> = ({
|
||||
className="permission-radio"
|
||||
/>
|
||||
<span className="permission-option-content">
|
||||
<span className="permission-option-number">
|
||||
{index + 1}
|
||||
</span>
|
||||
{isAlways && (
|
||||
<span className="permission-always-badge">⚡</span>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user