feat: add CPU limiting and Ralph Loop wizard plan generation

CPU Limiting:
- Add CpuLimitConfig type and settings in App Settings
- Use nice command for process priority (-20 to 19)
- Optional cpulimit integration for hard CPU limits (if installed)
- Applied to new sessions via screen-manager and session modules

Ralph Loop Wizard:
- 3-step wizard: Describe → Plan → Launch
- New /api/generate-plan endpoint generates implementation steps via Claude
- Plan editor with checkboxes, priorities (P0/P1/P2), reordering
- Skip plan option for manual workflows
- Compact wizard modal layout

UI Updates:
- Form section headers for grouped settings
- CPU limit status indicator (cpulimit availability)
- Wizard modal wider layout and inline fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-27 01:58:14 +01:00
co-authored by Claude Opus 4.5
parent 7cb7bc96e9
commit 59b406f73f
7 changed files with 1074 additions and 52 deletions
+300
View File
@@ -2601,6 +2601,246 @@ class ClaudemanApp {
document.getElementById('summaryIterations').textContent =
config.maxIterations === 0 ? 'Unlimited' : config.maxIterations;
document.getElementById('summaryCase').textContent = config.caseName;
// Show plan status in summary if plan was generated
const planSummary = document.getElementById('summaryPlan');
if (planSummary) {
if (config.generatedPlan && config.generatedPlan.length > 0) {
const enabledCount = config.generatedPlan.filter(i => i.enabled).length;
planSummary.textContent = `${enabledCount} item${enabledCount !== 1 ? 's' : ''}`;
planSummary.parentElement.style.display = '';
} else {
planSummary.parentElement.style.display = 'none';
}
}
}
// ========== Plan Generation ==========
resetPlanGenerationUI() {
// Show initial controls, hide other states
document.getElementById('planGenerationControls')?.classList.remove('hidden');
document.getElementById('planGenerationLoading')?.classList.add('hidden');
document.getElementById('planGenerationError')?.classList.add('hidden');
document.getElementById('planEditor')?.classList.add('hidden');
}
async generatePlan() {
const config = this.ralphWizardConfig;
// Show loading state
document.getElementById('planGenerationControls')?.classList.add('hidden');
document.getElementById('planGenerationError')?.classList.add('hidden');
document.getElementById('planEditor')?.classList.add('hidden');
document.getElementById('planGenerationLoading')?.classList.remove('hidden');
try {
const res = await fetch('/api/generate-plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskDescription: config.taskDescription,
maxItems: 8,
}),
});
const data = await res.json();
if (!data.success) {
this.showPlanError(data.error || 'Failed to generate plan');
return;
}
if (!data.data?.items || data.data.items.length === 0) {
this.showPlanError('No plan items generated. Try adding more detail to your task.');
return;
}
// Store plan with enabled state and IDs
config.generatedPlan = data.data.items.map((item, idx) => ({
...item,
enabled: true,
id: `plan-${Date.now()}-${idx}`,
}));
config.planGenerated = true;
config.skipPlanGeneration = false;
// Show editor
this.renderPlanEditor();
} catch (err) {
console.error('Plan generation failed:', err);
this.showPlanError('Network error: ' + err.message);
}
}
showPlanError(message) {
document.getElementById('planGenerationLoading')?.classList.add('hidden');
document.getElementById('planGenerationControls')?.classList.add('hidden');
document.getElementById('planEditor')?.classList.add('hidden');
const errorEl = document.getElementById('planGenerationError');
const msgEl = document.getElementById('planErrorMsg');
if (msgEl) msgEl.textContent = message;
errorEl?.classList.remove('hidden');
}
renderPlanEditor() {
document.getElementById('planGenerationLoading')?.classList.add('hidden');
document.getElementById('planGenerationControls')?.classList.add('hidden');
document.getElementById('planGenerationError')?.classList.add('hidden');
document.getElementById('planEditor')?.classList.remove('hidden');
const list = document.getElementById('planItemsList');
if (!list) return;
list.innerHTML = '';
const items = this.ralphWizardConfig.generatedPlan || [];
items.forEach((item, index) => {
const row = this.renderPlanItem(item, index);
list.appendChild(row);
});
}
renderPlanItem(item, index) {
const row = document.createElement('div');
row.className = 'plan-item';
row.dataset.index = index;
row.innerHTML = `
<input type="checkbox" class="plan-item-checkbox" ${item.enabled ? 'checked' : ''}
onchange="app.togglePlanItem(${index})">
<input type="text" class="plan-item-content" value="${this.escapeHtml(item.content)}"
onchange="app.updatePlanItemContent(${index}, this.value)"
placeholder="Implementation step...">
<select class="plan-item-priority" onchange="app.updatePlanItemPriority(${index}, this.value)">
<option value="" ${!item.priority ? 'selected' : ''}>-</option>
<option value="P0" ${item.priority === 'P0' ? 'selected' : ''}>P0</option>
<option value="P1" ${item.priority === 'P1' ? 'selected' : ''}>P1</option>
<option value="P2" ${item.priority === 'P2' ? 'selected' : ''}>P2</option>
</select>
<button class="plan-item-remove" onclick="app.removePlanItem(${index})" title="Remove">&times;</button>
`;
return row;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
togglePlanItem(index) {
const plan = this.ralphWizardConfig.generatedPlan;
if (plan && plan[index]) {
plan[index].enabled = !plan[index].enabled;
this.renderPlanEditor();
}
}
updatePlanItemContent(index, value) {
const plan = this.ralphWizardConfig.generatedPlan;
if (plan && plan[index]) {
plan[index].content = value.trim();
}
}
updatePlanItemPriority(index, value) {
const plan = this.ralphWizardConfig.generatedPlan;
if (plan && plan[index]) {
plan[index].priority = value || null;
}
}
removePlanItem(index) {
const plan = this.ralphWizardConfig.generatedPlan;
if (plan) {
plan.splice(index, 1);
this.renderPlanEditor();
}
}
addPlanItem() {
const plan = this.ralphWizardConfig.generatedPlan || [];
plan.push({
content: '',
priority: null,
enabled: true,
id: `plan-${Date.now()}-new`,
});
this.ralphWizardConfig.generatedPlan = plan;
this.renderPlanEditor();
// Focus the new item's input
setTimeout(() => {
const list = document.getElementById('planItemsList');
const lastInput = list?.querySelector('.plan-item:last-child .plan-item-content');
lastInput?.focus();
}, 50);
}
skipPlanGeneration() {
this.ralphWizardConfig.skipPlanGeneration = true;
this.ralphWizardConfig.planGenerated = false;
this.ralphWizardConfig.generatedPlan = null;
// Generate preview and go to step 3
this.updateRalphPromptPreview();
this.ralphWizardStep = 3;
this.updateRalphWizardUI();
}
regeneratePlan() {
this.ralphWizardConfig.generatedPlan = null;
this.ralphWizardConfig.planGenerated = false;
this.generatePlan();
}
generateFixPlanContent(items) {
// Group items by priority
const p0Items = items.filter(i => i.priority === 'P0');
const p1Items = items.filter(i => i.priority === 'P1');
const p2Items = items.filter(i => i.priority === 'P2');
const noPriorityItems = items.filter(i => !i.priority);
let content = '# Implementation Plan\n\n';
content += `Generated: ${new Date().toISOString().slice(0, 10)}\n\n`;
if (p0Items.length > 0) {
content += '## Critical Path (P0)\n\n';
p0Items.forEach(item => {
content += `- [ ] ${item.content}\n`;
});
content += '\n';
}
if (p1Items.length > 0) {
content += '## Standard (P1)\n\n';
p1Items.forEach(item => {
content += `- [ ] ${item.content}\n`;
});
content += '\n';
}
if (p2Items.length > 0) {
content += '## Nice-to-Have (P2)\n\n';
p2Items.forEach(item => {
content += `- [ ] ${item.content}\n`;
});
content += '\n';
}
if (noPriorityItems.length > 0) {
content += '## Tasks\n\n';
noPriorityItems.forEach(item => {
content += `- [ ] ${item.content}\n`;
});
content += '\n';
}
return content;
}
async startRalphLoop() {
@@ -2653,6 +2893,22 @@ class ClaudemanApp {
})
});
// Step 2.5: Write @fix_plan.md if plan was generated
if (config.generatedPlan && config.generatedPlan.length > 0) {
const enabledItems = config.generatedPlan.filter(i => i.enabled);
if (enabledItems.length > 0) {
const planContent = this.generateFixPlanContent(enabledItems);
// Import the plan content
await fetch(`/api/sessions/${sessionId}/fix-plan/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: planContent })
});
// Write to disk
await fetch(`/api/sessions/${sessionId}/fix-plan/write`, { method: 'POST' });
}
}
// Step 3: Enable respawn if requested
if (config.enableRespawn) {
await fetch(`/api/sessions/${sessionId}/respawn/enable`, {
@@ -4296,6 +4552,13 @@ class ClaudemanApp {
claudeModeSelect.onchange = () => {
allowedToolsRow.style.display = claudeModeSelect.value === 'allowedTools' ? '' : 'none';
};
// CPU Limiting settings
const cpuLimitSettings = settings.cpuLimit || {};
document.getElementById('appSettingsCpuLimitEnabled').checked = cpuLimitSettings.enabled ?? false;
document.getElementById('appSettingsCpuNiceValue').value = cpuLimitSettings.niceValue ?? 10;
document.getElementById('appSettingsCpuLimitPercent').value = cpuLimitSettings.cpuLimitPercent ?? 80;
// Check cpulimit availability and update status indicator
this.checkCpulimitAvailability();
// Notification settings
const notifPrefs = this.notificationManager?.preferences || {};
document.getElementById('appSettingsNotifEnabled').checked = notifPrefs.enabled ?? true;
@@ -4357,6 +4620,13 @@ class ClaudemanApp {
// Claude CLI settings
claudeMode: document.getElementById('appSettingsClaudeMode').value,
allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(),
// CPU Limiting settings
cpuLimit: {
enabled: document.getElementById('appSettingsCpuLimitEnabled').checked,
niceValue: parseInt(document.getElementById('appSettingsCpuNiceValue').value) || 10,
cpuLimitPercent: parseInt(document.getElementById('appSettingsCpuLimitPercent').value) || 80,
useCpulimitIfAvailable: true,
},
};
// Save to localStorage
@@ -4417,6 +4687,36 @@ class ClaudemanApp {
return {};
}
// Check if cpulimit is available and update the UI status indicator
async checkCpulimitAvailability() {
const statusEl = document.getElementById('cpuLimitStatus');
const percentInput = document.getElementById('appSettingsCpuLimitPercent');
if (!statusEl) return;
try {
const res = await fetch('/api/system/cpu-limit-status');
if (res.ok) {
const data = await res.json();
if (data.cpulimitAvailable) {
statusEl.textContent = '\u2713'; // Checkmark
statusEl.title = 'cpulimit is installed';
statusEl.classList.add('available');
statusEl.classList.remove('unavailable');
if (percentInput) percentInput.disabled = false;
} else {
statusEl.textContent = '\u2717'; // X mark
statusEl.title = 'cpulimit not installed (install with: apt install cpulimit)';
statusEl.classList.add('unavailable');
statusEl.classList.remove('available');
// Don't disable input, still allow setting the value for when it gets installed
}
}
} catch (err) {
statusEl.textContent = '?';
statusEl.title = 'Could not check cpulimit availability';
}
}
applyHeaderVisibilitySettings() {
const settings = this.loadAppSettingsFromStorage();
// Default all to true (enabled) if not set
+100 -42
View File
@@ -782,6 +782,29 @@
</label>
<span class="form-hint">Auto-enable for new sessions (otherwise auto-enables on Ralph pattern detection)</span>
</div>
<!-- CPU Limiting Section -->
<div class="form-section-header">CPU Limiting</div>
<div class="form-row form-row-switch">
<label>Enable CPU Limiting</label>
<label class="switch">
<input type="checkbox" id="appSettingsCpuLimitEnabled">
<span class="slider"></span>
</label>
<span class="form-hint">Limit CPU usage for Claude sessions (reduces system impact, only affects new sessions)</span>
</div>
<div class="form-row" id="cpuLimitNiceRow">
<label>Nice Value</label>
<input type="number" id="appSettingsCpuNiceValue" min="-20" max="19" value="10" style="width: 80px;">
<span class="form-hint">Process priority (-20 to 19, higher = lower priority, default: 10)</span>
</div>
<div class="form-row" id="cpuLimitPercentRow">
<label>CPU Limit %</label>
<div class="settings-item-actions">
<input type="number" id="appSettingsCpuLimitPercent" min="1" max="100" value="80" style="width: 80px;">
<span class="cpu-limit-status" id="cpuLimitStatus" title="cpulimit availability status"></span>
</div>
<span class="form-hint">Hard CPU limit (requires cpulimit installed, default: 80%)</span>
</div>
</div>
<!-- Paths Tab -->
<div class="modal-tab-content hidden" id="settings-paths">
@@ -921,7 +944,7 @@
<!-- Ralph Loop Wizard Modal -->
<div class="modal" id="ralphWizardModal">
<div class="modal-backdrop" onclick="app.closeRalphWizard()"></div>
<div class="modal-content modal-lg">
<div class="modal-content modal-wizard">
<div class="modal-header">
<h3>Start Ralph Loop</h3>
<button class="modal-close" onclick="app.closeRalphWizard()">&times;</button>
@@ -931,73 +954,104 @@
<div class="wizard-progress">
<div class="wizard-step active" data-step="1">
<span class="wizard-step-number">1</span>
<span class="wizard-step-label">Task Setup</span>
<span class="wizard-step-label">Describe</span>
</div>
<div class="wizard-step" data-step="2">
<span class="wizard-step-number">2</span>
<span class="wizard-step-label">Plan</span>
</div>
<div class="wizard-step" data-step="3">
<span class="wizard-step-number">3</span>
<span class="wizard-step-label">Launch</span>
</div>
</div>
<div class="modal-body">
<!-- Step 1: Task & Completion Setup -->
<div class="wizard-page" id="ralphWizardStep1">
<div class="wizard-page wizard-page-compact" id="ralphWizardStep1">
<div class="wizard-step-intro">
<p>Describe your task below. Claude will generate an implementation plan with testing steps.</p>
</div>
<div class="form-row">
<label>Task Description</label>
<label>What do you want to build?</label>
<div class="textarea-with-assist">
<textarea
id="ralphTaskDescription"
rows="6"
placeholder="Describe what you want Claude to accomplish...
Example:
Build a REST API for user authentication:
- JWT token generation and validation
- Login/logout endpoints
- Password reset flow
- Tests for all endpoints"></textarea>
rows="4"
placeholder="Build a REST API for user authentication with JWT tokens, login/logout endpoints, and password reset flow."></textarea>
</div>
<span class="form-hint">Be specific about requirements and success criteria</span>
</div>
<div class="form-row">
<label>Completion Phrase</label>
<div class="phrase-input-group">
<input type="text" id="ralphCompletionPhrase" value="COMPLETE" placeholder="COMPLETE" list="phrasesSuggestions">
<datalist id="phrasesSuggestions">
<option value="COMPLETE">
<option value="DONE">
<option value="FINISHED">
<option value="TESTS_PASS">
<option value="ALL_TASKS_COMPLETE">
</datalist>
<div class="wizard-inline-row">
<div class="wizard-inline-field">
<label>Working Directory</label>
<div class="case-select-inline">
<select id="ralphCaseSelect" class="form-select"></select>
<button class="btn-toolbar btn-sm" onclick="app.showCreateCaseModal()">+</button>
</div>
</div>
<span class="form-hint">Claude outputs <code>&lt;promise&gt;PHRASE&lt;/promise&gt;</code> when done</span>
<div class="wizard-inline-field wizard-inline-field-small">
<label>Iterations</label>
<div class="iteration-presets-compact">
<button type="button" class="iteration-preset-btn active" data-iterations="10" onclick="app.selectIterationPreset(10)">10</button>
<button type="button" class="iteration-preset-btn" data-iterations="25" onclick="app.selectIterationPreset(25)">25</button>
<button type="button" class="iteration-preset-btn" data-iterations="50" onclick="app.selectIterationPreset(50)">50</button>
<button type="button" class="iteration-preset-btn" data-iterations="0" onclick="app.selectIterationPreset(0)">&infin;</button>
</div>
</div>
</div>
<!-- Hidden completion phrase with default -->
<input type="hidden" id="ralphCompletionPhrase" value="COMPLETE">
</div>
<!-- Step 2: Plan Generation -->
<div class="wizard-page wizard-page-compact hidden" id="ralphWizardStep2">
<!-- Loading state (shown immediately) -->
<div id="planGenerationLoading" class="plan-loading-state">
<div class="plan-loading-content">
<div class="plan-spinner"></div>
<div class="plan-loading-text">
<span class="plan-loading-title">Generating implementation plan...</span>
<span class="plan-loading-time" id="planLoadingTime">0s</span>
</div>
</div>
<p class="plan-loading-hint">Claude is breaking down your task into actionable steps with tests</p>
</div>
<div class="form-row">
<label>Max Iterations</label>
<div class="iteration-presets">
<button type="button" class="iteration-preset-btn active" data-iterations="10" onclick="app.selectIterationPreset(10)">10</button>
<button type="button" class="iteration-preset-btn" data-iterations="25" onclick="app.selectIterationPreset(25)">25</button>
<button type="button" class="iteration-preset-btn" data-iterations="50" onclick="app.selectIterationPreset(50)">50</button>
<button type="button" class="iteration-preset-btn" data-iterations="100" onclick="app.selectIterationPreset(100)">100</button>
<button type="button" class="iteration-preset-btn" data-iterations="0" onclick="app.selectIterationPreset(0)">&infin;</button>
<!-- Error state -->
<div id="planGenerationError" class="plan-section hidden">
<p class="plan-error-msg" id="planErrorMsg"></p>
<div class="plan-actions">
<button class="btn-toolbar btn-primary" onclick="app.generatePlan()">Retry</button>
<button class="btn-toolbar" onclick="app.skipPlanGeneration()">Skip</button>
</div>
<span class="form-hint">Safety limit - loop stops after this many iterations</span>
</div>
<div class="form-row">
<label>Working Directory</label>
<div class="case-select-inline">
<select id="ralphCaseSelect" class="form-select"></select>
<button class="btn-toolbar btn-sm" onclick="app.showCreateCaseModal()">+ New</button>
<!-- Editor state -->
<div id="planEditor" class="plan-section hidden">
<div class="plan-editor-header">
<span>Implementation Plan</span>
<div class="plan-editor-actions">
<div class="plan-option-group">
<label>Detail:</label>
<div class="plan-detail-btns">
<button type="button" class="plan-detail-btn" data-detail="brief" onclick="app.setPlanDetail('brief')">Brief</button>
<button type="button" class="plan-detail-btn active" data-detail="standard" onclick="app.setPlanDetail('standard')">Standard</button>
<button type="button" class="plan-detail-btn" data-detail="detailed" onclick="app.setPlanDetail('detailed')">Detailed</button>
</div>
</div>
<button class="btn-toolbar btn-sm" onclick="app.regeneratePlan()">Regenerate</button>
</div>
</div>
<div id="planItemsList" class="plan-items-list"></div>
<div class="plan-editor-footer">
<button class="btn-toolbar btn-sm" onclick="app.addPlanItem()">+ Add Step</button>
</div>
</div>
</div>
<!-- Step 2: Preview & Launch -->
<div class="wizard-page hidden" id="ralphWizardStep2">
<!-- Step 3: Preview & Launch -->
<div class="wizard-page hidden" id="ralphWizardStep3">
<div class="ralph-preview-section">
<label>Prompt Preview</label>
<div class="ralph-prompt-preview" id="ralphPromptPreview"></div>
@@ -1016,6 +1070,10 @@ Build a REST API for user authentication:
<span class="config-label">Case:</span>
<span id="summaryCase">testcase</span>
</div>
<div class="config-summary-row" style="display: none;">
<span class="config-label">Plan:</span>
<span id="summaryPlan">-</span>
</div>
</div>
<!-- Advanced Options (Collapsible) -->
+312 -2
View File
@@ -2059,6 +2059,21 @@ body {
}
/* Switch toggle for form rows */
.form-section-header {
font-size: 0.7rem;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 1rem 0 0.5rem 0;
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--border);
}
.form-section-header:first-child {
margin-top: 0;
}
.form-row-switch {
display: flex;
align-items: center;
@@ -2238,6 +2253,26 @@ body {
color: var(--red);
}
/* CPU limit status indicator */
.cpu-limit-status {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 3px;
background: var(--bg-hover);
color: var(--text-dim);
margin-left: 6px;
}
.cpu-limit-status.available {
background: rgba(34, 197, 94, 0.2);
color: var(--green);
}
.cpu-limit-status.unavailable {
background: rgba(239, 68, 68, 0.2);
color: var(--red);
}
.form-actions {
display: flex;
justify-content: flex-end;
@@ -5331,6 +5366,281 @@ kbd {
gap: 0.5rem;
}
/* Wizard Modal - wider layout */
.modal-wizard {
width: 720px;
max-width: 95vw;
}
.wizard-page-compact {
min-height: auto;
padding: 0.5rem 0;
}
.wizard-step-intro {
text-align: center;
margin-bottom: 0.75rem;
color: var(--text-dim);
font-size: 0.9rem;
}
.wizard-step-intro p {
margin: 0;
}
/* Wizard inline row for compact layout */
.wizard-inline-row {
display: flex;
gap: 1rem;
margin-top: 0.75rem;
}
.wizard-inline-field {
flex: 1;
}
.wizard-inline-field-small {
flex: 0 0 auto;
}
.wizard-inline-field label {
display: block;
font-size: 0.8rem;
color: var(--text-dim);
margin-bottom: 0.25rem;
}
.iteration-presets-compact {
display: flex;
gap: 0.25rem;
}
.iteration-presets-compact .iteration-preset-btn {
padding: 0.35rem 0.6rem;
font-size: 0.75rem;
min-width: 36px;
}
/* Plan Generation Step */
.plan-section {
padding: 1rem;
text-align: center;
}
.plan-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
/* Plan Loading State */
.plan-loading-state {
padding: 1.5rem 1rem;
text-align: center;
}
.plan-loading-content {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 0.75rem;
}
.plan-spinner {
width: 28px;
height: 28px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.plan-loading-text {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.plan-loading-title {
font-weight: 500;
color: var(--text);
}
.plan-loading-time {
font-size: 0.8rem;
color: var(--text-muted);
font-family: 'SF Mono', Monaco, monospace;
}
.plan-loading-hint {
color: var(--text-dim);
font-size: 0.85rem;
margin: 0;
}
.plan-error-msg {
color: var(--red);
margin-bottom: 1rem;
}
/* Plan Options (detail level, model) */
.plan-options {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.plan-option-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.plan-option-group label {
font-size: 0.8rem;
color: var(--text-dim);
}
.plan-option-group select {
padding: 0.3rem 0.5rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.8rem;
}
.plan-detail-btns {
display: flex;
gap: 0.25rem;
}
.plan-detail-btn {
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-dim);
cursor: pointer;
transition: all 0.15s;
}
.plan-detail-btn:hover {
border-color: var(--border-light);
}
.plan-detail-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
/* Plan Editor */
.plan-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-weight: 500;
font-size: 0.9rem;
}
.plan-editor-actions {
display: flex;
gap: 0.35rem;
}
.plan-items-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 160px;
overflow-y: auto;
margin-bottom: 0.5rem;
}
.plan-item {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.5rem;
background: var(--bg-input);
border-radius: 4px;
}
.plan-item-checkbox {
flex-shrink: 0;
width: 14px;
height: 14px;
}
.plan-item-content {
flex: 1;
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.2rem 0.4rem;
color: var(--text);
font-size: 0.8rem;
}
.plan-item-content:focus {
outline: none;
border-color: var(--accent);
}
.plan-item-priority {
width: 50px;
flex-shrink: 0;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text);
padding: 0.2rem;
border-radius: 4px;
font-size: 0.75rem;
}
.plan-item-remove {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 1rem;
padding: 0 0.2rem;
line-height: 1;
}
.plan-item-remove:hover {
color: var(--red);
}
.plan-item:has(.plan-item-checkbox:not(:checked)) {
opacity: 0.5;
}
.plan-item:has(.plan-item-checkbox:not(:checked)) .plan-item-content {
text-decoration: line-through;
}
#planEditor {
text-align: left;
padding: 0.75rem;
}
.plan-editor-footer {
text-align: left;
}
/* Textarea with AI Assist button */
.textarea-with-assist {
position: relative;
@@ -5338,7 +5648,7 @@ kbd {
.textarea-with-assist textarea {
width: 100%;
padding: 0.75rem;
padding: 0.6rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
@@ -5346,7 +5656,7 @@ kbd {
font-size: 0.85rem;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
resize: vertical;
min-height: 140px;
min-height: 80px;
}
.textarea-with-assist textarea:focus {