feat(vscode-ide-companion): 实现自定义权限请求 UI 并添加文件读写功能

- 新增 fs/read_text_file 和 fs/write_text_file 方法处理
- 实现精美的 Claude 风格权限请求 UI
- 优化权限请求处理逻辑,支持取消操作
- 添加日志输出以便调试
This commit is contained in:
yiliang114
2025-11-17 21:44:39 +08:00
parent 247c237647
commit eeeb1d490a
3 changed files with 302 additions and 11 deletions

View File

@@ -338,3 +338,149 @@ body {
font-family: monospace;
}
/* Claude-style Inline Permission Request */
.permission-request-inline {
margin: 16px 0;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.permission-card {
background: linear-gradient(
135deg,
rgba(79, 134, 247, 0.08) 0%,
rgba(79, 134, 247, 0.03) 100%
);
border: 1.5px solid rgba(79, 134, 247, 0.3);
border-radius: 10px;
padding: 16px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.permission-card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.permission-icon-wrapper {
width: 40px;
height: 40px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(79, 134, 247, 0.2), rgba(79, 134, 247, 0.1));
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.permission-info {
flex: 1;
}
.permission-tool-title {
font-size: 14px;
font-weight: 600;
color: var(--vscode-editor-foreground);
margin-bottom: 2px;
}
.permission-subtitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.permission-actions-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.permission-btn-inline {
flex: 1;
min-width: 100px;
padding: 10px 16px;
border: 1.5px solid transparent;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: var(--vscode-font-family);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
position: relative;
overflow: hidden;
}
.permission-btn-inline::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
}
.permission-btn-inline:hover::before {
width: 300px;
height: 300px;
}
.permission-btn-inline.allow {
background: linear-gradient(135deg, rgba(46, 160, 67, 0.25), rgba(46, 160, 67, 0.15));
color: #4ec9b0;
border-color: rgba(46, 160, 67, 0.4);
}
.permission-btn-inline.allow:hover {
background: linear-gradient(135deg, rgba(46, 160, 67, 0.35), rgba(46, 160, 67, 0.25));
border-color: rgba(46, 160, 67, 0.6);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(46, 160, 67, 0.3);
}
.permission-btn-inline.reject {
background: linear-gradient(135deg, rgba(200, 40, 40, 0.25), rgba(200, 40, 40, 0.15));
color: #f48771;
border-color: rgba(200, 40, 40, 0.4);
}
.permission-btn-inline.reject:hover {
background: linear-gradient(135deg, rgba(200, 40, 40, 0.35), rgba(200, 40, 40, 0.25));
border-color: rgba(200, 40, 40, 0.6);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(200, 40, 40, 0.3);
}
.permission-btn-inline.always {
border-style: dashed;
}
.always-badge {
font-size: 14px;
animation: pulse 1.5s ease-in-out infinite;
}
.permission-btn-inline:active {
transform: translateY(0);
}

View File

@@ -19,6 +19,10 @@ export const App: React.FC = () => {
Array<Record<string, unknown>>
>([]);
const [showSessionSelector, setShowSessionSelector] = useState(false);
const [permissionRequest, setPermissionRequest] = useState<{
options: Array<{ name: string; kind: string; optionId: string }>;
toolCall: { title?: string };
} | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const handlePermissionRequest = React.useCallback(
@@ -26,19 +30,21 @@ export const App: React.FC = () => {
options: Array<{ name: string; kind: string; optionId: string }>;
toolCall: { title?: string };
}) => {
const optionNames = request.options.map((opt) => opt.name).join(', ');
const confirmed = window.confirm(
`Tool permission request:\n${request.toolCall.title || 'Tool Call'}\n\nOptions: ${optionNames}\n\nAllow?`,
);
const selectedOption = confirmed
? request.options.find((opt) => opt.kind === 'allow_once')
: request.options.find((opt) => opt.kind === 'reject_once');
console.log('[WebView] Permission request received:', request);
// Show custom modal instead of window.confirm()
setPermissionRequest(request);
},
[],
);
const handlePermissionResponse = React.useCallback(
(optionId: string) => {
console.log('[WebView] Sending permission response:', optionId);
vscode.postMessage({
type: 'permissionResponse',
data: { optionId: selectedOption?.optionId || 'reject_once' },
data: { optionId },
});
setPermissionRequest(null);
},
[vscode],
);
@@ -244,6 +250,45 @@ export const App: React.FC = () => {
</div>
))}
{/* Claude-style Inline Permission Request */}
{permissionRequest && (
<div className="permission-request-inline">
<div className="permission-card">
<div className="permission-card-header">
<div className="permission-icon-wrapper">
<span className="permission-icon">🔧</span>
</div>
<div className="permission-info">
<div className="permission-tool-title">
{permissionRequest.toolCall.title || 'Tool Request'}
</div>
<div className="permission-subtitle">
Waiting for your approval
</div>
</div>
</div>
<div className="permission-actions-row">
{permissionRequest.options.map((option) => {
const isAllow = option.kind.includes('allow');
const isAlways = option.kind.includes('always');
return (
<button
key={option.optionId}
onClick={() => handlePermissionResponse(option.optionId)}
className={`permission-btn-inline ${isAllow ? 'allow' : 'reject'} ${isAlways ? 'always' : ''}`}
>
{isAlways && <span className="always-badge"></span>}
{option.name}
</button>
);
})}
</div>
</div>
</div>
)}
{isStreaming && currentStreamContent && (
<div className="message assistant streaming">
<div className="message-content">{currentStreamContent}</div>