Initial commit: Obsidian Web Interface for Claude Code

- Full IDE with terminal integration using xterm.js
- Session management with local and web sessions
- HTML preview functionality
- Multi-terminal support with session picker

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-19 16:29:44 +00:00
Unverified
commit 0dd2083556
44 changed files with 18955 additions and 0 deletions

View File

@@ -0,0 +1,543 @@
/**
* Tag Renderer - Display and manage dyad-style operations in UI
*/
class TagRenderer {
constructor() {
this.pendingOperations = null;
this.isExecuting = false;
}
/**
* Parse tags from text response
*/
parseTags(response) {
const tags = {
writes: this.extractWriteTags(response),
renames: this.extractRenameTags(response),
deletes: this.extractDeleteTags(response),
dependencies: this.extractDependencyTags(response),
commands: this.extractCommandTags(response)
};
return tags;
}
/**
* Extract dyad-write tags
*/
extractWriteTags(response) {
const regex = /<dyad-write\s+path="([^"]+)">([\s\S]*?)<\/dyad-write>/g;
const tags = [];
let match;
while ((match = regex.exec(response)) !== null) {
tags.push({
type: 'write',
path: match[1],
content: match[2].trim()
});
}
return tags;
}
/**
* Extract dyad-rename tags
*/
extractRenameTags(response) {
const regex = /<dyad-rename\s+from="([^"]+)"\s+to="([^"]+)">/g;
const tags = [];
let match;
while ((match = regex.exec(response)) !== null) {
tags.push({
type: 'rename',
from: match[1],
to: match[2]
});
}
return tags;
}
/**
* Extract dyad-delete tags
*/
extractDeleteTags(response) {
const regex = /<dyad-delete\s+path="([^"]+)">/g;
const tags = [];
let match;
while ((match = regex.exec(response)) !== null) {
tags.push({
type: 'delete',
path: match[1]
});
}
return tags;
}
/**
* Extract dyad-add-dependency tags
*/
extractDependencyTags(response) {
const regex = /<dyad-add-dependency\s+packages="([^"]+)">/g;
const tags = [];
let match;
while ((match = regex.exec(response)) !== null) {
tags.push({
type: 'install',
packages: match[1].split(' ').filter(p => p.trim())
});
}
return tags;
}
/**
* Extract dyad-command tags
*/
extractCommandTags(response) {
const regex = /<dyad-command\s+type="([^"]+)">/g;
const tags = [];
let match;
while ((match = regex.exec(response)) !== null) {
tags.push({
type: 'command',
command: match[1]
});
}
return tags;
}
/**
* Generate operation summary for display
*/
generateOperationsSummary(tags) {
const operations = [];
tags.deletes.forEach(tag => {
operations.push({
type: 'delete',
description: `Delete ${tag.path}`,
tag,
status: 'pending'
});
});
tags.renames.forEach(tag => {
operations.push({
type: 'rename',
description: `Rename ${tag.from}${tag.to}`,
tag,
status: 'pending'
});
});
tags.writes.forEach(tag => {
operations.push({
type: 'write',
description: `Create/update ${tag.path}`,
tag,
status: 'pending'
});
});
tags.dependencies.forEach(tag => {
operations.push({
type: 'install',
description: `Install packages: ${tag.packages.join(', ')}`,
tag,
status: 'pending'
});
});
tags.commands.forEach(tag => {
operations.push({
type: 'command',
description: `Execute command: ${tag.command}`,
tag,
status: 'pending'
});
});
return operations;
}
/**
* Strip tags from response for clean display
*/
stripTags(response) {
let stripped = response;
stripped = stripped.replace(/<dyad-write\s+path="[^"]+">[\s\S]*?<\/dyad-write>/g, '[File: code]');
stripped = stripped.replace(/<dyad-rename\s+from="[^"]+"\s+to="[^"]+">/g, '[Rename]');
stripped = stripped.replace(/<dyad-delete\s+path="[^"]+">/g, '[Delete]');
stripped = stripped.replace(/<dyad-add-dependency\s+packages="[^"]+">/g, '[Install]');
stripped = stripped.replace(/<dyad-command\s+type="[^"]+">/g, '[Command]');
return stripped;
}
/**
* Render operations panel HTML
*/
renderOperationsPanel(operations) {
const panel = document.createElement('div');
panel.className = 'operations-panel';
panel.innerHTML = `
<div class="operations-header">
<h3>Pending Operations (${operations.length})</h3>
<div class="operations-actions">
<button class="btn btn-approve-all" onclick="approveAllOperations()">
✓ Approve All
</button>
<button class="btn btn-reject-all" onclick="rejectAllOperations()">
✗ Reject All
</button>
</div>
</div>
<div class="operations-list">
${operations.map((op, index) => this.renderOperationItem(op, index)).join('')}
</div>
`;
return panel;
}
/**
* Render single operation item
*/
renderOperationItem(operation, index) {
const icon = this.getOperationIcon(operation.type);
const colorClass = this.getOperationColorClass(operation.type);
return `
<div class="operation-item ${colorClass}" data-index="${index}">
<div class="operation-icon">${icon}</div>
<div class="operation-details">
<div class="operation-description">${operation.description}</div>
${operation.tag.content ? `<button class="btn-view-code" onclick="viewCode(${index})">View Code</button>` : ''}
</div>
<div class="operation-actions">
<button class="btn btn-approve" onclick="approveOperation(${index})" title="Approve">✓</button>
<button class="btn btn-reject" onclick="rejectOperation(${index})" title="Reject">✗</button>
</div>
<div class="operation-status">
<span class="status-pending">Pending</span>
</div>
</div>
`;
}
/**
* Get icon for operation type
*/
getOperationIcon(type) {
const icons = {
write: '📄',
rename: '✏️',
delete: '🗑️',
install: '📦',
command: '⚡'
};
return icons[type] || '⚙️';
}
/**
* Get color class for operation type
*/
getOperationColorClass(type) {
const classes = {
write: 'op-write',
rename: 'op-rename',
delete: 'op-delete',
install: 'op-install',
command: 'op-command'
};
return classes[type] || '';
}
/**
* Show operations panel
*/
showOperationsPanel(operations, response) {
this.pendingOperations = operations;
window.currentOperationsResponse = response;
// Remove existing panel
const existing = document.querySelector('.operations-panel');
if (existing) {
existing.remove();
}
// Add new panel
const chatContainer = document.getElementById('chat-messages');
const panel = this.renderOperationsPanel(operations);
chatContainer.appendChild(panel);
// Make it visible
panel.style.display = 'block';
}
/**
* Update operation status
*/
updateOperationStatus(index, status) {
const operationItem = document.querySelector(`.operation-item[data-index="${index}"]`);
if (!operationItem) return;
const statusElement = operationItem.querySelector('.operation-status');
statusElement.innerHTML = `<span class="status-${status}">${status.charAt(0).toUpperCase() + status.slice(1)}</span>`;
if (status === 'approved' || status === 'rejected') {
// Disable buttons
const approveBtn = operationItem.querySelector('.btn-approve');
const rejectBtn = operationItem.querySelector('.btn-reject');
approveBtn.disabled = true;
rejectBtn.disabled = true;
}
}
/**
* Update all operations status
*/
updateAllOperationsStatus(status) {
if (!this.pendingOperations) return;
this.pendingOperations.forEach((_, index) => {
this.updateOperationStatus(index, status);
});
}
/**
* Hide operations panel
*/
hideOperationsPanel() {
const panel = document.querySelector('.operations-panel');
if (panel) {
panel.remove();
}
this.pendingOperations = null;
}
/**
* Show progress indicator
*/
showProgress(message) {
const chatContainer = document.getElementById('chat-messages');
const progress = document.createElement('div');
progress.className = 'operation-progress';
progress.innerHTML = `
<div class="progress-spinner"></div>
<div class="progress-message">${message}</div>
`;
chatContainer.appendChild(progress);
progress.style.display = 'block';
}
/**
* Update progress message
*/
updateProgress(message) {
const progressElement = document.querySelector('.operation-progress');
if (progressElement) {
const messageElement = progressElement.querySelector('.progress-message');
if (messageElement) {
messageElement.textContent = message;
}
}
}
/**
* Hide progress indicator
*/
hideProgress() {
const progressElement = document.querySelector('.operation-progress');
if (progressElement) {
progressElement.remove();
}
}
/**
* Show completion message
*/
showCompletion(results) {
const chatContainer = document.querySelector('.chat-container');
const completion = document.createElement('div');
completion.className = 'operation-completion';
const successCount = results.operations.filter(op => op.success).length;
const errorCount = results.errors.length;
completion.innerHTML = `
<div class="completion-header">
<span class="completion-icon">✓</span>
<h4>Operations Complete</h4>
</div>
<div class="completion-summary">
<span class="success">${successCount} succeeded</span>
${errorCount > 0 ? `<span class="error">${errorCount} failed</span>` : ''}
</div>
`;
chatContainer.appendChild(completion);
completion.style.display = 'block';
// Auto-hide after 3 seconds
setTimeout(() => {
completion.remove();
}, 3000);
}
/**
* Show error message
*/
showError(message) {
const chatContainer = document.querySelector('.chat-container');
const error = document.createElement('div');
error.className = 'operation-error';
error.innerHTML = `
<div class="error-header">
<span class="error-icon">⚠️</span>
<h4>Operation Failed</h4>
</div>
<div class="error-message">${message}</div>
`;
chatContainer.appendChild(error);
error.style.display = 'block';
}
}
// Global instance
const tagRenderer = new TagRenderer();
// Store current response for execution
window.currentOperationsResponse = null;
// Global functions for onclick handlers
window.approveOperation = async function(index) {
if (!tagRenderer.pendingOperations) return;
tagRenderer.updateOperationStatus(index, 'approved');
};
window.rejectOperation = function(index) {
if (!tagRenderer.pendingOperations) return;
tagRenderer.updateOperationStatus(index, 'rejected');
};
window.approveAllOperations = async function() {
if (!tagRenderer.pendingOperations) return;
tagRenderer.updateAllOperationsStatus('approved');
// Execute operations
await executeApprovedOperations();
};
window.rejectAllOperations = function() {
if (!tagRenderer.pendingOperations) return;
tagRenderer.hideOperationsPanel();
window.currentOperationsResponse = null;
};
window.viewCode = function(index) {
if (!tagRenderer.pendingOperations) return;
const operation = tagRenderer.pendingOperations[index];
if (operation.tag && operation.tag.content) {
// Show code in modal
const modal = document.createElement('div');
modal.className = 'code-modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>${operation.tag.path}</h3>
<button class="btn-close" onclick="this.closest('.code-modal').remove()">✕</button>
</div>
<pre><code>${escapeHtml(operation.tag.content)}</code></pre>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'block';
}
};
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Execute approved operations
*/
async function executeApprovedOperations() {
if (!window.currentOperationsResponse) {
console.error('No response to execute');
tagRenderer.showError('No operations to execute');
return;
}
// Use attachedSessionId (from chat-functions.js) instead of chatSessionId
const sessionId = window.attachedSessionId || window.chatSessionId;
if (!sessionId) {
console.error('No session ID - not attached to a session');
tagRenderer.showError('Not attached to a session. Please start or attach to a session first.');
return;
}
const response = window.currentOperationsResponse;
console.log('Executing operations for session:', sessionId);
try {
// Show progress
tagRenderer.showProgress('Executing operations...');
// Call execute API with credentials
const res = await fetch(`/claude/api/claude/sessions/${sessionId}/operations/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ response })
});
const data = await res.json();
if (!res.ok) {
console.error('Execute API error:', data);
throw new Error(data.error || 'Failed to execute operations');
}
console.log('Operations executed successfully:', data.results);
// The WebSocket will handle showing completion
// when it receives the operations-executed event
} catch (error) {
console.error('Error executing operations:', error);
tagRenderer.hideProgress();
tagRenderer.showError(error.message);
}
}
// Export for use in other files
if (typeof module !== 'undefined' && module.exports) {
module.exports = TagRenderer;
}