14 Commits

Author SHA1 Message Date
andrepimenta
a156881a08 Migrate permission system from MCP file-based to stdio-based
Replace MCP permission server with stdio-based permission flow that
communicates directly with Claude CLI via stdin/stdout. This simplifies
the architecture and fixes permission expiration issues.

Key changes:
- Use --permission-prompt-tool stdio and --input-format stream-json
- Handle control_request messages for permission prompts
- Send control_response via stdin to approve/deny
- Check local permissions for auto-approval of pre-approved tools
- Only expire pending permissions when VS Code restarts, not panel close

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 18:07:45 +00:00
andrepimenta
0764bf8202 Improve process termination and update diff icon colors
- Add AbortController for clean process management
- Add _killProcessGroup() with platform-specific handling (Unix/Windows/WSL)
- Add _killClaudeProcess() with proper SIGTERM→wait→SIGKILL flow
- Update spawn options with detached and signal support
- Handle WSL specially with pkill inside the distro
- Update Open Diff button icon to pastel colors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 16:33:27 +00:00
andrepimenta
82899ebb40 Handle conversation compacting with status messages and token reset
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 00:22:47 +00:00
andrepimenta
abf81a1176 Add morphing orange dot processing indicator
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 00:07:08 +00:00
andrepimenta
da46d5e3d9 Optimize diff storage and improve Open Diff button behavior
- Stop storing full file contents in conversation history to reduce memory
- Compute and store only startLine/startLines for accurate line numbers on reload
- Open Diff button now only shows on last pending edit request
- Button uses stored diff data directly instead of re-reading file
- Hide button when edit result arrives (edit no longer pending)
- Show simple completion messages for Edit/MultiEdit/Write results
- Use virtual document scheme (claude-diff:) for read-only diff viewer
- Simplify tool result handling for Read/TodoWrite

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 23:19:04 +00:00
andrepimenta
d20d8667f3 Show full diff in Edit, MultiEdit, and Write tool use messages
Instead of showing simple previews like "Editing X lines" or "Writing X lines",
now displays the actual diff with added/removed lines during the tool request phase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 16:29:58 +00:00
andrepimenta
6c37394015 Add Open Diff button to open VS Code's native side-by-side diff editor
- Add _openDiffEditor method using vscode.diff command
- Store temp files in extension's storageUri instead of workspace
- Clean up temp files when diff editor is closed
- Force side-by-side mode when opening diff
- Add Open Diff button with red/green icon in summary row

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 16:20:11 +00:00
andrepimenta
2b1ad70f6b Add truncation with expand button to diff display
Show first 6 lines of diff by default with a "Show X more lines"
button to expand and view the full diff when there are more lines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:42:19 +00:00
andrepimenta
bf527bb922 Strip tool_use_error tags from error messages
Remove XML-like <tool_use_error> tags from error content before
displaying to users, showing only the clean error message text.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:39:00 +00:00
andrepimenta
df8188380d Fix auto-scroll for diff tool results
Replace incorrect scrollToBottom() calls with scrollToBottomIfNeeded()
to restore auto-scrolling functionality when new Edit, MultiEdit, and
Write tool results are displayed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:26:16 +00:00
andrepimenta
79a0b6b4b2 Fix diff line alignment by removing ::before pseudo-elements
Remove ::before pseudo-elements from added/removed diff lines that were
causing inconsistent margins and misaligned text. Lines now align properly
with consistent spacing across context, added, and removed lines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:23:04 +00:00
andrepimenta
dd47efec04 Implement unified diff visualization for Edit, MultiEdit, and Write tools
- Add LCS-based diff algorithm with intelligent line matching
- Show proper line numbers from actual file positions
- Display color-coded additions (green), removals (red), and context lines
- Include summary statistics (+X lines added, -Y lines removed)
- Simplify tool use previews, show detailed diffs in tool results
- Parse tool result content to extract line numbers
- Update styling with monospace font and VS Code git colors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:09:20 +00:00
andrepimenta
d891070d9e Modify icon and name 2025-10-14 17:23:49 +01:00
andrepimenta
1be89d43a4 Added more built-in commands 2025-10-01 23:20:26 +01:00
10 changed files with 1409 additions and 510 deletions

View File

@@ -12,3 +12,5 @@ vsc-extension-quickstart.md
backup
.claude
claude-code-chat-permissions-mcp/**
node_modules
mcp-permissions.js

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

After

Width:  |  Height:  |  Size: 689 KiB

View File

@@ -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"
}
]
},

File diff suppressed because it is too large Load Diff

View File

@@ -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' || 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);
contentDiv.innerHTML = formatEditToolDiff(data.rawInput, data.fileContentBefore, showButton, data.startLine);
} else if (data.toolName === 'MultiEdit') {
contentDiv.innerHTML = formatMultiEditToolDiff(data.rawInput);
} else if (data.toolName === 'Write') {
contentDiv.innerHTML = formatWriteToolDiff(data.rawInput);
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);
}
@@ -226,26 +282,42 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
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;
let completionText;
if (toolName === 'Read') {
completionText = '✅ Read completed';
} else if (toolName === 'Edit') {
completionText = '✅ Edit completed';
} else if (toolName === 'TodoWrite') {
completionText = '✅ Update Todos completed';
} else {
completionText = '✅ ' + toolName + ' completed';
// 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';
}
addMessage(completionText, 'system');
return; // Don't show the result message
}
lastPendingEditIndex = -1;
lastPendingEditData = null;
}
if(data.isError && data.content === "File has not been read yet. Read it first before writing to it."){
// 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 (data.toolName === 'Edit') {
completionText = '✅ Edit completed';
} else if (data.toolName === 'MultiEdit') {
completionText = '✅ MultiEdit completed';
} else {
completionText = '✅ Write completed';
}
addMessage(completionText, 'system');
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
return;
}
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');
}
@@ -277,6 +349,11 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
// 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';
let html = '<div class="diff-file-header">';
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(input.file_path) + '\\\')">' + formattedPath + '</div>';
html += '</div>\\n';
// Count total lines across all edits for truncation
let totalLines = 0;
for (const edit of input.edits) {
input.edits.forEach((edit, index) => {
if (edit.old_string && edit.new_string) {
const oldLines = edit.old_string.split('\\n');
const newLines = edit.new_string.split('\\n');
totalLines += oldLines.length + newLines.length;
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 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;
const diff = computeLineDiff(oldLines, newLines);
if (shouldTruncate && currentLineCount + editLines > maxLines && visibleEdits.length > 0) {
hiddenEdits.push(edit);
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 {
visibleEdits.push(edit);
currentLineCount += editLines;
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>';
}
html += '</div>';
return html;
}
// 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;
}
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';
// fileContentBefore may be empty string if new file, or existing content if overwriting
const fullFileBefore = fileContentBefore || '';
// 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;
// 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;
@@ -2042,6 +2193,22 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
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,8 +2295,11 @@ 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}\`;
@@ -2137,7 +2313,10 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
alwaysAllowTooltip = displayPattern.length > 30 ? \`title="\${displayPattern}"\` : '';
}
messageDiv.innerHTML = \`
// Show different content based on status
let contentHtml = '';
if (status === 'pending') {
contentHtml = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
@@ -2163,11 +2342,93 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
</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
vscode.postMessage({

View File

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

View File

@@ -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')">