Files
SuperCharged-Claude-Code-Up…/docs/plans/2025-01-19-landing-page-implementation.md
uroma 0dd2083556 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>
2026-01-19 16:29:44 +00:00

37 KiB
Raw Blame History

Landing Page Workflow Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Enhance the /claude/ landing page with improved project/session cards, inline editing, quick actions, and smooth navigation to IDE.

Architecture: Single-page application with REST API backend. Frontend manages session state locally and communicates via Express.js endpoints. Uses WebSocket for real-time updates (already implemented).

Tech Stack: Express.js (Node.js), vanilla JavaScript (ES6+), CSS3, WebSocket API


Phase 1: Backend Enhancements

Task 1.1: Add Session Metadata Update Endpoint

Files:

  • Modify: server.js (find line 469, after executeOperations endpoint)
  • Test: Manual testing via curl/Postman

Step 1: Add PATCH endpoint to server.js

Find the line with app.post('/claude/api/claude/sessions/:id/operations/execute' (around line 469). Add this immediately after:

// Update session metadata
app.patch('/claude/api/claude/sessions/:id', requireAuth, async (req, res) => {
  try {
    const { metadata } = req.body;
    const sessionId = req.params.id;

    // Get session
    let session = claudeService.sessions.get(sessionId);

    if (!session) {
      // Try to load from historical sessions
      const historicalSessions = claudeService.loadHistoricalSessions();
      const historical = historicalSessions.find(s => s.id === sessionId);

      if (!historical) {
        return res.status(404).json({ error: 'Session not found' });
      }

      // For historical sessions, we can't update metadata directly
      // Return error for now
      return res.status(400).json({ error: 'Cannot update historical session metadata' });
    }

    // Update metadata
    if (metadata && typeof metadata === 'object') {
      session.metadata = { ...session.metadata, ...metadata };
    }

    res.json({
      success: true,
      session: {
        id: session.id,
        metadata: session.metadata,
        workingDir: session.workingDir
      }
    });
  } catch (error) {
    console.error('Error updating session:', error);
    res.status(500).json({ error: error.message });
  }
});

Step 2: Test the endpoint manually

# First, create a session to get a session ID
curl -X POST http://localhost:3010/claude/api/claude/sessions \
  -H "Content-Type: application/json" \
  -d '{"workingDir": "/home/uroma/obsidian-vault"}' \
  --cookie-jar cookies.txt

# Get session ID from response, then test PATCH
curl -X PATCH http://localhost:3010/claude/api/claude/sessions/SESSION_ID \
  -H "Content-Type: application/json" \
  -d '{"metadata": {"project": "Test Project"}}' \
  --cookie cookies.txt

Expected: {"success":true,"session":{...}}

Step 3: Commit

git add server.js
git commit -m "feat: add PATCH endpoint for session metadata updates"

Task 1.2: Add Session Duplicate Endpoint

Files:

  • Modify: server.js (after the PATCH endpoint from Task 1.1)
  • Test: Manual testing

Step 1: Add duplicate endpoint to server.js

Add this after the PATCH endpoint:

// Duplicate session
app.post('/claude/api/claude/sessions/:id/duplicate', requireAuth, async (req, res) => {
  try {
    const sessionId = req.params.id;

    // Get source session
    let sourceSession = claudeService.sessions.get(sessionId);

    if (!sourceSession) {
      return res.status(404).json({ error: 'Source session not found' });
    }

    // Create new session with same settings
    const newSession = await claudeService.createSession({
      workingDir: sourceSession.workingDir,
      metadata: {
        ...sourceSession.metadata,
        duplicatedFrom: sessionId,
        source: 'web-ide'
      }
    });

    res.json({
      success: true,
      session: {
        id: newSession.id,
        workingDir: newSession.workingDir,
        metadata: newSession.metadata
      }
    });
  } catch (error) {
    console.error('Error duplicating session:', error);
    res.status(500).json({ error: error.message });
  }
});

Step 2: Test the endpoint

# Duplicate a session
curl -X POST http://localhost:3010/claude/api/claude/sessions/SESSION_ID/duplicate \
  --cookie cookies.txt

Expected: {"success":true,"session":{"id":"new-session-id",...}}

Step 3: Commit

git add server.js
git commit -m "feat: add endpoint to duplicate sessions"

Task 1.3: Add Session Delete Endpoint

Files:

  • Modify: server.js (after duplicate endpoint)
  • Test: Manual testing

Step 1: Add DELETE endpoint to server.js

Add this after the duplicate endpoint:

// Delete session
app.delete('/claude/api/claude/sessions/:id', requireAuth, async (req, res) => {
  try {
    const sessionId = req.params.id;

    // Check if session exists
    const session = claudeService.sessions.get(sessionId);

    if (!session) {
      return res.status(404).json({ error: 'Session not found' });
    }

    // Kill the claude process if running
    if (claudeService.processes && claudeService.processes.has(sessionId)) {
      const procInfo = claudeService.processes.get(sessionId);
      if (procInfo.claude) {
        procInfo.claude.kill();
      }
      claudeService.processes.delete(sessionId);
    }

    // Remove from sessions map
    claudeService.sessions.delete(sessionId);

    // Delete session file if exists
    const fs = require('fs');
    const path = require('path');
    const sessionFile = path.join(claudeService.claudeSessionsDir, `${sessionId}.md`);

    if (fs.existsSync(sessionFile)) {
      fs.unlinkSync(sessionFile);
    }

    res.json({ success: true });
  } catch (error) {
    console.error('Error deleting session:', error);
    res.status(500).json({ error: error.message });
  }
});

Step 2: Test the endpoint

# Delete a session
curl -X DELETE http://localhost:3010/claude/api/claude/sessions/SESSION_ID \
  --cookie cookies.txt

Expected: {"success":true}

Step 3: Commit

git add server.js
git commit -m "feat: add endpoint to delete sessions"

Task 1.4: Enhance Session List Response

Files:

  • Modify: services/claude-service.js
  • Test: Check API response

Step 1: Find getSessions method in claude-service.js

Search for getSessions() method. It should return { active, historical }.

Step 2: Add helper method to calculate session metadata

Add this method to the ClaudeCodeService class (around line 300, or after other helper methods):

/**
 * Calculate enhanced session metadata
 */
calculateSessionMetadata(session) {
  const metadata = {
    lastMessage: null,
    fileCount: 0,
    messageCount: 0
  };

  if (session.outputBuffer && session.outputBuffer.length > 0) {
    // Get last message
    const lastEntry = session.outputBuffer[session.outputBuffer.length - 1];
    metadata.lastMessage = this.extractMessagePreview(lastEntry.content);

    // Count dyad-write tags (files created/modified)
    metadata.fileCount = session.outputBuffer.reduce((count, entry) => {
      const writeMatches = entry.content.match(/<dyad-write\s+path="/g);
      return count + (writeMatches ? writeMatches.length : 0);
    }, 0);

    metadata.messageCount = session.outputBuffer.length;
  }

  return metadata;
}

/**
 * Extract message preview (first 100 chars, stripped of tags)
 */
extractMessagePreview(content) {
  // Remove dyad tags
  let preview = content.replace(/<dyad-write[^>]*>[\s\S]*?<\/dyad-write>/g, '[File]');
  preview = preview.replace(/<dyad-[^>]+>/g, '');

  // Strip markdown code blocks
  preview = preview.replace(/```[\s\S]*?```/g, '[Code]');

  // Get first 100 chars
  preview = preview.substring(0, 100);

  // Truncate at last word boundary
  if (preview.length === 100) {
    const lastSpace = preview.lastIndexOf(' ');
    if (lastSpace > 50) {
      preview = preview.substring(0, lastSpace);
    }
    preview += '...';
  }

  return preview.trim() || 'No messages yet';
}

Step 3: Modify getSessions to include metadata

Find where sessions are returned in getSessions() and add metadata:

// In the active sessions loop
const activeSessions = Array.from(this.sessions.values()).map(session => {
  const metadata = this.calculateSessionMetadata(session);
  return {
    ...session,
    ...metadata,
    status: 'running'
  };
});

// In the historical sessions loop
const historicalSessions = historicalFiles.map(file => {
  // ... existing code ...
  const metadata = this.calculateSessionMetadata(session);
  return {
    ...session,
    ...metadata,
    status: 'historical'
  };
});

Step 4: Test the enhanced response

curl http://localhost:3010/claude/api/claude/sessions \
  --cookie cookies.txt | jq

Expected: Sessions now have lastMessage, fileCount, messageCount fields.

Step 5: Commit

git add services/claude-service.js
git commit -m "feat: enhance session list with lastMessage and fileCount"

Phase 2: Frontend Components

Task 2.1: Create SessionCard Component

Files:

  • Create: public/claude-ide/components/session-card.js
  • Create: public/claude-ide/components/session-card.css
  • Modify: public/claude-ide/sessions-landing.js (to use new component)

Step 1: Create session-card.js component

/**
 * SessionCard Component
 * Renders an enhanced session card with inline editing and quick actions
 */

class SessionCard {
  constructor(session, onClick) {
    this.session = session;
    this.onClick = onClick;
    this.isEditing = false;
    this.element = null;
  }

  render() {
    const card = document.createElement('div');
    card.className = 'session-card enhanced';

    const icon = this.session.status === 'running' ? '💬' : '📁';
    const relativeTime = this.getRelativeTime(new Date(this.session.createdAt || this.session.created_at));
    const fileCount = this.session.fileCount || 0;
    const lastMessage = this.session.lastMessage || 'No messages yet';

    card.innerHTML = `
      <div class="session-card-left">
        <div class="session-icon">${icon}</div>
      </div>

      <div class="session-card-middle">
        <div class="session-project-name" data-session-id="${this.session.id}">
          ${this.escapeHtml(this.getProjectName())}
        </div>
        <div class="session-path">${this.escapeHtml(this.session.workingDir)}</div>
        <div class="session-preview">${this.escapeHtml(lastMessage)}</div>
        <div class="session-meta">
          <span class="session-badge">📄 ${fileCount} files</span>
          <span class="session-time">${relativeTime}</span>
          <span class="session-status ${this.session.status}">
            ${this.session.status === 'running' ? 'Active' : 'Historical'}
          </span>
        </div>
      </div>

      <div class="session-card-right">
        <button class="btn btn-primary btn-continue" data-session-id="${this.session.id}">
          Continue →
        </button>
        <button class="btn btn-icon btn-menu" aria-label="Session menu">
        </button>
      </div>
    `;

    this.element = card;
    this.attachEventListeners();
    return card;
  }

  getProjectName() {
    return this.session.metadata?.project ||
           this.session.project ||
           this.session.workingDir.split('/').pop() ||
           'Session ' + this.session.id.substring(0, 8);
  }

  getRelativeTime(date) {
    const seconds = Math.floor((new Date() - date) / 1000);
    if (seconds < 60) return 'just now';
    if (seconds < 3600) return `${Math.floor(seconds/60)} min ago`;
    if (seconds < 86400) return `${Math.floor(seconds/3600)} hours ago`;
    return `${Math.floor(seconds/86400)} days ago`;
  }

  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

  attachEventListeners() {
    // Continue button
    const continueBtn = this.element.querySelector('.btn-continue');
    continueBtn.addEventListener('click', (e) => {
      e.preventDefault();
      this.onClick(this.session);
    });

    // Project name inline edit
    const nameEl = this.element.querySelector('.session-project-name');
    nameEl.addEventListener('click', () => this.startInlineEdit());

    // Menu button
    const menuBtn = this.element.querySelector('.btn-menu');
    menuBtn.addEventListener('click', (e) => this.showMenu(e));
  }

  startInlineEdit() {
    if (this.isEditing || this.session.status === 'historical') return;

    this.isEditing = true;
    const nameEl = this.element.querySelector('.session-project-name');
    const currentName = this.getProjectName();

    nameEl.innerHTML = `
      <input type="text" class="session-name-input" value="${this.escapeHtml(currentName)}" maxlength="50" />
      <button class="btn-icon btn-save" title="Save">✓</button>
      <button class="btn-icon btn-cancel" title="Cancel">✕</button>
    `;

    const input = nameEl.querySelector('input');
    const saveBtn = nameEl.querySelector('.btn-save');
    const cancelBtn = nameEl.querySelector('.btn-cancel');

    input.focus();
    input.select();

    const save = async () => {
      const newName = input.value.trim();
      if (newName && newName !== currentName) {
        nameEl.innerHTML = `<span class="saving">Saving...</span>`;
        await this.saveProjectName(newName);
      } else {
        this.cancelEdit();
      }
    };

    const cancel = () => {
      this.cancelEdit();
    };

    saveBtn.addEventListener('click', save);
    cancelBtn.addEventListener('click', cancel);
    input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') save();
      if (e.key === 'Escape') cancel();
    });
    input.addEventListener('blur', () => {
      // Delay to allow save button click to process
      setTimeout(() => {
        if (this.isEditing) cancel();
      }, 200);
    });
  }

  async saveProjectName(newName) {
    try {
      const res = await fetch(`/claude/api/claude/sessions/${this.session.id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({
          metadata: { project: newName }
        })
      });

      if (res.ok) {
        this.session.metadata = { ...this.session.metadata, project: newName };
        const nameEl = this.element.querySelector('.session-project-name');
        nameEl.textContent = newName;
        this.isEditing = false;
      } else {
        throw new Error('Failed to save name');
      }
    } catch (error) {
      console.error('Error saving project name:', error);
      this.cancelEdit();
      showToast('Failed to save name', 'error');
    }
  }

  cancelEdit() {
    this.isEditing = false;
    const nameEl = this.element.querySelector('.session-project-name');
    nameEl.textContent = this.getProjectName();
  }

  showMenu(e) {
    e.preventDefault();
    e.stopPropagation();

    // Remove existing menu
    const existing = document.querySelector('.session-menu-dropdown');
    if (existing) existing.remove();

    const menu = document.createElement('div');
    menu.className = 'session-menu-dropdown';
    menu.innerHTML = `
      <button class="menu-item" data-action="duplicate">
        <span>📋</span> Duplicate
      </button>
      <button class="menu-item" data-action="delete">
        <span>🗑️</span> Delete
      </button>
    `;

    const rect = e.target.getBoundingClientRect();
    menu.style.position = 'fixed';
    menu.style.top = `${rect.bottom + 5}px`;
    menu.style.right = `${window.innerWidth - rect.right}px`;

    document.body.appendChild(menu);

    menu.querySelectorAll('.menu-item').forEach(item => {
      item.addEventListener('click', () => {
        const action = item.dataset.action;
        this.handleMenuAction(action);
        menu.remove();
      });
    });

    // Close on click outside
    setTimeout(() => {
      document.addEventListener('click', function closeMenu(e) {
        if (!menu.contains(e.target)) {
          menu.remove();
          document.removeEventListener('click', closeMenu);
        }
      });
    }, 0);
  }

  async handleMenuAction(action) {
    if (action === 'duplicate') {
      await this.duplicateSession();
    } else if (action === 'delete') {
      if (confirm(`Delete project "${this.getProjectName()}"?`)) {
        await this.deleteSession();
      }
    }
  }

  async duplicateSession() {
    try {
      showToast('Duplicating project...', 'info');
      const res = await fetch(`/claude/api/claude/sessions/${this.session.id}/duplicate`, {
        method: 'POST',
        credentials: 'same-origin'
      });

      const data = await res.json();
      if (data.success) {
        showToast('Project duplicated!', 'success');
        // Refresh sessions list
        if (typeof loadSessions === 'function') {
          loadSessions();
        }
      }
    } catch (error) {
      console.error('Error duplicating session:', error);
      showToast('Failed to duplicate project', 'error');
    }
  }

  async deleteSession() {
    try {
      const res = await fetch(`/claude/api/claude/sessions/${this.session.id}`, {
        method: 'DELETE',
        credentials: 'same-origin'
      });

      if (res.ok) {
        showToast('Project deleted', 'success');
        // Refresh sessions list
        if (typeof loadSessions === 'function') {
          loadSessions();
        }
      }
    } catch (error) {
      console.error('Error deleting session:', error);
      showToast('Failed to delete project', 'error');
    }
  }
}

// Export
if (typeof window !== 'undefined') {
  window.SessionCard = SessionCard;
}

Step 2: Create session-card.css

/* Enhanced Session Card */
.session-card.enhanced {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 20px;
  align-items: center;
  padding: 24px;
  background: #1a1a1a;
  border: 1px solid #333;
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.3s ease;
  text-decoration: none;
  color: inherit;
  position: relative;
}

.session-card.enhanced:hover {
  background: #252525;
  border-color: #4a9eff;
  transform: translateX(8px);
  box-shadow: 0 4px 16px rgba(74, 158, 255, 0.2);
}

.session-card-left {
  display: flex;
  align-items: center;
}

.session-card-middle {
  min-width: 0;
  flex: 1;
}

.session-card-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

.session-project-name {
  font-size: 20px;
  font-weight: 600;
  color: #e0e0e0;
  margin-bottom: 4px;
  cursor: text;
  display: inline-block;
  transition: color 0.2s;
}

.session-project-name:hover {
  color: #4a9eff;
}

.session-path {
  font-size: 13px;
  color: #888;
  font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
  margin-bottom: 8px;
}

.session-preview {
  font-size: 14px;
  color: #aaa;
  margin-bottom: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.session-meta {
  display: flex;
  gap: 12px;
  font-size: 13px;
  align-items: center;
  flex-wrap: wrap;
}

.session-badge {
  background: #0d0d0d;
  padding: 4px 10px;
  border-radius: 6px;
  color: #888;
}

.session-time {
  color: #888;
}

.session-status.active {
  color: #51cf66;
  background: rgba(81, 207, 102, 0.1);
  padding: 4px 10px;
  border-radius: 6px;
  font-weight: 500;
}

.session-status.historical {
  color: #ffa94d;
  background: rgba(255, 169, 77, 0.1);
  padding: 4px 10px;
  border-radius: 6px;
  font-weight: 500;
}

.btn-continue {
  background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
}

.btn-continue:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(74, 158, 255, 0.4);
}

.btn-icon {
  background: transparent;
  border: 1px solid #333;
  color: #888;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 18px;
  transition: all 0.2s;
}

.btn-icon:hover {
  background: #333;
  color: #e0e0e0;
  border-color: #555;
}

/* Inline edit styles */
.session-name-input {
  background: #0d0d0d;
  border: 1px solid #4a9eff;
  color: #e0e0e0;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 18px;
  font-weight: 600;
  width: 200px;
  outline: none;
}

.session-name-input:focus {
  box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.3);
}

.saving {
  color: #888;
  font-style: italic;
}

/* Dropdown menu */
.session-menu-dropdown {
  background: #252525;
  border: 1px solid #444;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  min-width: 160px;
  z-index: 1000;
  overflow: hidden;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 12px 16px;
  background: transparent;
  border: none;
  color: #e0e0e0;
  cursor: pointer;
  transition: background 0.2s;
  font-size: 14px;
}

.menu-item:hover {
  background: #333;
}

.menu-item span:first-child {
  font-size: 16px;
}

Step 3: Update sessions-landing.js to use SessionCard

Modify the loadSessions() function to use the new component. Replace the session card rendering code:

// In loadSessions(), replace the sessionsList.innerHTML = allSessions.map(...) section with:

sessionsList.innerHTML = '';

allSessions.forEach(session => {
  const card = new SessionCard(session, (clickedSession) => {
    // Navigate to IDE
    showLoadingOverlay();
    window.location.href = `/claude/ide?session=${clickedSession.id}`;
  });

  const cardElement = card.render();
  sessionsList.appendChild(cardElement);
});

Step 4: Add helper functions

Add these functions to sessions-landing.js:

// Loading overlay
function showLoadingOverlay() {
  const overlay = document.createElement('div');
  overlay.className = 'loading-overlay';
  overlay.innerHTML = `
    <div class="loading-spinner"></div>
    <div class="loading-text">Opening workspace...</div>
  `;
  document.body.appendChild(overlay);
}

// Toast notifications
function showToast(message, type = 'info') {
  const toast = document.createElement('div');
  toast.className = `toast toast-${type}`;
  toast.textContent = message;
  document.body.appendChild(toast);

  setTimeout(() => toast.classList.add('show'), 10);
  setTimeout(() => {
    toast.classList.remove('show');
    setTimeout(() => toast.remove(), 300);
  }, 3000);
}

Step 5: Add loading overlay styles to sessions-landing.css

/* Loading overlay */
.loading-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  animation: fadeIn 0.2s ease;
}

.loading-spinner {
  width: 48px;
  height: 48px;
  border: 4px solid #333;
  border-top-color: #4a9eff;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

.loading-text {
  color: #e0e0e0;
  margin-top: 20px;
  font-size: 16px;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Toast notifications */
.toast {
  position: fixed;
  bottom: 20px;
  right: 20px;
  background: #333;
  color: #e0e0e0;
  padding: 12px 20px;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  transform: translateY(100px);
  opacity: 0;
  transition: all 0.3s ease;
  z-index: 10000;
}

.toast.show {
  transform: translateY(0);
  opacity: 1;
}

.toast-success {
  border-left: 4px solid #51cf66;
}

.toast-error {
  border-left: 4px solid #ff6b6b;
}

.toast-info {
  border-left: 4px solid #4a9eff;
}

Step 6: Add script tag to HTML

Update claude-landing.html to include the new component:

<script src="/claude/js/app.js"></script>
<script src="/claude/claude-ide/components/session-card.js"></script>
<script src="/claude/claude-ide/sessions-landing.js"></script>

Step 7: Test the component

  1. Load /claude/ in browser
  2. Click on project name → should enter edit mode
  3. Edit name and press Enter → should save
  4. Click menu button → should show dropdown
  5. Test Duplicate and Delete actions
  6. Click Continue → should show loading overlay then navigate

Step 8: Commit

git add public/claude-ide/components/session-card.js
git add public/claude-ide/components/session-card.css
git add public/claude-ide/sessions-landing.js
git add public/claude-ide/sessions-landing.css
git add public/claude-landing.html
git commit -m "feat: add enhanced SessionCard component with inline editing"

Phase 3: Quick Start Enhancement

Task 3.1: Add Project Name Input to Blank Project Card

Files:

  • Modify: public/claude-landing.html
  • Modify: public/claude-ide/sessions-landing.js

Step 1: Update blank project card HTML

Find the "Blank Project" card in claude-landing.html (around line 50) and replace it:

<div class="quick-start-card" id="blank-project-card">
  <span class="card-icon"></span>
  <h4>Blank Project</h4>
  <input type="text" id="blank-project-name" class="project-name-input"
         placeholder="Enter project name..." maxlength="50" />
  <button class="btn btn-primary" id="create-blank-btn" disabled>
    Start Project →
  </button>
</div>

Step 2: Add CSS for the input

Add to sessions-landing.css:

.project-name-input {
  width: 100%;
  background: #1a1a1a;
  border: 1px solid #444;
  color: #e0e0e0;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 14px;
  margin: 12px 0;
  transition: border-color 0.2s;
}

.project-name-input:focus {
  outline: none;
  border-color: #4a9eff;
  box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.2);
}

.project-name-input::placeholder {
  color: #666;
}

#create-blank-btn {
  width: 100%;
  margin-top: 8px;
}

#create-blank-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Step 3: Add JavaScript handler

Add to sessions-landing.js, in the DOMContentLoaded section:

// Blank project name input
const blankProjectInput = document.getElementById('blank-project-name');
const blankProjectBtn = document.getElementById('create-blank-btn');

if (blankProjectInput && blankProjectBtn) {
  blankProjectInput.addEventListener('input', () => {
    const name = blankProjectInput.value.trim();
    blankProjectBtn.disabled = name.length === 0;
  });

  blankProjectInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && blankProjectInput.value.trim()) {
      createBlankProject();
    }
  });

  blankProjectBtn.addEventListener('click', createBlankProject);
}

Step 4: Add createBlankProject function

Add to sessions-landing.js:

async function createBlankProject() {
  const input = document.getElementById('blank-project-name');
  const projectName = input.value.trim();

  if (!projectName) return;

  try {
    showLoadingOverlay('Creating project...');

    const res = await fetch('/claude/api/claude/sessions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'same-origin',
      body: JSON.stringify({
        workingDir: '/home/uroma/obsidian-vault',
        metadata: {
          type: 'chat',
          source: 'web-ide',
          project: projectName
        }
      })
    });

    const data = await res.json();

    if (data.success) {
      // Navigate to IDE
      window.location.href = `/claude/ide?session=${data.session.id}`;
    } else {
      hideLoadingOverlay();
      showToast('Failed to create project: ' + (data.error || 'Unknown error'), 'error');
    }
  } catch (error) {
    hideLoadingOverlay();
    console.error('Error creating blank project:', error);
    showToast('Failed to create project: ' + error.message, 'error');
  }
}

function hideLoadingOverlay() {
  const overlay = document.querySelector('.loading-overlay');
  if (overlay) {
    overlay.style.opacity = '0';
    setTimeout(() => overlay.remove(), 200);
  }
}

Step 5: Update showLoadingOverlay to accept custom message

Modify the showLoadingOverlay function:

function showLoadingOverlay(message = 'Opening workspace...') {
  const overlay = document.createElement('div');
  overlay.className = 'loading-overlay';
  overlay.innerHTML = `
    <div class="loading-spinner"></div>
    <div class="loading-text">${message}</div>
  `;
  document.body.appendChild(overlay);
}

Step 6: Test blank project creation

  1. Load /claude/
  2. Type project name in blank project card
  3. Button should become enabled
  4. Click button or press Enter
  5. Should show loading overlay
  6. Should navigate to IDE with new session

Step 7: Commit

git add public/claude-landing.html
git add public/claude-ide/sessions-landing.js
git add public/claude-ide/sessions-landing.css
git commit -m "feat: add project name input to blank project card"

Phase 4: Polish & Testing

Task 4.1: Add Character Counter to Project Name Input

Files:

  • Modify: public/claude-landing.html
  • Modify: public/claude-ide/sessions-landing.js
  • Modify: public/claude-ide/sessions-landing.css

Step 1: Update HTML to include counter

<div class="quick-start-card" id="blank-project-card">
  <span class="card-icon"></span>
  <h4>Blank Project</h4>
  <div class="input-wrapper">
    <input type="text" id="blank-project-name" class="project-name-input"
           placeholder="Enter project name..." maxlength="50" />
    <span class="char-count" id="char-count">0/50</span>
  </div>
  <button class="btn btn-primary" id="create-blank-btn" disabled>
    Start Project →
  </button>
</div>

Step 2: Add CSS

.input-wrapper {
  position: relative;
  width: 100%;
  margin: 12px 0;
}

.char-count {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 12px;
  color: #666;
  pointer-events: none;
}

Step 3: Update JavaScript

Modify the input event handler:

blankProjectInput.addEventListener('input', () => {
  const name = blankProjectInput.value.trim();
  const length = blankProjectInput.value.length;
  blankProjectBtn.disabled = name.length === 0;

  // Update character count
  const charCount = document.getElementById('char-count');
  if (charCount) {
    charCount.textContent = `${length}/50`;
    if (length >= 45) {
      charCount.style.color = '#ffa94d';
    }
    if (length >= 50) {
      charCount.style.color = '#ff6b6b';
    }
  }
});

Step 4: Test character counter

Type in the input and verify counter updates: 0/50 → 25/50 → 50/50 (red)

Step 5: Commit

git add public/claude-landing.html
git add public/claude-ide/sessions-landing.js
git add public/claude-ide/sessions-landing.css
git commit -m "feat: add character counter to project name input"

Task 4.2: Add Input Validation (Special Characters)

Files:

  • Modify: public/claude-ide/sessions-landing.js

Step 1: Add validation function

function validateProjectName(name) {
  // Disallow special characters that cause issues
  const invalidChars = /[\/\\<>:"|?*]/;
  if (invalidChars.test(name)) {
    return false;
  }
  return true;
}

Step 2: Update input handler to validate

blankProjectInput.addEventListener('input', () => {
  const name = blankProjectInput.value;
  const trimmedName = name.trim();
  const isValid = validateProjectName(trimmedName);

  blankProjectBtn.disabled = trimmedName.length === 0 || !isValid;

  // Update character count
  const length = name.length;
  const charCount = document.getElementById('char-count');
  if (charCount) {
    charCount.textContent = `${length}/50`;

    if (!isValid) {
      charCount.textContent = 'Invalid characters';
      charCount.style.color = '#ff6b6b';
    } else if (length >= 45) {
      charCount.style.color = '#ffa94d';
    } else if (length >= 50) {
      charCount.style.color = '#ff6b6b';
    } else {
      charCount.style.color = '#666';
    }
  }
});

Step 3: Test validation

Try typing invalid characters: /, \, <, >, etc. Button should stay disabled.

Step 4: Commit

git add public/claude-ide/sessions-landing.js
git commit -m "feat: add project name validation for special characters"

Task 4.3: End-to-End Testing

Files:

  • Create: test-manual.md (test checklist)

Step 1: Create test checklist document

# Manual Test Checklist

## Create New Project Flows

- [ ] Blank project with valid name
- [ ] Blank project with invalid characters (should be disabled)
- [ ] Blank project with empty name (should be disabled)
- [ ] Template project (React)
- [ ] Template project (Node.js)
- [ ] Template project (HTML Calculator)
- [ ] Template project (Portfolio)
- [ ] Verify project name is set correctly in session metadata

## Load Existing Project Flows

- [ ] Click Continue button on active session
- [ ] Verify navigation to IDE with correct session ID
- [ ] Click on session card (not Continue button)
- [ ] Verify navigation works
- [ ] Test with historical session

## Inline Editing

- [ ] Click project name → enters edit mode
- [ ] Type new name and press Enter → saves
- [ ] Type new name and click away → cancels
- [ ] Press Escape → cancels
- [ ] Try to edit historical session → should not work
- [ ] Verify name persists after page refresh

## Quick Actions Menu

- [ ] Click menu button → dropdown appears
- [ ] Click Duplicate → creates new session
- [ ] Click Delete → confirms and deletes
- [ ] Click outside menu → menu closes
- [ ] Verify Duplicate increments session count
- [ ] Verify Delete removes session from list

## Loading States

- [ ] Create blank project → shows "Creating project..."
- [ ] Click Continue → shows "Opening workspace..."
- [ ] Verify overlay appears for minimum 300ms
- [ ] Verify smooth transition to IDE

## Empty States

- [ ] Delete all sessions → shows "No projects yet"
- [ ] Verify empty state displays correctly
- [ ] Verify template cards still work

## Error Handling

- [ ] Disconnect network → try to load sessions
- [ ] Verify error banner with retry button
- [ ] Try to create session while offline
- [ ] Verify error toast appears
- [ ] Reconnect → verify retry works

## Responsive Design

- [ ] Desktop (1920px) - all cards visible
- [ ] Tablet (768px) - cards stack properly
- [ ] Mobile (375px) - single column layout
- [ ] Verify all buttons are tappable on mobile

## Session Metadata

- [ ] Verify lastMessage displays correctly
- [ ] Verify fileCount is accurate
- [ ] Verify relative time is correct
- [ ] Check session with no messages
- [ ] Check session with many files

## Browser Compatibility

- [ ] Chrome/Edge - all features work
- [ ] Firefox - all features work
- [ ] Safari - all features work

Step 2: Run through test checklist

Go through each test case systematically. Document any failures.

Step 3: Fix any issues found

Create tasks for any bugs discovered during testing.

Step 4: Commit test documentation

git add test-manual.md
git commit -m "test: add manual testing checklist"

Final Steps

Task 5.1: Code Review & Cleanup

Files:

  • All modified files

Step 1: Review all changes

git diff HEAD~5

Look for:

  • Unused code
  • Console.log statements (remove or replace with proper logging)
  • Inconsistent naming
  • Magic numbers (extract to constants)

Step 2: Add JSDoc comments

Add documentation to key functions:

/**
 * Creates a new blank project with the given name
 * @param {string} projectName - The name for the new project
 * @returns {Promise<void>}
 */
async function createBlankProject(projectName) {
  // ...
}

Step 3: Run final browser test

Test all flows one more time to ensure nothing broke.

Step 4: Final commit

git add -A
git commit -m "polish: code cleanup and documentation"

Success Criteria

Verify all success criteria from design document are met:

  • Users can create/load projects in 1-2 clicks
  • Navigation to IDE takes < 1 second perceived time
  • All error states handled gracefully
  • Project names are editable and persist
  • Mobile-responsive design
  • No data loss on network failures

Implementation Complete!

Next Steps:

  1. Deploy to production
  2. Monitor for bugs
  3. Gather user feedback
  4. Plan iteration based on feedback

Estimated Time: 5-7 hours Actual Time: _____ hours

Notes:

  • Document any deviations from the plan
  • Note any technical challenges encountered
  • Record ideas for future improvements