mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 04:00:36 +00:00
feat(vscode-ide-companion): add image paste support (#1978)
* feat(vscode-ide-companion): add image paste support - Add clipboard image paste functionality with drag-and-drop support - Implement image preview component with removal capability - Support multimodal content in ACP session manager for text and images - Save pasted images to temporary .gemini-clipboard directory - Add image attachment display in user messages - Update CSP to allow data: URIs for inline image display - Add comprehensive image utilities with size validation (max 10MB) - Include tests for image processing utilities * refactor: simplify VS Code paste image implementation - Remove dead code and redundant error handling - Extract common isAuthError() helper function - Simplify SessionMessageHandler methods (80% reduction) - Change temp directory from .gemini-clipboard to clipboard (aligned with CLI) - Keep multimodal image sending format (type: image + base64) Stats: - 6 files changed - 367 insertions (+) - 1176 deletions (-) * refactor: align paste image handling * chore: trim paste image diff * refactor(vscode-ide-companion): remove unused attachments logic - Remove unused ImageAttachment type imports - Remove attachments field from TextMessage interface - Remove attachments from message data sent to WebView - Clean up debug console.log statements - Simplify SessionMessageHandler handleSendMessage method This removes dead code from the previous image paste implementation that was no longer needed after switching to @path reference approach. * refactor(vscode-ide-companion/webview): extract image handling into dedicated hooks and utils - extract ImagePreview and ImageMessageRenderer components from App.tsx - create useImageAttachments hook for managing image attachments - create useImageResolution hook for image path resolution - add imageAttachmentHandler for saving images to temp files - add imageMessageUtils for message expansion and resolution - add imagePathResolver for resolving image paths in webview - integrate image resolution in useWebViewMessages - extract shouldSendMessage utility from useMessageSubmit - add getLocalResourceRoots in PanelManager for resource access Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: harden vscode image handling and webview hosts * fix: remove this alias in acp connection * feat: add path escaping utility functions and tests * feat: add support for image attachments and improve prompt handling * refactor(webview): Optimize editing mode switching function * refactor(vscode-ide-companion): move path escaping utilities to local module - Move escapePath and unescapePath functions from qwen-code-core to local utils - Add pathEscaping.ts with shell special characters handling - Update imports in imageFormats.ts, imageAttachmentHandler.ts, and imageMessageUtils.ts - Add unit tests for path escaping round-trip and browser bundle verification - Fix browser bundling issue by avoiding node-only module dependencies in webview Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * refactor: consolidate image handling logic across vscode-ide-companion and webui - Merge分散的 image hooks (useImageAttachments, useImageResolution, usePasteHandler) into unified useImage hook - Replace image utils (imageMessageUtils, imagePathResolver, imageUtils) with imageHandler and imageSupport - Remove clipboard image storage from core package - Consolidate webui image components into ImageComponents.tsx - Update imports and tests to reflect new structure Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: drop unrelated core tool changes * test: fix webview provider mocks and drop unrelated core diffs * fix(cli): resolve original prompt through standard path in no_command case Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
85ed1a801d
commit
87f03cf2e9
26 changed files with 2188 additions and 150 deletions
|
|
@ -64,7 +64,7 @@ export interface InputFormProps {
|
|||
/** Current input text */
|
||||
inputText: string;
|
||||
/** Ref for the input field */
|
||||
inputFieldRef: React.RefObject<HTMLDivElement>;
|
||||
inputFieldRef: React.RefObject<HTMLDivElement | null>;
|
||||
/** Whether AI is currently generating */
|
||||
isStreaming: boolean;
|
||||
/** Whether waiting for response */
|
||||
|
|
@ -117,8 +117,14 @@ export interface InputFormProps {
|
|||
onCompletionFill?: (item: CompletionItem) => void;
|
||||
/** Completion close callback */
|
||||
onCompletionClose?: () => void;
|
||||
/** Optional paste handler for the contentEditable input */
|
||||
onPaste?: (e: React.ClipboardEvent) => void;
|
||||
/** Optional content rendered between the input and actions */
|
||||
extraContent?: ReactNode;
|
||||
/** Placeholder text */
|
||||
placeholder?: string;
|
||||
/** Whether the current draft is eligible to submit */
|
||||
canSubmit?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,9 +180,14 @@ export const InputForm: FC<InputFormProps> = ({
|
|||
onCompletionSelect,
|
||||
onCompletionFill,
|
||||
onCompletionClose,
|
||||
onPaste,
|
||||
extraContent,
|
||||
placeholder = 'Ask Qwen Code …',
|
||||
canSubmit,
|
||||
}) => {
|
||||
const composerDisabled = isStreaming || isWaitingForResponse;
|
||||
const hasDraftContent =
|
||||
canSubmit ?? inputText.replace(/\u200B/g, '').trim().length > 0;
|
||||
const completionItemsResolved = completionItems ?? [];
|
||||
const completionActive =
|
||||
completionIsOpen &&
|
||||
|
|
@ -275,10 +286,15 @@ export const InputForm: FC<InputFormProps> = ({
|
|||
onCompositionStart={onCompositionStart}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={onPaste}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
</div>
|
||||
|
||||
{extraContent ? (
|
||||
<div className="relative z-[1]">{extraContent}</div>
|
||||
) : null}
|
||||
|
||||
<div className="composer-actions">
|
||||
{/* Edit mode button */}
|
||||
<button
|
||||
|
|
@ -357,7 +373,7 @@ export const InputForm: FC<InputFormProps> = ({
|
|||
<button
|
||||
type="submit"
|
||||
className="btn-send-compact [&>svg]:w-5 [&>svg]:h-5"
|
||||
disabled={composerDisabled || !inputText.trim()}
|
||||
disabled={composerDisabled || !hasDraftContent}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
|
|
|
|||
119
packages/webui/src/components/messages/ImageComponents.tsx
Normal file
119
packages/webui/src/components/messages/ImageComponents.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import { CloseSmallIcon } from '../icons/NavigationIcons.js';
|
||||
|
||||
// ======================== ImagePreview ========================
|
||||
|
||||
export interface ImagePreviewItem {
|
||||
id: string;
|
||||
name: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface ImagePreviewProps {
|
||||
images: ImagePreviewItem[];
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ImagePreview: FC<ImagePreviewProps> = ({ images, onRemove }) => {
|
||||
if (images.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-preview-container flex gap-2 px-2 pb-2">
|
||||
{images.map((image) => (
|
||||
<div key={image.id} className="image-preview-item relative group">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={image.data}
|
||||
alt={image.name}
|
||||
className="w-14 h-14 object-cover rounded-md border border-gray-500 dark:border-gray-600"
|
||||
title={image.name}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemove(image.id)}
|
||||
className="absolute -top-2 -right-2 w-5 h-5 bg-gray-700 dark:bg-gray-600 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-800 dark:hover:bg-gray-500"
|
||||
aria-label={`Remove ${image.name}`}
|
||||
>
|
||||
<CloseSmallIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ======================== ImageMessageRenderer ========================
|
||||
|
||||
export interface ImageMessageLike {
|
||||
kind: 'image';
|
||||
imagePath: string;
|
||||
imageSrc?: string;
|
||||
imageMissing?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageMessageRendererProps {
|
||||
msg: ImageMessageLike;
|
||||
imageIndex: number;
|
||||
}
|
||||
|
||||
export const ImageMessageRenderer: FC<ImageMessageRendererProps> = ({
|
||||
msg,
|
||||
imageIndex,
|
||||
}) => {
|
||||
if (msg.kind !== 'image' || !msg.imagePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = `[Image #${imageIndex}]`;
|
||||
const showImage = Boolean(msg.imageSrc) && !msg.imageMissing;
|
||||
|
||||
return (
|
||||
<div className="qwen-message user-message-container flex gap-0 my-1 items-start text-left flex-col relative">
|
||||
<div
|
||||
className="inline-block relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
|
||||
style={{
|
||||
border: '1px solid var(--app-input-border)',
|
||||
borderRadius: 'var(--corner-radius-medium)',
|
||||
backgroundColor: 'var(--app-input-background)',
|
||||
padding: '6px 8px',
|
||||
color: 'var(--app-primary-foreground)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--app-secondary-foreground)',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{showImage ? (
|
||||
<img
|
||||
src={msg.imageSrc}
|
||||
alt={msg.imagePath}
|
||||
className="max-w-full rounded-md border border-gray-600"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--app-secondary-foreground)',
|
||||
}}
|
||||
>
|
||||
@{msg.imagePath}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -92,6 +92,16 @@ export type {
|
|||
Question,
|
||||
QuestionOption,
|
||||
} from './components/messages/AskUserQuestionDialog';
|
||||
export {
|
||||
ImagePreview,
|
||||
ImageMessageRenderer,
|
||||
} from './components/messages/ImageComponents';
|
||||
export type {
|
||||
ImagePreviewProps,
|
||||
ImagePreviewItem,
|
||||
ImageMessageRendererProps,
|
||||
ImageMessageLike,
|
||||
} from './components/messages/ImageComponents';
|
||||
|
||||
// ChatViewer - standalone chat display component
|
||||
export {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue