Skip to content

Commit a9d83ec

Browse files
Ark0Nclaude
andcommitted
chore: version packages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 84137cd commit a9d83ec

18 files changed

Lines changed: 175 additions & 369 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# aicodeman
22

3+
## 0.5.1
4+
5+
### Patch Changes
6+
7+
- refactor: codebase cleanup — extract route helpers, eliminate boilerplate, optimize hot paths
8+
- Add `parseBody()` helper to route-helpers.ts: validates request body against Zod schema with structured 400 error on failure, replacing 37 identical safeParse + error-check blocks across 10 route files
9+
- Add `persistAndBroadcastSession()` helper: combines persist + SessionUpdated broadcast into one call, replacing 5 repeated 2-line pairs
10+
- Migrate session-routes.ts to use `findSessionOrFail()` consistently (17 inline session lookups replaced) and `parseBody()` (12 patterns)
11+
- Migrate ralph-routes.ts to use `findSessionOrFail()` (9 lookups) and `parseBody()` (4 patterns)
12+
- Migrate 8 remaining route files to use `parseBody()` (21 patterns total)
13+
- Fix O(n log n) eviction in bash-tool-parser.ts: replace `Array.from().sort()[0]` with O(n) min-scan for oldest active tool
14+
- Extract `_debouncedCall()` utility in frontend: replaces 4 manual debounce patterns (7 lines each → 1 line) in app.js, panels-ui.js, ralph-panel.js
15+
- Net reduction: 208 lines removed across 16 files
16+
317
## 0.5.0
418

