- 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>
37 KiB
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
- Load
/claude/in browser - Click on project name → should enter edit mode
- Edit name and press Enter → should save
- Click menu button → should show dropdown
- Test Duplicate and Delete actions
- 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
- Load
/claude/ - Type project name in blank project card
- Button should become enabled
- Click button or press Enter
- Should show loading overlay
- 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:
- Deploy to production
- Monitor for bugs
- Gather user feedback
- 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