Feat/add ide opened file to chat context (#1106)
* feat(chat): add active editor file context and related functionality * feat(chat): improve active editor file context handling and update translations * feat(i18n): standardize quotation marks in file attachment messages * update Korean translation for image removal action in file attachment * feat(chat): refine active editor file handling and optimize broadcast logic * fix(chat): stabilize VS Code editor context chips --------- Signed-off-by: David Saz <david.saz.g@gmail.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
7fc22bc69b
commit
5475ef2db3
@@ -8,6 +8,26 @@ import { openSseProxy } from './sseProxy';
|
||||
import { resolveWebviewDevServerUrl } from './webviewDevServer';
|
||||
import { normalizeWindowsDriveLetter } from './pathUtils';
|
||||
|
||||
type ActiveEditorFilePayload = {
|
||||
filePath: string;
|
||||
fileName: string;
|
||||
relativePath: string;
|
||||
fileSize: number | null;
|
||||
selection: { startLine: number; endLine: number; text: string } | null;
|
||||
};
|
||||
|
||||
const isSameActiveEditorFilePayload = (a: ActiveEditorFilePayload | null, b: ActiveEditorFilePayload | null): boolean => {
|
||||
if (a === b) return true;
|
||||
if (!a || !b) return false;
|
||||
return a.filePath === b.filePath
|
||||
&& a.fileName === b.fileName
|
||||
&& a.relativePath === b.relativePath
|
||||
&& a.fileSize === b.fileSize
|
||||
&& a.selection?.startLine === b.selection?.startLine
|
||||
&& a.selection?.endLine === b.selection?.endLine
|
||||
&& a.selection?.text === b.selection?.text;
|
||||
};
|
||||
|
||||
export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
public static readonly viewType = 'openchamber.chatView';
|
||||
|
||||
@@ -23,6 +43,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
private _sseCounter = 0;
|
||||
private _sseStreams = new Map<string, AbortController>();
|
||||
private readonly _webviewDevServerUrl: string | null;
|
||||
private _broadcastSelectionDebounce: ReturnType<typeof setTimeout> | undefined;
|
||||
private _clearActiveEditorFileTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
private _lastActiveEditorFilePayload: ActiveEditorFilePayload | null = null;
|
||||
|
||||
// Message delivery confirmation and retry
|
||||
private readonly _pendingMessages = new Set<string>();
|
||||
@@ -48,6 +71,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
private readonly _openCodeManager?: OpenCodeManager
|
||||
) {
|
||||
this._webviewDevServerUrl = resolveWebviewDevServerUrl(this._context);
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor(() => void this._broadcastActiveEditorFile()),
|
||||
vscode.window.onDidChangeTextEditorSelection(() => this._scheduleBroadcast()),
|
||||
);
|
||||
}
|
||||
|
||||
public resolveWebviewView(
|
||||
@@ -70,6 +98,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
// Send cached connection status and API URL (may have been set before webview was resolved)
|
||||
this._sendCachedState();
|
||||
|
||||
// Send current active editor file state to the new webview
|
||||
this._lastActiveEditorFilePayload = null;
|
||||
void this._broadcastActiveEditorFile();
|
||||
|
||||
webviewView.onDidDispose(() => {
|
||||
this._clearPendingMessages();
|
||||
});
|
||||
@@ -304,6 +336,85 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
}
|
||||
|
||||
private _scheduleBroadcast(): void {
|
||||
if (this._broadcastSelectionDebounce !== undefined) {
|
||||
clearTimeout(this._broadcastSelectionDebounce);
|
||||
}
|
||||
this._broadcastSelectionDebounce = setTimeout(() => {
|
||||
this._broadcastSelectionDebounce = undefined;
|
||||
void this._broadcastActiveEditorFile();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
private _scheduleClearActiveEditorFile(): void {
|
||||
if (this._clearActiveEditorFileTimer !== undefined) {
|
||||
clearTimeout(this._clearActiveEditorFileTimer);
|
||||
}
|
||||
this._clearActiveEditorFileTimer = setTimeout(() => {
|
||||
this._clearActiveEditorFileTimer = undefined;
|
||||
if (!this._view || this._lastActiveEditorFilePayload === null) {
|
||||
return;
|
||||
}
|
||||
this._lastActiveEditorFilePayload = null;
|
||||
this._view.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'activeEditorFile',
|
||||
payload: null,
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private async _broadcastActiveEditorFile() {
|
||||
if (!this._view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document.uri.scheme !== 'file') {
|
||||
this._scheduleClearActiveEditorFile();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._clearActiveEditorFileTimer !== undefined) {
|
||||
clearTimeout(this._clearActiveEditorFileTimer);
|
||||
this._clearActiveEditorFileTimer = undefined;
|
||||
}
|
||||
|
||||
const filePath = normalizeWindowsDriveLetter(editor.document.uri.fsPath);
|
||||
const rawFileName = editor.document.uri.fsPath;
|
||||
const fileName = rawFileName.replace(/\\/g, '/').split('/').pop() || '';
|
||||
const relativePath = vscode.workspace.asRelativePath(editor.document.uri, false);
|
||||
|
||||
let fileSize: number | null = null;
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(editor.document.uri);
|
||||
fileSize = stat.size;
|
||||
} catch {
|
||||
// File may not be saved yet or inaccessible
|
||||
}
|
||||
|
||||
let selection: { startLine: number; endLine: number; text: string } | null = null;
|
||||
if (!editor.selection.isEmpty) {
|
||||
selection = {
|
||||
startLine: editor.selection.start.line + 1,
|
||||
endLine: editor.selection.end.line + 1,
|
||||
text: editor.document.getText(editor.selection),
|
||||
};
|
||||
}
|
||||
|
||||
const payload: ActiveEditorFilePayload = { filePath, fileName, relativePath, fileSize, selection };
|
||||
if (isSameActiveEditorFilePayload(this._lastActiveEditorFilePayload, payload)) {
|
||||
return;
|
||||
}
|
||||
this._lastActiveEditorFilePayload = payload;
|
||||
|
||||
this._view.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'activeEditorFile',
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
private _buildSseHeaders(extra?: Record<string, string>): Record<string, string> {
|
||||
return {
|
||||
Accept: 'text/event-stream',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type VSCodeThemeKind,
|
||||
type VSCodeThemePayload,
|
||||
} from '@openchamber/ui/lib/theme/vscode/adapter';
|
||||
import type { VSCodeActiveEditorFile } from '@/sync/input-store';
|
||||
|
||||
type ConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected';
|
||||
type PanelType = 'chat' | 'agentManager';
|
||||
@@ -1234,6 +1235,13 @@ onCommand('settingsSynced', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for active editor file changes from the extension
|
||||
onCommand('activeEditorFile', (payload) => {
|
||||
import('@/sync/input-store').then(({ useInputStore }) => {
|
||||
useInputStore.getState().setActiveEditorFile((payload as VSCodeActiveEditorFile | null) ?? null);
|
||||
});
|
||||
});
|
||||
|
||||
import('@/main')
|
||||
.then(async () => {
|
||||
await waitForUiMount();
|
||||
|
||||
Reference in New Issue
Block a user