mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
chore: bump version to 0.1585
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
ANSI_ESCAPE_PATTERN_FULL,
|
||||
ANSI_ESCAPE_PATTERN_SIMPLE,
|
||||
TOKEN_PATTERN,
|
||||
SPINNER_PATTERN,
|
||||
createAnsiPatternFull,
|
||||
createAnsiPatternSimple,
|
||||
stripAnsi,
|
||||
|
||||
@@ -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_\/\-. ~]+$/;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user