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;
|
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 {
|
.permission-always-badge {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { useVSCode } from './hooks/useVSCode.js';
|
import { useVSCode } from './hooks/useVSCode.js';
|
||||||
import type { Conversation } from '../storage/conversationStore.js';
|
import type { Conversation } from '../storage/conversationStore.js';
|
||||||
import {
|
import {
|
||||||
PermissionRequest,
|
|
||||||
type PermissionOption,
|
type PermissionOption,
|
||||||
type ToolCall as PermissionToolCall,
|
type ToolCall as PermissionToolCall,
|
||||||
} from './components/PermissionRequest.js';
|
} from './components/PermissionRequest.js';
|
||||||
|
import { PermissionDrawer } from './components/PermissionDrawer.js';
|
||||||
import { ToolCall, type ToolCallData } from './components/ToolCall.js';
|
import { ToolCall, type ToolCallData } from './components/ToolCall.js';
|
||||||
import { EmptyState } from './components/EmptyState.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
|
// Check if there are any messages or active content
|
||||||
const hasContent =
|
const hasContent = messages.length > 0 || isStreaming || toolCalls.size > 0;
|
||||||
messages.length > 0 ||
|
|
||||||
isStreaming ||
|
|
||||||
toolCalls.size > 0 ||
|
|
||||||
permissionRequest !== null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-container">
|
<div className="chat-container">
|
||||||
@@ -810,15 +806,6 @@ export const App: React.FC = () => {
|
|||||||
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
|
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Permission Request */}
|
|
||||||
{permissionRequest && (
|
|
||||||
<PermissionRequest
|
|
||||||
options={permissionRequest.options}
|
|
||||||
toolCall={permissionRequest.toolCall}
|
|
||||||
onResponse={handlePermissionResponse}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Loading/Waiting Message - in message list */}
|
{/* Loading/Waiting Message - in message list */}
|
||||||
{isWaitingForResponse && loadingMessage && (
|
{isWaitingForResponse && loadingMessage && (
|
||||||
<div className="message assistant waiting-message">
|
<div className="message assistant waiting-message">
|
||||||
@@ -1003,6 +990,17 @@ export const App: React.FC = () => {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Permission Drawer - Cursor style */}
|
||||||
|
{permissionRequest && (
|
||||||
|
<PermissionDrawer
|
||||||
|
isOpen={!!permissionRequest}
|
||||||
|
options={permissionRequest.options}
|
||||||
|
toolCall={permissionRequest.toolCall}
|
||||||
|
onResponse={handlePermissionResponse}
|
||||||
|
onClose={() => setPermissionRequest(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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-label">Choose an action:</div>
|
||||||
<div className="permission-options-list">
|
<div className="permission-options-list">
|
||||||
{options && options.length > 0 ? (
|
{options && options.length > 0 ? (
|
||||||
options.map((option) => {
|
options.map((option, index) => {
|
||||||
const isSelected = selected === option.optionId;
|
const isSelected = selected === option.optionId;
|
||||||
const isAllow = option.kind.includes('allow');
|
const isAllow = option.kind.includes('allow');
|
||||||
const isAlways = option.kind.includes('always');
|
const isAlways = option.kind.includes('always');
|
||||||
@@ -171,6 +171,9 @@ export const PermissionRequest: React.FC<PermissionRequestProps> = ({
|
|||||||
className="permission-radio"
|
className="permission-radio"
|
||||||
/>
|
/>
|
||||||
<span className="permission-option-content">
|
<span className="permission-option-content">
|
||||||
|
<span className="permission-option-number">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
{isAlways && (
|
{isAlways && (
|
||||||
<span className="permission-always-badge">⚡</span>
|
<span className="permission-always-badge">⚡</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ export const formatValue = (value: unknown): string => {
|
|||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return value;
|
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') {
|
if (typeof value === 'object') {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(value, null, 2);
|
return JSON.stringify(value, null, 2);
|
||||||
@@ -84,10 +93,34 @@ export const groupContent = (content?: ToolCallContent[]): GroupedContent => {
|
|||||||
|
|
||||||
// Handle error content
|
// Handle error content
|
||||||
if (contentObj.type === 'error' || 'error' in contentObj) {
|
if (contentObj.type === 'error' || 'error' in contentObj) {
|
||||||
const errorMsg =
|
// Try to extract meaningful error message
|
||||||
formatValue(contentObj.error) ||
|
let errorMsg = '';
|
||||||
formatValue(contentObj.text) ||
|
|
||||||
'An error occurred';
|
// 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);
|
errors.push(errorMsg);
|
||||||
}
|
}
|
||||||
// Handle text content
|
// Handle text content
|
||||||
|
|||||||
Reference in New Issue
Block a user