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 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) {

View File

@@ -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()}

View File

@@ -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}

View File

@@ -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);

View File

@@ -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;