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,499 @@
// ============================================
// Chat Interface Functions
// ============================================
let chatSessionId = null;
let chatMessages = [];
let attachedSessionId = null;
// Reset all chat state
function resetChatState() {
console.log('Resetting chat state...');
chatSessionId = null;
chatMessages = [];
attachedSessionId = null;
console.log('Chat state reset complete');
}
// Load Chat View
async function loadChatView() {
console.log('[loadChatView] Loading chat view...');
// Reset state on view load to prevent stale session references
resetChatState();
// Load chat sessions
try {
console.log('[loadChatView] Fetching sessions...');
const res = await fetch('/claude/api/claude/sessions');
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
const data = await res.json();
console.log('[loadChatView] Sessions data received:', data);
const sessionsListEl = document.getElementById('chat-history-list');
if (!sessionsListEl) {
console.error('[loadChatView] chat-history-list element not found!');
return;
}
// ONLY show active sessions - no historical sessions in chat view
// Historical sessions are read-only and can't receive new messages
const activeSessions = (data.active || []).filter(s => s.status === 'running');
console.log('Active sessions (can receive messages):', activeSessions.length);
if (activeSessions.length > 0) {
sessionsListEl.innerHTML = activeSessions.map(session => {
const projectName = session.metadata && session.metadata.project ?
session.metadata.project :
session.id.substring(0, 20);
return `
<div class="chat-history-item ${session.id === attachedSessionId ? 'active' : ''}"
onclick="attachToSession('${session.id}')">
<div class="chat-history-icon">💬</div>
<div class="chat-history-content">
<div class="chat-history-title">${projectName}</div>
<div class="chat-history-meta">
<span class="chat-history-date">${new Date(session.createdAt).toLocaleDateString()}</span>
<span class="chat-history-status active">Running</span>
</div>
</div>
</div>
`;
}).join('');
} else {
sessionsListEl.innerHTML = `
<div class="chat-history-empty">
<p>No active sessions</p>
<button class="btn-primary" onclick="startNewChat()" style="margin-top: 12px;">+ Start New Chat</button>
</div>
`;
}
console.log('[loadChatView] Chat view loaded successfully');
} catch (error) {
console.error('[loadChatView] Error loading chat sessions:', error);
const sessionsListEl = document.getElementById('chat-history-list');
if (sessionsListEl) {
sessionsListEl.innerHTML = `
<div class="chat-history-empty">
<p>Error: ${error.message}</p>
<button class="btn-secondary" onclick="loadChatView()" style="margin-top: 12px;">Retry</button>
</div>
`;
}
}
}
// Start New Chat
async function startNewChat() {
// Reset all state first
resetChatState();
// Clear current chat
clearChatDisplay();
appendSystemMessage('Creating new chat session...');
// Create new session
try {
console.log('Creating new Claude Code session...');
const res = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workingDir: '/home/uroma/obsidian-vault',
metadata: { type: 'chat', source: 'web-ide' }
})
});
const data = await res.json();
console.log('Session creation response:', data);
if (data.success) {
attachedSessionId = data.session.id;
chatSessionId = data.session.id;
console.log('New session created:', data.session.id);
// Update UI
document.getElementById('current-session-id').textContent = data.session.id;
document.getElementById('chat-title').textContent = 'New Chat';
// Subscribe to session via WebSocket
subscribeToSession(data.session.id);
// Reload sessions list
loadChatView();
// Show success message
appendSystemMessage('✅ New chat session started! You can now chat with Claude Code.');
} else {
console.error('Session creation failed:', data);
appendSystemMessage('❌ Failed to create session: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('Error starting new chat:', error);
appendSystemMessage('❌ Failed to start new chat session: ' + error.message);
}
}
// Attach to Existing Session
function attachToSession(sessionId) {
attachedSessionId = sessionId;
chatSessionId = sessionId;
// Update UI
document.getElementById('current-session-id').textContent = sessionId;
// Load session messages
loadSessionMessages(sessionId);
// Subscribe to session via WebSocket
subscribeToSession(sessionId);
// Update active state in sidebar
document.querySelectorAll('.chat-session-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('onclick') && item.getAttribute('onclick').includes(sessionId)) {
item.classList.add('active');
}
});
appendSystemMessage('Attached to session: ' + sessionId);
}
// Subscribe to session via WebSocket
function subscribeToSession(sessionId) {
if (window.ws && window.ws.readyState === WebSocket.OPEN) {
window.ws.send(JSON.stringify({
type: 'subscribe',
sessionId: sessionId
}));
console.log('Subscribed to session:', sessionId);
}
}
// Load Session Messages
async function loadSessionMessages(sessionId) {
try {
const res = await fetch('/claude/api/claude/sessions/' + sessionId);
const data = await res.json();
if (data.session) {
clearChatDisplay();
// Add existing messages from output buffer
data.session.outputBuffer.forEach(entry => {
appendMessage('assistant', entry.content, false);
});
}
} catch (error) {
console.error('Error loading session messages:', error);
}
}
// Handle Chat Key Press
function handleChatKeypress(event) {
const input = document.getElementById('chat-input');
// Update character count
const charCount = input.value.length;
document.getElementById('char-count').textContent = charCount + ' characters';
// Send on Enter (but allow Shift+Enter for new line)
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendChatMessage();
}
// Auto-resize textarea
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 150) + 'px';
}
// Send Chat Message
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
if (!attachedSessionId) {
appendSystemMessage('Please start or attach to a session first.');
return;
}
// Add user message to chat
appendMessage('user', message);
clearInput();
// Show streaming indicator
showStreamingIndicator();
try {
// Check WebSocket state
if (!window.ws) {
console.error('WebSocket is null/undefined');
appendSystemMessage('WebSocket not initialized. Please refresh the page.');
hideStreamingIndicator();
return;
}
const state = window.ws.readyState;
const stateName = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][state] || 'UNKNOWN';
console.log('WebSocket state:', state, stateName);
if (state !== WebSocket.OPEN) {
console.error('WebSocket not in OPEN state:', stateName);
appendSystemMessage(`WebSocket not ready (state: ${stateName}). Retrying...`);
hideStreamingIndicator();
// Trigger reconnection if closed
if (state === WebSocket.CLOSED) {
console.log('WebSocket closed, triggering reconnection...');
if (typeof connectWebSocket === 'function') {
connectWebSocket();
}
}
return;
}
// Send command via WebSocket
window.ws.send(JSON.stringify({
type: 'command',
sessionId: attachedSessionId,
command: message
}));
console.log('Sent command via WebSocket:', message.substring(0, 50));
} catch (error) {
console.error('Error sending message:', error);
hideStreamingIndicator();
appendSystemMessage('Failed to send message: ' + error.message);
}
}
// Append Message to Chat
function appendMessage(role, content, scroll) {
const messagesContainer = document.getElementById('chat-messages');
// Remove welcome message if present
const welcome = messagesContainer.querySelector('.chat-welcome');
if (welcome) {
welcome.remove();
}
// Remove streaming indicator if present
const streaming = messagesContainer.querySelector('.streaming-indicator');
if (streaming) {
streaming.remove();
}
const messageDiv = document.createElement('div');
messageDiv.className = 'chat-message ' + role;
const avatar = document.createElement('div');
avatar.className = 'chat-message-avatar';
avatar.textContent = role === 'user' ? '👤' : '🤖';
const contentDiv = document.createElement('div');
contentDiv.className = 'chat-message-content';
const bubble = document.createElement('div');
bubble.className = 'chat-message-bubble';
// Format content (handle code blocks, etc.)
bubble.innerHTML = formatMessage(content);
const timestamp = document.createElement('div');
timestamp.className = 'chat-message-timestamp';
timestamp.textContent = new Date().toLocaleTimeString();
contentDiv.appendChild(bubble);
contentDiv.appendChild(timestamp);
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
messagesContainer.appendChild(messageDiv);
// Scroll to bottom
if (scroll) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Update token usage (estimated)
updateTokenUsage(content.length);
}
// Append System Message
function appendSystemMessage(text) {
const messagesContainer = document.getElementById('chat-messages');
// Remove welcome message if present
const welcome = messagesContainer.querySelector('.chat-welcome');
if (welcome) {
welcome.remove();
}
const systemDiv = document.createElement('div');
systemDiv.className = 'chat-message assistant';
systemDiv.style.opacity = '0.8';
const avatar = document.createElement('div');
avatar.className = 'chat-message-avatar';
avatar.textContent = '';
const contentDiv = document.createElement('div');
contentDiv.className = 'chat-message-content';
const bubble = document.createElement('div');
bubble.className = 'chat-message-bubble';
bubble.innerHTML = '<em>' + escapeHtml(text) + '</em>';
contentDiv.appendChild(bubble);
systemDiv.appendChild(avatar);
systemDiv.appendChild(contentDiv);
messagesContainer.appendChild(systemDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Format Message (handle code blocks, markdown, etc.)
function formatMessage(content) {
// Escape HTML first
let formatted = escapeHtml(content);
// Handle code blocks
formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, function(match, lang, code) {
return '<pre><code class="language-' + (lang || 'text') + '">' + code.trim() + '</code></pre>';
});
// Handle inline code
formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
// Handle line breaks
formatted = formatted.replace(/\n/g, '<br>');
return formatted;
}
// Show Streaming Indicator
function showStreamingIndicator() {
const messagesContainer = document.getElementById('chat-messages');
// Remove existing streaming indicator
const existing = messagesContainer.querySelector('.streaming-indicator');
if (existing) {
existing.remove();
}
const streamingDiv = document.createElement('div');
streamingDiv.className = 'streaming-indicator';
streamingDiv.innerHTML = '<div class="streaming-dot"></div><div class="streaming-dot"></div><div class="streaming-dot"></div>';
messagesContainer.appendChild(streamingDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Hide Streaming Indicator
function hideStreamingIndicator() {
const streaming = document.querySelector('.streaming-indicator');
if (streaming) {
streaming.remove();
}
}
// Clear Chat Display
function clearChatDisplay() {
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.innerHTML = '';
// Reset token usage
document.getElementById('token-usage').textContent = '0 tokens used';
}
// Clear Chat
function clearChat() {
if (confirm('Clear all messages in this chat?')) {
clearChatDisplay();
// Show welcome message again
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.innerHTML = `
<div class="chat-welcome">
<h2>👋 Chat Cleared</h2>
<p>Start a new conversation with Claude Code.</p>
</div>
`;
}
}
// Clear Input
function clearInput() {
const input = document.getElementById('chat-input');
input.value = '';
input.style.height = 'auto';
document.getElementById('char-count').textContent = '0 characters';
}
// Update Token Usage
function updateTokenUsage(charCount) {
// Rough estimation: ~4 characters per token
const estimatedTokens = Math.ceil(charCount / 4);
const currentUsage = parseInt(document.getElementById('token-usage').textContent) || 0;
document.getElementById('token-usage').textContent = (currentUsage + estimatedTokens) + ' tokens used';
}
// Show Attach CLI Modal
function showAttachCliModal() {
document.getElementById('modal-overlay').classList.remove('hidden');
document.getElementById('attach-cli-modal').classList.remove('hidden');
}
// Submit Attach CLI Session
async function submitAttachCliSession() {
const sessionId = document.getElementById('cli-session-id').value.trim();
if (!sessionId) {
alert('Please enter a session ID');
return;
}
attachToSession(sessionId);
closeModal();
}
// Attach File (placeholder for now)
function attachFile() {
appendSystemMessage('File attachment feature coming soon! For now, use @filename to reference files.');
}
// Show Chat Settings (placeholder)
function showChatSettings() {
appendSystemMessage('Chat settings coming soon!');
}
// Export variables to window for global access
if (typeof window !== 'undefined') {
window.attachedSessionId = attachedSessionId;
window.chatSessionId = chatSessionId;
window.chatMessages = chatMessages;
// Create a proxy to keep window vars in sync
Object.defineProperty(window, 'attachedSessionId', {
get: function() { return attachedSessionId; },
set: function(value) { attachedSessionId = value; }
});
Object.defineProperty(window, 'chatSessionId', {
get: function() { return chatSessionId; },
set: function(value) { chatSessionId = value; }
});
}