chore: bump version to 0.1529

This commit is contained in:
arkon
2026-02-18 07:28:15 +01:00
parent 31d3e7bc70
commit 50b17184ee
9 changed files with 172 additions and 14 deletions
+29 -3
View File
@@ -3719,10 +3719,19 @@ class ClaudemanApp {
const minimizedCount = minimizedAgents?.size || 0;
const subagentBadge = minimizedCount > 0 ? this.renderSubagentTabBadge(id, minimizedAgents) : '';
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}" data-id="${id}" data-color="${color}" onclick="app.selectSession('${this.escapeHtml(id)}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${this.escapeHtml(id)}')" tabindex="0" role="tab" aria-selected="${isActive ? 'true' : 'false'}" aria-label="${this.escapeHtml(name)} session">
// Show folder name if session has a custom name (to still show the directory context)
const folderName = session.workingDir ? session.workingDir.split('/').pop() || '' : '';
const showFolder = session.name && folderName && folderName !== name;
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}" data-id="${id}" data-color="${color}" onclick="app.selectSession('${this.escapeHtml(id)}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${this.escapeHtml(id)}')" tabindex="0" role="tab" aria-selected="${isActive ? 'true' : 'false'}" aria-label="${this.escapeHtml(name)} session" ${session.workingDir ? `title="${this.escapeHtml(session.workingDir)}"` : ''}>
<span class="tab-status ${status}" aria-hidden="true"></span>
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : ''}
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
<span class="tab-info">
<span class="tab-name-row">
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : ''}
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
</span>
${showFolder ? `<span class="tab-folder">\u{1F4C1} ${this.escapeHtml(folderName)}</span>` : ''}
</span>
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()" aria-label="${taskStats.running} running tasks">${taskStats.running}</span>` : ''}
${subagentBadge}
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${this.escapeHtml(id)}')" title="Session options" aria-label="Session options" tabindex="0">&#x2699;</span>
@@ -8482,6 +8491,9 @@ class ClaudemanApp {
document.getElementById('modalImageWatcherEnabled').checked = session.imageWatcherEnabled ?? true;
document.getElementById('modalFlickerFilterEnabled').checked = session.flickerFilterEnabled ?? false;
// Populate session name input
document.getElementById('modalSessionName').value = session.name || '';
// Initialize color picker with current session color
const currentColor = session.color || 'default';
const colorPicker = document.getElementById('sessionColorPicker');
@@ -8510,6 +8522,20 @@ class ClaudemanApp {
this.activeFocusTrap.activate();
}
async saveSessionName() {
if (!this.editingSessionId) return;
const name = document.getElementById('modalSessionName').value.trim();
try {
await fetch(`/api/sessions/${this.editingSessionId}/name`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
} catch (err) {
this.showToast('Failed to save session name: ' + err.message, 'error');
}
}
async autoSaveAutoCompact() {
if (!this.editingSessionId) return;
try {
+5
View File
@@ -548,6 +548,11 @@
<!-- Context Management Tab Content -->
<div class="modal-tab-content hidden" id="context-tab">
<div class="form-section-header">Appearance</div>
<div class="form-row">
<label>Session Name</label>
<input type="text" id="modalSessionName" maxlength="128" placeholder="Auto (directory name)" onblur="app.saveSessionName()">
<span class="form-hint">Custom name shown in the tab (right-click tab to rename inline)</span>
</div>
<div class="form-row">
<label>Session Color</label>
<div class="color-picker" id="sessionColorPicker">
+30 -2
View File
@@ -284,11 +284,39 @@ body {
.session-tab.active[data-color="purple"] { background: rgba(168, 85, 247, 0.1); }
.session-tab.active[data-color="pink"] { background: rgba(236, 72, 153, 0.1); }
.session-tab .tab-info {
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.session-tab .tab-name-row {
display: flex;
align-items: center;
gap: 0.25rem;
}
.session-tab .tab-name {
overflow: hidden;
text-overflow: ellipsis;
}
.session-tab .tab-folder {
font-size: 0.6rem;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
@media (max-width: 768px) {
.session-tab .tab-folder {
display: none;
}
}
.session-tab .tab-close {
opacity: 0;
width: 0;
@@ -2785,7 +2813,7 @@ body {
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
padding-top: 0.75rem;
padding: 0.75rem;
border-top: 1px solid var(--border);
}
@@ -2846,7 +2874,7 @@ body {
}
.modal-content.modal-lg {
max-width: 520px;
max-width: 540px;
max-height: 85vh;
display: flex;
flex-direction: column;
+63 -4
View File
@@ -542,6 +542,22 @@ export class WebServer extends EventEmitter {
threshold: 1024,
});
// Optional HTTP Basic Auth (set CLAUDEMAN_PASSWORD env var to enable)
const authPassword = process.env.CLAUDEMAN_PASSWORD;
if (authPassword) {
const authUsername = process.env.CLAUDEMAN_USERNAME || 'admin';
const expectedHeader = 'Basic ' + Buffer.from(`${authUsername}:${authPassword}`).toString('base64');
this.app.addHook('onRequest', (req, reply, done) => {
const auth = req.headers.authorization;
if (auth === expectedHeader) {
done();
return;
}
reply.header('WWW-Authenticate', 'Basic realm="Claudeman"');
reply.code(401).send('Unauthorized');
});
}
// Security headers on every response
this.app.addHook('onRequest', (_req, reply, done) => {
reply.header('X-Content-Type-Options', 'nosniff');
@@ -2887,7 +2903,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
this.app.get('/api/cases/:caseName/ralph-wizard/files', async (req) => {
const { caseName } = req.params as { caseName: string };
const casesDir = join(homedir(), 'claudeman-cases');
const casePath = join(casesDir, caseName);
let casePath = join(casesDir, caseName);
// Security: Path traversal protection - use relative path check
const resolvedCase = resolve(casePath);
@@ -2897,6 +2913,19 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
}
// Check linked cases if path doesn't exist
if (!existsSync(casePath)) {
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[caseName]) {
casePath = linkedCases[caseName];
}
} catch {
// No linked cases file
}
}
const wizardDir = join(casePath, 'ralph-wizard');
if (!existsSync(wizardDir)) {
@@ -2935,7 +2964,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req, reply) => {
const { caseName, filePath } = req.params as { caseName: string; filePath: string };
const casesDir = join(homedir(), 'claudeman-cases');
const casePath = join(casesDir, caseName);
let casePath = join(casesDir, caseName);
// Prevent browser caching - prompts change between plan generations
reply.header('Cache-Control', 'no-store, no-cache, must-revalidate');
@@ -2950,6 +2979,19 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
}
// Check linked cases if path doesn't exist
if (!existsSync(casePath)) {
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[caseName]) {
casePath = linkedCases[caseName];
}
} catch {
// No linked cases file
}
}
const wizardDir = join(casePath, 'ralph-wizard');
// Decode the file path (it may be URL encoded)
@@ -2974,13 +3016,30 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
}
const isJson = filePath.endsWith('.json');
// Parse JSON content safely (may contain invalid JSON)
// Parse JSON content safely (may contain invalid JSON or unescaped control characters)
let parsed: unknown = null;
if (isJson) {
try {
parsed = JSON.parse(content);
} catch {
// Invalid JSON - return null for parsed, content still available as raw string
// Try repairing common JSON issues (unescaped control characters, trailing commas)
try {
let repaired = content;
// Fix trailing commas before closing brackets
repaired = repaired.replace(/,(\s*[\]}])/g, '$1');
// Fix unescaped control characters within JSON strings
repaired = repaired.replace(/"([^"\\]|\\.)*"/g, (match) => {
return match
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
// eslint-disable-next-line no-control-regex
.replace(/[\x00-\x1f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
});
parsed = JSON.parse(repaired);
} catch {
// Still invalid - return null for parsed, content available as raw string
}
}
}