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