mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion/ui): improve permission drawer UI and logic
This commit is contained in:
@@ -29,17 +29,40 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const [customMessage, setCustomMessage] = useState('');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const customInputRef = useRef<HTMLDivElement>(null);
|
||||
// 将自定义输入的 ref 类型修正为 HTMLInputElement,避免后续强转
|
||||
const customInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
console.log('PermissionDrawer rendered with isOpen:', isOpen, toolCall);
|
||||
// Prefer file name from locations, fall back to content[].path if present
|
||||
const getAffectedFileName = (): string => {
|
||||
const fromLocations = toolCall.locations?.[0]?.path;
|
||||
if (fromLocations) {
|
||||
return fromLocations.split('/').pop() || fromLocations;
|
||||
}
|
||||
// Some tool calls (e.g. write/edit with diff content) only include path in content
|
||||
const fromContent = Array.isArray(toolCall.content)
|
||||
? (
|
||||
toolCall.content.find(
|
||||
(c: unknown) =>
|
||||
typeof c === 'object' &&
|
||||
c !== null &&
|
||||
'path' in (c as Record<string, unknown>),
|
||||
) as { path?: unknown } | undefined
|
||||
)?.path
|
||||
: undefined;
|
||||
if (typeof fromContent === 'string' && fromContent.length > 0) {
|
||||
return fromContent.split('/').pop() || fromContent;
|
||||
}
|
||||
return 'file';
|
||||
};
|
||||
|
||||
// Get the title for the permission request
|
||||
const getTitle = () => {
|
||||
if (toolCall.kind === 'edit' || toolCall.kind === 'write') {
|
||||
const fileName =
|
||||
toolCall.locations?.[0]?.path?.split('/').pop() || 'file';
|
||||
const fileName = getAffectedFileName();
|
||||
return (
|
||||
<>
|
||||
Allow write to{' '}
|
||||
Make this edit to{' '}
|
||||
<span className="font-mono text-[var(--app-primary-foreground)]">
|
||||
{fileName}
|
||||
</span>
|
||||
@@ -51,8 +74,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
return 'Allow this bash command?';
|
||||
}
|
||||
if (toolCall.kind === 'read') {
|
||||
const fileName =
|
||||
toolCall.locations?.[0]?.path?.split('/').pop() || 'file';
|
||||
const fileName = getAffectedFileName();
|
||||
return (
|
||||
<>
|
||||
Allow read from{' '}
|
||||
@@ -126,6 +148,13 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Reset focus to the first option when the drawer opens or the options change
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFocusedIndex(0);
|
||||
}
|
||||
}, [isOpen, options.length]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
@@ -135,7 +164,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
{/* Main container */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex flex-col rounded-large border p-2 outline-none animate-[slideUp_0.2s_ease-out]"
|
||||
className="relative flex flex-col rounded-large border p-2 outline-none animate-slide-up"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-input-secondary-background)',
|
||||
borderColor: 'var(--app-input-border)',
|
||||
@@ -149,11 +178,25 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
style={{ backgroundColor: 'var(--app-input-background)' }}
|
||||
/>
|
||||
|
||||
{/* Title */}
|
||||
{/* Title + Description (from toolCall.title) */}
|
||||
<div className="relative z-[1] px-1 text-[1.1em] text-[var(--app-primary-foreground)] flex flex-col min-h-0">
|
||||
<div className="font-bold text-[var(--app-primary-foreground)] mb-1">
|
||||
<div className="font-bold text-[var(--app-primary-foreground)] mb-0.5">
|
||||
{getTitle()}
|
||||
</div>
|
||||
{(toolCall.kind === 'edit' ||
|
||||
toolCall.kind === 'write' ||
|
||||
toolCall.kind === 'read' ||
|
||||
toolCall.kind === 'execute' ||
|
||||
toolCall.kind === 'bash') &&
|
||||
toolCall.title && (
|
||||
<div
|
||||
/* 13px,常规字重;正常空白折行 + 长词断行;最多 3 行溢出省略 */
|
||||
className="text-[13px] font-normal text-[var(--app-secondary-foreground)] opacity-90 font-mono whitespace-normal break-words q-line-clamp-3 mb-2"
|
||||
title={toolCall.title}
|
||||
>
|
||||
{toolCall.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
@@ -164,10 +207,10 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
return (
|
||||
<button
|
||||
key={option.optionId}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 text-[var(--app-primary-foreground)] ${
|
||||
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 text-[var(--app-primary-foreground)] hover:bg-[var(--app-input-background)] ${
|
||||
isFocused
|
||||
? 'text-[var(--app-list-active-foreground)] hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0 hover:border-[var(--app-button-background)]'
|
||||
: 'hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0 hover:border-[var(--app-button-background)]'
|
||||
? 'text-[var(--app-list-active-foreground)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
|
||||
: 'hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
|
||||
}`}
|
||||
onClick={() => onResponse(option.optionId)}
|
||||
onMouseEnter={() => setFocusedIndex(index)}
|
||||
@@ -177,7 +220,6 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
<span className="inline-flex items-center justify-center min-w-[10px] h-5 font-semibold">
|
||||
{index + 1}
|
||||
</span>
|
||||
|
||||
{/* Option text */}
|
||||
<span className="font-semibold">{option.name}</span>
|
||||
|
||||
@@ -187,70 +229,79 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Custom message input (styled consistently with option items) */}
|
||||
{/* Custom message input (extracted component) */}
|
||||
{(() => {
|
||||
const isFocused = focusedIndex === options.length;
|
||||
const rejectOptionId = options.find((o) =>
|
||||
o.kind.includes('reject'),
|
||||
)?.optionId;
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 cursor-text text-[var(--app-primary-foreground)] ${
|
||||
isFocused
|
||||
? 'text-[var(--app-list-active-foreground)] hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0 hover:border-[var(--app-button-background)]'
|
||||
: 'hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0 hover:border-[var(--app-button-background)]'
|
||||
}`}
|
||||
onMouseEnter={() => setFocusedIndex(options.length)}
|
||||
onClick={() => customInputRef.current?.focus()}
|
||||
>
|
||||
{/* Number badge (N+1) */}
|
||||
{/* Plain number badge without hover background */}
|
||||
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded">
|
||||
{options.length + 1}
|
||||
</span>
|
||||
|
||||
{/* Input field */}
|
||||
<input
|
||||
ref={customInputRef as React.RefObject<HTMLInputElement>}
|
||||
type="text"
|
||||
placeholder="Tell Qwen what to do instead"
|
||||
spellCheck={false}
|
||||
className="flex-1 bg-transparent border-0 outline-none text-sm placeholder:opacity-70"
|
||||
style={{ color: 'var(--app-input-foreground)' }}
|
||||
value={customMessage}
|
||||
onChange={(e) => setCustomMessage(e.target.value)}
|
||||
onFocus={() => setFocusedIndex(options.length)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
!e.shiftKey &&
|
||||
customMessage.trim()
|
||||
) {
|
||||
e.preventDefault();
|
||||
const rejectOption = options.find((o) =>
|
||||
o.kind.includes('reject'),
|
||||
);
|
||||
if (rejectOption) {
|
||||
onResponse(rejectOption.optionId);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<CustomMessageInputRow
|
||||
isFocused={isFocused}
|
||||
customMessage={customMessage}
|
||||
setCustomMessage={setCustomMessage}
|
||||
onFocusRow={() => setFocusedIndex(options.length)}
|
||||
onSubmitReject={() => {
|
||||
if (rejectOptionId) onResponse(rejectOptionId);
|
||||
}}
|
||||
inputRef={customInputRef}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
{/* Moved slide-up keyframes to Tailwind theme (tailwind.config.js) */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* CustomMessageInputRow: 复用的自定义输入行组件(无 hooks)
|
||||
*/
|
||||
interface CustomMessageInputRowProps {
|
||||
isFocused: boolean;
|
||||
customMessage: string;
|
||||
setCustomMessage: (val: string) => void;
|
||||
onFocusRow: () => void; // 鼠标移入或输入框 focus 时设置焦点
|
||||
onSubmitReject: () => void; // Enter 提交时触发(选择 reject 选项)
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const CustomMessageInputRow: React.FC<CustomMessageInputRowProps> = ({
|
||||
isFocused,
|
||||
customMessage,
|
||||
setCustomMessage,
|
||||
onFocusRow,
|
||||
onSubmitReject,
|
||||
inputRef,
|
||||
}) => (
|
||||
<div
|
||||
// 无过渡:hover 样式立即生效;输入行不加 hover 背景,也不加粗文字
|
||||
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] cursor-text text-[var(--app-primary-foreground)] ${
|
||||
isFocused ? 'text-[var(--app-list-active-foreground)]' : ''
|
||||
}`}
|
||||
onMouseEnter={onFocusRow}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
{/* 输入行不显示序号徽标 */}
|
||||
{/* Input field */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Tell Qwen what to do instead"
|
||||
spellCheck={false}
|
||||
className="flex-1 bg-transparent border-0 outline-none text-sm placeholder:opacity-70"
|
||||
style={{ color: 'var(--app-input-foreground)' }}
|
||||
value={customMessage}
|
||||
onChange={(e) => setCustomMessage(e.target.value)}
|
||||
onFocus={onFocusRow}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && customMessage.trim()) {
|
||||
e.preventDefault();
|
||||
onSubmitReject();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user