Enable sidebar view

This commit is contained in:
andrepimenta
2025-07-08 12:48:15 +01:00
parent b2579ed0f8
commit 586b004273
2 changed files with 222 additions and 182 deletions

View File

@@ -130,6 +130,7 @@
"claude-code-chat": [ "claude-code-chat": [
{ {
"id": "claude-code-chat.chat", "id": "claude-code-chat.chat",
"type": "webview",
"name": "Claude Code Chat", "name": "Claude Code Chat",
"when": "true", "when": "true",
"icon": "icon.png", "icon": "icon.png",

View File

@@ -19,12 +19,9 @@ export function activate(context: vscode.ExtensionContext) {
provider.loadConversation(filename); provider.loadConversation(filename);
}); });
// Register tree data provider for the activity bar view // Register webview view provider for sidebar chat (using shared provider instance)
const treeProvider = new ClaudeChatViewProvider(context.extensionUri, context); const webviewProvider = new ClaudeChatWebviewProvider(context.extensionUri, context, provider);
vscode.window.registerTreeDataProvider('claude-code-chat.chat', treeProvider); vscode.window.registerWebviewViewProvider('claude-code-chat.chat', webviewProvider);
// Make tree provider accessible to chat provider for refreshing
provider.setTreeProvider(treeProvider);
// Listen for configuration changes // Listen for configuration changes
const configChangeDisposable = vscode.workspace.onDidChangeConfiguration(event => { const configChangeDisposable = vscode.workspace.onDidChangeConfiguration(event => {
@@ -47,70 +44,47 @@ export function activate(context: vscode.ExtensionContext) {
export function deactivate() { } export function deactivate() { }
class ClaudeChatViewProvider implements vscode.TreeDataProvider<vscode.TreeItem> { class ClaudeChatWebviewProvider implements vscode.WebviewViewProvider {
private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined | null | void> = new vscode.EventEmitter<vscode.TreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
constructor( constructor(
private extensionUri: vscode.Uri, private readonly _extensionUri: vscode.Uri,
private context: vscode.ExtensionContext private readonly _context: vscode.ExtensionContext,
) { } private readonly _chatProvider: ClaudeChatProvider
) {}
refresh(): void { public resolveWebviewView(
this._onDidChangeTreeData.fire(); webviewView: vscode.WebviewView,
} _context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
getTreeItem(element: vscode.TreeItem): vscode.TreeItem { webviewView.webview.options = {
return element; enableScripts: true,
} localResourceRoots: [this._extensionUri]
getChildren(): vscode.TreeItem[] {
const items: vscode.TreeItem[] = [];
// Add "Open Claude Code Chat" item
const openChatItem = new vscode.TreeItem('Open Claude Code Chat', vscode.TreeItemCollapsibleState.None);
openChatItem.command = {
command: 'claude-code-chat.openChat',
title: 'Open Claude Code Chat'
}; };
openChatItem.iconPath = vscode.Uri.joinPath(this.extensionUri, 'icon.png');
openChatItem.tooltip = 'Open Claude Code Chat (Ctrl+Shift+C)';
items.push(openChatItem);
// Add conversation history items // Use the shared chat provider instance for the sidebar
const conversationIndex = this.context.workspaceState.get('claude.conversationIndex', []) as any[]; this._chatProvider.showInWebview(webviewView.webview, webviewView);
if (conversationIndex.length > 0) { // Handle visibility changes to reinitialize when sidebar reopens
// Add separator webviewView.onDidChangeVisibility(() => {
const separatorItem = new vscode.TreeItem('Recent Conversations', vscode.TreeItemCollapsibleState.None); if (webviewView.visible) {
separatorItem.description = ''; // Close main panel when sidebar becomes visible
separatorItem.tooltip = 'Click on any conversation to load it'; if (this._chatProvider._panel) {
items.push(separatorItem); console.log('Closing main panel because sidebar became visible');
this._chatProvider._panel.dispose();
// Add conversation items (show only last 5 for cleaner UI) this._chatProvider._panel = undefined;
conversationIndex.slice(0, 20).forEach((conv, index) => { }
const item = new vscode.TreeItem( this._chatProvider.reinitializeWebview();
conv.firstUserMessage.substring(0, 50) + (conv.firstUserMessage.length > 50 ? '...' : ''), }
vscode.TreeItemCollapsibleState.None });
);
item.description = new Date(conv.startTime).toLocaleDateString();
item.tooltip = `First: ${conv.firstUserMessage}\nLast: ${conv.lastUserMessage}\nMessages: ${conv.messageCount}, Cost: $${conv.totalCost.toFixed(3)}`;
item.command = {
command: 'claude-code-chat.loadConversation',
title: 'Load Conversation',
arguments: [conv.filename]
};
item.iconPath = new vscode.ThemeIcon('comment-discussion');
items.push(item);
});
}
return items;
} }
} }
class ClaudeChatProvider { class ClaudeChatProvider {
private _panel: vscode.WebviewPanel | undefined; public _panel: vscode.WebviewPanel | undefined;
private _webview: vscode.Webview | undefined;
private _webviewView: vscode.WebviewView | undefined;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private _totalCost: number = 0; private _totalCost: number = 0;
private _totalTokensInput: number = 0; private _totalTokensInput: number = 0;
@@ -132,7 +106,6 @@ class ClaudeChatProvider {
firstUserMessage: string, firstUserMessage: string,
lastUserMessage: string lastUserMessage: string
}> = []; }> = [];
private _treeProvider: ClaudeChatViewProvider | undefined;
private _currentClaudeProcess: cp.ChildProcess | undefined; private _currentClaudeProcess: cp.ChildProcess | undefined;
private _selectedModel: string = 'default'; // Default model private _selectedModel: string = 'default'; // Default model
@@ -159,6 +132,9 @@ class ClaudeChatProvider {
public show() { public show() {
const column = vscode.ViewColumn.Two; const column = vscode.ViewColumn.Two;
// Close sidebar if it's open
this._closeSidebar();
if (this._panel) { if (this._panel) {
this._panel.reveal(column); this._panel.reveal(column);
return; return;
@@ -183,65 +159,7 @@ class ClaudeChatProvider {
this._panel.onDidDispose(() => this.dispose(), null, this._disposables); this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
this._panel.webview.onDidReceiveMessage( this._setupWebviewMessageHandler(this._panel.webview);
message => {
switch (message.type) {
case 'sendMessage':
this._sendMessageToClaude(message.text, message.planMode, message.thinkingMode);
return;
case 'newSession':
this._newSession();
return;
case 'restoreCommit':
this._restoreToCommit(message.commitSha);
return;
case 'getConversationList':
this._sendConversationList();
return;
case 'getWorkspaceFiles':
this._sendWorkspaceFiles(message.searchTerm);
return;
case 'selectImageFile':
this._selectImageFile();
return;
case 'loadConversation':
this.loadConversation(message.filename);
return;
case 'stopRequest':
this._stopClaudeProcess();
return;
case 'getSettings':
this._sendCurrentSettings();
return;
case 'updateSettings':
this._updateSettings(message.settings);
return;
case 'getClipboardText':
this._getClipboardText();
return;
case 'selectModel':
this._setSelectedModel(message.model);
return;
case 'openModelTerminal':
this._openModelTerminal();
return;
case 'executeSlashCommand':
this._executeSlashCommand(message.command);
return;
case 'dismissWSLAlert':
this._dismissWSLAlert();
return;
case 'openFile':
this._openFileInEditor(message.filePath);
return;
case 'createImageFile':
this._createImageFile(message.imageData, message.imageType);
return;
}
},
null,
this._disposables
);
// Resume session from latest conversation // Resume session from latest conversation
const latestConversation = this._getLatestConversation(); const latestConversation = this._getLatestConversation();
@@ -254,33 +172,160 @@ class ClaudeChatProvider {
// Send ready message immediately // Send ready message immediately
setTimeout(() => { setTimeout(() => {
// Send current session info if available // If no conversation to load, send ready immediately
if (this._currentSessionId) { if (!latestConversation) {
this._panel?.webview.postMessage({ this._sendReadyMessage();
type: 'sessionResumed',
data: {
sessionId: this._currentSessionId
}
});
} }
this._panel?.webview.postMessage({
type: 'ready',
data: 'Ready to chat with Claude Code! Type your message below.'
});
// Send current model to webview
this._panel?.webview.postMessage({
type: 'modelSelected',
model: this._selectedModel
});
// Send platform information to webview
this._sendPlatformInfo();
}, 100); }, 100);
} }
private _postMessage(message: any) {
if (this._panel && this._panel.webview) {
this._panel.webview.postMessage(message);
} else if (this._webview) {
this._webview.postMessage(message);
}
}
private _sendReadyMessage() {
// Send current session info if available
if (this._currentSessionId) {
this._postMessage({
type: 'sessionResumed',
data: {
sessionId: this._currentSessionId
}
});
}
this._postMessage({
type: 'ready',
data: 'Ready to chat with Claude Code! Type your message below.'
});
// Send current model to webview
this._postMessage({
type: 'modelSelected',
model: this._selectedModel
});
// Send platform information to webview
this._sendPlatformInfo();
}
private _handleWebviewMessage(message: any) {
switch (message.type) {
case 'sendMessage':
this._sendMessageToClaude(message.text, message.planMode, message.thinkingMode);
return;
case 'newSession':
this._newSession();
return;
case 'restoreCommit':
this._restoreToCommit(message.commitSha);
return;
case 'getConversationList':
this._sendConversationList();
return;
case 'getWorkspaceFiles':
this._sendWorkspaceFiles(message.searchTerm);
return;
case 'selectImageFile':
this._selectImageFile();
return;
case 'loadConversation':
this.loadConversation(message.filename);
return;
case 'stopRequest':
this._stopClaudeProcess();
return;
case 'getSettings':
this._sendCurrentSettings();
return;
case 'updateSettings':
this._updateSettings(message.settings);
return;
case 'getClipboardText':
this._getClipboardText();
return;
case 'selectModel':
this._setSelectedModel(message.model);
return;
case 'openModelTerminal':
this._openModelTerminal();
return;
case 'executeSlashCommand':
this._executeSlashCommand(message.command);
return;
case 'dismissWSLAlert':
this._dismissWSLAlert();
return;
case 'openFile':
this._openFileInEditor(message.filePath);
return;
case 'createImageFile':
this._createImageFile(message.imageData, message.imageType);
return;
}
}
private _setupWebviewMessageHandler(webview: vscode.Webview) {
webview.onDidReceiveMessage(
message => this._handleWebviewMessage(message),
null,
this._disposables
);
}
private _closeSidebar() {
if (this._webviewView) {
// Switch VS Code to show Explorer view instead of chat sidebar
vscode.commands.executeCommand('workbench.view.explorer');
}
}
public showInWebview(webview: vscode.Webview, webviewView?: vscode.WebviewView) {
// Close main panel if it's open
if (this._panel) {
console.log('Closing main panel because sidebar is opening');
this._panel.dispose();
this._panel = undefined;
}
this._webview = webview;
this._webviewView = webviewView;
this._webview.html = this._getHtmlForWebview();
this._setupWebviewMessageHandler(this._webview);
// Initialize the webview
this._initializeWebview();
}
private _initializeWebview() {
// Resume session from latest conversation
const latestConversation = this._getLatestConversation();
this._currentSessionId = latestConversation?.sessionId;
// Load latest conversation history if available
if (latestConversation) {
this._loadConversationHistory(latestConversation.filename);
} else {
// If no conversation to load, send ready immediately
setTimeout(() => {
this._sendReadyMessage();
}, 100);
}
}
public reinitializeWebview() {
// Only reinitialize if we have a webview (sidebar)
if (this._webview) {
this._initializeWebview();
this._setupWebviewMessageHandler(this._webview);
}
}
private async _sendMessageToClaude(message: string, planMode?: boolean, thinkingMode?: boolean) { private async _sendMessageToClaude(message: string, planMode?: boolean, thinkingMode?: boolean) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const cwd = workspaceFolder ? workspaceFolder.uri.fsPath : process.cwd(); const cwd = workspaceFolder ? workspaceFolder.uri.fsPath : process.cwd();
@@ -323,7 +368,7 @@ class ClaudeChatProvider {
}); });
// Set processing state // Set processing state
this._panel?.webview.postMessage({ this._postMessage({
type: 'setProcessing', type: 'setProcessing',
data: true data: true
}); });
@@ -337,7 +382,7 @@ class ClaudeChatProvider {
} }
// Show loading indicator // Show loading indicator
this._panel?.webview.postMessage({ this._postMessage({
type: 'loading', type: 'loading',
data: 'Claude is working...' data: 'Claude is working...'
}); });
@@ -450,7 +495,7 @@ class ClaudeChatProvider {
this._currentClaudeProcess = undefined; this._currentClaudeProcess = undefined;
// Clear loading indicator // Clear loading indicator
this._panel?.webview.postMessage({ this._postMessage({
type: 'clearLoading' type: 'clearLoading'
}); });
@@ -469,7 +514,7 @@ class ClaudeChatProvider {
// Clear process reference // Clear process reference
this._currentClaudeProcess = undefined; this._currentClaudeProcess = undefined;
this._panel?.webview.postMessage({ this._postMessage({
type: 'clearLoading' type: 'clearLoading'
}); });
@@ -646,7 +691,7 @@ class ClaudeChatProvider {
} }
// Clear processing state // Clear processing state
this._panel?.webview.postMessage({ this._postMessage({
type: 'setProcessing', type: 'setProcessing',
data: false data: false
}); });
@@ -664,7 +709,7 @@ class ClaudeChatProvider {
}); });
// Send updated totals to webview // Send updated totals to webview
this._panel?.webview.postMessage({ this._postMessage({
type: 'updateTotals', type: 'updateTotals',
data: { data: {
totalCost: this._totalCost, totalCost: this._totalCost,
@@ -698,7 +743,7 @@ class ClaudeChatProvider {
this._requestCount = 0; this._requestCount = 0;
// Notify webview to clear all messages and reset session // Notify webview to clear all messages and reset session
this._panel?.webview.postMessage({ this._postMessage({
type: 'sessionCleared' type: 'sessionCleared'
}); });
} }
@@ -722,13 +767,13 @@ class ClaudeChatProvider {
private _handleLoginRequired() { private _handleLoginRequired() {
// Clear processing state // Clear processing state
this._panel?.webview.postMessage({ this._postMessage({
type: 'setProcessing', type: 'setProcessing',
data: false data: false
}); });
// Show login required message // Show login required message
this._panel?.webview.postMessage({ this._postMessage({
type: 'loginRequired' type: 'loginRequired'
}); });
@@ -755,7 +800,7 @@ class ClaudeChatProvider {
); );
// Send message to UI about terminal // Send message to UI about terminal
this._panel?.webview.postMessage({ this._postMessage({
type: 'terminalOpened', type: 'terminalOpened',
data: `Please login to Claude in the terminal, then come back to this chat to continue.`, data: `Please login to Claude in the terminal, then come back to this chat to continue.`,
}); });
@@ -860,7 +905,7 @@ class ClaudeChatProvider {
try { try {
const commit = this._commits.find(c => c.sha === commitSha); const commit = this._commits.find(c => c.sha === commitSha);
if (!commit) { if (!commit) {
this._panel?.webview.postMessage({ this._postMessage({
type: 'restoreError', type: 'restoreError',
data: 'Commit not found' data: 'Commit not found'
}); });
@@ -875,7 +920,7 @@ class ClaudeChatProvider {
const workspacePath = workspaceFolder.uri.fsPath; const workspacePath = workspaceFolder.uri.fsPath;
this._panel?.webview.postMessage({ this._postMessage({
type: 'restoreProgress', type: 'restoreProgress',
data: 'Restoring files from backup...' data: 'Restoring files from backup...'
}); });
@@ -896,7 +941,7 @@ class ClaudeChatProvider {
} catch (error: any) { } catch (error: any) {
console.error('Failed to restore commit:', error.message); console.error('Failed to restore commit:', error.message);
vscode.window.showErrorMessage(`Failed to restore commit: ${error.message}`); vscode.window.showErrorMessage(`Failed to restore commit: ${error.message}`);
this._panel?.webview.postMessage({ this._postMessage({
type: 'restoreError', type: 'restoreError',
data: `Failed to restore: ${error.message}` data: `Failed to restore: ${error.message}`
}); });
@@ -935,8 +980,8 @@ class ClaudeChatProvider {
message.data.sessionId; message.data.sessionId;
} }
// Send to UI // Send to UI using the helper method
this._panel?.webview.postMessage(message); this._postMessage(message);
// Save to conversation // Save to conversation
this._currentConversation.push({ this._currentConversation.push({
@@ -997,20 +1042,14 @@ class ClaudeChatProvider {
} }
} }
public setTreeProvider(treeProvider: ClaudeChatViewProvider): void {
this._treeProvider = treeProvider;
}
public async loadConversation(filename: string): Promise<void> { public async loadConversation(filename: string): Promise<void> {
// Show the webview first
this.show();
// Load the conversation history // Load the conversation history
await this._loadConversationHistory(filename); await this._loadConversationHistory(filename);
} }
private _sendConversationList(): void { private _sendConversationList(): void {
this._panel?.webview.postMessage({ this._postMessage({
type: 'conversationList', type: 'conversationList',
data: this._conversationIndex data: this._conversationIndex
}); });
@@ -1053,13 +1092,13 @@ class ClaudeChatProvider {
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.slice(0, 50); .slice(0, 50);
this._panel?.webview.postMessage({ this._postMessage({
type: 'workspaceFiles', type: 'workspaceFiles',
data: fileList data: fileList
}); });
} catch (error) { } catch (error) {
console.error('Error getting workspace files:', error); console.error('Error getting workspace files:', error);
this._panel?.webview.postMessage({ this._postMessage({
type: 'workspaceFiles', type: 'workspaceFiles',
data: [] data: []
}); });
@@ -1082,7 +1121,7 @@ class ClaudeChatProvider {
if (result && result.length > 0) { if (result && result.length > 0) {
// Send the selected file paths back to webview // Send the selected file paths back to webview
result.forEach(uri => { result.forEach(uri => {
this._panel?.webview.postMessage({ this._postMessage({
type: 'imagePath', type: 'imagePath',
path: uri.fsPath path: uri.fsPath
}); });
@@ -1115,12 +1154,12 @@ class ClaudeChatProvider {
this._currentClaudeProcess = undefined; this._currentClaudeProcess = undefined;
// Update UI state // Update UI state
this._panel?.webview.postMessage({ this._postMessage({
type: 'setProcessing', type: 'setProcessing',
data: false data: false
}); });
this._panel?.webview.postMessage({ this._postMessage({
type: 'clearLoading' type: 'clearLoading'
}); });
@@ -1167,9 +1206,6 @@ class ClaudeChatProvider {
// Save to workspace state // Save to workspace state
this._context.workspaceState.update('claude.conversationIndex', this._conversationIndex); this._context.workspaceState.update('claude.conversationIndex', this._conversationIndex);
// Refresh tree view
this._treeProvider?.refresh();
} }
private _getLatestConversation(): any | undefined { private _getLatestConversation(): any | undefined {
@@ -1204,21 +1240,21 @@ class ClaudeChatProvider {
// Clear UI messages first, then send all messages to recreate the conversation // Clear UI messages first, then send all messages to recreate the conversation
setTimeout(() => { setTimeout(() => {
// Clear existing messages // Clear existing messages
this._panel?.webview.postMessage({ this._postMessage({
type: 'sessionCleared' type: 'sessionCleared'
}); });
// Small delay to ensure messages are cleared before loading new ones // Small delay to ensure messages are cleared before loading new ones
setTimeout(() => { setTimeout(() => {
for (const message of this._currentConversation) { for (const message of this._currentConversation) {
this._panel?.webview.postMessage({ this._postMessage({
type: message.messageType, type: message.messageType,
data: message.data data: message.data
}); });
} }
// Send updated totals // Send updated totals
this._panel?.webview.postMessage({ this._postMessage({
type: 'updateTotals', type: 'updateTotals',
data: { data: {
totalCost: this._totalCost, totalCost: this._totalCost,
@@ -1227,6 +1263,9 @@ class ClaudeChatProvider {
requestCount: this._requestCount requestCount: this._requestCount
} }
}); });
// Send ready message after conversation is loaded
this._sendReadyMessage();
}, 50); }, 50);
}, 100); // Small delay to ensure webview is ready }, 100); // Small delay to ensure webview is ready
@@ -1250,7 +1289,7 @@ class ClaudeChatProvider {
'wsl.claudePath': config.get<string>('wsl.claudePath', '/usr/local/bin/claude') 'wsl.claudePath': config.get<string>('wsl.claudePath', '/usr/local/bin/claude')
}; };
this._panel?.webview.postMessage({ this._postMessage({
type: 'settingsData', type: 'settingsData',
data: settings data: settings
}); });
@@ -1274,7 +1313,7 @@ class ClaudeChatProvider {
private async _getClipboardText(): Promise<void> { private async _getClipboardText(): Promise<void> {
try { try {
const text = await vscode.env.clipboard.readText(); const text = await vscode.env.clipboard.readText();
this._panel?.webview.postMessage({ this._postMessage({
type: 'clipboardText', type: 'clipboardText',
data: text data: text
}); });
@@ -1332,7 +1371,7 @@ class ClaudeChatProvider {
); );
// Send message to UI about terminal // Send message to UI about terminal
this._panel?.webview.postMessage({ this._postMessage({
type: 'terminalOpened', type: 'terminalOpened',
data: 'Check the terminal to update your default model configuration. Come back to this chat here after making changes.' data: 'Check the terminal to update your default model configuration. Come back to this chat here after making changes.'
}); });
@@ -1369,7 +1408,7 @@ class ClaudeChatProvider {
); );
// Send message to UI about terminal // Send message to UI about terminal
this._panel?.webview.postMessage({ this._postMessage({
type: 'terminalOpened', type: 'terminalOpened',
data: `Executing /${command} command in terminal. Check the terminal output and return when ready.`, data: `Executing /${command} command in terminal. Check the terminal output and return when ready.`,
}); });
@@ -1383,7 +1422,7 @@ class ClaudeChatProvider {
const config = vscode.workspace.getConfiguration('claudeCodeChat'); const config = vscode.workspace.getConfiguration('claudeCodeChat');
const wslEnabled = config.get<boolean>('wsl.enabled', false); const wslEnabled = config.get<boolean>('wsl.enabled', false);
this._panel?.webview.postMessage({ this._postMessage({
type: 'platformInfo', type: 'platformInfo',
data: { data: {
platform: platform, platform: platform,
@@ -1434,7 +1473,7 @@ class ClaudeChatProvider {
await vscode.workspace.fs.writeFile(imagePath, buffer); await vscode.workspace.fs.writeFile(imagePath, buffer);
// Send the file path back to webview // Send the file path back to webview
this._panel?.webview.postMessage({ this._postMessage({
type: 'imagePath', type: 'imagePath',
data: { data: {
filePath: imagePath.fsPath filePath: imagePath.fsPath