// ============================================
// Chat Interface Functions
// ============================================
let chatSessionId = null;
let chatMessages = [];
let attachedSessionId = null;
let isGenerating = false; // Track if Claude is currently generating
let modeSuggestionTimeout = null; // Track mode suggestion auto-hide timer
// 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...');
// Check if there's a pending session from Sessions view
if (window.pendingSessionId) {
console.log('[loadChatView] Detected pending session:', window.pendingSessionId);
const sessionId = window.pendingSessionId;
const sessionData = window.pendingSessionData;
// Clear pending session (consume it)
window.pendingSessionId = null;
window.pendingSessionData = null;
// Load the session
await loadSessionIntoChat(sessionId, sessionData);
return;
}
// Preserve attached session ID if it exists (for auto-session workflow)
const preservedSessionId = attachedSessionId;
// Reset state on view load to prevent stale session references
resetChatState();
// Restore attached session if it was set (e.g., from auto-session initialization)
if (preservedSessionId) {
console.log('[loadChatView] Restoring attached session:', preservedSessionId);
attachedSessionId = preservedSessionId;
chatSessionId = preservedSessionId;
}
// 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
let activeSessions = (data.active || []).filter(s => s.status === 'running');
// Filter by current project if in project context
const currentProjectDir = window.currentProjectDir;
if (currentProjectDir) {
console.log('[loadChatView] Filtering sessions for project path:', currentProjectDir);
// Filter sessions that belong to this project
activeSessions = activeSessions.filter(session => {
// Check if session's working directory is within current project directory
const sessionWorkingDir = session.workingDir || '';
// Direct match: session working dir starts with project dir
const directMatch = sessionWorkingDir.startsWith(currentProjectDir);
// Metadata match: session metadata project matches
const metadataMatch = session.metadata?.project === currentProjectDir;
// For project sessions, also check if project path is in working dir
const pathMatch = sessionWorkingDir.includes(currentProjectDir) || currentProjectDir.includes(sessionWorkingDir);
const isMatch = directMatch || metadataMatch || pathMatch;
console.log(`[loadChatView] Session ${session.id}:`, {
workingDir: sessionWorkingDir,
projectDir: currentProjectDir,
directMatch,
metadataMatch,
pathMatch,
isMatch
});
return isMatch;
});
console.log('[loadChatView] Project sessions found:', activeSessions.length, 'out of', (data.active || []).length);
}
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 `
π¬
${projectName}
${new Date(session.createdAt).toLocaleDateString()}
Running
`;
}).join('');
} else {
// Zero-friction entry: Auto-create session in project context
if (currentProjectName && window.currentProjectDir) {
console.log('[loadChatView] No sessions for project, auto-creating...');
sessionsListEl.innerHTML = `
Creating session for project: ${currentProjectName}
`;
// Auto-create session
startNewChat();
} else {
const emptyMessage = `No active sessions
`;
sessionsListEl.innerHTML = `
${emptyMessage}
`;
}
}
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 = `
Error: ${error.message}
`;
}
}
}
/**
* Load a specific session into Chat view
* Called when continuing from Sessions view
*/
async function loadSessionIntoChat(sessionId, sessionData = null) {
try {
appendSystemMessage('π Loading session...');
// If no session data provided, fetch it
if (!sessionData) {
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
sessionData = data.session;
}
if (!sessionData) {
throw new Error('Session not found');
}
// Set session IDs
attachedSessionId = sessionId;
chatSessionId = sessionId;
// Update UI
document.getElementById('current-session-id').textContent = sessionId;
// Clear chat display
clearChatDisplay();
// Load session messages (both user and assistant)
if (sessionData.outputBuffer && sessionData.outputBuffer.length > 0) {
sessionData.outputBuffer.forEach(entry => {
if (entry.role) {
appendMessage(entry.role, entry.content, false);
} else {
// Legacy format - default to assistant
appendMessage('assistant', entry.content, false);
}
});
}
// Show success message
const isRunning = sessionData.status === 'running';
const statusText = isRunning ? 'Active session' : 'Historical session';
appendSystemMessage(`β
Loaded ${statusText} from ${new Date(sessionData.createdAt).toLocaleString()}`);
if (!isRunning) {
appendSystemMessage('βΉοΈ This is a historical session. Messages are read-only.');
}
// Update chat history sidebar to highlight this session
if (typeof loadChatHistory === 'function') {
loadChatHistory();
}
// Subscribe to session for live updates (if running)
if (isRunning) {
subscribeToSession(sessionId);
}
// Focus input for active sessions
if (isRunning) {
setTimeout(() => {
const input = document.getElementById('chat-input');
if (input) input.focus();
}, 100);
}
} catch (error) {
console.error('[loadSessionIntoChat] Error:', error);
appendSystemMessage('β Failed to load session: ' + error.message);
}
}
// Start New Chat
async function startNewChat() {
// Reset all state first
resetChatState();
// Clear current chat
clearChatDisplay();
appendSystemMessage('Creating new chat session...');
// Determine working directory based on context
let workingDir = '/home/uroma/obsidian-vault'; // default
let projectName = null;
// If we're in a project context, use the project directory
if (window.currentProjectDir) {
workingDir = window.currentProjectDir;
projectName = window.currentProjectDir.split('/').pop();
console.log('[startNewChat] Creating session for project:', projectName, 'at', workingDir);
}
// 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: workingDir,
metadata: {
type: 'chat',
source: 'web-ide',
project: projectName,
projectPath: window.currentProjectDir || null
}
})
});
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 = projectName ? `Project: ${projectName}` : 'New Chat';
// Subscribe to session via WebSocket
subscribeToSession(data.session.id);
// Reload sessions list
loadChatView();
// Hide the creation success message after a short delay
setTimeout(() => {
const loadingMsg = document.querySelector('.chat-system');
if (loadingMsg && loadingMsg.textContent.includes('Creating new chat session')) {
loadingMsg.remove();
}
}, 2000);
// Focus on input
const input = document.getElementById('chat-input');
if (input) {
input.focus();
}
} else {
throw new Error(data.error || 'Failed to create session');
}
} catch (error) {
console.error('Error creating new chat session:', error);
appendSystemMessage('β Failed to create 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;
// Update context panel with session
if (typeof contextPanel !== 'undefined' && contextPanel) {
contextPanel.setSession(sessionId, 'active');
}
// 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 - restore both user and assistant messages
data.session.outputBuffer.forEach(entry => {
// Check if entry has role information (newer format)
if (entry.role) {
appendMessage(entry.role, entry.content, false);
} else {
// Legacy format - assume assistant if no role specified
appendMessage('assistant', entry.content, false);
}
});
}
} catch (error) {
console.error('Error loading session messages:', error);
}
}
// Handle Chat Input (modern input handler)
function handleChatInput(event) {
const input = event.target;
const wrapper = document.getElementById('chat-input-wrapper');
const charCountBadge = document.getElementById('char-count-badge');
// Update character count badge
const charCount = input.value.length;
charCountBadge.textContent = charCount + ' chars';
// Toggle typing state for badge visibility
if (charCount > 0) {
wrapper.classList.add('typing');
} else {
wrapper.classList.remove('typing');
}
// Auto-resize textarea
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 200) + 'px';
// Check for terminal command suggestion in Auto/Native modes
if ((currentChatMode === 'auto' || currentChatMode === 'native') && charCount > 0) {
checkForTerminalCommand(input.value);
} else {
hideModeSuggestion();
}
}
// Handle Chat Key Press
function handleChatKeypress(event) {
const input = document.getElementById('chat-input');
// Send on Enter (but allow Shift+Enter for new line)
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendChatMessage();
}
}
// Check for Terminal Command (Task 4: Auto-Suggest Terminal Mode)
function checkForTerminalCommand(message) {
const banner = document.getElementById('mode-suggestion-banner');
// Don't show suggestion if already in webcontainer mode
if (currentChatMode === 'webcontainer') {
hideModeSuggestion();
return;
}
// Check if message looks like a shell command
if (isShellCommand(message)) {
showModeSuggestion();
} else {
hideModeSuggestion();
}
}
// Show Mode Suggestion Banner
function showModeSuggestion() {
const banner = document.getElementById('mode-suggestion-banner');
if (banner && banner.style.display === 'none') {
banner.style.display = 'flex';
// Auto-hide after 10 seconds if no action
if (modeSuggestionTimeout) {
clearTimeout(modeSuggestionTimeout);
}
modeSuggestionTimeout = setTimeout(() => {
hideModeSuggestion();
}, 10000);
}
}
// Hide Mode Suggestion Banner
function hideModeSuggestion() {
const banner = document.getElementById('mode-suggestion-banner');
if (banner && banner.style.display !== 'none') {
banner.classList.add('fade-out');
setTimeout(() => {
banner.style.display = 'none';
banner.classList.remove('fade-out');
}, 300);
if (modeSuggestionTimeout) {
clearTimeout(modeSuggestionTimeout);
modeSuggestionTimeout = null;
}
}
}
// Switch to Terminal Mode
function switchToTerminalMode() {
hideModeSuggestion();
setChatMode('webcontainer');
appendSystemMessage('β
Switched to Terminal mode. Your commands will execute in a persistent terminal session.');
}
// Dismiss Mode Suggestion and Send Anyway
function dismissModeSuggestion() {
hideModeSuggestion();
// Proceed with sending the message in current mode
sendChatMessage();
}
// Send Chat Message (Enhanced with smart input parsing)
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;
}
// Hide mode suggestion banner
hideModeSuggestion();
// Parse smart input (file references, commands)
let parsed = null;
if (typeof smartInput !== 'undefined' && smartInput) {
try {
parsed = smartInput.parser.parse(message);
// Update context panel with referenced files
if (parsed.files.length > 0 && typeof contextPanel !== 'undefined' && contextPanel) {
parsed.files.forEach(filePath => {
const fileName = filePath.split('/').pop();
const ext = fileName.split('.').pop();
const icon = getFileIcon(ext);
contextPanel.addActiveFile(filePath, fileName, icon);
});
console.log('Added', parsed.files.length, 'referenced files to context panel');
}
console.log('Parsed input:', parsed);
} catch (error) {
console.error('Error parsing smart input:', error);
}
}
// Use selected mode from buttons, or fall back to parsed mode
const selectedMode = currentChatMode || 'auto';
// Add user message to chat
appendMessage('user', message);
clearInput();
// Show streaming indicator and update button state
showStreamingIndicator();
setGeneratingState(true);
// Handle WebContainer mode separately
if (selectedMode === 'webcontainer') {
await handleWebContainerCommand(message);
return;
}
try {
// Check WebSocket state
if (!window.ws) {
console.error('WebSocket is null/undefined');
appendSystemMessage('WebSocket not initialized. Please refresh the page.');
hideStreamingIndicator();
setGeneratingState(false);
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();
setGeneratingState(false);
// Trigger reconnection if closed
if (state === WebSocket.CLOSED) {
console.log('WebSocket closed, triggering reconnection...');
if (typeof connectWebSocket === 'function') {
connectWebSocket();
}
}
return;
}
// Send command via WebSocket with parsed metadata
const payload = {
type: 'command',
sessionId: attachedSessionId,
command: message
};
// Add metadata if available (files, commands, mode)
const hasFiles = parsed && parsed.files.length > 0;
const hasCommands = parsed && parsed.commands.length > 0;
const modeNotAuto = selectedMode !== 'auto';
if (hasFiles || hasCommands || modeNotAuto) {
payload.metadata = {
files: parsed ? parsed.files : [],
commands: parsed ? parsed.commands : [],
mode: selectedMode,
hasFileReferences: parsed ? parsed.hasFileReferences : false
};
console.log('Sending with metadata:', payload.metadata);
}
window.ws.send(JSON.stringify(payload));
console.log('Sent command via WebSocket:', message.substring(0, 50));
} catch (error) {
console.error('Error sending message:', error);
hideStreamingIndicator();
setGeneratingState(false);
appendSystemMessage('Failed to send message: ' + error.message);
}
}
// Task 1: Set Generating State (show/hide stop button)
function setGeneratingState(generating) {
isGenerating = generating;
const sendButton = document.getElementById('send-button');
const stopButton = document.getElementById('stop-button');
if (generating) {
// Show stop button, hide send button
sendButton.classList.add('hidden');
stopButton.classList.remove('hidden');
} else {
// Show send button, hide stop button
sendButton.classList.remove('hidden');
stopButton.classList.add('hidden');
}
}
// Task 1: Stop Generation
function stopGeneration() {
console.log('Stopping generation...');
if (!window.ws || window.ws.readyState !== WebSocket.OPEN) {
appendSystemMessage('β οΈ Cannot stop: WebSocket not connected');
return;
}
// Send stop signal via WebSocket
window.ws.send(JSON.stringify({
type: 'stop',
sessionId: attachedSessionId
}));
appendSystemMessage('βΈοΈ Stopping generation...');
// Update UI state
hideStreamingIndicator();
setGeneratingState(false);
}
// Helper function to get file icon for context panel
function getFileIcon(ext) {
const icons = {
'js': 'π', 'mjs': 'π', 'ts': 'π', 'tsx': 'βοΈ', 'jsx': 'βοΈ',
'html': 'π', 'htm': 'π', 'css': 'π¨', 'scss': 'π¨',
'py': 'π', 'rb': 'π', 'php': 'π',
'json': 'π', 'xml': 'π',
'md': 'π', 'txt': 'π',
'png': 'πΌοΈ', 'jpg': 'πΌοΈ', 'jpeg': 'πΌοΈ', 'gif': 'πΌοΈ', 'svg': 'πΌοΈ',
'sh': 'π₯οΈ', 'bash': 'π₯οΈ', 'zsh': 'π₯οΈ',
'yml': 'βοΈ', 'yaml': 'βοΈ', 'toml': 'βοΈ'
};
return icons[ext] || 'π';
}
// Chat Mode Management
let currentChatMode = 'auto';
function setChatMode(mode) {
currentChatMode = mode;
// Update button states
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.mode === mode) {
btn.classList.add('active');
}
});
// Update context panel with mode
if (typeof contextPanel !== 'undefined' && contextPanel) {
contextPanel.setMode(mode);
}
// Initialize WebContainer if switching to webcontainer mode
if (mode === 'webcontainer') {
initializeWebContainer();
}
// Show mode change message
const modeNames = {
'auto': 'π€ Auto (best mode will be detected automatically)',
'native': 'π» Native (commands execute directly on your system)',
'webcontainer': 'π» Terminal (executes commands in persistent terminal session)'
};
appendSystemMessage(`Execution mode changed to: ${modeNames[mode]}`);
console.log('Chat mode set to:', mode);
}
// Initialize WebContainer for current session
async function initializeWebContainer() {
try {
if (typeof webContainerManager === 'undefined') {
appendSystemMessage('β οΈ WebContainer Manager not loaded. Refresh the page.');
return;
}
// Check if already initialized for current session
const sessionId = attachedSessionId || chatSessionId;
if (!sessionId) {
appendSystemMessage('β οΈ Please start a chat session first.');
return;
}
const status = webContainerManager.getStatus();
if (status.initialized && status.currentSession === sessionId) {
appendSystemMessage('β
WebContainer already initialized for this session');
return;
}
appendSystemMessage('π Initializing WebContainer environment...');
await webContainerManager.initialize(sessionId);
appendSystemMessage('β
WebContainer ready! Commands will execute in browser sandbox.');
} catch (error) {
console.error('Failed to initialize WebContainer:', error);
appendSystemMessage('β Failed to initialize WebContainer: ' + error.message);
}
}
// Detect if message is a shell command
function isShellCommand(message) {
const trimmed = message.trim().toLowerCase();
// Check for common shell command patterns
const commandPatterns = [
// Shell built-ins
/^(cd|ls|pwd|echo|cat|grep|find|rm|cp|mv|mkdir|rmdir|touch|chmod|chown|ln|head|tail|less|more|sort|uniq|wc|tar|zip|unzip|gzip|gunzip|df|du|ps|top|kill|killall|nohup|bg|fg|jobs|export|unset|env|source|\.|alias|unalias|history|clear|reset)(\s|$)/,
// Package managers
/^(npm|yarn|pnpm|pip|pip3|conda|brew|apt|apt-get|yum|dnf|pacman|curl|wget)(\s|$)/,
// Node.js commands
/^(node|npx)(\s|$)/,
// Python commands
/^(python|python3|pip|pip3|python3-m)(\s|$)/,
// Git commands
/^git(\s|$)/,
// Docker commands
/^docker(\s|$)/,
// File operations with paths
/^[a-z0-9_\-./]+\s*[\|>]/,
// Commands with arguments
/^[a-z][a-z0-9_\-]*\s+/,
// Absolute paths
/^\//,
// Scripts
/^(sh|bash|zsh|fish|powershell|pwsh)(\s|$)/
];
return commandPatterns.some(pattern => pattern.test(trimmed));
}
// Send shell command to active Claude CLI session
async function sendShellCommand(sessionId, command) {
try {
const response = await fetch('/claude/api/shell-command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, command })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Command execution failed');
}
return data;
} catch (error) {
console.error('[Shell Command] Error:', error);
throw error;
}
}
// Handle command execution in Full Stack mode (via Claude CLI session's stdin)
async function handleWebContainerCommand(message) {
const sessionId = attachedSessionId || chatSessionId;
if (!sessionId) {
appendSystemMessage('β οΈ No active session.');
hideStreamingIndicator();
setGeneratingState(false);
return;
}
// Smart command detection
if (!isShellCommand(message)) {
hideStreamingIndicator();
setGeneratingState(false);
appendSystemMessage(`π¬ This looks like a conversational message, not a shell command.
Terminal mode executes commands directly. For example:
β’ npm install
β’ ls -la
β’ git status
β’ python script.py
Would you like to:
1. Switch to Chat mode for conversational AI
2. Rephrase as a shell command`);
// Auto-switch to Chat mode after brief delay
setTimeout(() => {
if (currentChatMode === 'webcontainer') {
setChatMode('auto');
appendSystemMessage('β
Switched to Chat mode. You can continue your conversation.');
}
}, 3000);
return;
}
try {
appendSystemMessage(`π» Executing in session: ${message}`);
// Send shell command to the active Claude CLI session
const result = await sendShellCommand(sessionId, message);
hideStreamingIndicator();
setGeneratingState(false);
// Show command output
if (result.stdout) {
appendMessage('assistant', result.stdout);
}
if (result.stderr) {
appendMessage('assistant', `${result.stderr}`);
}
// Show completion status
if (result.exitCode === 0) {
appendSystemMessage(`β
Command completed successfully`);
} else {
appendSystemMessage(`β οΈ Command exited with code ${result.exitCode}`);
}
// Record tool usage
if (typeof contextPanel !== 'undefined' && contextPanel) {
contextPanel.recordToolUsage('shell_command');
}
} catch (error) {
console.error('Shell command execution failed:', error);
hideStreamingIndicator();
setGeneratingState(false);
appendSystemMessage('β Failed to execute command: ' + error.message);
}
}
// Fallback: Execute command in native mode via WebSocket
async function executeNativeCommand(message, sessionId) {
try {
// Send via WebSocket (backend will execute through native mode)
window.ws.send(JSON.stringify({
type: 'command',
sessionId: sessionId,
command: message,
metadata: {
executionMode: 'native',
timestamp: new Date().toISOString()
}
}));
appendSystemMessage('β
Command sent in native execution mode');
} catch (error) {
console.error('Native execution failed:', error);
throw new Error('Failed to execute in native mode: ' + error.message);
}
}
// Append Message to Chat (Enhanced with OpenCode-style rendering)
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();
}
// Use enhanced message system if available, otherwise fall back to basic
if (typeof ChatMessage !== 'undefined' && typeof renderEnhancedMessage !== 'undefined') {
// Create enhanced message with part-based structure
const message = new ChatMessage(role, [
new MessagePart('text', content)
]);
const messageDiv = renderEnhancedMessage(message);
messagesContainer.appendChild(messageDiv);
} else {
// Fallback to basic rendering
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 || scroll === undefined) {
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 = '' + escapeHtml(text) + '';
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 '' + code.trim() + '
';
});
// Handle inline code
formatted = formatted.replace(/`([^`]+)`/g, '$1');
// Handle line breaks
formatted = formatted.replace(/\n/g, '
');
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 = '';
messagesContainer.appendChild(streamingDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Hide Streaming Indicator
function hideStreamingIndicator() {
const streaming = document.querySelector('.streaming-indicator');
if (streaming) {
streaming.remove();
}
// Also reset generating state
setGeneratingState(false);
}
// 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 = `
π Chat Cleared
Start a new conversation with Claude Code.
`;
}
}
// Clear Input
function clearInput() {
const input = document.getElementById('chat-input');
const wrapper = document.getElementById('chat-input-wrapper');
const charCountBadge = document.getElementById('char-count-badge');
input.value = '';
input.style.height = 'auto';
wrapper.classList.remove('typing');
charCountBadge.textContent = '0 chars';
}
// 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.');
}
// Attach Image (placeholder for now)
function attachImage() {
appendSystemMessage('Image attachment feature coming soon! For now, use @filename to reference image files.');
}
// Insert Code Snippet (placeholder for now)
function insertCodeSnippet() {
const input = document.getElementById('chat-input');
const snippet = '```\n// Your code here\n```\n';
input.value += snippet;
input.focus();
handleChatInput({ target: input });
}
// 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; }
});
}