mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +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`)
|
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
@@ -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
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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_\/\-. ~]+$/;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user