mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion/extension): relay diff accept/cancel events to chat webview
Added functionality to relay diff accepted/cancelled events from the IDE to the chat webview so users get immediate feedback when they accept or cancel diffs in the Claude Code style.
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'node:path';
|
||||||
import { IDEServer } from './ide-server.js';
|
import { IDEServer } from './ide-server.js';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
||||||
@@ -166,6 +167,45 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
createWebViewProvider,
|
createWebViewProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Relay diff accept/cancel events to the chat webview as assistant notices
|
||||||
|
// so the user sees immediate feedback in the chat thread (Claude Code style).
|
||||||
|
context.subscriptions.push(
|
||||||
|
diffManager.onDidChange((notification) => {
|
||||||
|
try {
|
||||||
|
const method = (notification as { method?: string }).method;
|
||||||
|
if (method !== 'ide/diffAccepted' && method !== 'ide/diffClosed') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = (
|
||||||
|
notification as unknown as {
|
||||||
|
params?: { filePath?: string };
|
||||||
|
}
|
||||||
|
).params;
|
||||||
|
const filePath = params?.filePath ?? '';
|
||||||
|
const fileBase = filePath ? path.basename(filePath) : '';
|
||||||
|
const text =
|
||||||
|
method === 'ide/diffAccepted'
|
||||||
|
? `Accepted changes${fileBase ? ` to ${fileBase}` : ''}.`
|
||||||
|
: `Cancelled changes${fileBase ? ` to ${fileBase}` : ''}.`;
|
||||||
|
|
||||||
|
for (const provider of webViewProviders) {
|
||||||
|
const panel = provider.getPanel();
|
||||||
|
panel?.webview.postMessage({
|
||||||
|
type: 'message',
|
||||||
|
data: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: text,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Extension] Failed to relay diff event to chat:', e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.workspace.onDidCloseTextDocument((doc) => {
|
vscode.workspace.onDidCloseTextDocument((doc) => {
|
||||||
if (doc.uri.scheme === DIFF_SCHEME) {
|
if (doc.uri.scheme === DIFF_SCHEME) {
|
||||||
|
|||||||
@@ -145,16 +145,13 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
>
|
>
|
||||||
{/* Background layer */}
|
{/* Background layer */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 rounded-large"
|
className="p-2 absolute inset-0 rounded-large"
|
||||||
style={{ backgroundColor: 'var(--app-input-background)' }}
|
style={{ backgroundColor: 'var(--app-input-background)' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="relative z-[1] px-3 py-3">
|
<div className="relative z-[1] px-1 text-[1.1em] text-[var(--app-primary-foreground)] flex flex-col min-h-0">
|
||||||
<div
|
<div className="font-bold text-[var(--app-primary-foreground)] mb-1">
|
||||||
className="text-sm font-medium"
|
|
||||||
style={{ color: 'var(--app-secondary-foreground)' }}
|
|
||||||
>
|
|
||||||
{getTitle()}
|
{getTitle()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,20 +166,20 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
key={option.optionId}
|
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)] ${
|
||||||
isFocused
|
isFocused
|
||||||
? 'bg-[var(--app-list-active-background)] text-[var(--app-list-active-foreground)]'
|
? 'bg-[var(--app-list-active-background)] 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-list-hover-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)]'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onResponse(option.optionId)}
|
onClick={() => onResponse(option.optionId)}
|
||||||
onMouseEnter={() => setFocusedIndex(index)}
|
onMouseEnter={() => setFocusedIndex(index)}
|
||||||
>
|
>
|
||||||
{/* Number badge */}
|
{/* Number badge */}
|
||||||
{/* Plain number badge without hover background */}
|
{/* 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">
|
<span className="inline-flex items-center justify-center min-w-[10px] h-5 font-semibold">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Option text */}
|
{/* Option text */}
|
||||||
<span className="text-sm">{option.name}</span>
|
<span className="font-semibold">{option.name}</span>
|
||||||
|
|
||||||
{/* Always badge */}
|
{/* Always badge */}
|
||||||
{/* {isAlways && <span className="text-sm">⚡</span>} */}
|
{/* {isAlways && <span className="text-sm">⚡</span>} */}
|
||||||
@@ -197,8 +194,8 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
<div
|
<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)] ${
|
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
|
isFocused
|
||||||
? 'bg-[var(--app-list-active-background)] text-[var(--app-list-active-foreground)]'
|
? 'bg-[var(--app-list-active-background)] 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-list-hover-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)}
|
onMouseEnter={() => setFocusedIndex(options.length)}
|
||||||
onClick={() => customInputRef.current?.focus()}
|
onClick={() => customInputRef.current?.focus()}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
|
|||||||
|
|
||||||
{/* File context indicator */}
|
{/* File context indicator */}
|
||||||
{fileContextDisplay && (
|
{fileContextDisplay && (
|
||||||
<div className="mt-6">
|
<div className="mt-1">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
const queryText = safeTitle(title);
|
const queryText = safeTitle(title);
|
||||||
|
|
||||||
// Group content by type
|
// Group content by type
|
||||||
const { errors } = groupContent(content);
|
const { errors, textOutputs } = groupContent(content);
|
||||||
|
|
||||||
// Error case: show search query + error in card layout
|
// Error case: show search query + error in card layout
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
@@ -77,6 +77,31 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show content text if available (e.g., "Listed 4 item(s).")
|
||||||
|
if (textOutputs.length > 0) {
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
|
return (
|
||||||
|
<ToolCallContainer
|
||||||
|
label="Search"
|
||||||
|
status={containerStatus}
|
||||||
|
className="search-toolcall"
|
||||||
|
labelSuffix={queryText ? `(${queryText})` : undefined}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{textOutputs.map((text, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1"
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||||
|
<span className="flex-shrink-0 w-full">{text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ToolCallContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// No results - show query only
|
// No results - show query only
|
||||||
if (queryText) {
|
if (queryText) {
|
||||||
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
|
|||||||
@@ -63,7 +63,8 @@
|
|||||||
content: '\25cf';
|
content: '\25cf';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
padding-top: 2px;
|
/* TODO: */
|
||||||
|
/* padding-top: 2px; */
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #c74e39;
|
color: #c74e39;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user