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:
yiliang114
2025-12-05 02:45:44 +08:00
parent 2d844d11df
commit 8203f6582f
5 changed files with 78 additions and 15 deletions

View File

@@ -5,6 +5,7 @@
*/
import * as vscode from 'vscode';
import * as path from 'node:path';
import { IDEServer } from './ide-server.js';
import semver from 'semver';
import { DiffContentProvider, DiffManager } from './diff-manager.js';
@@ -166,6 +167,45 @@ export async function activate(context: vscode.ExtensionContext) {
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(
vscode.workspace.onDidCloseTextDocument((doc) => {
if (doc.uri.scheme === DIFF_SCHEME) {

View File

@@ -145,16 +145,13 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
>
{/* Background layer */}
<div
className="absolute inset-0 rounded-large"
className="p-2 absolute inset-0 rounded-large"
style={{ backgroundColor: 'var(--app-input-background)' }}
/>
{/* Title */}
<div className="relative z-[1] px-3 py-3">
<div
className="text-sm font-medium"
style={{ color: 'var(--app-secondary-foreground)' }}
>
<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">
{getTitle()}
</div>
</div>
@@ -169,20 +166,20 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
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)] ${
isFocused
? 'bg-[var(--app-list-active-background)] text-[var(--app-list-active-foreground)]'
: 'hover:bg-[var(--app-list-hover-background)]'
? '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-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)}
onMouseEnter={() => setFocusedIndex(index)}
>
{/* Number badge */}
{/* 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}
</span>
{/* Option text */}
<span className="text-sm">{option.name}</span>
<span className="font-semibold">{option.name}</span>
{/* Always badge */}
{/* {isAlways && <span className="text-sm">⚡</span>} */}
@@ -197,8 +194,8 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
<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
? 'bg-[var(--app-list-active-background)] text-[var(--app-list-active-foreground)]'
: 'hover:bg-[var(--app-list-hover-background)]'
? '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-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()}

View File

@@ -63,7 +63,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
{/* File context indicator */}
{fileContextDisplay && (
<div className="mt-6">
<div className="mt-1">
<div
role="button"
tabIndex={0}

View File

@@ -30,7 +30,7 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const queryText = safeTitle(title);
// Group content by type
const { errors } = groupContent(content);
const { errors, textOutputs } = groupContent(content);
// Error case: show search query + error in card layout
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
if (queryText) {
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);

View File

@@ -63,7 +63,8 @@
content: '\25cf';
position: absolute;
left: 8px;
padding-top: 2px;
/* TODO: */
/* padding-top: 2px; */
font-size: 10px;
color: #c74e39;
z-index: 1;