mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
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:
+122
-1
@@ -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');
|
||||
|
||||
|
||||
@@ -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">⚡</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">⚡</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user