mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-08 17:09:44 +00:00
Compare commits
14 Commits
1.0.6
...
a156881a08
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a156881a08 | ||
|
|
0764bf8202 | ||
|
|
82899ebb40 | ||
|
|
abf81a1176 | ||
|
|
da46d5e3d9 | ||
|
|
d20d8667f3 | ||
|
|
6c37394015 | ||
|
|
2b1ad70f6b | ||
|
|
bf527bb922 | ||
|
|
df8188380d | ||
|
|
79a0b6b4b2 | ||
|
|
dd47efec04 | ||
|
|
d891070d9e | ||
|
|
1be89d43a4 |
@@ -11,4 +11,6 @@ vsc-extension-quickstart.md
|
||||
**/.vscode-test.*
|
||||
backup
|
||||
.claude
|
||||
claude-code-chat-permissions-mcp/**
|
||||
claude-code-chat-permissions-mcp/**
|
||||
node_modules
|
||||
mcp-permissions.js
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -4,6 +4,23 @@ All notable changes to the "claude-code-chat" extension will be documented in th
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [1.0.7] - 2025-10-01
|
||||
|
||||
### 🚀 Features Added
|
||||
- **Slash Commands Update**: Added 4 new slash commands to the commands modal
|
||||
- `/add-dir` - Add additional working directories
|
||||
- `/agents` - Manage custom AI subagents for specialized tasks
|
||||
- `/rewind` - Rewind the conversation and/or code
|
||||
- `/usage` - Show plan usage limits and rate limit status (subscription plans only)
|
||||
|
||||
### 📚 Documentation Updates
|
||||
- Updated slash commands count from 19+ to 23+ built-in commands
|
||||
- Enhanced command descriptions for better clarity:
|
||||
- `/config` - Now specifies "Open the Settings interface (Config tab)"
|
||||
- `/cost` - Added note about cost tracking guide for subscription-specific details
|
||||
- `/status` - Expanded description to mention version, model, account, and connectivity
|
||||
- `/terminal-setup` - Added clarification about iTerm2 and VSCode only support
|
||||
|
||||
## [1.0.6] - 2025-08-26
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -103,7 +103,7 @@ Ditch the command line and experience Claude Code like never before. This extens
|
||||
|
||||
### ⚡ **Slash Commands Integration**
|
||||
- **Slash Commands Modal** - Type "/" to access all Claude Code commands instantly
|
||||
- **19+ Built-in Commands** - /cost, /status, /config, /help, /memory, /review, and more
|
||||
- **23+ Built-in Commands** - /agents, /cost, /config, /memory, /review, and more
|
||||
- **Custom Command Support** - Execute any Claude Code command with session context
|
||||
- **Session-Aware Execution** - All commands run with current conversation context
|
||||
- **Terminal Integration** - Commands open directly in VS Code terminal with WSL support
|
||||
|
||||
BIN
icon-bubble.png
Normal file
BIN
icon-bubble.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
BIN
icon.png
BIN
icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 689 KiB After Width: | Height: | Size: 689 KiB |
10
package.json
10
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "claude-code-chat",
|
||||
"displayName": "Claude Code Chat",
|
||||
"displayName": "Chat for Claude Code",
|
||||
"description": "Beautiful Claude Code Chat Interface for VS Code",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"publisher": "AndrePimenta",
|
||||
"author": "Andre Pimenta",
|
||||
"repository": {
|
||||
@@ -56,7 +56,7 @@
|
||||
"command": "claude-code-chat.openChat",
|
||||
"title": "Open Claude Code Chat",
|
||||
"category": "Claude Code Chat",
|
||||
"icon": "icon.png"
|
||||
"icon": "icon-bubble.png"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
@@ -133,7 +133,7 @@
|
||||
"type": "webview",
|
||||
"name": "Claude Code Chat",
|
||||
"when": "true",
|
||||
"icon": "icon.png",
|
||||
"icon": "icon-bubble.png",
|
||||
"contextualTitle": "Claude Code Chat"
|
||||
}
|
||||
]
|
||||
@@ -143,7 +143,7 @@
|
||||
{
|
||||
"id": "claude-code-chat",
|
||||
"title": "Claude Code Chat",
|
||||
"icon": "icon.png"
|
||||
"icon": "icon-bubble.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
927
src/extension.ts
927
src/extension.ts
File diff suppressed because it is too large
Load Diff
765
src/script.ts
765
src/script.ts
@@ -15,6 +15,20 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
let selectedFileIndex = -1;
|
||||
let planModeEnabled = false;
|
||||
let thinkingModeEnabled = false;
|
||||
let lastPendingEditIndex = -1; // Track the last Edit/MultiEdit/Write toolUse without result
|
||||
let lastPendingEditData = null; // Store diff data for the pending edit { filePath, oldContent, newContent }
|
||||
|
||||
// Open diff using stored data (no file read needed)
|
||||
function openDiffEditor() {
|
||||
if (lastPendingEditData) {
|
||||
vscode.postMessage({
|
||||
type: 'openDiff',
|
||||
filePath: lastPendingEditData.filePath,
|
||||
oldContent: lastPendingEditData.oldContent,
|
||||
newContent: lastPendingEditData.newContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAutoScroll(messagesDiv) {
|
||||
const threshold = 100; // pixels from bottom
|
||||
@@ -111,6 +125,7 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
}
|
||||
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
moveProcessingIndicatorToLast();
|
||||
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
|
||||
}
|
||||
|
||||
@@ -162,12 +177,52 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
} else {
|
||||
// Format raw input with expandable content for long values
|
||||
// Use diff format for Edit, MultiEdit, and Write tools, regular format for others
|
||||
if (data.toolName === 'Edit') {
|
||||
contentDiv.innerHTML = formatEditToolDiff(data.rawInput);
|
||||
} else if (data.toolName === 'MultiEdit') {
|
||||
contentDiv.innerHTML = formatMultiEditToolDiff(data.rawInput);
|
||||
} else if (data.toolName === 'Write') {
|
||||
contentDiv.innerHTML = formatWriteToolDiff(data.rawInput);
|
||||
if (data.toolName === 'Edit' || data.toolName === 'MultiEdit' || data.toolName === 'Write') {
|
||||
// Only show Open Diff button if we have fileContentBefore (live session, not reload)
|
||||
const showButton = data.fileContentBefore !== undefined && data.messageIndex >= 0;
|
||||
|
||||
// Hide any existing pending edit button before showing new one
|
||||
if (showButton && lastPendingEditIndex >= 0) {
|
||||
const prevContent = document.querySelector('[data-edit-message-index="' + lastPendingEditIndex + '"]');
|
||||
if (prevContent) {
|
||||
const btn = prevContent.querySelector('.diff-open-btn');
|
||||
if (btn) btn.style.display = 'none';
|
||||
}
|
||||
lastPendingEditData = null;
|
||||
}
|
||||
|
||||
if (showButton) {
|
||||
lastPendingEditIndex = data.messageIndex;
|
||||
contentDiv.setAttribute('data-edit-message-index', data.messageIndex);
|
||||
|
||||
// Compute and store diff data for when button is clicked
|
||||
const oldContent = data.fileContentBefore || '';
|
||||
let newContent = oldContent;
|
||||
if (data.toolName === 'Edit' && data.rawInput.old_string && data.rawInput.new_string) {
|
||||
newContent = oldContent.replace(data.rawInput.old_string, data.rawInput.new_string);
|
||||
} else if (data.toolName === 'MultiEdit' && data.rawInput.edits) {
|
||||
for (const edit of data.rawInput.edits) {
|
||||
if (edit.old_string && edit.new_string) {
|
||||
newContent = newContent.replace(edit.old_string, edit.new_string);
|
||||
}
|
||||
}
|
||||
} else if (data.toolName === 'Write' && data.rawInput.content) {
|
||||
newContent = data.rawInput.content;
|
||||
}
|
||||
lastPendingEditData = {
|
||||
filePath: data.rawInput.file_path,
|
||||
oldContent: oldContent,
|
||||
newContent: newContent
|
||||
};
|
||||
}
|
||||
|
||||
if (data.toolName === 'Edit') {
|
||||
contentDiv.innerHTML = formatEditToolDiff(data.rawInput, data.fileContentBefore, showButton, data.startLine);
|
||||
} else if (data.toolName === 'MultiEdit') {
|
||||
contentDiv.innerHTML = formatMultiEditToolDiff(data.rawInput, data.fileContentBefore, showButton, data.startLines);
|
||||
} else {
|
||||
contentDiv.innerHTML = formatWriteToolDiff(data.rawInput, data.fileContentBefore, showButton);
|
||||
}
|
||||
} else {
|
||||
contentDiv.innerHTML = formatToolInputUI(data.rawInput);
|
||||
}
|
||||
@@ -193,6 +248,7 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
}
|
||||
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
moveProcessingIndicatorToLast();
|
||||
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
|
||||
}
|
||||
|
||||
@@ -225,27 +281,43 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
function addToolResultMessage(data) {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const shouldScroll = shouldAutoScroll(messagesDiv);
|
||||
|
||||
// For Read and Edit tools with hidden flag, just hide loading state and show completion message
|
||||
if (data.hidden && (data.toolName === 'Read' || data.toolName === 'Edit' || data.toolName === 'TodoWrite' || data.toolName === 'MultiEdit') && !data.isError) {
|
||||
return
|
||||
// Show completion message
|
||||
const toolName = data.toolName;
|
||||
|
||||
// When result comes in for Edit/MultiEdit/Write, hide the Open Diff button on the request
|
||||
// since the edit has now been applied (no longer pending)
|
||||
if (lastPendingEditIndex >= 0) {
|
||||
// Find and hide the button on the corresponding toolUse
|
||||
const toolUseContent = document.querySelector('[data-edit-message-index="' + lastPendingEditIndex + '"]');
|
||||
if (toolUseContent) {
|
||||
const btn = toolUseContent.querySelector('.diff-open-btn');
|
||||
if (btn) {
|
||||
btn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
lastPendingEditIndex = -1;
|
||||
lastPendingEditData = null;
|
||||
}
|
||||
|
||||
// For Read and TodoWrite tools, just hide loading state (no result message needed)
|
||||
if ((data.toolName === 'Read' || data.toolName === 'TodoWrite') && !data.isError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For Edit/MultiEdit/Write, show simple completion message (diff is already shown on request)
|
||||
if ((data.toolName === 'Edit' || data.toolName === 'MultiEdit' || data.toolName === 'Write') && !data.isError) {
|
||||
let completionText;
|
||||
if (toolName === 'Read') {
|
||||
completionText = '✅ Read completed';
|
||||
} else if (toolName === 'Edit') {
|
||||
if (data.toolName === 'Edit') {
|
||||
completionText = '✅ Edit completed';
|
||||
} else if (toolName === 'TodoWrite') {
|
||||
completionText = '✅ Update Todos completed';
|
||||
} else if (data.toolName === 'MultiEdit') {
|
||||
completionText = '✅ MultiEdit completed';
|
||||
} else {
|
||||
completionText = '✅ ' + toolName + ' completed';
|
||||
completionText = '✅ Write completed';
|
||||
}
|
||||
addMessage(completionText, 'system');
|
||||
return; // Don't show the result message
|
||||
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.isError && data.content === "File has not been read yet. Read it first before writing to it."){
|
||||
if(data.isError && data.content?.includes("File has not been read yet. Read it first before writing to it.")){
|
||||
return addMessage("File has not been read yet. Let me read it first before writing to it.", 'system');
|
||||
}
|
||||
|
||||
@@ -274,9 +346,14 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
// 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;
|
||||
|
||||
// Clean up error messages by removing XML-like tags
|
||||
if (data.isError && content) {
|
||||
content = content.replace(/<tool_use_error>/g, '').replace(/<\\/tool_use_error>/g, '').trim();
|
||||
}
|
||||
if (content.length > 200 && !data.isError) {
|
||||
const truncateAt = 197;
|
||||
const truncated = content.substring(0, truncateAt);
|
||||
@@ -319,6 +396,7 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
}
|
||||
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
moveProcessingIndicatorToLast();
|
||||
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
|
||||
}
|
||||
|
||||
@@ -369,7 +447,183 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatEditToolDiff(input) {
|
||||
// Simple LCS-based diff algorithm
|
||||
function computeLineDiff(oldLines, newLines) {
|
||||
// Compute longest common subsequence
|
||||
const m = oldLines.length;
|
||||
const n = newLines.length;
|
||||
const lcs = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (oldLines[i - 1] === newLines[j - 1]) {
|
||||
lcs[i][j] = lcs[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backtrack to build diff
|
||||
const diff = [];
|
||||
let i = m, j = n;
|
||||
|
||||
while (i > 0 || j > 0) {
|
||||
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
||||
diff.unshift({type: 'context', oldLine: i - 1, newLine: j - 1, content: oldLines[i - 1]});
|
||||
i--;
|
||||
j--;
|
||||
} else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
||||
diff.unshift({type: 'added', newLine: j - 1, content: newLines[j - 1]});
|
||||
j--;
|
||||
} else if (i > 0) {
|
||||
diff.unshift({type: 'removed', oldLine: i - 1, content: oldLines[i - 1]});
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Parse tool result to extract line numbers
|
||||
function parseToolResult(resultContent) {
|
||||
if (!resultContent || typeof resultContent !== 'string') {
|
||||
return {startLine: 1, lines: []};
|
||||
}
|
||||
|
||||
const lines = resultContent.split('\\n');
|
||||
const parsed = [];
|
||||
let startLine = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\\s*(\\d+)→(.*)$/);
|
||||
if (match) {
|
||||
const lineNum = parseInt(match[1]);
|
||||
const content = match[2];
|
||||
if (startLine === null) startLine = lineNum;
|
||||
parsed.push({num: lineNum, content});
|
||||
}
|
||||
}
|
||||
|
||||
return {startLine: startLine || 1, lines: parsed};
|
||||
}
|
||||
|
||||
// Generate unified diff HTML with line numbers
|
||||
// showButton controls whether to show the "Open Diff" button
|
||||
function generateUnifiedDiffHTML(oldString, newString, filePath, startLine = 1, showButton = false) {
|
||||
const oldLines = oldString.split('\\n');
|
||||
const newLines = newString.split('\\n');
|
||||
const diff = computeLineDiff(oldLines, newLines);
|
||||
|
||||
// Generate unique ID for this diff (used for truncation)
|
||||
const diffId = 'diff_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
let html = '';
|
||||
const formattedPath = formatFilePath(filePath);
|
||||
|
||||
// Header with file path
|
||||
html += '<div class="diff-file-header">';
|
||||
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(filePath) + '\\\')">' + formattedPath + '</div>';
|
||||
html += '</div>\\n';
|
||||
|
||||
// Calculate line range
|
||||
let firstLine = startLine;
|
||||
let lastLine = startLine;
|
||||
let addedCount = 0;
|
||||
let removedCount = 0;
|
||||
|
||||
// Calculate actual line numbers
|
||||
for (const change of diff) {
|
||||
if (change.type === 'added') addedCount++;
|
||||
if (change.type === 'removed') removedCount++;
|
||||
}
|
||||
|
||||
lastLine = startLine + newLines.length - 1;
|
||||
|
||||
html += '<div class="diff-container">';
|
||||
html += '<div class="diff-header">Lines ' + firstLine + '-' + lastLine + '</div>';
|
||||
|
||||
// Build diff lines with proper line numbers
|
||||
let oldLineNum = startLine;
|
||||
let newLineNum = startLine;
|
||||
const maxLines = 6;
|
||||
let lineIndex = 0;
|
||||
|
||||
// First pass: build all line HTML
|
||||
const allLinesHtml = [];
|
||||
for (const change of diff) {
|
||||
let lineNum, prefix, cssClass;
|
||||
|
||||
if (change.type === 'context') {
|
||||
lineNum = newLineNum;
|
||||
prefix = ' ';
|
||||
cssClass = 'context';
|
||||
oldLineNum++;
|
||||
newLineNum++;
|
||||
} else if (change.type === 'added') {
|
||||
lineNum = newLineNum;
|
||||
prefix = '+';
|
||||
cssClass = 'added';
|
||||
newLineNum++;
|
||||
} else {
|
||||
lineNum = oldLineNum;
|
||||
prefix = '-';
|
||||
cssClass = 'removed';
|
||||
oldLineNum++;
|
||||
}
|
||||
|
||||
const lineNumStr = lineNum.toString().padStart(3);
|
||||
allLinesHtml.push('<div class="diff-line ' + cssClass + '">' + prefix + lineNumStr + ' ' + escapeHtml(change.content) + '</div>');
|
||||
}
|
||||
|
||||
// Show visible lines
|
||||
const shouldTruncate = allLinesHtml.length > maxLines;
|
||||
const visibleLines = shouldTruncate ? allLinesHtml.slice(0, maxLines) : allLinesHtml;
|
||||
const hiddenLines = shouldTruncate ? allLinesHtml.slice(maxLines) : [];
|
||||
|
||||
html += '<div id="' + diffId + '_visible">';
|
||||
html += visibleLines.join('');
|
||||
html += '</div>';
|
||||
|
||||
// Show hidden lines (initially hidden)
|
||||
if (shouldTruncate) {
|
||||
html += '<div id="' + diffId + '_hidden" style="display: none;">';
|
||||
html += hiddenLines.join('');
|
||||
html += '</div>';
|
||||
|
||||
// Add expand button
|
||||
html += '<div class="diff-expand-container">';
|
||||
html += '<button class="diff-expand-btn" onclick="toggleDiffExpansion(\\'' + diffId + '\\')">Show ' + hiddenLines.length + ' more lines</button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Summary
|
||||
let summary = '';
|
||||
if (addedCount > 0 && removedCount > 0) {
|
||||
summary = '+' + addedCount + ' line' + (addedCount > 1 ? 's' : '') + ' added, -' + removedCount + ' line' + (removedCount > 1 ? 's' : '') + ' removed';
|
||||
} else if (addedCount > 0) {
|
||||
summary = '+' + addedCount + ' line' + (addedCount > 1 ? 's' : '') + ' added';
|
||||
} else if (removedCount > 0) {
|
||||
summary = '-' + removedCount + ' line' + (removedCount > 1 ? 's' : '') + ' removed';
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
html += '<div class="diff-summary-row">';
|
||||
html += '<span class="diff-summary">Summary: ' + summary + '</span>';
|
||||
if (showButton) {
|
||||
html += '<button class="diff-open-btn" onclick="openDiffEditor()" title="Open side-by-side diff in VS Code">';
|
||||
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#e8a0a0" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#8fd48f" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#8fd48f" stroke-width="1.5"/></svg>';
|
||||
html += 'Open Diff</button>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatEditToolDiff(input, fileContentBefore, showButton = false, providedStartLine = null) {
|
||||
if (!input || typeof input !== 'object') {
|
||||
return formatToolInputUI(input);
|
||||
}
|
||||
@@ -379,66 +633,21 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
return formatToolInputUI(input);
|
||||
}
|
||||
|
||||
// Format file path with better display
|
||||
const formattedPath = formatFilePath(input.file_path);
|
||||
let result = '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(input.file_path) + '\\\')">' + formattedPath + '</div>\\n';
|
||||
|
||||
// Create diff view
|
||||
const oldLines = input.old_string.split('\\n');
|
||||
const newLines = input.new_string.split('\\n');
|
||||
const allLines = [...oldLines.map(line => ({type: 'removed', content: line})),
|
||||
...newLines.map(line => ({type: 'added', content: line}))];
|
||||
|
||||
const maxLines = 6;
|
||||
const shouldTruncate = allLines.length > maxLines;
|
||||
const visibleLines = shouldTruncate ? allLines.slice(0, maxLines) : allLines;
|
||||
const hiddenLines = shouldTruncate ? allLines.slice(maxLines) : [];
|
||||
|
||||
result += '<div class="diff-container">';
|
||||
result += '<div class="diff-header">Changes:</div>';
|
||||
|
||||
// Create a unique ID for this diff
|
||||
const diffId = 'diff_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Show visible lines
|
||||
result += '<div id="' + diffId + '_visible">';
|
||||
for (const line of visibleLines) {
|
||||
const prefix = line.type === 'removed' ? '- ' : '+ ';
|
||||
const cssClass = line.type === 'removed' ? 'removed' : 'added';
|
||||
result += '<div class="diff-line ' + cssClass + '">' + prefix + escapeHtml(line.content) + '</div>';
|
||||
}
|
||||
result += '</div>';
|
||||
|
||||
// Show hidden lines (initially hidden)
|
||||
if (shouldTruncate) {
|
||||
result += '<div id="' + diffId + '_hidden" style="display: none;">';
|
||||
for (const line of hiddenLines) {
|
||||
const prefix = line.type === 'removed' ? '- ' : '+ ';
|
||||
const cssClass = line.type === 'removed' ? 'removed' : 'added';
|
||||
result += '<div class="diff-line ' + cssClass + '">' + prefix + escapeHtml(line.content) + '</div>';
|
||||
}
|
||||
result += '</div>';
|
||||
|
||||
// Add expand button
|
||||
result += '<div class="diff-expand-container">';
|
||||
result += '<button class="diff-expand-btn" onclick="toggleDiffExpansion(\\\'' + diffId + '\\\')">Show ' + hiddenLines.length + ' more lines</button>';
|
||||
result += '</div>';
|
||||
}
|
||||
|
||||
result += '</div>';
|
||||
|
||||
// Add other properties if they exist
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (key !== 'file_path' && key !== 'old_string' && key !== 'new_string') {
|
||||
const valueStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
||||
result += '\\n<strong>' + key + ':</strong> ' + valueStr;
|
||||
// Use provided startLine if available (from saved data), otherwise compute from fileContentBefore
|
||||
let startLine = providedStartLine || 1;
|
||||
if (!providedStartLine && fileContentBefore) {
|
||||
const position = fileContentBefore.indexOf(input.old_string);
|
||||
if (position !== -1) {
|
||||
// Count newlines before the match to get line number
|
||||
const textBefore = fileContentBefore.substring(0, position);
|
||||
startLine = (textBefore.match(/\\n/g) || []).length + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
return generateUnifiedDiffHTML(input.old_string, input.new_string, input.file_path, startLine, showButton);
|
||||
}
|
||||
|
||||
function formatMultiEditToolDiff(input) {
|
||||
function formatMultiEditToolDiff(input, fileContentBefore, showButton = false, providedStartLines = null) {
|
||||
if (!input || typeof input !== 'object') {
|
||||
return formatToolInputUI(input);
|
||||
}
|
||||
@@ -448,111 +657,71 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
return formatToolInputUI(input);
|
||||
}
|
||||
|
||||
// Format file path with better display
|
||||
// Show full diffs for each edit
|
||||
const formattedPath = formatFilePath(input.file_path);
|
||||
let result = '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(input.file_path) + '\\\')">' + formattedPath + '</div>\\n';
|
||||
|
||||
// Count total lines across all edits for truncation
|
||||
let totalLines = 0;
|
||||
for (const edit of input.edits) {
|
||||
let html = '<div class="diff-file-header">';
|
||||
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(input.file_path) + '\\\')">' + formattedPath + '</div>';
|
||||
html += '</div>\\n';
|
||||
|
||||
input.edits.forEach((edit, index) => {
|
||||
if (edit.old_string && edit.new_string) {
|
||||
if (index > 0) {
|
||||
html += '<div class="diff-edit-separator"></div>';
|
||||
}
|
||||
|
||||
// Use provided startLine if available, otherwise compute from fileContentBefore
|
||||
let startLine = (providedStartLines && providedStartLines[index]) || 1;
|
||||
if (!providedStartLines && fileContentBefore) {
|
||||
const position = fileContentBefore.indexOf(edit.old_string);
|
||||
if (position !== -1) {
|
||||
const textBefore = fileContentBefore.substring(0, position);
|
||||
startLine = (textBefore.match(/\\n/g) || []).length + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const oldLines = edit.old_string.split('\\n');
|
||||
const newLines = edit.new_string.split('\\n');
|
||||
totalLines += oldLines.length + newLines.length;
|
||||
}
|
||||
}
|
||||
const diff = computeLineDiff(oldLines, newLines);
|
||||
|
||||
const maxLines = 6;
|
||||
const shouldTruncate = totalLines > maxLines;
|
||||
|
||||
result += '<div class="diff-container">';
|
||||
result += '<div class="diff-header">Changes (' + input.edits.length + ' edit' + (input.edits.length > 1 ? 's' : '') + '):</div>';
|
||||
|
||||
// Create a unique ID for this diff
|
||||
const diffId = 'multiedit_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
let currentLineCount = 0;
|
||||
let visibleEdits = [];
|
||||
let hiddenEdits = [];
|
||||
|
||||
// Determine which edits to show/hide based on line count
|
||||
for (let i = 0; i < input.edits.length; i++) {
|
||||
const edit = input.edits[i];
|
||||
if (!edit.old_string || !edit.new_string) continue;
|
||||
|
||||
const oldLines = edit.old_string.split('\\n');
|
||||
const newLines = edit.new_string.split('\\n');
|
||||
const editLines = oldLines.length + newLines.length;
|
||||
|
||||
if (shouldTruncate && currentLineCount + editLines > maxLines && visibleEdits.length > 0) {
|
||||
hiddenEdits.push(edit);
|
||||
} else {
|
||||
visibleEdits.push(edit);
|
||||
currentLineCount += editLines;
|
||||
html += '<div class="diff-container">';
|
||||
html += '<div class="diff-header">Edit ' + (index + 1) + ' (Line ' + startLine + ')</div>';
|
||||
|
||||
let lineNum = startLine;
|
||||
for (const change of diff) {
|
||||
let prefix, cssClass;
|
||||
if (change.type === 'context') {
|
||||
prefix = ' ';
|
||||
cssClass = 'context';
|
||||
lineNum++;
|
||||
} else if (change.type === 'added') {
|
||||
prefix = '+';
|
||||
cssClass = 'added';
|
||||
lineNum++;
|
||||
} else {
|
||||
prefix = '-';
|
||||
cssClass = 'removed';
|
||||
}
|
||||
const lineNumStr = (change.type === 'removed' ? '' : lineNum - 1).toString().padStart(3);
|
||||
html += '<div class="diff-line ' + cssClass + '">' + prefix + lineNumStr + ' ' + escapeHtml(change.content) + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
});
|
||||
|
||||
// Add summary row with Open Diff button
|
||||
html += '<div class="diff-summary-row">';
|
||||
html += '<span class="diff-summary">Summary: ' + input.edits.length + ' edit' + (input.edits.length > 1 ? 's' : '') + '</span>';
|
||||
if (showButton) {
|
||||
html += '<button class="diff-open-btn" onclick="openDiffEditor()" title="Open side-by-side diff in VS Code">';
|
||||
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#e8a0a0" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#8fd48f" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#8fd48f" stroke-width="1.5"/></svg>';
|
||||
html += 'Open Diff</button>';
|
||||
}
|
||||
|
||||
// Show visible edits
|
||||
result += '<div id="' + diffId + '_visible">';
|
||||
for (let i = 0; i < visibleEdits.length; i++) {
|
||||
const edit = visibleEdits[i];
|
||||
if (i > 0) result += '<div class="diff-edit-separator"></div>';
|
||||
result += formatSingleEdit(edit, i + 1);
|
||||
}
|
||||
result += '</div>';
|
||||
|
||||
// Show hidden edits (initially hidden)
|
||||
if (hiddenEdits.length > 0) {
|
||||
result += '<div id="' + diffId + '_hidden" style="display: none;">';
|
||||
for (let i = 0; i < hiddenEdits.length; i++) {
|
||||
const edit = hiddenEdits[i];
|
||||
result += '<div class="diff-edit-separator"></div>';
|
||||
result += formatSingleEdit(edit, visibleEdits.length + i + 1);
|
||||
}
|
||||
result += '</div>';
|
||||
|
||||
// Add expand button
|
||||
result += '<div class="diff-expand-container">';
|
||||
result += '<button class="diff-expand-btn" onclick="toggleDiffExpansion(\\\'' + diffId + '\\\')">Show ' + hiddenEdits.length + ' more edit' + (hiddenEdits.length > 1 ? 's' : '') + '</button>';
|
||||
result += '</div>';
|
||||
}
|
||||
|
||||
result += '</div>';
|
||||
|
||||
// Add other properties if they exist
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (key !== 'file_path' && key !== 'edits') {
|
||||
const valueStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
||||
result += '\\n<strong>' + key + ':</strong> ' + valueStr;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatSingleEdit(edit, editNumber) {
|
||||
let result = '<div class="single-edit">';
|
||||
result += '<div class="edit-number">Edit #' + editNumber + '</div>';
|
||||
|
||||
// Create diff view for this single edit
|
||||
const oldLines = edit.old_string.split('\\n');
|
||||
const newLines = edit.new_string.split('\\n');
|
||||
|
||||
// Show removed lines
|
||||
for (const line of oldLines) {
|
||||
result += '<div class="diff-line removed">- ' + escapeHtml(line) + '</div>';
|
||||
}
|
||||
|
||||
// Show added lines
|
||||
for (const line of newLines) {
|
||||
result += '<div class="diff-line added">+ ' + escapeHtml(line) + '</div>';
|
||||
}
|
||||
|
||||
result += '</div>';
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatWriteToolDiff(input) {
|
||||
function formatWriteToolDiff(input, fileContentBefore, showButton = false) {
|
||||
if (!input || typeof input !== 'object') {
|
||||
return formatToolInputUI(input);
|
||||
}
|
||||
@@ -562,56 +731,11 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
return formatToolInputUI(input);
|
||||
}
|
||||
|
||||
// Format file path with better display
|
||||
const formattedPath = formatFilePath(input.file_path);
|
||||
let result = '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(input.file_path) + '\\\')">' + formattedPath + '</div>\\n';
|
||||
|
||||
// Create diff view showing all content as additions
|
||||
const contentLines = input.content.split('\\n');
|
||||
|
||||
const maxLines = 6;
|
||||
const shouldTruncate = contentLines.length > maxLines;
|
||||
const visibleLines = shouldTruncate ? contentLines.slice(0, maxLines) : contentLines;
|
||||
const hiddenLines = shouldTruncate ? contentLines.slice(maxLines) : [];
|
||||
|
||||
result += '<div class="diff-container">';
|
||||
result += '<div class="diff-header">New file content:</div>';
|
||||
|
||||
// Create a unique ID for this diff
|
||||
const diffId = 'write_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Show visible lines (all as additions)
|
||||
result += '<div id="' + diffId + '_visible">';
|
||||
for (const line of visibleLines) {
|
||||
result += '<div class="diff-line added">+ ' + escapeHtml(line) + '</div>';
|
||||
}
|
||||
result += '</div>';
|
||||
|
||||
// Show hidden lines (initially hidden)
|
||||
if (shouldTruncate) {
|
||||
result += '<div id="' + diffId + '_hidden" style="display: none;">';
|
||||
for (const line of hiddenLines) {
|
||||
result += '<div class="diff-line added">+ ' + escapeHtml(line) + '</div>';
|
||||
}
|
||||
result += '</div>';
|
||||
|
||||
// Add expand button
|
||||
result += '<div class="diff-expand-container">';
|
||||
result += '<button class="diff-expand-btn" onclick="toggleDiffExpansion(\\\'' + diffId + '\\\')">Show ' + hiddenLines.length + ' more lines</button>';
|
||||
result += '</div>';
|
||||
}
|
||||
|
||||
result += '</div>';
|
||||
|
||||
// Add other properties if they exist
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (key !== 'file_path' && key !== 'content') {
|
||||
const valueStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
||||
result += '\\n<strong>' + key + ':</strong> ' + valueStr;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
// fileContentBefore may be empty string if new file, or existing content if overwriting
|
||||
const fullFileBefore = fileContentBefore || '';
|
||||
|
||||
// Show full content as added lines (new file or replacement)
|
||||
return generateUnifiedDiffHTML(fullFileBefore, input.content, input.file_path, 1, showButton);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -831,6 +955,31 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
requestStartTime = null;
|
||||
}
|
||||
|
||||
function showProcessingIndicator() {
|
||||
// Remove any existing indicator first
|
||||
hideProcessingIndicator();
|
||||
|
||||
// Create the indicator and append after all messages
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'processing-indicator';
|
||||
indicator.innerHTML = '<div class="morph-dot"></div>';
|
||||
messagesDiv.appendChild(indicator);
|
||||
}
|
||||
|
||||
function hideProcessingIndicator() {
|
||||
const indicator = document.querySelector('.processing-indicator');
|
||||
if (indicator) {
|
||||
indicator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function moveProcessingIndicatorToLast() {
|
||||
// Only move if we're processing
|
||||
if (isProcessing) {
|
||||
showProcessingIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-resize textarea
|
||||
function adjustTextareaHeight() {
|
||||
// Reset height to calculate new height
|
||||
@@ -1887,10 +2036,12 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
startRequestTimer(message.data.requestStartTime);
|
||||
showStopButton();
|
||||
disableButtons();
|
||||
showProcessingIndicator();
|
||||
} else {
|
||||
stopRequestTimer();
|
||||
hideStopButton();
|
||||
enableButtons();
|
||||
hideProcessingIndicator();
|
||||
}
|
||||
updateStatusWithTotals();
|
||||
break;
|
||||
@@ -2041,7 +2192,23 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
requestCount = 0;
|
||||
updateStatusWithTotals();
|
||||
break;
|
||||
|
||||
|
||||
case 'compacting':
|
||||
if (message.data.isCompacting) {
|
||||
addMessage('📦 Compacting conversation...', 'system');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'compactBoundary':
|
||||
// Reset token counts since conversation was compacted
|
||||
totalTokensInput = 0;
|
||||
totalTokensOutput = 0;
|
||||
updateStatusWithTotals();
|
||||
|
||||
const preTokens = message.data.preTokens ? message.data.preTokens.toLocaleString() : 'unknown';
|
||||
addMessage('✅ Compacted (' + preTokens + ' tokens → summary)', 'system');
|
||||
break;
|
||||
|
||||
case 'loginRequired':
|
||||
sendStats('Login required');
|
||||
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');
|
||||
@@ -2098,6 +2265,12 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
case 'permissionRequest':
|
||||
addPermissionRequestMessage(message.data);
|
||||
break;
|
||||
case 'updatePermissionStatus':
|
||||
updatePermissionStatus(message.data.id, message.data.status);
|
||||
break;
|
||||
case 'expirePendingPermissions':
|
||||
expireAllPendingPermissions();
|
||||
break;
|
||||
case 'mcpServers':
|
||||
displayMCPServers(message.data);
|
||||
break;
|
||||
@@ -2122,9 +2295,12 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message permission-request';
|
||||
|
||||
messageDiv.id = \`permission-\${data.id}\`;
|
||||
messageDiv.dataset.status = data.status || 'pending';
|
||||
|
||||
const toolName = data.tool || 'Unknown Tool';
|
||||
|
||||
const status = data.status || 'pending';
|
||||
|
||||
// Create always allow button text with command styling for Bash
|
||||
let alwaysAllowText = \`Always allow \${toolName}\`;
|
||||
let alwaysAllowTooltip = '';
|
||||
@@ -2136,37 +2312,122 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
|
||||
alwaysAllowText = \`Always allow <code>\${truncatedPattern}</code>\`;
|
||||
alwaysAllowTooltip = displayPattern.length > 30 ? \`title="\${displayPattern}"\` : '';
|
||||
}
|
||||
|
||||
messageDiv.innerHTML = \`
|
||||
<div class="permission-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span>Permission Required</span>
|
||||
<div class="permission-menu">
|
||||
<button class="permission-menu-btn" onclick="togglePermissionMenu('\${data.id}')" title="More options">⋮</button>
|
||||
<div class="permission-menu-dropdown" id="permissionMenu-\${data.id}" style="display: none;">
|
||||
<button class="permission-menu-item" onclick="enableYoloMode('\${data.id}')">
|
||||
<span class="menu-icon">⚡</span>
|
||||
<div class="menu-content">
|
||||
<span class="menu-title">Enable YOLO Mode</span>
|
||||
<span class="menu-subtitle">Auto-allow all permissions</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
// Show different content based on status
|
||||
let contentHtml = '';
|
||||
if (status === 'pending') {
|
||||
contentHtml = \`
|
||||
<div class="permission-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span>Permission Required</span>
|
||||
<div class="permission-menu">
|
||||
<button class="permission-menu-btn" onclick="togglePermissionMenu('\${data.id}')" title="More options">⋮</button>
|
||||
<div class="permission-menu-dropdown" id="permissionMenu-\${data.id}" style="display: none;">
|
||||
<button class="permission-menu-item" onclick="enableYoloMode('\${data.id}')">
|
||||
<span class="menu-icon">⚡</span>
|
||||
<div class="menu-content">
|
||||
<span class="menu-title">Enable YOLO Mode</span>
|
||||
<span class="menu-subtitle">Auto-allow all permissions</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="permission-content">
|
||||
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
|
||||
<div class="permission-buttons">
|
||||
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">Deny</button>
|
||||
<button class="btn always-allow" onclick="respondToPermission('\${data.id}', true, true)" \${alwaysAllowTooltip}>\${alwaysAllowText}</button>
|
||||
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">Allow</button>
|
||||
<div class="permission-content">
|
||||
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
|
||||
<div class="permission-buttons">
|
||||
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">Deny</button>
|
||||
<button class="btn always-allow" onclick="respondToPermission('\${data.id}', true, true)" \${alwaysAllowTooltip}>\${alwaysAllowText}</button>
|
||||
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">Allow</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
\`;
|
||||
} else if (status === 'approved') {
|
||||
contentHtml = \`
|
||||
<div class="permission-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span>Permission Required</span>
|
||||
</div>
|
||||
<div class="permission-content">
|
||||
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
|
||||
<div class="permission-decision allowed">✅ You allowed this</div>
|
||||
</div>
|
||||
\`;
|
||||
messageDiv.classList.add('permission-decided', 'allowed');
|
||||
} else if (status === 'denied') {
|
||||
contentHtml = \`
|
||||
<div class="permission-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span>Permission Required</span>
|
||||
</div>
|
||||
<div class="permission-content">
|
||||
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
|
||||
<div class="permission-decision denied">❌ You denied this</div>
|
||||
</div>
|
||||
\`;
|
||||
messageDiv.classList.add('permission-decided', 'denied');
|
||||
} else if (status === 'cancelled' || status === 'expired') {
|
||||
contentHtml = \`
|
||||
<div class="permission-header">
|
||||
<span class="icon">🔐</span>
|
||||
<span>Permission Required</span>
|
||||
</div>
|
||||
<div class="permission-content">
|
||||
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
|
||||
<div class="permission-decision expired">⏱️ This request expired</div>
|
||||
</div>
|
||||
\`;
|
||||
messageDiv.classList.add('permission-decided', 'expired');
|
||||
}
|
||||
|
||||
messageDiv.innerHTML = contentHtml;
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
|
||||
}
|
||||
|
||||
function updatePermissionStatus(id, status) {
|
||||
const permissionMsg = document.getElementById(\`permission-\${id}\`);
|
||||
if (!permissionMsg) return;
|
||||
|
||||
permissionMsg.dataset.status = status;
|
||||
const permissionContent = permissionMsg.querySelector('.permission-content');
|
||||
const buttons = permissionMsg.querySelector('.permission-buttons');
|
||||
const menuDiv = permissionMsg.querySelector('.permission-menu');
|
||||
|
||||
// Hide buttons and menu if present
|
||||
if (buttons) buttons.style.display = 'none';
|
||||
if (menuDiv) menuDiv.style.display = 'none';
|
||||
|
||||
// Remove existing decision div if any
|
||||
const existingDecision = permissionContent.querySelector('.permission-decision');
|
||||
if (existingDecision) existingDecision.remove();
|
||||
|
||||
// Add new decision div
|
||||
const decisionDiv = document.createElement('div');
|
||||
if (status === 'approved') {
|
||||
decisionDiv.className = 'permission-decision allowed';
|
||||
decisionDiv.innerHTML = '✅ You allowed this';
|
||||
permissionMsg.classList.add('permission-decided', 'allowed');
|
||||
} else if (status === 'denied') {
|
||||
decisionDiv.className = 'permission-decision denied';
|
||||
decisionDiv.innerHTML = '❌ You denied this';
|
||||
permissionMsg.classList.add('permission-decided', 'denied');
|
||||
} else if (status === 'cancelled' || status === 'expired') {
|
||||
decisionDiv.className = 'permission-decision expired';
|
||||
decisionDiv.innerHTML = '⏱️ This request expired';
|
||||
permissionMsg.classList.add('permission-decided', 'expired');
|
||||
}
|
||||
permissionContent.appendChild(decisionDiv);
|
||||
}
|
||||
|
||||
function expireAllPendingPermissions() {
|
||||
document.querySelectorAll('.permission-request').forEach(permissionMsg => {
|
||||
if (permissionMsg.dataset.status === 'pending') {
|
||||
const id = permissionMsg.id.replace('permission-', '');
|
||||
updatePermissionStatus(id, 'expired');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function respondToPermission(id, approved, alwaysAllow = false) {
|
||||
// Send response back to extension
|
||||
|
||||
158
src/ui-styles.ts
158
src/ui-styles.ts
@@ -302,6 +302,12 @@ const styles = `
|
||||
border: 1px solid rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
.permission-decision.expired {
|
||||
background-color: rgba(128, 128, 128, 0.15);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border: 1px solid rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
|
||||
.permission-decided {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
@@ -321,6 +327,11 @@ const styles = `
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
}
|
||||
|
||||
.permission-decided.expired {
|
||||
border-color: var(--vscode-panel-border);
|
||||
background-color: rgba(128, 128, 128, 0.05);
|
||||
}
|
||||
|
||||
/* Permissions Management */
|
||||
.permissions-list {
|
||||
max-height: 300px;
|
||||
@@ -1076,34 +1087,25 @@ const styles = `
|
||||
|
||||
.diff-line {
|
||||
padding: 2px 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
white-space: pre;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.diff-line.context {
|
||||
color: var(--vscode-editor-foreground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.diff-line.removed {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
border-left: 3px solid rgba(244, 67, 54, 0.6);
|
||||
color: var(--vscode-foreground);
|
||||
color: var(--vscode-gitDecoration-deletedResourceForeground, rgba(244, 67, 54, 0.9));
|
||||
}
|
||||
|
||||
.diff-line.added {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
border-left: 3px solid rgba(76, 175, 80, 0.6);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.diff-line.removed::before {
|
||||
content: '';
|
||||
color: rgba(244, 67, 54, 0.8);
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.diff-line.added::before {
|
||||
content: '';
|
||||
color: rgba(76, 175, 80, 0.8);
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
color: var(--vscode-gitDecoration-addedResourceForeground, rgba(76, 175, 80, 0.9));
|
||||
}
|
||||
|
||||
.diff-expand-container {
|
||||
@@ -1159,7 +1161,39 @@ const styles = `
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.diff-summary-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding: 6px 12px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.diff-summary {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.diff-preview {
|
||||
padding: 4px 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* File path display styles */
|
||||
.diff-file-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.diff-file-path {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
@@ -1167,6 +1201,7 @@ const styles = `
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.diff-file-path:hover {
|
||||
@@ -1178,6 +1213,35 @@ const styles = `
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.diff-open-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--vscode-button-secondaryBorder, var(--vscode-panel-border));
|
||||
color: var(--vscode-foreground);
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.diff-open-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.diff-open-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground, rgba(255, 255, 255, 0.1));
|
||||
border-color: var(--vscode-focusBorder);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.diff-open-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.file-path-short,
|
||||
.file-path-truncated {
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
@@ -2874,6 +2938,60 @@ const styles = `
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Processing indicator - morphing orange dot */
|
||||
.processing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 12px 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.processing-indicator .morph-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: linear-gradient(135deg, #ff9500 0%, #ff6b00 100%);
|
||||
box-shadow: 0 0 8px rgba(255, 149, 0, 0.5);
|
||||
animation: morphShape 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes morphShape {
|
||||
0%, 100% {
|
||||
border-radius: 50%;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
15% {
|
||||
border-radius: 50%;
|
||||
transform: scale(1.3) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
border-radius: 20%;
|
||||
transform: scale(1) rotate(45deg);
|
||||
}
|
||||
40% {
|
||||
border-radius: 20%;
|
||||
transform: scale(1.2) rotate(90deg);
|
||||
}
|
||||
50% {
|
||||
border-radius: 50% 50% 50% 0%;
|
||||
transform: scale(1) rotate(135deg);
|
||||
}
|
||||
65% {
|
||||
border-radius: 0%;
|
||||
transform: scale(1.3) rotate(180deg);
|
||||
}
|
||||
75% {
|
||||
border-radius: 50% 0% 50% 0%;
|
||||
transform: scale(1) rotate(270deg);
|
||||
}
|
||||
85% {
|
||||
border-radius: 30%;
|
||||
transform: scale(1.2) rotate(315deg);
|
||||
}
|
||||
}
|
||||
|
||||
</style>`
|
||||
|
||||
export default styles
|
||||
36
src/ui.ts
36
src/ui.ts
@@ -545,6 +545,20 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
|
||||
<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" id="nativeCommandsList">
|
||||
<div class="slash-command-item" onclick="executeSlashCommand('add-dir')">
|
||||
<div class="slash-command-icon">📁</div>
|
||||
<div class="slash-command-content">
|
||||
<div class="slash-command-title">/add-dir</div>
|
||||
<div class="slash-command-description">Add additional working directories</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slash-command-item" onclick="executeSlashCommand('agents')">
|
||||
<div class="slash-command-icon">🤖</div>
|
||||
<div class="slash-command-content">
|
||||
<div class="slash-command-title">/agents</div>
|
||||
<div class="slash-command-description">Manage custom AI subagents for specialized tasks</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slash-command-item" onclick="executeSlashCommand('bug')">
|
||||
<div class="slash-command-icon">🐛</div>
|
||||
<div class="slash-command-content">
|
||||
@@ -570,14 +584,14 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
|
||||
<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 class="slash-command-description">Open the Settings interface (Config tab)</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 class="slash-command-description">Show token usage statistics (see cost tracking guide for subscription-specific details)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slash-command-item" onclick="executeSlashCommand('doctor')">
|
||||
@@ -657,18 +671,32 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
|
||||
<div class="slash-command-description">Request code review</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slash-command-item" onclick="executeSlashCommand('rewind')">
|
||||
<div class="slash-command-icon">⏪</div>
|
||||
<div class="slash-command-content">
|
||||
<div class="slash-command-title">/rewind</div>
|
||||
<div class="slash-command-description">Rewind the conversation and/or code</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 class="slash-command-description">Open the Settings interface (Status tab) showing version, model, account, and connectivity</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 class="slash-command-description">Install Shift+Enter key binding for newlines (iTerm2 and VSCode only)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slash-command-item" onclick="executeSlashCommand('usage')">
|
||||
<div class="slash-command-icon">📈</div>
|
||||
<div class="slash-command-content">
|
||||
<div class="slash-command-title">/usage</div>
|
||||
<div class="slash-command-description">Show plan usage limits and rate limit status (subscription plans only)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slash-command-item" onclick="executeSlashCommand('vim')">
|
||||
|
||||
Reference in New Issue
Block a user