Files
claude-code-chat/src/ui.ts
2025-06-24 01:59:24 +01:00

2040 lines
69 KiB
TypeScript

import styles from './ui-styles'
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code Chat</title>
${styles}
</head>
<body>
<div class="header">
<div style="display: flex; align-items: center;">
<h2>Claude Code Chat</h2>
<!-- <div id="sessionInfo" class="session-badge" style="display: none;">
<span class="session-icon">💬</span>
<span id="sessionId">-</span>
<span class="session-label">session</span>
</div> -->
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<div id="sessionStatus" class="session-status" style="display: none;">No session</div>
<button class="btn outlined" id="settingsBtn" onclick="toggleSettings()" title="Settings">⚙️</button>
<button class="btn outlined" id="historyBtn" onclick="toggleConversationHistory()" style="display: none;">📚 History</button>
<button class="btn primary" id="newSessionBtn" onclick="newSession()" style="display: none;">New Chat</button>
</div>
</div>
<div id="conversationHistory" class="conversation-history" style="display: none;">
<div class="conversation-header">
<h3>Conversation History</h3>
<button class="btn" onclick="toggleConversationHistory()">✕ Close</button>
</div>
<div id="conversationList" class="conversation-list">
<!-- Conversations will be loaded here -->
</div>
</div>
<div class="chat-container" id="chatContainer">
<div class="messages" id="messages"></div>
<!-- WSL Alert for Windows users -->
<div id="wslAlert" class="wsl-alert" style="display: none;">
<div class="wsl-alert-content">
<div class="wsl-alert-icon">💻</div>
<div class="wsl-alert-text">
<strong>Looks like you are using Windows!</strong><br/>
If you are using WSL to run Claude Code, you should enable WSL integration in the settings.
</div>
<div class="wsl-alert-actions">
<button class="btn" onclick="openWSLSettings()">Enable WSL</button>
<button class="btn outlined" onclick="dismissWSLAlert()">Dismiss</button>
</div>
</div>
</div>
<div class="input-container" id="inputContainer">
<div class="input-modes">
<div class="mode-toggle">
<span onclick="togglePlanMode()">Plan First</span>
<div class="mode-switch" id="planModeSwitch" onclick="togglePlanMode()"></div>
</div>
<div class="mode-toggle">
<span id="thinkingModeLabel" onclick="toggleThinkingMode()">Thinking Mode</span>
<div class="mode-switch" id="thinkingModeSwitch" onclick="toggleThinkingMode()"></div>
</div>
</div>
<div class="textarea-container">
<div class="textarea-wrapper">
<textarea class="input-field" id="messageInput" placeholder="Type your message to Claude Code..." rows="1"></textarea>
<div class="input-controls">
<div class="left-controls">
<button class="model-selector" id="modelSelector" onclick="showModelSelector()" title="Select model">
<span id="selectedModel">Opus</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M1 2.5l3 3 3-3"></path>
</svg>
</button>
<button class="tools-btn" onclick="showToolsModal()" title="Configure tools">
Tools: All
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M1 2.5l3 3 3-3"></path>
</svg>
</button>
</div>
<div class="right-controls">
<button class="at-btn" onclick="showFilePicker()" title="Reference files">@</button>
<button class="image-btn" id="imageBtn" onclick="selectImage()" title="Attach images">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width="14"
height="16"
>
<g fill="currentColor">
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0"></path>
<path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71l-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54L1 12.5v-9a.5.5 0 0 1 .5-.5z"></path>
</g>
</svg>
</button>
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
<div>
<span>Send </span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="11"
height="11"
>
<path
fill="currentColor"
d="M20 4v9a4 4 0 0 1-4 4H6.914l2.5 2.5L8 20.914L3.086 16L8 11.086L9.414 12.5l-2.5 2.5H16a2 2 0 0 0 2-2V4z"
></path>
</svg>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="status ready" id="status">
<div class="status-indicator"></div>
<div class="status-text" id="statusText">Initializing...</div>
<button class="btn stop" id="stopBtn" onclick="stopRequest()" style="display: none;">
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 6h12v12H6z"/>
</svg>
Stop
</button>
</div>
<div class="beta-warning">
In Beta. All Claude Code tools are allowed. Use at your own risk.
</div>
<!-- File picker modal -->
<div id="filePickerModal" class="file-picker-modal" style="display: none;">
<div class="file-picker-content">
<div class="file-picker-header">
<span>Select File</span>
<input type="text" id="fileSearchInput" placeholder="Search files..." class="file-search-input">
</div>
<div id="fileList" class="file-list">
<!-- Files will be loaded here -->
</div>
</div>
</div>
<!-- Tools modal -->
<div id="toolsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-header">
<span>Claude Code Tools</span>
<button class="tools-close-btn" onclick="hideToolsModal()">✕</button>
</div>
<div class="tools-beta-warning">
In Beta: All tools are enabled by default. Use at your own risk.
</div>
<div id="toolsList" class="tools-list">
<div class="tool-item">
<input type="checkbox" id="tool-bash" checked disabled>
<label for="tool-bash">Bash - Execute shell commands</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-read" checked disabled>
<label for="tool-read">Read - Read file contents</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-edit" checked disabled>
<label for="tool-edit">Edit - Modify files</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-write" checked disabled>
<label for="tool-write">Write - Create new files</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-glob" checked disabled>
<label for="tool-glob">Glob - Find files by pattern</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-grep" checked disabled>
<label for="tool-grep">Grep - Search file contents</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-ls" checked disabled>
<label for="tool-ls">LS - List directory contents</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-multiedit" checked disabled>
<label for="tool-multiedit">MultiEdit - Edit multiple files</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-websearch" checked disabled>
<label for="tool-websearch">WebSearch - Search the web</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-webfetch" checked disabled>
<label for="tool-webfetch">WebFetch - Fetch web content</label>
</div>
</div>
</div>
</div>
<!-- Settings modal -->
<div id="settingsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-header">
<span>Claude Code Chat Settings</span>
<button class="tools-close-btn" onclick="hideSettingsModal()">✕</button>
</div>
<div class="tools-list">
<h3 style="margin-top: 0; margin-bottom: 16px; font-size: 14px; font-weight: 600;">WSL Configuration</h3>
<div>
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 0;">
WSL integration allows you to run Claude Code from within Windows Subsystem for Linux.
This is useful if you have Claude installed in WSL instead of Windows.
</p>
</div>
<div class="settings-group">
<div class="tool-item">
<input type="checkbox" id="wsl-enabled" onchange="updateSettings()">
<label for="wsl-enabled">Enable WSL Integration</label>
</div>
<div id="wslOptions" style="margin-left: 24px; margin-top: 12px;">
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--vscode-descriptionForeground);">WSL Distribution</label>
<input type="text" id="wsl-distro" class="file-search-input" style="width: 100%;" placeholder="Ubuntu" onchange="updateSettings()">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--vscode-descriptionForeground);">Node.js Path in WSL</label>
<input type="text" id="wsl-node-path" class="file-search-input" style="width: 100%;" placeholder="/usr/bin/node" onchange="updateSettings()">
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 4px 0 0 0;">
Find your node installation path in WSL by running: <code style="background: var(--vscode-textCodeBlock-background); padding: 2px 4px; border-radius: 3px;">which node</code>
</p>
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--vscode-descriptionForeground);">Claude Path in WSL</label>
<input type="text" id="wsl-claude-path" class="file-search-input" style="width: 100%;" placeholder="/usr/local/bin/claude" onchange="updateSettings()">
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 4px 0 0 0;">
Find your claude installation path in WSL by running: <code style="background: var(--vscode-textCodeBlock-background); padding: 2px 4px; border-radius: 3px;">which claude</code>
</p>
</div>
</div>
</div>
<h3 style="margin-top: 24px; margin-bottom: 16px; font-size: 14px; font-weight: 600;">MCP Configuration (coming soon)</h3>
<div>
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 0;">
Model Context Protocol (MCP) allows Claude Code to connect to external systems and services for enhanced capabilities like databases, APIs, and tools.
</p>
</div>
<div class="settings-group">
<div class="tool-item">
<input type="checkbox" id="mcp-enabled" disabled>
<label for="mcp-enabled">Enable MCP Integration <span style="font-style: italic; opacity: 0.7;">(Coming Soon)</span></label>
</div>
</div>
<h3 style="margin-top: 24px; margin-bottom: 16px; font-size: 14px; font-weight: 600;">Custom Slash Commands (coming soon)</h3>
<div>
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 0;">
Add your own custom slash commands that will appear in the commands modal. Define shortcuts for frequently used terminal commands.
</p>
</div>
<div class="settings-group">
<div class="tool-item">
<input type="checkbox" id="custom-commands-enabled" disabled>
<label for="custom-commands-enabled">Enable Custom Commands <span style="font-style: italic; opacity: 0.7;">(Coming Soon)</span></label>
</div>
</div>
</div>
</div>
</div>
<!-- Model selector modal -->
<div id="modelModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 400px;">
<div class="tools-modal-header">
<span>Enforce Model</span>
<button class="tools-close-btn" onclick="hideModelModal()">✕</button>
</div>
<div class="model-explanatory-text">
This overrides your default model setting for this conversation only.
</div>
<div class="tools-list">
<div class="tool-item" onclick="selectModel('opus')">
<input type="radio" name="model" id="model-opus" value="opus" checked>
<label for="model-opus">
<div class="model-title">Opus - Most capable model</div>
<div class="model-description">
Best for complex tasks and highest quality output
</div>
</label>
</div>
<div class="tool-item" onclick="selectModel('sonnet')">
<input type="radio" name="model" id="model-sonnet" value="sonnet">
<label for="model-sonnet">
<div class="model-title">Sonnet - Balanced model</div>
<div class="model-description">
Good balance of speed and capability
</div>
</label>
</div>
<div class="tool-item" onclick="selectModel('default')">
<input type="radio" name="model" id="model-default" value="default">
<label for="model-default" class="default-model-layout">
<div class="model-option-content">
<div class="model-title">Default - User configured</div>
<div class="model-description">
Uses the model configured in your settings
</div>
</div>
<button class="secondary-button configure-button" onclick="event.stopPropagation(); openModelTerminal();">
Configure
</button>
</label>
</div>
</div>
</div>
</div>
<!-- Thinking intensity modal -->
<div id="thinkingIntensityModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 450px;">
<div class="tools-modal-header">
<span>Thinking Mode Intensity</span>
<button class="tools-close-btn" onclick="hideThinkingIntensityModal()">✕</button>
</div>
<div class="thinking-modal-description">
Configure the intensity of thinking mode. Higher levels provide more detailed reasoning but consume more tokens.
</div>
<div class="tools-list">
<div class="thinking-slider-container">
<input type="range" min="0" max="3" value="0" step="1" class="thinking-slider" id="thinkingIntensitySlider" oninput="updateThinkingIntensityDisplay(this.value)">
<div class="slider-labels">
<div class="slider-label active" id="thinking-label-0" onclick="setThinkingIntensityValue(0)">Think</div>
<div class="slider-label" id="thinking-label-1" onclick="setThinkingIntensityValue(1)">Think Hard</div>
<div class="slider-label" id="thinking-label-2" onclick="setThinkingIntensityValue(2)">Think Harder</div>
<div class="slider-label" id="thinking-label-3" onclick="setThinkingIntensityValue(3)">Ultrathink</div>
</div>
</div>
<div class="thinking-modal-actions">
<button class="confirm-btn" onclick="confirmThinkingIntensity()">Confirm</button>
</div>
</div>
</div>
</div>
<!-- Slash commands modal -->
<div id="slashCommandsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-header">
<span>Claude Code Commands</span>
<button class="tools-close-btn" onclick="hideSlashCommandsModal()">✕</button>
</div>
<div class="slash-commands-info">
<p>These commands require the Claude CLI and will open in VS Code terminal. Return here after completion.</p>
</div>
<div class="slash-commands-list">
<div class="slash-command-item" onclick="executeSlashCommand('bug')">
<div class="slash-command-icon">🐛</div>
<div class="slash-command-content">
<div class="slash-command-title">/bug</div>
<div class="slash-command-description">Report bugs (sends conversation to Anthropic)</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('clear')">
<div class="slash-command-icon">🗑️</div>
<div class="slash-command-content">
<div class="slash-command-title">/clear</div>
<div class="slash-command-description">Clear conversation history</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('compact')">
<div class="slash-command-icon">📦</div>
<div class="slash-command-content">
<div class="slash-command-title">/compact</div>
<div class="slash-command-description">Compact conversation with optional focus instructions</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('config')">
<div class="slash-command-icon">⚙️</div>
<div class="slash-command-content">
<div class="slash-command-title">/config</div>
<div class="slash-command-description">View/modify configuration</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('cost')">
<div class="slash-command-icon">💰</div>
<div class="slash-command-content">
<div class="slash-command-title">/cost</div>
<div class="slash-command-description">Show token usage statistics</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('doctor')">
<div class="slash-command-icon">🩺</div>
<div class="slash-command-content">
<div class="slash-command-title">/doctor</div>
<div class="slash-command-description">Checks the health of your Claude Code installation</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('help')">
<div class="slash-command-icon">❓</div>
<div class="slash-command-content">
<div class="slash-command-title">/help</div>
<div class="slash-command-description">Get usage help</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('init')">
<div class="slash-command-icon">🚀</div>
<div class="slash-command-content">
<div class="slash-command-title">/init</div>
<div class="slash-command-description">Initialize project with CLAUDE.md guide</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('login')">
<div class="slash-command-icon">🔑</div>
<div class="slash-command-content">
<div class="slash-command-title">/login</div>
<div class="slash-command-description">Switch Anthropic accounts</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('logout')">
<div class="slash-command-icon">🚪</div>
<div class="slash-command-content">
<div class="slash-command-title">/logout</div>
<div class="slash-command-description">Sign out from your Anthropic account</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('mcp')">
<div class="slash-command-icon">🔌</div>
<div class="slash-command-content">
<div class="slash-command-title">/mcp</div>
<div class="slash-command-description">Manage MCP server connections and OAuth authentication</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('memory')">
<div class="slash-command-icon">🧠</div>
<div class="slash-command-content">
<div class="slash-command-title">/memory</div>
<div class="slash-command-description">Edit CLAUDE.md memory files</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('model')">
<div class="slash-command-icon">🤖</div>
<div class="slash-command-content">
<div class="slash-command-title">/model</div>
<div class="slash-command-description">Select or change the AI model</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('permissions')">
<div class="slash-command-icon">🔒</div>
<div class="slash-command-content">
<div class="slash-command-title">/permissions</div>
<div class="slash-command-description">View or update permissions</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('pr_comments')">
<div class="slash-command-icon">💬</div>
<div class="slash-command-content">
<div class="slash-command-title">/pr_comments</div>
<div class="slash-command-description">View pull request comments</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('review')">
<div class="slash-command-icon">👀</div>
<div class="slash-command-content">
<div class="slash-command-title">/review</div>
<div class="slash-command-description">Request code review</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('status')">
<div class="slash-command-icon">📊</div>
<div class="slash-command-content">
<div class="slash-command-title">/status</div>
<div class="slash-command-description">View account and system statuses</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('terminal-setup')">
<div class="slash-command-icon">⌨️</div>
<div class="slash-command-content">
<div class="slash-command-title">/terminal-setup</div>
<div class="slash-command-description">Install Shift+Enter key binding for newlines</div>
</div>
</div>
<div class="slash-command-item" onclick="executeSlashCommand('vim')">
<div class="slash-command-icon">📝</div>
<div class="slash-command-content">
<div class="slash-command-title">/vim</div>
<div class="slash-command-description">Enter vim mode for alternating insert and command modes</div>
</div>
</div>
<div class="slash-command-item custom-command-item">
<div class="slash-command-icon">⚡</div>
<div class="slash-command-content">
<div class="slash-command-title">Custom Command</div>
<div class="slash-command-description">
<div class="custom-command-input-container">
<span class="command-prefix">/</span>
<input type="text"
class="custom-command-input"
id="customCommandInput"
placeholder="enter-command"
onkeydown="handleCustomCommandKeydown(event)"
onclick="event.stopPropagation()">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const statusDiv = document.getElementById('status');
const statusTextDiv = document.getElementById('statusText');
const filePickerModal = document.getElementById('filePickerModal');
const fileSearchInput = document.getElementById('fileSearchInput');
const fileList = document.getElementById('fileList');
const imageBtn = document.getElementById('imageBtn');
let isProcessRunning = false;
let filteredFiles = [];
let selectedFileIndex = -1;
let planModeEnabled = false;
let thinkingModeEnabled = false;
function addMessage(content, type = 'claude') {
const messageDiv = document.createElement('div');
messageDiv.className = \`message \${type}\`;
// Add header for main message types (excluding system)
if (type === 'user' || type === 'claude' || type === 'error') {
const headerDiv = document.createElement('div');
headerDiv.className = 'message-header';
const iconDiv = document.createElement('div');
iconDiv.className = \`message-icon \${type}\`;
const labelDiv = document.createElement('div');
labelDiv.className = 'message-label';
// Set icon and label based on type
switch(type) {
case 'user':
iconDiv.textContent = '👤';
labelDiv.textContent = 'You';
break;
case 'claude':
iconDiv.textContent = '🤖';
labelDiv.textContent = 'Claude';
break;
case 'error':
iconDiv.textContent = '⚠️';
labelDiv.textContent = 'Error';
break;
}
// Add copy button
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.title = 'Copy message';
copyBtn.onclick = () => copyMessageContent(messageDiv);
copyBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>';
headerDiv.appendChild(iconDiv);
headerDiv.appendChild(labelDiv);
headerDiv.appendChild(copyBtn);
messageDiv.appendChild(headerDiv);
}
// Add content
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
if(type == 'user' || type === 'claude' || type === 'thinking'){
contentDiv.innerHTML = content;
} else {
const preElement = document.createElement('pre');
preElement.textContent = content;
contentDiv.appendChild(preElement);
}
messageDiv.appendChild(contentDiv);
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function addToolUseMessage(data) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message tool';
// Create modern header with icon
const headerDiv = document.createElement('div');
headerDiv.className = 'tool-header';
const iconDiv = document.createElement('div');
iconDiv.className = 'tool-icon';
iconDiv.textContent = '🔧';
const toolInfoElement = document.createElement('div');
toolInfoElement.className = 'tool-info';
toolInfoElement.textContent = data.toolInfo.replace('🔧 Executing: ', '');
headerDiv.appendChild(iconDiv);
headerDiv.appendChild(toolInfoElement);
messageDiv.appendChild(headerDiv);
if (data.rawInput) {
const inputElement = document.createElement('div');
inputElement.className = 'tool-input';
const contentDiv = document.createElement('div');
contentDiv.className = 'tool-input-content';
// Handle TodoWrite specially or format raw input
if (data.toolName === 'TodoWrite' && data.rawInput.todos) {
let todoHtml = 'Todo List Update:';
for (const todo of data.rawInput.todos) {
const status = todo.status === 'completed' ? '✅' :
todo.status === 'in_progress' ? '🔄' : '⏳';
todoHtml += '\\n' + status + ' ' + todo.content + ' <span class="priority-badge ' + todo.priority + '">' + todo.priority + '</span>';
}
contentDiv.innerHTML = todoHtml;
} else {
// Format raw input with expandable content for long values
contentDiv.innerHTML = formatToolInputUI(data.rawInput);
}
inputElement.appendChild(contentDiv);
messageDiv.appendChild(inputElement);
} else if (data.toolInput) {
// Fallback for pre-formatted input
const inputElement = document.createElement('div');
inputElement.className = 'tool-input';
const labelDiv = document.createElement('div');
labelDiv.className = 'tool-input-label';
labelDiv.textContent = 'INPUT';
inputElement.appendChild(labelDiv);
const contentDiv = document.createElement('div');
contentDiv.className = 'tool-input-content';
contentDiv.textContent = data.toolInput;
inputElement.appendChild(contentDiv);
messageDiv.appendChild(inputElement);
}
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function createExpandableInput(toolInput, rawInput) {
try {
let html = toolInput.replace(/\\[expand\\]/g, '<span class="expand-btn" onclick="toggleExpand(this)">expand</span>');
// Store raw input data for expansion
if (rawInput && typeof rawInput === 'object') {
let btnIndex = 0;
html = html.replace(/<span class="expand-btn"[^>]*>expand<\\/span>/g, (match) => {
const keys = Object.keys(rawInput);
const key = keys[btnIndex] || '';
const value = rawInput[key] || '';
const valueStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
const escapedValue = valueStr.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
btnIndex++;
return \`<span class="expand-btn" data-key="\${key}" data-value="\${escapedValue}" onclick="toggleExpand(this)">expand</span>\`;
});
}
return html;
} catch (error) {
console.error('Error creating expandable input:', error);
return toolInput;
}
}
function toggleExpand(element) {
try {
const value = element.getAttribute('data-value');
if (element.classList.contains('expanded')) {
// Collapse
element.textContent = 'expand';
element.classList.remove('expanded');
const expandedContent = element.parentNode.querySelector('.expanded-content');
if (expandedContent) {
expandedContent.remove();
}
} else {
// Expand
element.textContent = 'collapse';
element.classList.add('expanded');
const expandedDiv = document.createElement('div');
expandedDiv.className = 'expanded-content';
const preElement = document.createElement('pre');
preElement.textContent = value;
expandedDiv.appendChild(preElement);
element.parentNode.appendChild(expandedDiv);
}
} catch (error) {
console.error('Error toggling expand:', error);
}
}
function addToolResultMessage(data) {
const messageDiv = document.createElement('div');
messageDiv.className = data.isError ? 'message error' : 'message tool-result';
// Create header
const headerDiv = document.createElement('div');
headerDiv.className = 'message-header';
const iconDiv = document.createElement('div');
iconDiv.className = data.isError ? 'message-icon error' : 'message-icon';
iconDiv.style.background = data.isError ?
'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)' :
'linear-gradient(135deg, #1cc08c 0%, #16a974 100%)';
iconDiv.textContent = data.isError ? '❌' : '✅';
const labelDiv = document.createElement('div');
labelDiv.className = 'message-label';
labelDiv.textContent = data.isError ? 'Error' : 'Result';
headerDiv.appendChild(iconDiv);
headerDiv.appendChild(labelDiv);
messageDiv.appendChild(headerDiv);
// Add content
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
// Check if it's a tool result and truncate appropriately
let content = data.content;
if (content.length > 200 && !data.isError) {
const truncated = content.substring(0, 197) + '...';
const escapedValue = content.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
const preElement = document.createElement('pre');
preElement.innerHTML = truncated + ' <span class="expand-btn" data-value="' + escapedValue + '" onclick="toggleExpand(this)">expand</span>';
contentDiv.appendChild(preElement);
} else {
const preElement = document.createElement('pre');
preElement.textContent = content;
contentDiv.appendChild(preElement);
}
messageDiv.appendChild(contentDiv);
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function formatToolInputUI(input) {
if (!input || typeof input !== 'object') {
const str = String(input);
if (str.length > 100) {
return str.substring(0, 97) + '... <span class="expand-btn" data-value="' + str.replace(/"/g, '&quot;').replace(/'/g, '&#39;') + '" onclick="toggleExpand(this)">expand</span>';
}
return str;
}
let result = '';
let isFirst = true;
for (const [key, value] of Object.entries(input)) {
const valueStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
if (!isFirst) result += '\\n';
isFirst = false;
if (valueStr.length > 100) {
const truncated = valueStr.substring(0, 97) + '...';
const escapedValue = valueStr.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
result += '<strong>' + key + ':</strong> ' + truncated + ' <span class="expand-btn" data-key="' + key + '" data-value="' + escapedValue + '" onclick="toggleExpand(this)">expand</span>';
} else {
result += '<strong>' + key + ':</strong> ' + valueStr;
}
}
return result;
}
function sendMessage() {
const text = messageInput.value.trim();
if (text) {
vscode.postMessage({
type: 'sendMessage',
text: text,
planMode: planModeEnabled,
thinkingMode: thinkingModeEnabled
});
messageInput.value = '';
}
}
function togglePlanMode() {
planModeEnabled = !planModeEnabled;
const switchElement = document.getElementById('planModeSwitch');
if (planModeEnabled) {
switchElement.classList.add('active');
} else {
switchElement.classList.remove('active');
}
}
function toggleThinkingMode() {
thinkingModeEnabled = !thinkingModeEnabled;
const switchElement = document.getElementById('thinkingModeSwitch');
const toggleLabel = document.getElementById('thinkingModeLabel');
if (thinkingModeEnabled) {
switchElement.classList.add('active');
// Show thinking intensity modal when thinking mode is enabled
showThinkingIntensityModal();
} else {
switchElement.classList.remove('active');
// Reset to default "Thinking Mode" when turned off
if (toggleLabel) {
toggleLabel.textContent = 'Thinking Mode';
}
}
}
let totalCost = 0;
let totalTokensInput = 0;
let totalTokensOutput = 0;
let requestCount = 0;
let isProcessing = false;
let requestStartTime = null;
let requestTimer = null;
function updateStatus(text, state = 'ready') {
statusTextDiv.textContent = text;
statusDiv.className = \`status \${state}\`;
}
function updateStatusWithTotals() {
if (isProcessing) {
// While processing, show tokens and elapsed time
const totalTokens = totalTokensInput + totalTokensOutput;
const tokensStr = totalTokens > 0 ?
\`\${totalTokens.toLocaleString()} tokens\` : '0 tokens';
let elapsedStr = '';
if (requestStartTime) {
const elapsedSeconds = Math.floor((Date.now() - requestStartTime) / 1000);
elapsedStr = \`\${elapsedSeconds}s\`;
}
const statusText = \`Processing • \${tokensStr}\${elapsedStr}\`;
updateStatus(statusText, 'processing');
} else {
// When ready, show full info
const costStr = totalCost > 0 ? \`$\${totalCost.toFixed(4)}\` : '$0.00';
const totalTokens = totalTokensInput + totalTokensOutput;
const tokensStr = totalTokens > 0 ?
\`\${totalTokens.toLocaleString()} tokens\` : '0 tokens';
const requestStr = requestCount > 0 ? \`\${requestCount} requests\` : '';
const statusText = \`Ready • \${costStr}\${tokensStr}\${requestStr ? \`\${requestStr}\` : ''}\`;
updateStatus(statusText, 'ready');
}
}
function startRequestTimer() {
requestStartTime = Date.now();
// Update status every 100ms for smooth real-time display
requestTimer = setInterval(() => {
if (isProcessing) {
updateStatusWithTotals();
}
}, 100);
}
function stopRequestTimer() {
if (requestTimer) {
clearInterval(requestTimer);
requestTimer = null;
}
requestStartTime = null;
}
// Auto-resize textarea
function adjustTextareaHeight() {
// Reset height to calculate new height
messageInput.style.height = 'auto';
// Get computed styles
const computedStyle = getComputedStyle(messageInput);
const lineHeight = parseFloat(computedStyle.lineHeight);
const paddingTop = parseFloat(computedStyle.paddingTop);
const paddingBottom = parseFloat(computedStyle.paddingBottom);
const borderTop = parseFloat(computedStyle.borderTopWidth);
const borderBottom = parseFloat(computedStyle.borderBottomWidth);
// Calculate heights
const scrollHeight = messageInput.scrollHeight;
const maxRows = 5;
const minHeight = lineHeight + paddingTop + paddingBottom + borderTop + borderBottom;
const maxHeight = (lineHeight * maxRows) + paddingTop + paddingBottom + borderTop + borderBottom;
// Set height
if (scrollHeight <= maxHeight) {
messageInput.style.height = Math.max(scrollHeight, minHeight) + 'px';
messageInput.style.overflowY = 'hidden';
} else {
messageInput.style.height = maxHeight + 'px';
messageInput.style.overflowY = 'auto';
}
}
messageInput.addEventListener('input', adjustTextareaHeight);
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
} else if (e.key === '@' && !e.ctrlKey && !e.metaKey) {
// Don't prevent default, let @ be typed first
setTimeout(() => {
showFilePicker();
}, 0);
} else if (e.key === 'Escape' && filePickerModal.style.display === 'flex') {
e.preventDefault();
hideFilePicker();
} else if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
// Handle Ctrl+V/Cmd+V explicitly in case paste event doesn't fire
// Don't prevent default - let browser handle it first
setTimeout(() => {
// If value hasn't changed, manually trigger paste
const currentValue = messageInput.value;
setTimeout(() => {
if (messageInput.value === currentValue) {
// Value didn't change, request clipboard from VS Code
vscode.postMessage({
type: 'getClipboardText'
});
}
}, 50);
}, 0);
}
});
// Add explicit paste event handler for better clipboard support in VSCode webviews
messageInput.addEventListener('paste', async (e) => {
e.preventDefault();
try {
// Try to get clipboard data from the event first
const clipboardData = e.clipboardData;
let text = '';
if (clipboardData) {
text = clipboardData.getData('text/plain');
}
// If no text from event, try navigator.clipboard API
if (!text && navigator.clipboard && navigator.clipboard.readText) {
try {
text = await navigator.clipboard.readText();
} catch (err) {
console.log('Clipboard API failed:', err);
}
}
// If still no text, request from VS Code extension
if (!text) {
vscode.postMessage({
type: 'getClipboardText'
});
return;
}
// Insert text at cursor position
const start = messageInput.selectionStart;
const end = messageInput.selectionEnd;
const currentValue = messageInput.value;
const newValue = currentValue.substring(0, start) + text + currentValue.substring(end);
messageInput.value = newValue;
// Set cursor position after pasted text
const newCursorPos = start + text.length;
messageInput.setSelectionRange(newCursorPos, newCursorPos);
// Trigger input event to adjust height
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
} catch (error) {
console.error('Paste error:', error);
}
});
// Handle context menu paste
messageInput.addEventListener('contextmenu', (e) => {
// Don't prevent default - allow context menu to show
// but ensure paste will work when selected
});
// Initialize textarea height
adjustTextareaHeight();
// File picker event listeners
fileSearchInput.addEventListener('input', (e) => {
filterFiles(e.target.value);
});
fileSearchInput.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedFileIndex = Math.min(selectedFileIndex + 1, filteredFiles.length - 1);
renderFileList();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedFileIndex = Math.max(selectedFileIndex - 1, -1);
renderFileList();
} else if (e.key === 'Enter' && selectedFileIndex >= 0) {
e.preventDefault();
selectFile(filteredFiles[selectedFileIndex]);
} else if (e.key === 'Escape') {
e.preventDefault();
hideFilePicker();
}
});
// Close modal when clicking outside
filePickerModal.addEventListener('click', (e) => {
if (e.target === filePickerModal) {
hideFilePicker();
}
});
// Tools modal functions
function showToolsModal() {
document.getElementById('toolsModal').style.display = 'flex';
}
function hideToolsModal() {
document.getElementById('toolsModal').style.display = 'none';
}
// Close tools modal when clicking outside
document.getElementById('toolsModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('toolsModal')) {
hideToolsModal();
}
});
// Model selector functions
let currentModel = 'opus'; // Default model
function showModelSelector() {
document.getElementById('modelModal').style.display = 'flex';
// Select the current model radio button
const radioButton = document.getElementById('model-' + currentModel);
if (radioButton) {
radioButton.checked = true;
}
}
function hideModelModal() {
document.getElementById('modelModal').style.display = 'none';
}
// Slash commands modal functions
function showSlashCommandsModal() {
document.getElementById('slashCommandsModal').style.display = 'flex';
}
function hideSlashCommandsModal() {
document.getElementById('slashCommandsModal').style.display = 'none';
}
// Thinking intensity modal functions
function showThinkingIntensityModal() {
// Request current settings from VS Code first
vscode.postMessage({
type: 'getSettings'
});
document.getElementById('thinkingIntensityModal').style.display = 'flex';
}
function hideThinkingIntensityModal() {
document.getElementById('thinkingIntensityModal').style.display = 'none';
}
function saveThinkingIntensity() {
const thinkingSlider = document.getElementById('thinkingIntensitySlider');
const intensityValues = ['think', 'think-hard', 'think-harder', 'ultrathink'];
const thinkingIntensity = intensityValues[thinkingSlider.value] || 'think';
// Send settings to VS Code
vscode.postMessage({
type: 'updateSettings',
settings: {
'thinking.intensity': thinkingIntensity
}
});
}
function updateThinkingModeToggleName(intensityValue) {
const intensityNames = ['Thinking', 'Think Hard', 'Think Harder', 'Ultrathink'];
const modeName = intensityNames[intensityValue] || 'Thinking';
const toggleLabel = document.getElementById('thinkingModeLabel');
if (toggleLabel) {
toggleLabel.textContent = modeName + ' Mode';
}
}
function updateThinkingIntensityDisplay(value) {
// Update label highlighting for thinking intensity modal
for (let i = 0; i < 4; i++) {
const label = document.getElementById('thinking-label-' + i);
if (i == value) {
label.classList.add('active');
} else {
label.classList.remove('active');
}
}
// Update the toggle name
updateThinkingModeToggleName(value);
}
function setThinkingIntensityValue(value) {
// Set slider value for thinking intensity modal
document.getElementById('thinkingIntensitySlider').value = value;
// Update visual state
updateThinkingIntensityDisplay(value);
}
function confirmThinkingIntensity() {
// Save the current intensity setting
saveThinkingIntensity();
// Close the modal
hideThinkingIntensityModal();
}
// WSL Alert functions
function showWSLAlert() {
const alert = document.getElementById('wslAlert');
if (alert) {
alert.style.display = 'block';
}
}
function dismissWSLAlert() {
const alert = document.getElementById('wslAlert');
if (alert) {
alert.style.display = 'none';
}
// Send dismiss message to extension to store in globalState
vscode.postMessage({
type: 'dismissWSLAlert'
});
}
function openWSLSettings() {
// Dismiss the alert
dismissWSLAlert();
// Open settings modal
toggleSettings();
}
function executeSlashCommand(command) {
// Hide the modal
hideSlashCommandsModal();
// Clear the input since user selected a command
messageInput.value = '';
// Send command to VS Code to execute in terminal
vscode.postMessage({
type: 'executeSlashCommand',
command: command
});
// Show user feedback
addMessage('user', \`Executing /\${command} command in terminal. Check the terminal output and return when ready.\`, 'assistant');
}
function handleCustomCommandKeydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
const customCommand = event.target.value.trim();
if (customCommand) {
executeSlashCommand(customCommand);
// Clear the input for next use
event.target.value = '';
}
}
}
function openModelTerminal() {
vscode.postMessage({
type: 'openModelTerminal'
});
hideModelModal();
}
function selectModel(model, fromBackend = false) {
currentModel = model;
// Update the display text
const displayNames = {
'opus': 'Opus',
'sonnet': 'Sonnet',
'default': 'Model'
};
document.getElementById('selectedModel').textContent = displayNames[model] || model;
// Only send model selection to VS Code extension if not from backend
if (!fromBackend) {
vscode.postMessage({
type: 'selectModel',
model: model
});
// Save preference
localStorage.setItem('selectedModel', model);
}
// Update radio button if modal is open
const radioButton = document.getElementById('model-' + model);
if (radioButton) {
radioButton.checked = true;
}
hideModelModal();
}
// Initialize model display without sending message
currentModel = 'opus';
const displayNames = {
'opus': 'Opus',
'sonnet': 'Sonnet',
'default': 'Default'
};
document.getElementById('selectedModel').textContent = displayNames[currentModel];
// Close model modal when clicking outside
document.getElementById('modelModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('modelModal')) {
hideModelModal();
}
});
// Stop button functions
function showStopButton() {
document.getElementById('stopBtn').style.display = 'flex';
}
function hideStopButton() {
document.getElementById('stopBtn').style.display = 'none';
}
function stopRequest() {
vscode.postMessage({
type: 'stopRequest'
});
hideStopButton();
}
// Disable/enable buttons during processing
function disableButtons() {
const sendBtn = document.getElementById('sendBtn');
if (sendBtn) sendBtn.disabled = true;
}
function enableButtons() {
const sendBtn = document.getElementById('sendBtn');
if (sendBtn) sendBtn.disabled = false;
}
// Copy message content function
function copyMessageContent(messageDiv) {
const contentDiv = messageDiv.querySelector('.message-content');
if (contentDiv) {
// Get text content, preserving line breaks
const text = contentDiv.innerText || contentDiv.textContent;
// Copy to clipboard
navigator.clipboard.writeText(text).then(() => {
// Show brief feedback
const copyBtn = messageDiv.querySelector('.copy-btn');
const originalHtml = copyBtn.innerHTML;
copyBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>';
copyBtn.style.color = '#4caf50';
setTimeout(() => {
copyBtn.innerHTML = originalHtml;
copyBtn.style.color = '';
}, 1000);
}).catch(err => {
console.error('Failed to copy message:', err);
});
}
}
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'ready':
addMessage(message.data, 'system');
updateStatusWithTotals();
break;
case 'output':
if (message.data.trim()) {
addMessage(parseSimpleMarkdown(message.data), 'claude');
}
updateStatusWithTotals();
break;
case 'userInput':
if (message.data.trim()) {
addMessage(parseSimpleMarkdown(message.data), 'user');
}
break;
case 'loading':
addMessage(message.data, 'system');
updateStatusWithTotals();
break;
case 'setProcessing':
isProcessing = message.data;
if (isProcessing) {
startRequestTimer();
showStopButton();
disableButtons();
} else {
stopRequestTimer();
hideStopButton();
enableButtons();
}
updateStatusWithTotals();
break;
case 'clearLoading':
// Remove the last loading message
const messages = messagesDiv.children;
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage.classList.contains('system')) {
lastMessage.remove();
}
}
updateStatusWithTotals();
break;
case 'error':
if (message.data.trim()) {
addMessage(message.data, 'error');
}
updateStatusWithTotals();
break;
case 'toolUse':
if (typeof message.data === 'object') {
addToolUseMessage(message.data);
} else if (message.data.trim()) {
addMessage(message.data, 'tool');
}
break;
case 'toolResult':
if (message.data.content.trim()) {
// Don't show result for TodoWrite tool (it's redundant with the tool execution display)
const isTodoWrite = message.data.content.includes('Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable')
if (!isTodoWrite) {
addToolResultMessage(message.data);
}
}
break;
case 'thinking':
if (message.data.trim()) {
addMessage('💭 Thinking...' + parseSimpleMarkdown(message.data), 'thinking');
}
break;
case 'sessionInfo':
console.log('Session info:', message.data);
if (message.data.sessionId) {
showSessionInfo(message.data.sessionId);
// Show detailed session information
const sessionDetails = [
\`🆔 Session ID: \${message.data.sessionId}\`,
\`🔧 Tools Available: \${message.data.tools.length}\`,
\`🖥️ MCP Servers: \${message.data.mcpServers ? message.data.mcpServers.length : 0}\`
];
//addMessage(sessionDetails.join('\\n'), 'system');
}
break;
case 'updateTokens':
console.log('Tokens updated in real-time:', message.data);
// Update token totals in real-time
totalTokensInput = message.data.totalTokensInput || 0;
totalTokensOutput = message.data.totalTokensOutput || 0;
// Update status bar immediately
updateStatusWithTotals();
// Show detailed token breakdown for current message
const currentTotal = (message.data.currentInputTokens || 0) + (message.data.currentOutputTokens || 0);
if (currentTotal > 0) {
let tokenBreakdown = \`📊 Tokens: \${currentTotal.toLocaleString()}\`;
if (message.data.cacheCreationTokens || message.data.cacheReadTokens) {
const cacheInfo = [];
if (message.data.cacheCreationTokens) cacheInfo.push(\`\${message.data.cacheCreationTokens.toLocaleString()} cache created\`);
if (message.data.cacheReadTokens) cacheInfo.push(\`\${message.data.cacheReadTokens.toLocaleString()} cache read\`);
tokenBreakdown += \`\${cacheInfo.join(' • ')}\`;
}
addMessage(tokenBreakdown, 'system');
}
break;
case 'updateTotals':
console.log('Totals updated:', message.data);
console.log('Cost data received:', {
totalCost: message.data.totalCost,
currentCost: message.data.currentCost,
previousTotalCost: totalCost
});
// Update local tracking variables
totalCost = message.data.totalCost || 0;
totalTokensInput = message.data.totalTokensInput || 0;
totalTokensOutput = message.data.totalTokensOutput || 0;
requestCount = message.data.requestCount || 0;
// Update status bar with new totals
updateStatusWithTotals();
// Show current request info if available
if (message.data.currentCost || message.data.currentDuration) {
const currentCostStr = message.data.currentCost ? \`$\${message.data.currentCost.toFixed(4)}\` : 'N/A';
const currentDurationStr = message.data.currentDuration ? \`\${message.data.currentDuration}ms\` : 'N/A';
addMessage(\`Request completed - Cost: \${currentCostStr}, Duration: \${currentDurationStr}\`, 'system');
}
break;
case 'sessionResumed':
console.log('Session resumed:', message.data);
showSessionInfo(message.data.sessionId);
addMessage(\`📝 Resumed previous session\\n🆔 Session ID: \${message.data.sessionId}\\n💡 Your conversation history is preserved\`, 'system');
break;
case 'sessionCleared':
console.log('Session cleared');
// Clear all messages from UI
messagesDiv.innerHTML = '';
hideSessionInfo();
addMessage('🆕 Started new session', 'system');
// Reset totals
totalCost = 0;
totalTokensInput = 0;
totalTokensOutput = 0;
requestCount = 0;
updateStatusWithTotals();
break;
case 'loginRequired':
addMessage('🔐 Login Required\\n\\nYour Claude API key is invalid or expired.\\nA terminal has been opened - please run the login process there.\\n\\nAfter logging in, come back to this chat to continue.', 'error');
updateStatus('Login Required', 'error');
break;
case 'showRestoreOption':
console.log('Show restore option:', message.data);
showRestoreContainer(message.data);
break;
case 'restoreProgress':
addMessage('🔄 ' + message.data, 'system');
break;
case 'restoreSuccess':
//hideRestoreContainer(message.data.commitSha);
addMessage('✅ ' + message.data.message, 'system');
break;
case 'restoreError':
addMessage('❌ ' + message.data, 'error');
break;
case 'workspaceFiles':
filteredFiles = message.data;
selectedFileIndex = -1;
renderFileList();
break;
case 'imagePath':
// Add the image path to the textarea
const currentText = messageInput.value;
const pathIndicator = \`@\${message.path} \`;
messageInput.value = currentText + pathIndicator;
messageInput.focus();
adjustTextareaHeight();
break;
case 'conversationList':
displayConversationList(message.data);
break;
case 'clipboardText':
handleClipboardText(message.data);
break;
case 'modelSelected':
// Update the UI with the current model
currentModel = message.model;
selectModel(message.model, true);
break;
case 'terminalOpened':
// Display notification about checking the terminal
addMessage(message.data, 'system');
break;
}
});
// Session management functions
function newSession() {
vscode.postMessage({
type: 'newSession'
});
}
function restoreToCommit(commitSha) {
console.log('Restore button clicked for commit:', commitSha);
vscode.postMessage({
type: 'restoreCommit',
commitSha: commitSha
});
}
function showRestoreContainer(data) {
const restoreContainer = document.createElement('div');
restoreContainer.className = 'restore-container';
restoreContainer.id = \`restore-\${data.sha}\`;
const timeAgo = new Date(data.timestamp).toLocaleTimeString();
const shortSha = data.sha ? data.sha.substring(0, 8) : 'unknown';
restoreContainer.innerHTML = \`
<button class="restore-btn dark" onclick="restoreToCommit('\${data.sha}')">
Restore checkpoint
</button>
<span class="restore-date">\${timeAgo}</span>
\`;
messagesDiv.appendChild(restoreContainer);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function hideRestoreContainer(commitSha) {
const container = document.getElementById(\`restore-\${commitSha}\`);
if (container) {
container.remove();
}
}
function showSessionInfo(sessionId) {
// const sessionInfo = document.getElementById('sessionInfo');
// const sessionIdSpan = document.getElementById('sessionId');
const sessionStatus = document.getElementById('sessionStatus');
const newSessionBtn = document.getElementById('newSessionBtn');
const historyBtn = document.getElementById('historyBtn');
if (sessionStatus && newSessionBtn) {
// sessionIdSpan.textContent = sessionId.substring(0, 8);
// sessionIdSpan.title = \`Full session ID: \${sessionId} (click to copy)\`;
// sessionIdSpan.style.cursor = 'pointer';
// sessionIdSpan.onclick = () => copySessionId(sessionId);
// sessionInfo.style.display = 'flex';
sessionStatus.style.display = 'none';
newSessionBtn.style.display = 'block';
if (historyBtn) historyBtn.style.display = 'block';
}
}
function copySessionId(sessionId) {
navigator.clipboard.writeText(sessionId).then(() => {
// Show temporary feedback
const sessionIdSpan = document.getElementById('sessionId');
if (sessionIdSpan) {
const originalText = sessionIdSpan.textContent;
sessionIdSpan.textContent = 'Copied!';
setTimeout(() => {
sessionIdSpan.textContent = originalText;
}, 1000);
}
}).catch(err => {
console.error('Failed to copy session ID:', err);
});
}
function hideSessionInfo() {
// const sessionInfo = document.getElementById('sessionInfo');
const sessionStatus = document.getElementById('sessionStatus');
const newSessionBtn = document.getElementById('newSessionBtn');
const historyBtn = document.getElementById('historyBtn');
if (sessionStatus && newSessionBtn) {
// sessionInfo.style.display = 'none';
sessionStatus.style.display = 'none';
// Always show new session
newSessionBtn.style.display = 'block';
// Keep history button visible - don't hide it
if (historyBtn) historyBtn.style.display = 'block';
}
}
updateStatus('Initializing...', 'disconnected');
function parseSimpleMarkdown(markdown) {
const lines = markdown.split('\\n');
let html = '';
let inUnorderedList = false;
let inOrderedList = false;
for (let line of lines) {
line = line.trim();
// Bold
line = line.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>');
// Italic - only apply when underscores are surrounded by whitespace or at beginning/end
line = line.replace(/(?<!\\*)\\*(?!\\*)(.*?)\\*(?!\\*)/g, '<em>$1</em>');
line = line.replace(/(^|\\s)_([^_\\s][^_]*[^_\\s]|[^_\\s])_(?=\\s|$)/g, '$1<em>$2</em>');
// Headers
if (/^####\\s+/.test(line)) {
html += '<h4>' + line.replace(/^####\\s+/, '') + '</h4>';
continue;
} else if (/^###\\s+/.test(line)) {
html += '<h3>' + line.replace(/^###\\s+/, '') + '</h3>';
continue;
} else if (/^##\\s+/.test(line)) {
html += '<h2>' + line.replace(/^##\\s+/, '') + '</h2>';
continue;
} else if (/^#\\s+/.test(line)) {
html += '<h1>' + line.replace(/^#\\s+/, '') + '</h1>';
continue;
}
// Ordered list
if (/^\\d+\\.\\s+/.test(line)) {
if (!inOrderedList) {
html += '<ol>';
inOrderedList = true;
}
const item = line.replace(/^\\d+\\.\\s+/, '');
html += '<li>' + item + '</li>';
continue;
}
// Unordered list
if (line.startsWith('- ')) {
if (!inUnorderedList) {
html += '<ul>';
inUnorderedList = true;
}
html += '<li>' + line.slice(2) + '</li>';
continue;
}
// Close lists
if (inUnorderedList) {
html += '</ul>';
inUnorderedList = false;
}
if (inOrderedList) {
html += '</ol>';
inOrderedList = false;
}
// Paragraph or break
if (line !== '') {
html += '<p>' + line + '</p>';
} else {
html += '<br>';
}
}
if (inUnorderedList) html += '</ul>';
if (inOrderedList) html += '</ol>';
return html;
}
// Conversation history functions
function toggleConversationHistory() {
const historyDiv = document.getElementById('conversationHistory');
const chatContainer = document.getElementById('chatContainer');
if (historyDiv.style.display === 'none') {
// Show conversation history
requestConversationList();
historyDiv.style.display = 'block';
chatContainer.style.display = 'none';
} else {
// Hide conversation history
historyDiv.style.display = 'none';
chatContainer.style.display = 'flex';
}
}
function requestConversationList() {
vscode.postMessage({
type: 'getConversationList'
});
}
function loadConversation(filename) {
console.log('Loading conversation:', filename);
vscode.postMessage({
type: 'loadConversation',
filename: filename
});
// Hide conversation history and show chat
toggleConversationHistory();
}
// File picker functions
function showFilePicker() {
// Request initial file list from VS Code
vscode.postMessage({
type: 'getWorkspaceFiles',
searchTerm: ''
});
// Show modal
filePickerModal.style.display = 'flex';
fileSearchInput.focus();
selectedFileIndex = -1;
}
function hideFilePicker() {
filePickerModal.style.display = 'none';
fileSearchInput.value = '';
selectedFileIndex = -1;
}
function getFileIcon(filename) {
const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'js': case 'jsx': case 'ts': case 'tsx': return '📄';
case 'html': case 'htm': return '🌐';
case 'css': case 'scss': case 'sass': return '🎨';
case 'json': return '📋';
case 'md': return '📝';
case 'py': return '🐍';
case 'java': return '☕';
case 'cpp': case 'c': case 'h': return '⚙️';
case 'png': case 'jpg': case 'jpeg': case 'gif': case 'svg': return '🖼️';
case 'pdf': return '📄';
case 'zip': case 'tar': case 'gz': return '📦';
default: return '📄';
}
}
function renderFileList() {
fileList.innerHTML = '';
filteredFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
if (index === selectedFileIndex) {
fileItem.classList.add('selected');
}
fileItem.innerHTML = \`
<span class="file-icon">\${getFileIcon(file.name)}</span>
<div class="file-info">
<div class="file-name">\${file.name}</div>
<div class="file-path">\${file.path}</div>
</div>
\`;
fileItem.addEventListener('click', () => {
selectFile(file);
});
fileList.appendChild(fileItem);
});
}
function selectFile(file) {
// Insert file path at cursor position
const cursorPos = messageInput.selectionStart;
const textBefore = messageInput.value.substring(0, cursorPos);
const textAfter = messageInput.value.substring(cursorPos);
// Replace the @ symbol with the file path
const beforeAt = textBefore.substring(0, textBefore.lastIndexOf('@'));
const newText = beforeAt + '@' + file.path + ' ' + textAfter;
messageInput.value = newText;
messageInput.focus();
// Set cursor position after the inserted path
const newCursorPos = beforeAt.length + file.path.length + 2;
messageInput.setSelectionRange(newCursorPos, newCursorPos);
hideFilePicker();
adjustTextareaHeight();
}
function filterFiles(searchTerm) {
// Send search request to backend instead of filtering locally
vscode.postMessage({
type: 'getWorkspaceFiles',
searchTerm: searchTerm
});
selectedFileIndex = -1;
}
// Image handling functions
function selectImage() {
// Use VS Code's native file picker instead of browser file picker
vscode.postMessage({
type: 'selectImageFile'
});
}
function showImageAddedFeedback(fileName) {
// Create temporary feedback element
const feedback = document.createElement('div');
feedback.textContent = \`Added: \${fileName}\`;
feedback.style.cssText = \`
position: fixed;
top: 20px;
right: 20px;
background: var(--vscode-notifications-background);
color: var(--vscode-notifications-foreground);
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
\`;
document.body.appendChild(feedback);
// Animate in
setTimeout(() => feedback.style.opacity = '1', 10);
// Animate out and remove
setTimeout(() => {
feedback.style.opacity = '0';
setTimeout(() => feedback.remove(), 300);
}, 2000);
}
function displayConversationList(conversations) {
const listDiv = document.getElementById('conversationList');
listDiv.innerHTML = '';
if (conversations.length === 0) {
listDiv.innerHTML = '<p style="text-align: center; color: var(--vscode-descriptionForeground);">No conversations found</p>';
return;
}
conversations.forEach(conv => {
const item = document.createElement('div');
item.className = 'conversation-item';
item.onclick = () => loadConversation(conv.filename);
const date = new Date(conv.startTime).toLocaleDateString();
const time = new Date(conv.startTime).toLocaleTimeString();
item.innerHTML = \`
<div class="conversation-title">\${conv.firstUserMessage.substring(0, 60)}\${conv.firstUserMessage.length > 60 ? '...' : ''}</div>
<div class="conversation-meta">\${date} at \${time}\${conv.messageCount} messages • $\${conv.totalCost.toFixed(3)}</div>
<div class="conversation-preview">Last: \${conv.lastUserMessage.substring(0, 80)}\${conv.lastUserMessage.length > 80 ? '...' : ''}</div>
\`;
listDiv.appendChild(item);
});
}
function handleClipboardText(text) {
if (!text) return;
// Insert text at cursor position
const start = messageInput.selectionStart;
const end = messageInput.selectionEnd;
const currentValue = messageInput.value;
const newValue = currentValue.substring(0, start) + text + currentValue.substring(end);
messageInput.value = newValue;
// Set cursor position after pasted text
const newCursorPos = start + text.length;
messageInput.setSelectionRange(newCursorPos, newCursorPos);
// Trigger input event to adjust height
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
}
// Settings functions
function toggleSettings() {
const settingsModal = document.getElementById('settingsModal');
if (settingsModal.style.display === 'none') {
// Request current settings from VS Code
vscode.postMessage({
type: 'getSettings'
});
settingsModal.style.display = 'flex';
} else {
hideSettingsModal();
}
}
function hideSettingsModal() {
document.getElementById('settingsModal').style.display = 'none';
}
function updateSettings() {
// Note: thinking intensity is now handled separately in the thinking intensity modal
const wslEnabled = document.getElementById('wsl-enabled').checked;
const wslDistro = document.getElementById('wsl-distro').value;
const wslNodePath = document.getElementById('wsl-node-path').value;
const wslClaudePath = document.getElementById('wsl-claude-path').value;
// Update WSL options visibility
document.getElementById('wslOptions').style.display = wslEnabled ? 'block' : 'none';
// Send settings to VS Code immediately
vscode.postMessage({
type: 'updateSettings',
settings: {
'wsl.enabled': wslEnabled,
'wsl.distro': wslDistro || 'Ubuntu',
'wsl.nodePath': wslNodePath || '/usr/bin/node',
'wsl.claudePath': wslClaudePath || '/usr/local/bin/claude'
}
});
}
// Close settings modal when clicking outside
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('settingsModal')) {
hideSettingsModal();
}
});
// Close thinking intensity modal when clicking outside
document.getElementById('thinkingIntensityModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('thinkingIntensityModal')) {
hideThinkingIntensityModal();
}
});
// Close slash commands modal when clicking outside
document.getElementById('slashCommandsModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('slashCommandsModal')) {
hideSlashCommandsModal();
}
});
// Detect slash commands input
messageInput.addEventListener('input', (e) => {
const value = messageInput.value;
// Only trigger when "/" is the very first and only character
if (value === '/') {
showSlashCommandsModal();
}
});
// Add settings message handler to window message event
const originalMessageHandler = window.onmessage;
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'settingsData') {
// Update UI with current settings
const thinkingIntensity = message.data['thinking.intensity'] || 'think';
const intensityValues = ['think', 'think-hard', 'think-harder', 'ultrathink'];
const sliderValue = intensityValues.indexOf(thinkingIntensity);
// Update thinking intensity modal if it exists
const thinkingIntensitySlider = document.getElementById('thinkingIntensitySlider');
if (thinkingIntensitySlider) {
thinkingIntensitySlider.value = sliderValue >= 0 ? sliderValue : 0;
updateThinkingIntensityDisplay(thinkingIntensitySlider.value);
} else {
// Update toggle name even if modal isn't open
updateThinkingModeToggleName(sliderValue >= 0 ? sliderValue : 0);
}
document.getElementById('wsl-enabled').checked = message.data['wsl.enabled'] || false;
document.getElementById('wsl-distro').value = message.data['wsl.distro'] || 'Ubuntu';
document.getElementById('wsl-node-path').value = message.data['wsl.nodePath'] || '/usr/bin/node';
document.getElementById('wsl-claude-path').value = message.data['wsl.claudePath'] || '/usr/local/bin/claude';
// Show/hide WSL options
document.getElementById('wslOptions').style.display = message.data['wsl.enabled'] ? 'block' : 'none';
}
if (message.type === 'platformInfo') {
// Check if user is on Windows and show WSL alert if not dismissed and WSL not already enabled
if (message.data.isWindows && !message.data.wslAlertDismissed && !message.data.wslEnabled) {
// Small delay to ensure UI is ready
setTimeout(() => {
showWSLAlert();
}, 1000);
}
}
});
</script>
</body>
</html>`;
export default html;