feat: unified Run button with mode selector dropdown

Replace separate "Run Claude" and "Run OC" buttons with a single
split button: [Run ▾] where the chevron opens a dropdown to switch
between Claude Code and OpenCode modes.

- Run button label shows "Run" (Claude) or "Run OC" (OpenCode)
- Button color reflects selected mode (blue=Claude, green=OpenCode)
- Mode persisted in localStorage across sessions
- Ctrl+Enter uses the selected mode
- Welcome screen simplified to single "Run" button
- Removed standalone btn-claude, btn-opencode, welcome-btn-opencode CSS
- Updated welcome tagline: "Manage AI in persistent tmux sessions"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-26 12:04:57 +01:00
co-authored by Claude Opus 4.6
parent eba2fa32a3
commit 5ad0e7f9b4
3 changed files with 178 additions and 55 deletions
+63 -1
View File
@@ -2878,6 +2878,7 @@ class ClaudemanApp {
// Share a single settings fetch between both consumers
const settingsPromise = fetch('/api/settings').then(r => r.ok ? r.json() : null).catch(() => null);
this.loadQuickStartCases(null, settingsPromise);
this._initRunMode();
this.setupEventListeners();
// Mobile: ensure button taps register even when keyboard is visible.
// On mobile, tapping a button while the soft keyboard is up causes the
@@ -9286,10 +9287,71 @@ class ClaudemanApp {
}
async quickStart() {
// Alias for backward compatibility
return this.run();
}
/** Run using the selected mode (Claude Code or OpenCode) */
async run() {
const mode = this._runMode || 'claude';
if (mode === 'opencode') {
return this.runOpenCode();
}
return this.runClaude();
}
/** Get/set the run mode, persisted in localStorage */
get runMode() { return this._runMode || 'claude'; }
setRunMode(mode) {
this._runMode = mode;
try { localStorage.setItem('claudeman_runMode', mode); } catch {}
this._applyRunMode();
// Close menu
document.getElementById('runModeMenu')?.classList.remove('active');
}
toggleRunModeMenu(e) {
e?.stopPropagation();
const menu = document.getElementById('runModeMenu');
if (!menu) return;
menu.classList.toggle('active');
// Update selected state
menu.querySelectorAll('.run-mode-option').forEach(btn => {
btn.classList.toggle('selected', btn.dataset.mode === this.runMode);
});
// Close on click outside
if (menu.classList.contains('active')) {
const close = (ev) => {
if (!menu.contains(ev.target)) {
menu.classList.remove('active');
document.removeEventListener('click', close);
}
};
setTimeout(() => document.addEventListener('click', close), 0);
}
}
_applyRunMode() {
const mode = this.runMode;
const runBtn = document.getElementById('runBtn');
const gearBtn = runBtn?.nextElementSibling;
const label = document.getElementById('runBtnLabel');
if (runBtn) {
runBtn.className = `btn-toolbar btn-run mode-${mode}`;
}
if (gearBtn) {
gearBtn.className = `btn-toolbar btn-run-gear mode-${mode}`;
}
if (label) {
label.textContent = mode === 'opencode' ? 'Run OC' : 'Run';
}
}
_initRunMode() {
try { this._runMode = localStorage.getItem('claudeman_runMode') || 'claude'; } catch { this._runMode = 'claude'; }
this._applyRunMode();
}
// Tab count stepper functions
incrementTabCount() {
const input = document.getElementById('tabCount');
+21 -15
View File
@@ -232,15 +232,11 @@
<div class="welcome-overlay" id="welcomeOverlay">
<div class="welcome-content">
<h1 class="welcome-title">Claudeman</h1>
<p class="welcome-desc">Manage Claude Code in persistent tmux sessions.</p>
<p class="welcome-desc">Manage AI in persistent tmux sessions.</p>
<div class="welcome-actions">
<button class="welcome-btn welcome-btn-primary" onclick="app.runClaude()">
<button class="welcome-btn welcome-btn-primary" onclick="app.run()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Run Claude
</button>
<button class="welcome-btn welcome-btn-opencode" onclick="app.runOpenCode()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M8 12h8M12 8v8"/></svg>
Run OpenCode
Run
</button>
<button class="welcome-btn welcome-btn-ralph" onclick="app.showRalphWizard()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
@@ -299,12 +295,25 @@
<!-- Bottom Toolbar -->
<footer class="toolbar">
<div class="toolbar-left">
<!-- Run Claude -->
<!-- Run AI -->
<div class="toolbar-group">
<button class="btn-toolbar btn-claude" onclick="app.runClaude()" title="Run Claude (Ctrl+Enter)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Run Claude
</button>
<div class="run-btn-group">
<button class="btn-toolbar btn-run" id="runBtn" onclick="app.run()" title="Run (Ctrl+Enter)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span id="runBtnLabel">Run</span>
</button>
<button class="btn-toolbar btn-run-gear" onclick="app.toggleRunModeMenu(event)" title="Select AI backend">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="run-mode-menu" id="runModeMenu">
<button class="run-mode-option" data-mode="claude" onclick="app.setRunMode('claude')">
<span class="run-mode-dot claude"></span>Claude Code
</button>
<button class="run-mode-option" data-mode="opencode" onclick="app.setRunMode('opencode')">
<span class="run-mode-dot opencode"></span>OpenCode
</button>
</div>
</div>
<div class="tab-count-group" title="Instance count">
<button class="tab-count-btn" onclick="app.decrementTabCount()">−</button>
<input type="number" id="tabCount" class="tab-count-input" value="1" min="1" max="20" readonly>
@@ -321,9 +330,6 @@
<input type="number" id="shellCount" class="tab-count-input" value="1" min="1" max="20" readonly>
<button class="tab-count-btn" onclick="app.incrementShellCount()">+</button>
</div>
<button class="btn-toolbar btn-opencode" onclick="app.runOpenCode()" title="Run OpenCode (multi-model)">
Run OC
</button>
<div class="case-select-group">
<select id="quickStartCase" class="toolbar-select" title="Select case">
<option value="testcase">testcase</option>
+94 -39
View File
@@ -1732,19 +1732,6 @@ body {
color: #fff;
}
.welcome-btn-opencode {
background: linear-gradient(135deg, #0a2e2a 0%, #0d4a40 100%);
border-color: #10b981;
color: #6ee7b7;
}
.welcome-btn-opencode:hover {
background: linear-gradient(135deg, #0d4a40 0%, #116a58 100%);
border-color: #34d399;
box-shadow: 0 0 12px rgba(16, 185, 129, 0.4);
color: #a7f3d0;
}
.welcome-btn-ralph {
background: linear-gradient(135deg, #3d3415 0%, #4a3f1a 100%);
border-color: #6b5a1e;
@@ -1856,29 +1843,109 @@ body {
background: var(--accent-hover);
}
/* Claude Button - Special styling (matches welcome-btn-primary) */
.btn-toolbar.btn-claude {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
border-color: #3b82f6;
color: #e0e7ff;
/* Run Button Group — split button with dropdown */
.run-btn-group {
display: flex;
position: relative;
}
.btn-toolbar.btn-run {
border-radius: 4px 0 0 4px;
border-right: 1px solid rgba(255,255,255,0.15);
font-weight: 500;
padding: 0.4rem 0.85rem;
gap: 0.4rem;
}
.btn-toolbar.btn-claude:hover {
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
border-color: #3b82f6;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
color: #fff;
}
.btn-toolbar.btn-claude svg {
.btn-toolbar.btn-run svg {
flex-shrink: 0;
width: 12px;
height: 12px;
}
.btn-toolbar.btn-run-gear {
border-radius: 0 4px 4px 0;
padding: 0.4rem 0.4rem;
min-width: unset;
}
/* Claude mode colors (default) */
.btn-toolbar.btn-run.mode-claude,
.btn-toolbar.btn-run-gear.mode-claude {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
border-color: #3b82f6;
color: #e0e7ff;
}
.btn-toolbar.btn-run.mode-claude:hover,
.btn-toolbar.btn-run-gear.mode-claude:hover {
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
color: #fff;
}
/* OpenCode mode colors */
.btn-toolbar.btn-run.mode-opencode,
.btn-toolbar.btn-run-gear.mode-opencode {
background: linear-gradient(135deg, #0a2e2a 0%, #0d4a40 100%);
border-color: #10b981;
color: #6ee7b7;
}
.btn-toolbar.btn-run.mode-opencode:hover,
.btn-toolbar.btn-run-gear.mode-opencode:hover {
background: linear-gradient(135deg, #0d4a40 0%, #116a58 100%);
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
color: #a7f3d0;
}
/* Dropdown menu */
.run-mode-menu {
display: none;
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 4px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px;
z-index: 1000;
min-width: 150px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.run-mode-menu.active {
display: flex;
flex-direction: column;
gap: 2px;
}
.run-mode-option {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: none;
border: none;
border-radius: 4px;
color: var(--text-primary);
cursor: pointer;
font-size: 0.8rem;
white-space: nowrap;
}
.run-mode-option:hover {
background: var(--bg-hover);
}
.run-mode-option.selected {
background: var(--bg-hover);
font-weight: 600;
}
.run-mode-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.run-mode-dot.claude { background: #3b82f6; }
.run-mode-dot.opencode { background: #10b981; }
/* Shell Button - Special styling */
.btn-toolbar.btn-shell {
background: linear-gradient(135deg, #1a2e1a 0%, #234023 100%);
@@ -1894,19 +1961,7 @@ body {
color: #a8d8a8;
}
.btn-toolbar.btn-opencode {
background: linear-gradient(135deg, #0a2e2a 0%, #0d4a40 100%);
border-color: #10b981;
color: #6ee7b7;
font-weight: 500;
}
.btn-toolbar.btn-opencode:hover {
background: linear-gradient(135deg, #0d4a40 0%, #116a58 100%);
border-color: #34d399;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
color: #a7f3d0;
}
/* btn-opencode standalone removed — now part of run-btn-group mode system */
/* Stop button - hidden on desktop, shown on mobile */
.btn-toolbar.btn-stop {