feat(web): add Quick Start button for one-click Claude sessions

Adds a Quick Start section at the top of the Run tab that creates a case
folder (if needed) and starts an interactive Claude session in one click,
reducing the workflow from 5 steps to 1-2.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-18 20:11:46 +01:00
co-authored by Claude Opus 4.5
parent ea6e1090bb
commit 3a0adfc5dc
4 changed files with 288 additions and 2 deletions
+122 -1
View File
@@ -13,7 +13,7 @@ class ClaudemanApp {
this.fitAddon = null;
this.activeSessionId = null; // Currently active interactive session
this.respawnStatus = {}; // Respawn status per session
this.respawnEnabled = true; // Whether respawn is enabled for new sessions
this.respawnEnabled = false; // Whether respawn is enabled for new sessions (disabled by default)
this.init();
}
@@ -23,6 +23,7 @@ class ClaudemanApp {
this.connectSSE();
this.loadState();
this.loadCases();
this.loadQuickStartCases();
this.startTimerUpdates();
this.setupEventListeners();
@@ -123,6 +124,11 @@ class ClaudemanApp {
const customGroup = document.getElementById('customDurationGroup');
customGroup.style.display = e.target.value === 'custom' ? 'block' : 'none';
});
// Quick Start case select
document.getElementById('quickStartCase').addEventListener('change', (e) => {
this.handleQuickStartSelect(e.target.value);
});
}
// Settings
@@ -271,6 +277,7 @@ class ClaudemanApp {
this.eventSource.addEventListener('case:created', (e) => {
this.loadCases();
this.loadQuickStartCases();
});
// Respawn events
@@ -402,6 +409,120 @@ class ClaudemanApp {
}
}
// Quick Start
async loadQuickStartCases() {
try {
const res = await fetch('/api/cases');
const cases = await res.json();
const select = document.getElementById('quickStartCase');
// Preserve current selection
const currentValue = select.value;
// Clear and rebuild options
select.innerHTML = '<option value="testcase">testcase (default)</option>';
// Add existing cases (skip testcase if it exists)
cases.forEach(c => {
if (c.name !== 'testcase') {
const option = document.createElement('option');
option.value = c.name;
option.textContent = c.name;
select.appendChild(option);
}
});
// Add "Create new..." option
const createOption = document.createElement('option');
createOption.value = '__create_new__';
createOption.textContent = '+ Create new...';
select.appendChild(createOption);
// Restore selection if it still exists
if ([...select.options].some(o => o.value === currentValue)) {
select.value = currentValue;
}
} catch (err) {
console.error('Failed to load Quick Start cases:', err);
}
}
handleQuickStartSelect(value) {
if (value === '__create_new__') {
const newName = prompt('Enter a name for the new case:', '');
const select = document.getElementById('quickStartCase');
if (newName && newName.trim()) {
const cleanName = newName.trim().replace(/[^a-zA-Z0-9_-]/g, '-');
if (cleanName) {
// Add the new option and select it
const option = document.createElement('option');
option.value = cleanName;
option.textContent = cleanName;
// Insert before the "Create new..." option
select.insertBefore(option, select.lastElementChild);
select.value = cleanName;
} else {
select.value = 'testcase';
}
} else {
// Revert to default
select.value = 'testcase';
}
}
}
async quickStart() {
const select = document.getElementById('quickStartCase');
let caseName = select.value;
if (caseName === '__create_new__') {
caseName = 'testcase';
}
this.terminal.clear();
this.terminal.writeln('\x1b[1;32m⚡ Quick Start: Launching Claude session...\x1b[0m');
this.terminal.writeln(`\x1b[90mCase: ${caseName}\x1b[0m`);
this.terminal.writeln('');
try {
const res = await fetch('/api/quick-start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseName })
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error);
}
this.activeSessionId = data.sessionId;
this.terminal.writeln(`\x1b[1;32m✓ Session started in ${data.casePath}\x1b[0m`);
this.terminal.writeln('');
// Reload cases in case a new one was created
this.loadQuickStartCases();
this.loadCases();
// Send initial resize
const dims = this.fitAddon.proposeDimensions();
if (dims) {
await fetch(`/api/sessions/${data.sessionId}/resize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
});
}
// Focus the terminal
this.terminal.focus();
} catch (err) {
this.terminal.writeln(`\x1b[1;31m❌ Error: ${err.message}\x1b[0m`);
}
}
renderCases() {
const list = document.getElementById('casesList');
+23 -1
View File
@@ -77,6 +77,28 @@
<!-- Run Tab -->
<div class="tab-content active" id="tab-run">
<!-- Quick Start Section -->
<div class="quick-start-section">
<div class="quick-start-header">
<span class="quick-start-icon">&#9889;</span>
<span class="quick-start-title">Quick Start</span>
</div>
<div class="quick-start-controls">
<select id="quickStartCase" class="quick-start-select">
<option value="testcase">testcase (default)</option>
</select>
<button class="btn btn-quick-start" onclick="app.quickStart()">
<span class="btn-icon">&#9889;</span>
Quick Start
</button>
</div>
<p class="quick-start-hint">Start an interactive Claude session in one click</p>
</div>
<div class="section-divider">
<span>or configure a custom run</span>
</div>
<div class="form-group">
<label>Prompt</label>
<textarea id="promptInput" placeholder="What should Claude do?" rows="6"></textarea>
@@ -116,7 +138,7 @@
<div class="respawn-settings" id="respawnSettings">
<div class="respawn-header">
<label>
<input type="checkbox" id="respawnEnabled" checked>
<input type="checkbox" id="respawnEnabled">
Enable Auto-Respawn Loop
</label>
<button class="btn btn-sm" onclick="app.toggleRespawnDetails()">⚙️</button>
+95
View File
@@ -885,3 +885,98 @@ input[type="checkbox"] {
padding: 0.2rem 0.4rem;
font-size: 0.65rem;
}
/* Quick Start Section */
.quick-start-section {
background: linear-gradient(135deg, #0d4020 0%, #0a2510 100%);
border: 1px solid var(--green);
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 1rem;
}
.quick-start-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.quick-start-icon {
font-size: 1.25rem;
color: var(--green);
}
.quick-start-title {
font-size: 1rem;
font-weight: 600;
color: var(--green);
}
.quick-start-controls {
display: flex;
gap: 0.5rem;
align-items: stretch;
}
.quick-start-select {
flex: 1;
padding: 0.625rem 0.75rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: var(--radius);
color: var(--text);
font-size: 0.875rem;
}
.quick-start-select:focus {
outline: none;
border-color: var(--green);
}
.btn-quick-start {
background: var(--green);
color: #000;
font-weight: 600;
padding: 0.625rem 1.25rem;
border: none;
border-radius: var(--radius);
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.btn-quick-start:hover {
background: #16a34a;
transform: translateY(-1px);
}
.btn-quick-start:active {
transform: translateY(0);
}
.quick-start-hint {
font-size: 0.7rem;
color: rgba(34, 197, 94, 0.7);
margin-top: 0.5rem;
}
/* Section Divider */
.section-divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 1.25rem 0;
color: var(--text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.section-divider::before,
.section-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
+48
View File
@@ -442,6 +442,54 @@ export class WebServer extends EventEmitter {
hasClaudeMd: existsSync(join(casePath, 'CLAUDE.md')),
};
});
// Quick Start: Create case (if needed) and start interactive session in one click
this.app.post('/api/quick-start', async (req) => {
const { caseName = 'testcase' } = req.body as { caseName?: string };
// Validate case name
if (!/^[a-zA-Z0-9_-]+$/.test(caseName)) {
return { success: false, error: 'Invalid case name. Use only letters, numbers, hyphens, underscores.' };
}
const casePath = join(casesDir, caseName);
// Create case folder and CLAUDE.md if it doesn't exist
if (!existsSync(casePath)) {
try {
mkdirSync(casePath, { recursive: true });
mkdirSync(join(casePath, 'src'), { recursive: true });
const claudeMd = generateClaudeMd(caseName, '');
writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
this.broadcast('case:created', { name: caseName, path: casePath });
} catch (err) {
return { success: false, error: `Failed to create case: ${(err as Error).message}` };
}
}
// Create a new session with the case as working directory
const session = new Session({ workingDir: casePath });
this.sessions.set(session.id, session);
this.setupSessionListeners(session);
this.broadcast('session:created', session.toDetailedState());
// Start interactive mode
try {
await session.startInteractive();
this.broadcast('session:interactive', { id: session.id });
return {
success: true,
sessionId: session.id,
casePath,
caseName,
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
});
}
private setupSessionListeners(session: Session): void {