chore: bump version to 0.1585

This commit is contained in:
arkon
2026-02-21 07:12:57 +01:00
parent 85ee601b5b
commit 0fe6251424
8 changed files with 48 additions and 57 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`)
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1584 (must match `package.json` for npm publish)
**Version**: 0.1585 (must match `package.json` for npm publish)
## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1584",
"version": "0.1585",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+2 -4
View File
@@ -28,6 +28,7 @@ import { LRUMap } from './utils/lru-map.js';
import {
ANSI_ESCAPE_PATTERN_FULL,
TOKEN_PATTERN,
SPINNER_PATTERN,
MAX_SESSION_TOKENS,
} from './utils/index.js';
import {
@@ -1036,10 +1037,7 @@ export class Session extends EventEmitter {
// Detect when Claude starts working (thinking, writing, etc)
// Fast path: check spinner characters on raw data (Unicode, never in ANSI sequences)
const hasSpinner = data.includes('⠋') || data.includes('⠙') ||
data.includes('⠹') || data.includes('⠸') ||
data.includes('⠼') || data.includes('⠴') ||
data.includes('⠦') || data.includes('⠧');
const hasSpinner = SPINNER_PATTERN.test(data);
if (hasSpinner) {
if (!this._isWorking) {
this._isWorking = true;
+3 -3
View File
@@ -74,7 +74,7 @@ export class TeamWatcher extends EventEmitter {
getTeamForSession(sessionId: string): TeamConfig | undefined {
const teamName = this.sessionToTeam.get(sessionId);
if (teamName) {
const team = this.teams.get(teamName);
const team = this.teams.peek(teamName);
if (team) return team;
// Stale reverse index entry — clean up
this.sessionToTeam.delete(sessionId);
@@ -210,7 +210,7 @@ export class TeamWatcher extends EventEmitter {
}
// Detect removed teams
for (const name of Array.from(this.teams.keys())) {
for (const name of this.teams.keys()) {
if (!currentTeamNames.has(name)) {
const removed = this.teams.get(name);
this.teams.delete(name);
@@ -221,7 +221,7 @@ export class TeamWatcher extends EventEmitter {
}
// Prune stale mtime entries for removed teams
this.taskMtimes.delete(name);
for (const key of Array.from(this.inboxMtimes.keys())) {
for (const key of this.inboxMtimes.keys()) {
if (key.startsWith(`${name}/`)) {
this.inboxMtimes.delete(key);
}
+1
View File
@@ -14,6 +14,7 @@ export {
ANSI_ESCAPE_PATTERN_FULL,
ANSI_ESCAPE_PATTERN_SIMPLE,
TOKEN_PATTERN,
SPINNER_PATTERN,
createAnsiPatternFull,
createAnsiPatternSimple,
stripAnsi,
+7
View File
@@ -67,5 +67,12 @@ export function stripAnsi(text: string): string {
* Regex to validate safe file paths (no shell metacharacters).
* Shared by schemas.ts and tmux-manager.ts for consistent path validation.
*/
/**
* Braille spinner characters used by Claude Code's Ink UI.
* Matches any of: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧
* Used on the hot path (every PTY chunk) — pre-compiled for performance.
*/
export const SPINNER_PATTERN = /[⠋⠙⠹⠸⠼⠴⠦⠧]/;
export const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/;
+2 -13
View File
@@ -189,6 +189,7 @@ body {
.session-tabs::-webkit-scrollbar {
width: 4px;
height: 0;
}
.session-tabs::-webkit-scrollbar-track {
@@ -204,10 +205,6 @@ body {
background: var(--border-light);
}
.session-tabs::-webkit-scrollbar {
height: 0;
}
.session-tab {
display: flex;
align-items: center;
@@ -795,10 +792,6 @@ body {
color: var(--accent);
}
.respawn-action-entry.action-command .action-detail {
color: var(--accent-hover);
}
.respawn-action-entry.action-command .action-type {
color: var(--accent);
font-weight: 600;
@@ -1384,6 +1377,7 @@ body {
border-radius: 4px;
border-left: 2px solid var(--text-muted);
transition: border-color 0.2s, background 0.2s;
position: relative;
}
.ralph-task-card.task-pending {
@@ -7315,11 +7309,6 @@ kbd {
background: var(--bg-card);
}
/* Runtime task card enhancements */
.ralph-task-card {
position: relative;
}
.ralph-task-attempts {
position: absolute;
top: 2px;
+31 -35
View File
@@ -220,7 +220,7 @@ describe('SubagentWatcher performance', () => {
});
describe('extractDescriptionFromFile — full subagent file reads', () => {
it('should not read entire large agent file just for first 5 lines', async () => {
it('should extract description from first 8KB instead of reading entire file', async () => {
const agentDir = join(tmpDir, 'agent-files');
mkdirSync(agentDir, { recursive: true });
@@ -232,7 +232,7 @@ describe('SubagentWatcher performance', () => {
const fileStat = await statAsync(agentFile);
const fileSizeKB = fileStat.size / 1024;
// Current implementation: reads ENTIRE file then slices first 5 lines
// Approach 1: Read entire file then slice first 5 lines
const start = performance.now();
const content = await readFile(agentFile, 'utf8');
const lines = content.split('\n').filter((l: string) => l.trim());
@@ -253,48 +253,44 @@ describe('SubagentWatcher performance', () => {
}
const readAllElapsed = performance.now() - start;
// Compare with streaming approach (only read first N bytes)
const { createReadStream } = await import('node:fs');
const { createInterface } = await import('node:readline');
// Approach 2: Read only first 8KB via partial read
const start2 = performance.now();
const fd = await import('node:fs/promises').then(m => m.open(agentFile, 'r'));
const buf = Buffer.alloc(8192);
const { bytesRead } = await fd.read(buf, 0, 8192, 0);
await fd.close();
const partial = buf.subarray(0, bytesRead).toString('utf8');
const partialLines = partial.split('\n').filter((l: string) => l.trim());
let description2: string | undefined;
await new Promise<void>((resolve) => {
const stream = createReadStream(agentFile, { end: 8192 }); // Only read first 8KB
const rl = createInterface({ input: stream });
let lineCount = 0;
rl.on('line', (line) => {
if (lineCount >= 5) { rl.close(); stream.destroy(); return; }
lineCount++;
try {
const entry = JSON.parse(line);
if (entry.type === 'user' && entry.message?.content) {
const firstContent = Array.isArray(entry.message.content)
? entry.message.content[0]
: undefined;
if (firstContent?.type === 'text') {
description2 = firstContent.text.trim().slice(0, 45);
rl.close();
stream.destroy();
}
for (const line of partialLines.slice(0, 5)) {
try {
const entry = JSON.parse(line);
if (entry.type === 'user' && entry.message?.content) {
const firstContent = Array.isArray(entry.message.content)
? entry.message.content[0]
: undefined;
if (firstContent?.type === 'text') {
description2 = firstContent.text.trim().slice(0, 45);
}
} catch { /* skip */ }
});
rl.on('close', resolve);
rl.on('error', resolve);
});
const streamElapsed = performance.now() - start2;
}
} catch { /* skip */ }
}
const partialElapsed = performance.now() - start2;
console.log(`[extractDescription] file: ${fileSizeKB.toFixed(0)}KB, readAll: ${readAllElapsed.toFixed(1)}ms, stream-first-8KB: ${streamElapsed.toFixed(1)}ms`);
console.log(`[extractDescription] speedup: ${(readAllElapsed / Math.max(streamElapsed, 0.1)).toFixed(1)}x`);
console.log(`[extractDescription] file: ${fileSizeKB.toFixed(0)}KB, readAll: ${readAllElapsed.toFixed(1)}ms, partial-8KB: ${partialElapsed.toFixed(1)}ms`);
console.log(`[extractDescription] readAll bytes: ${content.length}, partial bytes: ${bytesRead}`);
// Both approaches must find the same description
expect(description).toBeDefined();
expect(description2).toBeDefined();
expect(description).toBe(description2);
// Stream approach should be significantly faster on large files
// This test documents the waste in the current approach
expect(streamElapsed).toBeLessThan(readAllElapsed);
// The partial read should touch far less data than readAll
expect(bytesRead).toBeLessThan(fileStat.size / 10);
// Both approaches should complete quickly (under 50ms for cached files)
expect(readAllElapsed).toBeLessThan(50);
expect(partialElapsed).toBeLessThan(50);
});
});