519
### Minor Changes

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ When user says "COM":
5252
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
5353
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
5454
55-
**Version**: 0.5.0 (must match `package.json`)
55+
**Version**: 0.5.1 (must match `package.json`)
5656
5757
## Project Overview
5858
@@ -111,15 +111,15 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
111111
| **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | |
112112
| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (14 route modules + barrel), `src/web/route-helpers.ts`, `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | |
113113
| **Frontend** | `src/web/public/app.js` (~2.6K lines, core) + 5 infra modules (`constants.js`, `mobile-handlers.js`, `voice-input.js`, `notification-manager.js`, `keyboard-accessory.js`) + 7 domain modules (`terminal-ui.js`, `respawn-ui.js`, `ralph-panel.js`, `orchestrator-panel.js`, `settings-ui.js`, `panels-ui.js`, `session-ui.js`) + 4 feature modules (`ralph-wizard.js`, `api-client.js`, `subagent-windows.js`, `input-cjk.js`) + `sw.js` | |
114-
| **Types** | `src/types/index.ts` → 15 domain files | See `@fileoverview` in index.ts |
114+
| **Types** | `src/types/index.ts` → 14 domain files | See `@fileoverview` in index.ts |
115115
116116
★ = Large file (>50KB). All files have `@fileoverview` JSDoc — read that before diving in.
117117
118118
**Local package**: `packages/xterm-zerolag-input/` — local echo overlay for xterm.js; copy embedded in `app.js`.
119119
120120
**Config**: `src/config/` — 9 files. Import from specific files, not barrel.
121121
122-
**Utilities**: `src/utils/` — re-exported via index. Key: `CleanupManager`, `LRUMap`, `StaleExpirationMap`, `BufferAccumulator`, `stripAnsi`, `Debouncer`, `KeyedDebouncer`. Also: `claude-cli-resolver`/`opencode-cli-resolver` (CLI path resolution), `string-similarity` (fuzzy matching), `regex-patterns` (ANSI/token/spinner patterns), `assertNever` (exhaustive checks).
122+
**Utilities**: `src/utils/` — re-exported via index. Key: `CleanupManager`, `LRUMap`, `StaleExpirationMap`, `BufferAccumulator`, `stripAnsi`, `Debouncer`, `KeyedDebouncer`. Also: `claude-cli-resolver`/`opencode-cli-resolver` (CLI path resolution), `string-similarity` (fuzzy matching), `regex-patterns` (ANSI/token/spinner patterns), `assertNever` (exhaustive checks), `token-validation` (auth tokens), `nice-wrapper` (process priority).
123123
124124
### Data Flow
125125

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aicodeman",
3-
"version": "0.5.0",
3+
"version": "0.5.1",
44
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
55
"type": "module",
66
"main": "dist/index.js",

src/bash-tool-parser.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -498,10 +498,17 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
498498

499499
// Enforce max tools limit
500500
if (this._activeTools.size >= MAX_ACTIVE_TOOLS) {
501-
// Remove oldest tool
502-
const oldest = Array.from(this._activeTools.entries()).sort((a, b) => a[1].startedAt - b[1].startedAt)[0];
503-
if (oldest) {
504-
this._activeTools.delete(oldest[0]);
501+
// Remove oldest tool (O(n) min-scan instead of O(n log n) sort)
502+
let oldestKey: string | undefined;
503+
let oldestTime = Infinity;
504+
for (const [key, entry] of this._activeTools) {
505+
if (entry.startedAt < oldestTime) {
506+
oldestTime = entry.startedAt;
507+
oldestKey = key;
508+
}
509+
}
510+
if (oldestKey) {
511+
this._activeTools.delete(oldestKey);
505512
}
506513
}
507514

src/web/public/app.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,8 @@ class CodemanApp {
380380
this.flickerFilterActive = false;
381381
this.flickerFilterTimeout = null;
382382

383-
// Render debouncing
384-
this.renderSessionTabsTimeout = null;
385-
this.renderRalphStatePanelTimeout = null;
386-
this.renderTaskPanelTimeout = null;
387-
this.renderMuxSessionsTimeout = null;
383+
// Render debounce timers (managed by _debouncedCall)
384+
this._debounceTimers = Object.create(null);
388385

389386
// System stats polling
390387
this.systemStatsInterval = null;
@@ -1543,18 +1540,27 @@ class CodemanApp {
15431540
}
15441541
}
15451542

1543+
// ═══════════════════════════════════════════════════════════════
1544+
// Debounce Utility
1545+
// ═══════════════════════════════════════════════════════════════
1546+
1547+
/** Debounce a method call using a named timer key. */
1548+
_debouncedCall(timerKey, fn, delayMs = 100) {
1549+
if (this._debounceTimers[timerKey]) {
1550+
clearTimeout(this._debounceTimers[timerKey]);
1551+
}
1552+
this._debounceTimers[timerKey] = setTimeout(() => {
1553+
this._debounceTimers[timerKey] = null;
1554+
fn.call(this);
1555+
}, delayMs);
1556+
}
1557+
15461558
// ═══════════════════════════════════════════════════════════════
15471559
// Session Tabs
15481560
// ═══════════════════════════════════════════════════════════════
15491561

15501562
renderSessionTabs() {
1551-
// Debounce renders at 100ms to prevent excessive DOM updates
1552-
if (this.renderSessionTabsTimeout) {
1553-
clearTimeout(this.renderSessionTabsTimeout);
1554-
}
1555-
this.renderSessionTabsTimeout = setTimeout(() => {
1556-
this._renderSessionTabsImmediate();
1557-
}, 100);
1563+
this._debouncedCall('sessionTabs', this._renderSessionTabsImmediate);
15581564
}
15591565

15601566
/** Toggle .active class on tabs immediately (no debounce). Used by selectSession(). */

src/web/public/panels-ui.js

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -613,13 +613,7 @@ Object.assign(CodemanApp.prototype, {
613613
},
614614

615615
renderTaskPanel() {
616-
// Debounce renders at 100ms to prevent excessive DOM updates
617-
if (this.renderTaskPanelTimeout) {
618-
clearTimeout(this.renderTaskPanelTimeout);
619-
}
620-
this.renderTaskPanelTimeout = setTimeout(() => {
621-
this._renderTaskPanelImmediate();
622-
}, 100);
616+
this._debouncedCall('taskPanel', this._renderTaskPanelImmediate);
623617
},
624618

625619
_renderTaskPanelImmediate() {
@@ -2974,13 +2968,7 @@ Object.assign(CodemanApp.prototype, {
29742968

29752969

29762970
renderMuxSessions() {
2977-
// Debounce renders at 100ms to prevent excessive DOM updates
2978-
if (this.renderMuxSessionsTimeout) {
2979-
clearTimeout(this.renderMuxSessionsTimeout);
2980-
}
2981-
this.renderMuxSessionsTimeout = setTimeout(() => {
2982-
this._renderMuxSessionsImmediate();
2983-
}, 100);
2971+
this._debouncedCall('muxSessions', this._renderMuxSessionsImmediate);
29842972
},
29852973

29862974
_renderMuxSessionsImmediate() {

src/web/public/ralph-panel.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -471,13 +471,7 @@ Object.assign(CodemanApp.prototype, {
471471
},
472472

473473
renderRalphStatePanel() {
474-
// Debounce renders at 50ms to prevent excessive DOM updates
475-
if (this.renderRalphStatePanelTimeout) {
476-
clearTimeout(this.renderRalphStatePanelTimeout);
477-
}
478-
this.renderRalphStatePanelTimeout = setTimeout(() => {
479-
this._renderRalphStatePanelImmediate();
480-
}, 50);
474+
this._debouncedCall('ralphStatePanel', this._renderRalphStatePanelImmediate, 50);
481475
},
482476

483477
_renderRalphStatePanelImmediate() {

src/web/route-helpers.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { join, resolve, relative, isAbsolute } from 'node:path';
99
import { homedir } from 'node:os';
10+
import type { z } from 'zod';
1011
import { Session } from '../session.js';
1112
import { ApiErrorCode, createErrorResponse } from '../types.js';
1213
import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
@@ -50,6 +51,31 @@ export function findSessionOrFail(ctx: SessionPort, sessionId: string): Session
5051
return session;
5152
}
5253

54+
/**
55+
* Parse and validate a request body against a Zod schema, or throw a structured 400 error.
56+
* Replaces the repeated pattern: `const r = Schema.safeParse(body); if (!r.success) return createErrorResponse(...)`.
57+
*/
58+
export function parseBody<T>(schema: z.ZodType<T>, body: unknown, errorMessage?: string): T {
59+
const result = schema.safeParse(body);
60+
if (!result.success) {
61+
const msg = errorMessage ?? result.error.issues[0]?.message ?? 'Validation failed';
62+
throw Object.assign(new Error(msg), {
63+
statusCode: 400,
64+
body: createErrorResponse(ApiErrorCode.INVALID_INPUT, msg),
65+
});
66+
}
67+
return result.data;
68+
}
69+
70+
/**
71+
* Persist session state and broadcast a SessionUpdated event.
72+
* Replaces the repeated two-line pattern across route handlers.
73+
*/
74+
export function persistAndBroadcastSession(ctx: SessionPort & EventPort, session: Session): void {
75+
ctx.persistSessionState(session);
76+
ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
77+
}
78+
5379
/**
5480
* Formats uptime in seconds to a human-readable string (e.g., "1d 2h 30m 15s").
5581
*/

src/web/routes/case-routes.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.
1414
import { CreateCaseSchema, LinkCaseSchema } from '../schemas.js';
1515
import { generateClaudeMd } from '../../templates/claude-md.js';
1616
import { writeHooksConfig } from '../../hooks-config.js';
17-
import { CASES_DIR, validatePathWithinBase } from '../route-helpers.js';
17+
import { CASES_DIR, validatePathWithinBase, parseBody } from '../route-helpers.js';
1818
import { SseEvent } from '../sse-events.js';
1919
import type { EventPort, ConfigPort } from '../ports/index.js';
2020

@@ -83,11 +83,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
8383
});
8484

8585
app.post('/api/cases', async (req): Promise<ApiResponse<{ case: { name: string; path: string } }>> => {
86-
const result = CreateCaseSchema.safeParse(req.body);
87-
if (!result.success) {
88-
return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
89-
}
90-
const { name, description } = result.data;
86+
const { name, description } = parseBody(CreateCaseSchema, req.body);
9187

9288
const casePath = validatePathWithinBase(name, CASES_DIR);
9389
if (!casePath) {
@@ -120,11 +116,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
120116

121117
// Link an existing folder as a case
122118
app.post('/api/cases/link', async (req): Promise<ApiResponse<{ case: { name: string; path: string } }>> => {
123-
const lcResult = LinkCaseSchema.safeParse(req.body);
124-
if (!lcResult.success) {
125-
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid request body');
126-
}
127-
const { name, path: folderPath } = lcResult.data;
119+
const { name, path: folderPath } = parseBody(LinkCaseSchema, req.body, 'Invalid request body');
128120

129121
// Expand ~ to home directory
130122
const expandedPath = folderPath.startsWith('~') ? join(homedir(), folderPath.slice(1)) : folderPath;

src/web/routes/hook-event-routes.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,15 @@
77
import { FastifyInstance } from 'fastify';
88
import { ApiErrorCode, createErrorResponse } from '../../types.js';
99
import { HookEventSchema, isValidWorkingDir } from '../schemas.js';
10-
import { sanitizeHookData } from '../route-helpers.js';
10+
import { sanitizeHookData, parseBody } from '../route-helpers.js';
1111
import type { SessionPort, EventPort, RespawnPort, ConfigPort, InfraPort } from '../ports/index.js';
1212

1313
export function registerHookEventRoutes(
1414
app: FastifyInstance,
1515
ctx: SessionPort & EventPort & RespawnPort & ConfigPort & InfraPort
1616
): void {
1717
app.post('/api/hook-event', async (req) => {
18-
const result = HookEventSchema.safeParse(req.body);
19-
if (!result.success) {
20-
return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.error.issues[0]?.message ?? 'Validation failed');
21-
}
22-
const { event, sessionId, data } = result.data;
18+
const { event, sessionId, data } = parseBody(HookEventSchema, req.body);
2319
if (!ctx.sessions.has(sessionId)) {
2420
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
2521
}

0 commit comments

Comments
 (0)