mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
chore: bump version to 0.1550
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.1549 (must match `package.json` for npm publish)
|
||||
**Version**: 0.1550 (must match `package.json` for npm publish)
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -81,6 +81,10 @@ journalctl --user -u claudeman-web -f
|
||||
- **Single-line prompts only** — `writeViaMux()` sends text and Enter separately; multi-line breaks Ink
|
||||
- **Don't kill tmux sessions blindly** — Check `$CLAUDEMAN_TMUX` first; you might be inside one
|
||||
- **Never run full test suite** — `npx vitest run` spawns/kills tmux sessions and will crash your Claudeman session. Run individual test files only.
|
||||
- **Global regex `lastIndex` sharing** — `ANSI_ESCAPE_PATTERN_FULL/SIMPLE` have `g` flag; use `createAnsiPatternFull/Simple()` factory functions for fresh instances in loops
|
||||
- **DEC 2026 sync blocks** — Never discard incomplete sync blocks (START without END); buffer up to 50ms then flush. See `app.js:extractSyncSegments()`
|
||||
- **Terminal writes during buffer load** — Live SSE writes are queued while `_isLoadingBuffer` is true to prevent interleaving with historical data
|
||||
- **Local echo prompt scanning** — Does NOT use `buffer.cursorY` (Ink moves it); scans buffer bottom-up for visible `>` prompt marker
|
||||
|
||||
## Import Conventions
|
||||
|
||||
@@ -123,12 +127,12 @@ journalctl --user -u claudeman-web -f
|
||||
| `src/prompts/*.ts` | Agent prompts (research-agent, code-reviewer, planner) |
|
||||
| `src/templates/claude-md.ts` | CLAUDE.md generation for new cases |
|
||||
| `src/cli.ts` | Command-line interface handlers |
|
||||
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` |
|
||||
| `src/web/schemas.ts` | Zod v4 validation schemas for API request bodies |
|
||||
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows |
|
||||
| `src/types.ts` | All TypeScript interfaces |
|
||||
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` (~90 routes) |
|
||||
| `src/web/schemas.ts` | Zod v4 validation schemas with path/env security allowlists |
|
||||
| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows, mobile support (~16K lines) |
|
||||
| `src/types.ts` | All TypeScript interfaces (~100 types, ~1500 lines) |
|
||||
|
||||
**Large files** (>50KB): `ralph-tracker.ts`, `respawn-controller.ts`, `session.ts`, `subagent-watcher.ts` — these contain complex state machines; read `docs/respawn-state-machine.md` before modifying.
|
||||
**Large files** (>50KB): `app.js`, `ralph-tracker.ts`, `respawn-controller.ts`, `session.ts`, `subagent-watcher.ts` — these contain complex state machines; read `docs/respawn-state-machine.md` before modifying.
|
||||
|
||||
### Config Files (`src/config/`)
|
||||
|
||||
@@ -170,18 +174,89 @@ journalctl --user -u claudeman-web -f
|
||||
|
||||
**Token tracking**: Interactive mode parses status line ("123.4k tokens"), estimates 60/40 input/output split.
|
||||
|
||||
**Hook events**: Claude Code hooks trigger notifications via `/api/hook-event`. Key events: `permission_prompt` (tool approval needed), `elicitation_dialog` (Claude asking question), `idle_prompt` (waiting for input), `stop` (response complete). See `src/hooks-config.ts`.
|
||||
**Hook events**: Claude Code hooks trigger notifications via `/api/hook-event`. Key events: `permission_prompt` (tool approval needed), `elicitation_dialog` (Claude asking question), `idle_prompt` (waiting for input), `stop` (response complete), `teammate_idle` (Agent Teams), `task_completed` (Agent Teams). See `src/hooks-config.ts`.
|
||||
|
||||
**Agent Teams (experimental)**: `TeamWatcher` polls `~/.claude/teams/` for team configs and matches teams to sessions via `leadSessionId`. Teammates are in-process threads (not separate OS processes) and appear as standard subagents. RespawnController checks `TeamWatcher.hasActiveTeammates()` before triggering respawn. Enable via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` env var in `settings.local.json`. See `agent-teams/` for full docs.
|
||||
|
||||
**Circuit breaker**: Prevents respawn thrashing when Claude is stuck. States: `CLOSED` (normal) → `HALF_OPEN` (testing) → `OPEN` (blocked). Tracks consecutive no-progress, same-error-repeated, and tests-failing-too-long. Reset via API at `/api/sessions/:id/ralph-circuit-breaker/reset`.
|
||||
|
||||
**Respawn cycle metrics & health scoring**: `RespawnCycleMetrics` tracks per-cycle outcomes (success, stuck_recovery, blocked, error). `RalphLoopHealthScore` computes 0-100 health with component scores (cycleSuccess, circuitBreaker, iterationProgress, aiChecker, stuckRecovery). Available via respawn status API.
|
||||
|
||||
**Subagent-session correlation**: Session parses Task tool output via `BashToolParser` → `SubagentWatcher` discovers new agent → calls `session.findTaskDescriptionNear()` to match description for window title.
|
||||
|
||||
### Frontend Architecture (`app.js`)
|
||||
|
||||
The frontend is a single 16K-line vanilla JS file with these key systems:
|
||||
|
||||
| System | Key Classes/Functions | Purpose |
|
||||
|--------|----------------------|---------|
|
||||
| **Terminal rendering** | `batchTerminalWrite()`, `flushPendingWrites()`, `chunkedTerminalWrite()` | 60fps batched writes with DEC 2026 sync |
|
||||
| **Local echo overlay** | `LocalEchoOverlay` class | DOM overlay for instant mobile keystroke feedback |
|
||||
| **Mobile support** | `MobileDetection`, `KeyboardHandler`, `SwipeHandler`, `KeyboardAccessoryBar` | Touch input, viewport adaptation, swipe navigation |
|
||||
| **Subagent windows** | `openSubagentWindow()`, `closeSubagentWindow()`, `updateConnectionLines()` | Floating terminal windows with parent connection lines |
|
||||
| **Notifications** | `NotificationManager` class | 4-layer: in-app drawer, tab flash, browser API, audio beep |
|
||||
| **SSE connection** | `connectSSE()`, `addListener()` | EventSource with exponential backoff (1-30s), offline queue (64KB) |
|
||||
| **Settings** | `openAppSettings()`, `apply*Visibility()` | Server-backed + localStorage persistence |
|
||||
| **Focus management** | `FocusTrap` class | Modal keyboard navigation with focus restore |
|
||||
|
||||
**Z-index layers**: subagent windows (1000), plan agents (1100), log viewers (2000), image popups (3000), local echo overlay (7).
|
||||
|
||||
**Built-in respawn presets**: `solo-work` (3s idle, 60min), `subagent-workflow` (45s idle, 240min), `team-lead` (90s idle, 480min), `overnight-autonomous` (10s idle, 480min, full reset).
|
||||
|
||||
**Keyboard shortcuts**: Escape (close panels), Ctrl+? (help), Ctrl+Enter (quick start), Ctrl+W (kill session), Ctrl+Tab (next session), Ctrl+K (kill all), Ctrl+L (clear), Ctrl+Shift+R (restore size), Ctrl/Cmd +/- (font size).
|
||||
|
||||
### Security
|
||||
|
||||
- **HTTP Basic Auth**: Optional via `CLAUDEMAN_USERNAME`/`CLAUDEMAN_PASSWORD` env vars
|
||||
- **CORS**: Restricted to localhost only
|
||||
- **Security headers**: X-Content-Type-Options, X-Frame-Options, CSP; HSTS if HTTPS
|
||||
- **Path validation** (`schemas.ts`): Strict allowlist regex, no shell metacharacters, no traversal, must be absolute
|
||||
- **Env var allowlist**: Only `CLAUDE_CODE_*` prefixes allowed; blocks `PATH`, `LD_PRELOAD`, `NODE_OPTIONS`, `CLAUDEMAN_*` keys
|
||||
- **File streaming TOCTOU protection**: `FileStreamManager` calls `realpathSync()` twice (at validation and before spawn) to catch symlink swaps
|
||||
|
||||
### SSE Event Categories
|
||||
|
||||
~80+ event types broadcast via `broadcast()`. Key categories:
|
||||
|
||||
| Category | Events | Purpose |
|
||||
|----------|--------|---------|
|
||||
| Session | `session:created/updated/deleted/working/idle/exit/error/completion` | Lifecycle |
|
||||
| Terminal | `session:terminal`, `session:clearTerminal`, `session:needsRefresh` | Output streaming |
|
||||
| Respawn | `respawn:stateChanged/cycleStarted/blocked/aiCheck*/planCheck*/timer*` | Respawn state machine |
|
||||
| Subagent | `subagent:discovered/updated/completed/tool_call/progress` | Background agents |
|
||||
| Ralph | `session:ralphLoopUpdate/ralphTodoUpdate/ralphCompletionDetected` | Ralph tracking |
|
||||
| Hooks | `hook:{eventName}` (dynamic) | Claude Code hook events |
|
||||
| Plan | `plan:started/progress/completed/cancelled/subagent` | Plan orchestration |
|
||||
| Mux | `mux:created/killed/died/statsUpdated` | tmux process monitor |
|
||||
| Image | `image:detected` | Screenshot detection |
|
||||
|
||||
### API Route Categories
|
||||
|
||||
~90 routes in `server.ts:buildServer()`. Key groups:
|
||||
|
||||
| Group | Prefix | Count | Key endpoints |
|
||||
|-------|--------|-------|---------------|
|
||||
| Sessions | `/api/sessions` | ~20 | CRUD, input, resize, interactive, shell |
|
||||
| Respawn | `/api/sessions/:id/respawn` | 5 | start, stop, enable, config |
|
||||
| Ralph | `/api/sessions/:id/ralph-*` | 6 | state, status, config, circuit-breaker |
|
||||
| Plan | `/api/sessions/:id/plan/*` | 5 | task CRUD, checkpoint, history, rollback |
|
||||
| Subagents | `/api/subagents` | 7 | list, transcript, kill, cleanup |
|
||||
| Cases | `/api/cases` | 5 | CRUD, link, fix-plan |
|
||||
| Scheduled | `/api/scheduled` | 4 | CRUD for scheduled runs |
|
||||
| System | `/api/status`, `/api/stats`, `/api/config`, `/api/settings` | 8 | App state, config |
|
||||
| Files | `/api/sessions/:id/file*`, `tail-file` | 5 | Browser, preview, raw, tail stream |
|
||||
| Mux | `/api/mux-sessions` | 4 | tmux management, stats |
|
||||
|
||||
## Adding Features
|
||||
|
||||
- **API endpoint**: Types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()`. Validate request bodies with Zod schemas.
|
||||
- **SSE event**: Emit via `broadcast()`, handle in `app.js:handleSSEEvent()`
|
||||
- **API endpoint**: Types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()`. Validate request bodies with Zod schemas in `schemas.ts`.
|
||||
- **SSE event**: Emit via `broadcast()`, handle in `app.js` SSE listener section (search `addListener(`)
|
||||
- **Session setting**: Add to `SessionState` in `types.ts`, include in `session.toState()`, call `persistSessionState()`
|
||||
- **New test**: Pick unique port (see below), add port comment to test file header
|
||||
- **Hook event**: Add to `HookEventType` in `types.ts`, add hook command in `hooks-config.ts:generateHooksConfig()`, update `HookEventSchema` in `schemas.ts`
|
||||
- **Mobile feature**: Add to relevant mobile singleton (`KeyboardHandler`, `KeyboardAccessoryBar`, etc.), test with `MobileDetection.isMobile()` guard
|
||||
- **New test**: Pick unique port (search `const PORT =`), add port comment to test file header. Tests use ports 3150+.
|
||||
|
||||
**Validation**: Uses Zod v4 for request validation. Define schemas near route handlers and use `.parse()` or `.safeParse()`. Note: Zod v4 has different API from v3 (e.g., `z.object()` options changed, error formatting differs).
|
||||
**Validation**: Uses Zod v4 for request validation. Define schemas in `schemas.ts` and use `.parse()` or `.safeParse()`. Note: Zod v4 has different API from v3 (e.g., `z.object()` options changed, error formatting differs).
|
||||
|
||||
## State Files
|
||||
|
||||
@@ -261,7 +336,10 @@ The app must stay fast with 20 sessions and 50 agent windows:
|
||||
- 60fps terminal (16ms batching + `requestAnimationFrame`)
|
||||
- Auto-trimming buffers (2MB terminal max)
|
||||
- Debounced state persistence (500ms)
|
||||
- SSE batching (16ms)
|
||||
- SSE adaptive batching: 16ms (normal), 32ms (moderate), 50ms (rapid); immediate flush at 32KB
|
||||
- SSE backpressure handling: skip writes to backpressured clients, recover via `session:needsRefresh` on drain
|
||||
- Cached endpoints: `/api/sessions` and `/api/status` use 1s TTL caches to avoid expensive serialization
|
||||
- Frontend buffer loads: 128KB chunks via `requestAnimationFrame` to prevent UI jank
|
||||
|
||||
## Terminal Anti-Flicker System
|
||||
|
||||
@@ -319,6 +397,11 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
|
||||
| **Plan orchestrator** | `src/plan-orchestrator.ts` file header |
|
||||
| **Agent prompts** | `src/prompts/` directory |
|
||||
| **Agent Teams (experimental)** | `agent-teams/README.md`, `agent-teams/design.md` |
|
||||
| **Local echo overlay** | `docs/local-echo-overlay-plan.md` |
|
||||
| **Browser testing** | `docs/browser-testing-guide.md` |
|
||||
| **Mobile testing** | `docs/mobile-testing-report.md` |
|
||||
| **Run summary design** | `docs/run-summary-plan.md` |
|
||||
| **Performance audit** | `docs/perf-audit-first-load.md` |
|
||||
|
||||
## Scripts
|
||||
|
||||
@@ -339,6 +422,7 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
|
||||
| `scripts/test-links-browser.mjs` | Browser test for clickable terminal file links |
|
||||
| `scripts/test-patterns.mjs` | Test file path link detection regex patterns |
|
||||
| `scripts/watch-subagents.ts` | Real-time subagent transcript watcher (list, follow by session/agent ID) |
|
||||
| `scripts/capture-readme-screenshots.mjs` | Capture screenshots for README |
|
||||
| `scripts/claudeman-web.service` | systemd service file for production deployment |
|
||||
|
||||
## Memory Leak Prevention
|
||||
@@ -351,9 +435,9 @@ When adding new event listeners or timers:
|
||||
2. Add cleanup to appropriate `stop()` or `cleanup*()` method
|
||||
3. For singleton watchers, store refs in class properties and remove in server `stop()`
|
||||
|
||||
**Backend**: Clear Maps in `stop()`, null promise callbacks on error, remove watcher listeners on shutdown.
|
||||
**Backend**: Clear Maps in `stop()`, null promise callbacks on error, remove watcher listeners on shutdown. Use `CleanupManager` for centralized disposal — supports timers, intervals, watchers, listeners, streams. Guard async callbacks with `if (this.cleanup.isStopped) return`.
|
||||
|
||||
**Frontend**: Store drag/resize handlers on elements, clean up in `close*()` functions. SSE reconnect calls `handleInit()` which resets state.
|
||||
**Frontend**: Store drag/resize handlers on elements, clean up in `close*()` functions. SSE reconnect calls `handleInit()` which resets state. SSE listeners are tracked in an array and removed on reconnect to prevent accumulation.
|
||||
|
||||
Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns.
|
||||
|
||||
@@ -364,3 +448,7 @@ Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns.
|
||||
**Adding a new API endpoint**: Define types in `types.ts`, add route in `server.ts:buildServer()`, broadcast SSE events if needed, handle in `app.js:handleSSEEvent()`.
|
||||
|
||||
**Modifying respawn behavior**: Study `docs/respawn-state-machine.md` first. The state machine is in `respawn-controller.ts`. Use MockSession from `test/respawn-test-utils.ts` for testing.
|
||||
|
||||
**Modifying mobile behavior**: Mobile singletons (`MobileDetection`, `KeyboardHandler`, `SwipeHandler`, `KeyboardAccessoryBar`) all have `init()`/`cleanup()` lifecycle. KeyboardHandler uses `visualViewport` API for iOS keyboard detection (100px threshold for address bar drift). All mobile handlers are re-initialized after SSE reconnect to prevent stale closures.
|
||||
|
||||
**Adding a file watcher**: Use `ImageWatcher` as a template pattern — chokidar with `awaitWriteFinish`, burst throttling (max 20/10s), debouncing (200ms), and auto-ignore of `node_modules/.git/dist/`.
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Codebase Cleanup Findings
|
||||
|
||||
Compiled from parallel analysis of the entire Claudeman codebase by 3 research agents (2026-02-19).
|
||||
|
||||
## P0 — Bug Fix
|
||||
|
||||
### 1. `{WORKING_DIR}` placeholder never replaced in plan-orchestrator.ts
|
||||
- **File:** `src/plan-orchestrator.ts:409`
|
||||
- `RESEARCH_AGENT_PROMPT` has `{WORKING_DIR}` placeholder but only `{TASK}` is replaced
|
||||
- The literal string `{WORKING_DIR}` gets sent to the AI model
|
||||
- **Fix:** Add `.replace('{WORKING_DIR}', this.workingDir)` after the `{TASK}` replacement
|
||||
|
||||
## P1 — Dead Code Removal (High Impact)
|
||||
|
||||
### 2. ~66 dead functions in app.js
|
||||
- Functions never called: `clearAll()`, `toggleSubagentDropdown()`, `goHome()`, `showRalphWizard()`, `minimizeRalphWizard()`, `restoreRalphWizard()`, `ralphWizardNext()`, `ralphWizardBack()`, `skipPlanGeneration()`, `regeneratePlan()`, `incrementTabCount()`, `decrementTabCount()`, `incrementShellCount()`, `decrementShellCount()`, `stopClaude()`, and ~50 more
|
||||
- Many are remnants of abandoned features (Ralph wizard, plan version history)
|
||||
- **Estimated savings:** 300-500 lines
|
||||
|
||||
### 3. ~74 dead CSS selectors in styles.css
|
||||
- Major dead blocks: Task Panel System (`.task-panel`), Process Panel System (`.process-panel`), Monitor Tabs (`.monitor-tabs`), Ralph Metadata (`.ralph-progress-section`, `.ralph-meta`), Plan Editor Toolbar, Plan Version History
|
||||
- Plus ~30 minor unused utility/component selectors
|
||||
- **Estimated savings:** ~400 lines
|
||||
|
||||
### 4. 13 dead type definitions in types.ts (~150 lines)
|
||||
- Dead request interfaces (superseded by Zod schemas): `CreateSessionRequest`, `RunPromptRequest`, `SessionInputRequest`, `ResizeRequest`, `CreateCaseRequest`, `QuickStartRequest`, `CreateScheduledRunRequest`, `QuickRunRequest`, `HookEventRequest`
|
||||
- Other dead types: `TaskAssignment`, `MemoryMetrics`, `RalphStateRecord`
|
||||
- Dead function: `createSuccessResponse` (exported, never imported)
|
||||
- **Estimated savings:** ~150 lines
|
||||
|
||||
### 5. 9 unused constants in map-limits.ts
|
||||
- `MAX_PENDING_HOOKS`, `MAX_SESSION_HISTORY`, `MAX_SSE_CLIENTS_PER_SESSION`, `MAX_TOTAL_SSE_CLIENTS`, `FILE_WATCHER_WARNING_THRESHOLD`, `MAX_QUEUED_TASKS`, `MAX_COMPLETED_TASKS_HISTORY`, `COMPLETED_TODO_TTL_MS`, `MAX_CONCURRENT_SESSIONS`
|
||||
- 9 of 14 exports are dead — only 5 are actually imported
|
||||
|
||||
### 6. Dead `SessionInputSchema` in schemas.ts
|
||||
- `SessionInputSchema` (line 87) is defined/exported but never imported
|
||||
- `SessionInputWithLimitSchema` is the one actually used
|
||||
|
||||
### 7. Dead `code-reviewer.ts` prompt file
|
||||
- `src/prompts/code-reviewer.ts` — entire file is dead, `CODE_REVIEWER_PROMPT` never imported
|
||||
- Re-exported in `src/prompts/index.ts` but no consumer
|
||||
|
||||
### 8. Dead utility exports
|
||||
- **Default exports** (4 files): `lru-map.ts`, `cleanup-manager.ts`, `stale-expiration-map.ts`, `buffer-accumulator.ts` — all have `export default` that's never used
|
||||
- **`stripAnsiSimple`** in `regex-patterns.ts` — exported, never imported (only `stripAnsi` used)
|
||||
- **String similarity**: `isSimilar`, `isSimilarByDistance`, `stringSimilarity`, `levenshteinDistance` — none imported externally
|
||||
- **LRUMap methods**: `oldest()`, `newest()`, `peek()`, `expireOlderThan()`, `valuesInOrder()`, `maxEntries`, `freeSlots` — never called
|
||||
- **StaleExpirationMap methods**: `touch()`, `getAge()`, `getRemainingTtl()`, `peek()` — never called
|
||||
- **CleanupManager methods**: `registerWatcher()`, `registerListener()`, `registerStream()`, `getRegistrations()`, `resourceCounts` — never called
|
||||
|
||||
### 9. Dead backend functions
|
||||
- `resetSessionManager()` in session-manager.ts:300 — never imported
|
||||
- `getStoredTasks()` in task-queue.ts:264 — never called
|
||||
- `start()` in session.ts:1918 — no-op legacy method
|
||||
- Empty `updateStatsFromEvent()` in run-summary.ts:397 — called every event, does nothing
|
||||
|
||||
### 10. Dead TS type exports
|
||||
- `AiCheckerEvents<R>`, `AiIdleCheckerEvents`, `AiPlanCheckerEvents` — never imported
|
||||
- `AiCheckStatus`, `AiPlanCheckStatus` — backwards compat aliases, never imported
|
||||
|
||||
## P2 — Performance & Efficiency
|
||||
|
||||
### 11. task-queue.ts `getCount()` iterates all tasks 5 times
|
||||
- Called every Ralph Loop tick — creates array from Map, then filters 4 times
|
||||
- **Fix:** Single-pass counting like `TaskTracker.getStats()` does
|
||||
|
||||
### 12. transcript-watcher.ts double file read
|
||||
- `readNewEntries()` reads the file twice: once for CRLF detection, once for parsing
|
||||
- `crlfDelay: Infinity` already handles both line endings
|
||||
- **Fix:** Remove the raw buffer CRLF check, read once
|
||||
|
||||
### 13. tmux-manager.ts `saveSessions()` no debounce
|
||||
- Rapid calls can overlap; no in-flight guard unlike `StateStore`
|
||||
- **Fix:** Add debouncing or in-flight tracking
|
||||
|
||||
## P3 — Consolidation & Consistency
|
||||
|
||||
### 14. Duplicate `SAFE_PATH_PATTERN` regex
|
||||
- `schemas.ts:15` and `tmux-manager.ts:81` — identical regex
|
||||
- **Fix:** Share from one location
|
||||
|
||||
### 15. Duplicate `MAX_CONCURRENT_SESSIONS`
|
||||
- `map-limits.ts:57` (dead) vs `server.ts:131` (used, hardcoded)
|
||||
- **Fix:** server.ts should import from map-limits
|
||||
|
||||
### 16. Duplicate cache TTLs in server.ts
|
||||
- `SESSIONS_LIST_CACHE_TTL` and `LIGHT_STATE_CACHE_TTL_MS` — both 1000ms
|
||||
- **Fix:** Consolidate into one constant
|
||||
|
||||
### 17. Inconsistent path import in server.ts
|
||||
- Imports both `path` default and destructured `{ join, dirname, resolve, relative, isAbsolute }`
|
||||
- 3 lines use `path.join()` while everywhere else uses `join()`
|
||||
- **Fix:** Remove default import, use `join()` consistently
|
||||
|
||||
### 18. Re-export indirection for `getAugmentedPath`
|
||||
- `session.ts:89` re-exports from `claude-cli-resolver.ts` for backwards compat
|
||||
- `ai-checker-base.ts` should import directly from source
|
||||
|
||||
### 19. `cliInfoUpdated` event missing from SessionEvents interface
|
||||
- Emitted in `session.ts:1742`, handled in `server.ts:4214`, but not in the interface
|
||||
- Type safety gap — handlers aren't type-checked
|
||||
|
||||
### 20. Array instead of Set for `_childAgentIds` in session.ts
|
||||
- Uses `includes()`/`indexOf()` for lookups (O(n))
|
||||
- Small lists in practice, but Set is more appropriate
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1549",
|
||||
"version": "0.1550",
|
||||
"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",
|
||||
|
||||
+1
-12
@@ -28,7 +28,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { getAugmentedPath } from './session.js';
|
||||
import { getAugmentedPath } from './utils/claude-cli-resolver.js';
|
||||
import { ANSI_ESCAPE_PATTERN_SIMPLE } from './utils/index.js';
|
||||
|
||||
// ========== Security Validation ==========
|
||||
@@ -95,17 +95,6 @@ export interface AiCheckerStateBase<V extends string> {
|
||||
disabledReason: string | null;
|
||||
}
|
||||
|
||||
/** Events emitted by AI checkers */
|
||||
export interface AiCheckerEvents<R> {
|
||||
checkStarted: () => void;
|
||||
checkCompleted: (result: R) => void;
|
||||
checkFailed: (error: string) => void;
|
||||
cooldownStarted: (endsAt: number) => void;
|
||||
cooldownEnded: () => void;
|
||||
disabled: (reason: string) => void;
|
||||
log: (message: string) => void;
|
||||
}
|
||||
|
||||
// ========== Constants ==========
|
||||
|
||||
/** Poll interval for checking temp file completion */
|
||||
|
||||
@@ -29,32 +29,18 @@ import {
|
||||
type AiCheckerConfigBase,
|
||||
type AiCheckerResultBase,
|
||||
type AiCheckerStateBase,
|
||||
type AiCheckerStatus,
|
||||
} from './ai-checker-base.js';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface AiIdleCheckConfig extends AiCheckerConfigBase {}
|
||||
|
||||
// Re-export the status type for backwards compatibility
|
||||
export type AiCheckStatus = AiCheckerStatus;
|
||||
export type AiCheckVerdict = 'IDLE' | 'WORKING' | 'ERROR';
|
||||
|
||||
export interface AiCheckResult extends AiCheckerResultBase<AiCheckVerdict> {}
|
||||
|
||||
export interface AiCheckState extends AiCheckerStateBase<AiCheckVerdict> {}
|
||||
|
||||
/** Events emitted by AiIdleChecker */
|
||||
export interface AiIdleCheckerEvents {
|
||||
checkStarted: () => void;
|
||||
checkCompleted: (result: AiCheckResult) => void;
|
||||
checkFailed: (error: string) => void;
|
||||
cooldownStarted: (endsAt: number) => void;
|
||||
cooldownEnded: () => void;
|
||||
disabled: (reason: string) => void;
|
||||
log: (message: string) => void;
|
||||
}
|
||||
|
||||
// ========== Constants ==========
|
||||
|
||||
const DEFAULT_AI_CHECK_CONFIG: AiIdleCheckConfig = {
|
||||
|
||||
@@ -28,32 +28,18 @@ import {
|
||||
type AiCheckerConfigBase,
|
||||
type AiCheckerResultBase,
|
||||
type AiCheckerStateBase,
|
||||
type AiCheckerStatus,
|
||||
} from './ai-checker-base.js';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface AiPlanCheckConfig extends AiCheckerConfigBase {}
|
||||
|
||||
// Re-export the status type for backwards compatibility
|
||||
export type AiPlanCheckStatus = AiCheckerStatus;
|
||||
export type AiPlanCheckVerdict = 'PLAN_MODE' | 'NOT_PLAN_MODE' | 'ERROR';
|
||||
|
||||
export interface AiPlanCheckResult extends AiCheckerResultBase<AiPlanCheckVerdict> {}
|
||||
|
||||
export interface AiPlanCheckState extends AiCheckerStateBase<AiPlanCheckVerdict> {}
|
||||
|
||||
/** Events emitted by AiPlanChecker */
|
||||
export interface AiPlanCheckerEvents {
|
||||
checkStarted: () => void;
|
||||
checkCompleted: (result: AiPlanCheckResult) => void;
|
||||
checkFailed: (error: string) => void;
|
||||
cooldownStarted: (endsAt: number) => void;
|
||||
cooldownEnded: () => void;
|
||||
disabled: (reason: string) => void;
|
||||
log: (message: string) => void;
|
||||
}
|
||||
|
||||
// ========== Constants ==========
|
||||
|
||||
const DEFAULT_PLAN_CHECK_CONFIG: AiPlanCheckConfig = {
|
||||
|
||||
@@ -36,16 +36,6 @@ export const MAX_SUBAGENT_ACTIVITY_PER_AGENT = 100;
|
||||
*/
|
||||
export const MAX_TOOL_RESULTS_PER_AGENT = 200;
|
||||
|
||||
// ============================================================================
|
||||
// Hook Event Limits
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Maximum pending hook events to queue.
|
||||
* Prevents unbounded growth if hook processing is slow.
|
||||
*/
|
||||
export const MAX_PENDING_HOOKS = 50;
|
||||
|
||||
// ============================================================================
|
||||
// Session Tracking Limits
|
||||
// ============================================================================
|
||||
@@ -56,56 +46,6 @@ export const MAX_PENDING_HOOKS = 50;
|
||||
*/
|
||||
export const MAX_CONCURRENT_SESSIONS = 50;
|
||||
|
||||
/**
|
||||
* Maximum session history entries to keep (for analytics).
|
||||
*/
|
||||
export const MAX_SESSION_HISTORY = 100;
|
||||
|
||||
// ============================================================================
|
||||
// SSE Client Limits
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Maximum SSE clients per session.
|
||||
* Prevents resource exhaustion from many browser tabs.
|
||||
*/
|
||||
export const MAX_SSE_CLIENTS_PER_SESSION = 10;
|
||||
|
||||
/**
|
||||
* Maximum total SSE clients across all sessions.
|
||||
*/
|
||||
export const MAX_TOTAL_SSE_CLIENTS = 100;
|
||||
|
||||
// ============================================================================
|
||||
// File Watcher Limits
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Maximum file watchers (FSWatcher) to allow.
|
||||
* Linux default max_user_watches is 8192-65536.
|
||||
* We warn at 80% capacity and evict idle watchers.
|
||||
*/
|
||||
export const MAX_FILE_WATCHERS = 500;
|
||||
|
||||
/**
|
||||
* Warning threshold as percentage of max watchers.
|
||||
*/
|
||||
export const FILE_WATCHER_WARNING_THRESHOLD = 0.8;
|
||||
|
||||
// ============================================================================
|
||||
// Task Tracking Limits
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Maximum tasks to keep in the task queue.
|
||||
*/
|
||||
export const MAX_QUEUED_TASKS = 100;
|
||||
|
||||
/**
|
||||
* Maximum completed tasks to keep for history.
|
||||
*/
|
||||
export const MAX_COMPLETED_TASKS_HISTORY = 50;
|
||||
|
||||
// ============================================================================
|
||||
// Todo Item Limits (Ralph Tracker)
|
||||
// ============================================================================
|
||||
|
||||
@@ -406,7 +406,7 @@ export class PlanOrchestrator {
|
||||
|
||||
this.runningSessions.add(session);
|
||||
|
||||
const prompt = RESEARCH_AGENT_PROMPT.replace('{TASK}', taskDescription);
|
||||
const prompt = RESEARCH_AGENT_PROMPT.replace('{TASK}', taskDescription).replace('{WORKING_DIR}', this.workingDir);
|
||||
|
||||
// Start progress interval before try block to ensure cleanup in finally
|
||||
const progressInterval = setInterval(() => {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Code Reviewer Prompt
|
||||
*
|
||||
* Designs post-implementation review tasks.
|
||||
* Reserved for future use.
|
||||
*
|
||||
* Placeholders: {TASK}
|
||||
*/
|
||||
|
||||
export const CODE_REVIEWER_PROMPT = `You are a Code Review Specialist designing post-implementation review tasks.
|
||||
|
||||
## YOUR TASK
|
||||
Design code review steps for implementations in this task:
|
||||
|
||||
## TASK DESCRIPTION
|
||||
{TASK}
|
||||
|
||||
## INSTRUCTIONS
|
||||
For each implementation identified, create a review task that checks:
|
||||
1. **Best Practices**: Language-specific conventions and idioms
|
||||
2. **Security**: OWASP top 10, input validation, authentication
|
||||
3. **Performance**: Time complexity, memory usage, N+1 queries
|
||||
4. **Error Handling**: Edge cases covered, meaningful error messages
|
||||
5. **Code Quality**: DRY, SOLID principles, readability
|
||||
6. **Type Safety**: Proper typing, no implicit any, null checks
|
||||
|
||||
## REVIEW TASK GUIDELINES
|
||||
- Review tasks run AFTER implementation, BEFORE merge
|
||||
- Each review should be specific and actionable
|
||||
- Include what to look for and how to verify
|
||||
- Reference language-specific linting tools where applicable
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return ONLY a JSON array:
|
||||
[
|
||||
{
|
||||
"category": "security|performance|quality|error-handling|best-practices|type-safety",
|
||||
"content": "Review authentication handler for XSS vulnerabilities",
|
||||
"rationale": "User input flows through auth - must sanitize",
|
||||
"verificationCriteria": "No unescaped user input, all inputs validated",
|
||||
"reviewChecklist": ["Check input sanitization", "Verify CSRF tokens", "Review session handling"],
|
||||
"implToReview": "Implement authentication handler"
|
||||
}
|
||||
]
|
||||
|
||||
Generate 5-10 review tasks. Code review catches bugs that tests miss.`;
|
||||
@@ -7,4 +7,3 @@
|
||||
|
||||
export { RESEARCH_AGENT_PROMPT } from './research-agent.js';
|
||||
export { PLANNER_PROMPT } from './planner.js';
|
||||
export { CODE_REVIEWER_PROMPT } from './code-reviewer.js';
|
||||
|
||||
@@ -112,9 +112,6 @@ export class RunSummaryTracker {
|
||||
this.events.push(event);
|
||||
this.lastUpdatedAt = event.timestamp;
|
||||
|
||||
// Update stats based on event type
|
||||
this.updateStatsFromEvent(event);
|
||||
|
||||
// Trim if needed
|
||||
if (this.events.length > MAX_EVENTS) {
|
||||
this.events = this.events.slice(-TRIM_TO_EVENTS);
|
||||
@@ -394,11 +391,6 @@ export class RunSummaryTracker {
|
||||
|
||||
// ========== Private Methods ==========
|
||||
|
||||
private updateStatsFromEvent(_event: RunSummaryEvent): void {
|
||||
// Most stats are updated in the specific record* methods
|
||||
// This is for any additional cross-cutting concerns
|
||||
}
|
||||
|
||||
private startStateStuckDetection(): void {
|
||||
this.stateStuckCheckTimer = setInterval(() => {
|
||||
if (!this.currentState || !this.stateEnteredAt || this.stateStuckWarned) {
|
||||
|
||||
@@ -293,13 +293,3 @@ export function getSessionManager(): SessionManager {
|
||||
return managerInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the singleton SessionManager instance.
|
||||
* Primarily used for testing to ensure test isolation.
|
||||
*/
|
||||
export async function resetSessionManager(): Promise<void> {
|
||||
if (managerInstance) {
|
||||
await managerInstance.stopAllSessions();
|
||||
managerInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -85,8 +85,6 @@ const NEWLINE_SPLIT_PATTERN = /\r?\n/;
|
||||
|
||||
// Claude CLI PATH resolution — shared utility
|
||||
import { getAugmentedPath } from './utils/claude-cli-resolver.js';
|
||||
// Re-export for backward compatibility (ai-checker-base imports from session)
|
||||
export { getAugmentedPath } from './utils/claude-cli-resolver.js';
|
||||
|
||||
/**
|
||||
* Wraps a promise with a timeout to prevent indefinite hangs.
|
||||
@@ -193,6 +191,8 @@ export interface SessionEvents {
|
||||
bashToolEnd: (tool: ActiveBashTool) => void;
|
||||
/** Active Bash tools list updated */
|
||||
bashToolsUpdate: (tools: ActiveBashTool[]) => void;
|
||||
/** CLI info (version, model, account) updated */
|
||||
cliInfoUpdated: (info: { version: string | null; model: string | null; accountType: string | null; latestVersion: string | null }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-14
@@ -13,7 +13,6 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Task, CreateTaskOptions } from './task.js';
|
||||
import { getStore } from './state-store.js';
|
||||
import { TaskState } from './types.js';
|
||||
|
||||
/**
|
||||
* Events emitted by TaskQueue
|
||||
@@ -214,16 +213,16 @@ export class TaskQueue extends EventEmitter {
|
||||
) || null;
|
||||
}
|
||||
|
||||
/** Gets counts of tasks by status. */
|
||||
/** Gets counts of tasks by status (single-pass). */
|
||||
getCount(): { total: number; pending: number; running: number; completed: number; failed: number } {
|
||||
const tasks = this.getAllTasks();
|
||||
return {
|
||||
total: tasks.length,
|
||||
pending: tasks.filter((t) => t.isPending()).length,
|
||||
running: tasks.filter((t) => t.isRunning()).length,
|
||||
completed: tasks.filter((t) => t.isCompleted()).length,
|
||||
failed: tasks.filter((t) => t.isFailed()).length,
|
||||
};
|
||||
let pending = 0, running = 0, completed = 0, failed = 0;
|
||||
for (const task of this.tasks.values()) {
|
||||
if (task.isPending()) pending++;
|
||||
else if (task.isRunning()) running++;
|
||||
else if (task.isCompleted()) completed++;
|
||||
else if (task.isFailed()) failed++;
|
||||
}
|
||||
return { total: this.tasks.size, pending, running, completed, failed };
|
||||
}
|
||||
|
||||
/** Removes all completed tasks. Returns count removed. */
|
||||
@@ -260,10 +259,6 @@ export class TaskQueue extends EventEmitter {
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Gets tasks from persistent storage. */
|
||||
getStoredTasks(): Record<string, TaskState> {
|
||||
return this.store.getTasks();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
+23
-14
@@ -32,6 +32,7 @@ import { dirname, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { ProcessStats, PersistedRespawnConfig, getErrorMessage, NiceConfig, DEFAULT_NICE_CONFIG, type PaneInfo } from './types.js';
|
||||
import { wrapWithNice } from './utils/nice-wrapper.js';
|
||||
import { SAFE_PATH_PATTERN } from './utils/regex-patterns.js';
|
||||
import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats } from './mux-interface.js';
|
||||
|
||||
// Claude CLI PATH resolution — shared utility
|
||||
@@ -77,9 +78,6 @@ const MUX_SESSIONS_FILE = join(homedir(), '.claudeman', 'mux-sessions.json');
|
||||
/** Regex to validate tmux session names (only allow safe characters) */
|
||||
const SAFE_MUX_NAME_PATTERN = /^claudeman-[a-f0-9-]+$/;
|
||||
|
||||
/** Regex to validate working directory paths (no shell metacharacters) */
|
||||
const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/;
|
||||
|
||||
/** Regex to validate tmux pane targets (e.g., "%0", "%1", "0", "1") */
|
||||
const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/;
|
||||
|
||||
@@ -795,7 +793,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
/**
|
||||
* Start periodic mouse mode sync for all tracked sessions.
|
||||
* Polls pane counts every 5s and toggles mouse on/off as needed.
|
||||
* Only calls tmux set-option when the pane count actually changes.
|
||||
* Polls every 5s. On pane count change, toggles mouse on (>1 pane) or off (1 pane).
|
||||
* If enableMouseMode/disableMouseMode fails, lastPaneCount is NOT updated so it retries next poll.
|
||||
*/
|
||||
startMouseModeSync(intervalMs: number = 5000): void {
|
||||
if (this.mouseSyncInterval) {
|
||||
@@ -804,18 +803,24 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
this.mouseSyncInterval = setInterval(() => {
|
||||
if (IS_TEST_MODE) return;
|
||||
|
||||
for (const session of this.sessions.values()) {
|
||||
const panes = this.listPanes(session.muxName);
|
||||
const count = panes.length;
|
||||
const prev = this.lastPaneCount.get(session.muxName);
|
||||
if (count === 0) continue;
|
||||
|
||||
// Only toggle when pane count crosses the 1↔N boundary
|
||||
if (prev !== count && count > 0) {
|
||||
this.lastPaneCount.set(session.muxName, count);
|
||||
if (count > 1) {
|
||||
this.enableMouseMode(session.muxName);
|
||||
} else {
|
||||
this.disableMouseMode(session.muxName);
|
||||
const prev = this.lastPaneCount.get(session.muxName);
|
||||
if (prev === count) continue;
|
||||
|
||||
// Pane count changed — toggle mouse mode
|
||||
if (count > 1) {
|
||||
if (this.enableMouseMode(session.muxName)) {
|
||||
this.lastPaneCount.set(session.muxName, count);
|
||||
}
|
||||
// If enableMouseMode fails, DON'T update lastPaneCount — retry next poll
|
||||
} else {
|
||||
if (this.disableMouseMode(session.muxName)) {
|
||||
this.lastPaneCount.set(session.muxName, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -950,8 +955,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
`tmux set-option -t "${muxName}" mouse on`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
console.log(`[TmuxManager] Mouse mode ON for ${muxName}`);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error(`[TmuxManager] Failed to enable mouse mode for ${muxName}:`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -972,8 +979,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
`tmux set-option -t "${muxName}" mouse off`,
|
||||
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
|
||||
);
|
||||
console.log(`[TmuxManager] Mouse mode OFF for ${muxName}`);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error(`[TmuxManager] Failed to disable mouse mode for ${muxName}:`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+30
-43
@@ -291,54 +291,41 @@ export class TranscriptWatcher extends EventEmitter {
|
||||
}
|
||||
|
||||
const entries: TranscriptEntry[] = [];
|
||||
// Read raw buffer to detect actual line endings (LF vs CRLF)
|
||||
const transcriptPath = this.transcriptPath;
|
||||
let rawChunks: Buffer[] = [];
|
||||
const rawStream = createReadStream(transcriptPath, {
|
||||
|
||||
const stream = createReadStream(transcriptPath, {
|
||||
start: this.filePosition,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
rawStream.on('data', (chunk: Buffer | string) => rawChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||
rawStream.on('end', () => {
|
||||
// Detect if file uses CRLF
|
||||
const raw = Buffer.concat(rawChunks);
|
||||
rawChunks = []; // Free memory
|
||||
const hasCRLF = raw.includes(0x0d); // 0x0d = \r
|
||||
const lineEndingSize = hasCRLF ? 2 : 1;
|
||||
|
||||
const stream = createReadStream(transcriptPath, {
|
||||
start: this.filePosition,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
const rl = createInterface({
|
||||
input: stream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let bytesRead = this.filePosition;
|
||||
|
||||
rl.on('line', (line) => {
|
||||
bytesRead += Buffer.byteLength(line, 'utf-8') + lineEndingSize;
|
||||
|
||||
if (!line.trim()) return;
|
||||
|
||||
try {
|
||||
const entry = JSON.parse(line) as TranscriptEntry;
|
||||
entries.push(entry);
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
}
|
||||
});
|
||||
|
||||
rl.on('close', () => {
|
||||
this.filePosition = bytesRead;
|
||||
resolve(entries);
|
||||
});
|
||||
|
||||
rl.on('error', reject);
|
||||
stream.on('error', reject);
|
||||
const rl = createInterface({
|
||||
input: stream,
|
||||
crlfDelay: Infinity, // Handles both LF and CRLF
|
||||
});
|
||||
rawStream.on('error', reject);
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (!line.trim()) return;
|
||||
|
||||
try {
|
||||
const entry = JSON.parse(line) as TranscriptEntry;
|
||||
entries.push(entry);
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
}
|
||||
});
|
||||
|
||||
rl.on('close', () => {
|
||||
// Update position to current file size (accounts for any line ending style)
|
||||
try {
|
||||
this.filePosition = statSync(transcriptPath).size;
|
||||
} catch {
|
||||
// File may have been deleted between read and stat
|
||||
}
|
||||
resolve(entries);
|
||||
});
|
||||
|
||||
rl.on('error', reject);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
-145
@@ -37,29 +37,6 @@ export interface BufferConfig {
|
||||
onTrim?: (trimmedBytes: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory metrics for monitoring and debugging.
|
||||
* Extends Node.js process.memoryUsage() with application-specific tracking.
|
||||
*/
|
||||
export interface MemoryMetrics {
|
||||
/** Heap memory used by V8 (bytes) */
|
||||
heapUsed: number;
|
||||
/** Total heap size allocated by V8 (bytes) */
|
||||
heapTotal: number;
|
||||
/** Memory used by C++ objects bound to JavaScript (bytes) */
|
||||
external: number;
|
||||
/** Memory used by ArrayBuffers and SharedArrayBuffers (bytes) */
|
||||
arrayBuffers: number;
|
||||
/** Sizes of tracked Maps by name */
|
||||
mapSizes: Record<string, number>;
|
||||
/** Number of active timers (setTimeout/setInterval) */
|
||||
timerCount: number;
|
||||
/** Number of active file system watchers */
|
||||
watcherCount: number;
|
||||
/** Timestamp when metrics were collected */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource types that can be registered for cleanup.
|
||||
*/
|
||||
@@ -621,18 +598,6 @@ export interface SessionOutput {
|
||||
exitCode: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task assignment record
|
||||
*/
|
||||
export interface TaskAssignment {
|
||||
/** Session ID that task is assigned to */
|
||||
sessionId: string;
|
||||
/** Task ID being assigned */
|
||||
taskId: string;
|
||||
/** Timestamp of assignment */
|
||||
assignedAt: number;
|
||||
}
|
||||
|
||||
// ========== API Error Handling ==========
|
||||
|
||||
/**
|
||||
@@ -665,101 +630,11 @@ export const ErrorMessages: Record<ApiErrorCode, string> = {
|
||||
[ApiErrorCode.INTERNAL_ERROR]: 'An internal error occurred',
|
||||
};
|
||||
|
||||
// ========== API Request Types ==========
|
||||
|
||||
/**
|
||||
* Request to create a new session
|
||||
*/
|
||||
export interface CreateSessionRequest {
|
||||
/** Optional working directory path */
|
||||
workingDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to run a prompt in a session
|
||||
*/
|
||||
export interface RunPromptRequest {
|
||||
/** Prompt to send to Claude */
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to send input to a session
|
||||
*/
|
||||
export interface SessionInputRequest {
|
||||
/** Input string to send */
|
||||
input: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to resize terminal
|
||||
*/
|
||||
export interface ResizeRequest {
|
||||
/** Number of columns */
|
||||
cols: number;
|
||||
/** Number of rows */
|
||||
rows: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to create a new case
|
||||
*/
|
||||
export interface CreateCaseRequest {
|
||||
/** Case name (alphanumeric with hyphens/underscores) */
|
||||
name: string;
|
||||
/** Optional case description */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for quick start (create case + session)
|
||||
*/
|
||||
export interface QuickStartRequest {
|
||||
/** Optional case name, defaults to 'testcase' */
|
||||
caseName?: string;
|
||||
/** Session mode: 'claude' for Claude CLI, 'shell' for bash shell */
|
||||
mode?: 'claude' | 'shell';
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to create a scheduled run
|
||||
*/
|
||||
export interface CreateScheduledRunRequest {
|
||||
/** Prompt to run */
|
||||
prompt: string;
|
||||
/** Optional working directory */
|
||||
workingDir?: string;
|
||||
/** Duration in minutes */
|
||||
durationMinutes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for quick run (one-shot prompt execution)
|
||||
*/
|
||||
export interface QuickRunRequest {
|
||||
/** Prompt to run */
|
||||
prompt: string;
|
||||
/** Optional working directory */
|
||||
workingDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook event types triggered by Claude Code's hooks system
|
||||
*/
|
||||
export type HookEventType = 'idle_prompt' | 'permission_prompt' | 'elicitation_dialog' | 'stop' | 'teammate_idle' | 'task_completed';
|
||||
|
||||
/**
|
||||
* Request body for the hook-event API endpoint
|
||||
*/
|
||||
export interface HookEventRequest {
|
||||
/** Type of hook event that fired */
|
||||
event: HookEventType;
|
||||
/** Session ID from CLAUDEMAN_SESSION_ID env var */
|
||||
sessionId: string;
|
||||
/** Additional event data (tool name, command, question, etc.) */
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ========== API Response Types ==========
|
||||
|
||||
/**
|
||||
@@ -784,19 +659,6 @@ export function createErrorResponse(code: ApiErrorCode, details?: string): ApiRe
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized success response
|
||||
* @template T Type of the data payload
|
||||
* @param data Optional response data
|
||||
* @returns Formatted success response
|
||||
*/
|
||||
export function createSuccessResponse<T>(data?: T): ApiResponse<T> {
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for session operations
|
||||
*/
|
||||
@@ -1074,13 +936,6 @@ export interface RalphSessionState {
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of session ID to inner state
|
||||
*/
|
||||
export interface RalphStateRecord {
|
||||
[sessionId: string]: RalphSessionState;
|
||||
}
|
||||
|
||||
// ========== RALPH_STATUS Block Types ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -193,5 +193,3 @@ export class BufferAccumulator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BufferAccumulator;
|
||||
|
||||
@@ -326,5 +326,3 @@ export class CleanupManager implements Disposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CleanupManager;
|
||||
|
||||
@@ -17,7 +17,6 @@ export {
|
||||
createAnsiPatternFull,
|
||||
createAnsiPatternSimple,
|
||||
stripAnsi,
|
||||
stripAnsiSimple,
|
||||
} from './regex-patterns.js';
|
||||
export {
|
||||
MAX_SESSION_TOKENS,
|
||||
@@ -25,10 +24,7 @@ export {
|
||||
validateTokensAndCost,
|
||||
} from './token-validation.js';
|
||||
export {
|
||||
levenshteinDistance,
|
||||
stringSimilarity,
|
||||
isSimilar,
|
||||
isSimilarByDistance,
|
||||
normalizePhrase,
|
||||
fuzzyPhraseMatch,
|
||||
todoContentHash,
|
||||
|
||||
@@ -261,5 +261,3 @@ export class LRUMap<K, V> extends Map<K, V> {
|
||||
return Math.max(0, this.maxSize - super.size);
|
||||
}
|
||||
}
|
||||
|
||||
export default LRUMap;
|
||||
|
||||
@@ -64,10 +64,8 @@ export function stripAnsi(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips simple ANSI CSI codes from text (faster, less comprehensive).
|
||||
* @param text - Text containing ANSI escape codes
|
||||
* @returns Text with ANSI CSI codes removed
|
||||
* Regex to validate safe file paths (no shell metacharacters).
|
||||
* Shared by schemas.ts and tmux-manager.ts for consistent path validation.
|
||||
*/
|
||||
export function stripAnsiSimple(text: string): string {
|
||||
return text.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '');
|
||||
}
|
||||
export const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/;
|
||||
|
||||
|
||||
@@ -334,5 +334,3 @@ export class StaleExpirationMap<K, V> implements Disposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StaleExpirationMap;
|
||||
|
||||
@@ -4384,11 +4384,6 @@ class ClaudemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy toggle for backwards compat
|
||||
toggleSubagentDropdown(badgeEl) {
|
||||
this.pinSubagentDropdown(badgeEl);
|
||||
}
|
||||
|
||||
// Permanently close a minimized subagent (remove from DOM and minimized set)
|
||||
permanentlyCloseMinimizedSubagent(agentId, sessionId) {
|
||||
// Remove from minimized set
|
||||
@@ -7307,27 +7302,6 @@ class ClaudemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
skipPlanGeneration() {
|
||||
// Stop any running timers
|
||||
if (this.planLoadingTimer) {
|
||||
clearInterval(this.planLoadingTimer);
|
||||
this.planLoadingTimer = null;
|
||||
}
|
||||
if (this.planPhaseTimer) {
|
||||
clearInterval(this.planPhaseTimer);
|
||||
this.planPhaseTimer = null;
|
||||
}
|
||||
|
||||
this.ralphWizardConfig.skipPlanGeneration = true;
|
||||
this.ralphWizardConfig.planGenerated = false;
|
||||
this.ralphWizardConfig.generatedPlan = null;
|
||||
|
||||
// Generate preview and go to step 3
|
||||
this.updateRalphPromptPreview();
|
||||
this.ralphWizardStep = 3;
|
||||
this.updateRalphWizardUI();
|
||||
}
|
||||
|
||||
regeneratePlan() {
|
||||
this.ralphWizardConfig.generatedPlan = null;
|
||||
this.ralphWizardConfig.planGenerated = false;
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
/* Utility classes */
|
||||
.mt-sm { margin-top: 0.5rem; }
|
||||
|
||||
/* ========== Accessibility: Focus Styles ========== */
|
||||
|
||||
/* Skip link for keyboard users */
|
||||
@@ -1319,22 +1316,7 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Legacy classes for backward compatibility */
|
||||
.ralph-progress-section {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Large Progress Ring */
|
||||
.ralph-ring {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ralph-ring-svg-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1378,66 +1360,6 @@ body {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
/* Meta Info */
|
||||
.ralph-meta {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ralph-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.ralph-meta-label {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ralph-meta code {
|
||||
background: var(--bg-input);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ralph-meta-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.ralph-meta-item span:not(.ralph-meta-label) {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Task Cards Section */
|
||||
.ralph-tasks-section {
|
||||
background: var(--bg-card);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ralph-tasks-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-input);
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ralph-tasks-count {
|
||||
color: var(--text-dim);
|
||||
font-weight: 400;
|
||||
@@ -1886,13 +1808,6 @@ body {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--border);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.btn-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2339,15 +2254,6 @@ body {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.btn-danger-sm {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.btn-danger-sm:hover {
|
||||
color: #fff;
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 1rem;
|
||||
@@ -2360,15 +2266,6 @@ body {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
@@ -2403,17 +2300,6 @@ body {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.form-row-inline {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row-inline input[type="text"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-col {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -2447,43 +2333,6 @@ body {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.checkbox-label:hover {
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.checkbox-label span:first-of-type {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.checkbox-hint {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
@@ -2667,10 +2516,6 @@ body {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.settings-item-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.settings-item-label {
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
@@ -2726,26 +2571,6 @@ body {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* CPU limit status indicator */
|
||||
.cpu-limit-status {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-dim);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.cpu-limit-status.available {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.cpu-limit-status.unavailable {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* Event Type Notification Grid */
|
||||
.event-type-grid {
|
||||
display: grid;
|
||||
@@ -2824,24 +2649,6 @@ body {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.form-actions-stack {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.form-actions-stack button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--yellow);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d4a107;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
@@ -3251,13 +3058,6 @@ body {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.modal-footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.auto-refresh-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3297,32 +3097,6 @@ body {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
}
|
||||
|
||||
#runSummaryModal .modal-footer-actions {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#runSummaryModal .modal-footer-actions .btn-toolbar {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Tab summary icon */
|
||||
.tab-summary {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: opacity 0.15s;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.session-tab:hover .tab-summary {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.session-tab:hover .tab-summary:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-warning-text {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.85rem;
|
||||
@@ -3399,35 +3173,12 @@ body {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.session-dir-display {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.8rem;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Session respawn section in options modal */
|
||||
.session-respawn-section {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.session-respawn-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3459,11 +3210,6 @@ body {
|
||||
}
|
||||
|
||||
/* Inline checkbox group */
|
||||
.checkbox-group-inline {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3481,13 +3227,6 @@ body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Auto-clear row */
|
||||
.auto-clear-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.input-suffix-sm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3583,11 +3322,6 @@ body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.session-respawn-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Modal Tabs */
|
||||
.modal-tabs {
|
||||
display: flex;
|
||||
@@ -4349,59 +4083,6 @@ kbd {
|
||||
transition: opacity 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
/* Task Panel */
|
||||
.task-panel {
|
||||
position: fixed;
|
||||
bottom: var(--toolbar-height);
|
||||
right: 0.5rem;
|
||||
width: 350px;
|
||||
max-width: calc(100vw - 1rem);
|
||||
/* Performance: GPU-accelerated transforms and isolated paint */
|
||||
contain: layout style;
|
||||
will-change: transform;
|
||||
max-height: 50vh;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
|
||||
z-index: 100;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-panel.open {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.task-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.task-panel-stats {
|
||||
margin-left: auto;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--bg-input);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.task-panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.task-empty {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
@@ -4522,53 +4203,6 @@ kbd {
|
||||
background: var(--border-light);
|
||||
}
|
||||
|
||||
/* Process Monitor Panel */
|
||||
.process-panel {
|
||||
position: fixed;
|
||||
bottom: var(--toolbar-height);
|
||||
left: 0.5rem;
|
||||
width: 400px;
|
||||
max-width: calc(100vw - 1rem);
|
||||
max-height: 50vh;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
|
||||
z-index: 100;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.process-panel.open {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.process-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.process-panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.process-panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.process-empty {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
@@ -4714,55 +4348,6 @@ kbd {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.monitor-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.monitor-tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.monitor-tab:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.monitor-tab.active {
|
||||
color: white;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.monitor-tab-badge {
|
||||
display: inline-block;
|
||||
background: var(--success);
|
||||
color: white;
|
||||
font-size: 0.6rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 8px;
|
||||
margin-left: 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.monitor-tab-badge:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monitor-tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monitor-tab-content.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monitor-panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -4864,47 +4449,6 @@ kbd {
|
||||
|
||||
/* ========== Subagent Panel (Claude Code Background Agents) ========== */
|
||||
|
||||
/* Toolbar button for subagents tab */
|
||||
.toolbar-btn-subagents {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.toolbar-btn-subagents:hover {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar-btn-subagents.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toolbar-badge {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 8px;
|
||||
min-width: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toolbar-badge:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ========== Separate Subagents Panel ========== */
|
||||
.subagents-panel {
|
||||
position: fixed;
|
||||
@@ -6312,25 +5856,6 @@ kbd {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Custom tooltip for file links */
|
||||
.file-link-tooltip {
|
||||
position: fixed;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.file-link-tooltip::before {
|
||||
content: "📄 Click to stream: ";
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ========== Ralph Loop Wizard ========== */
|
||||
|
||||
/* Wizard Progress Indicator */
|
||||
@@ -6767,28 +6292,11 @@ kbd {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.plan-skip-btn {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.plan-skip-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.plan-error-msg {
|
||||
color: var(--red);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Plan Options (detail level, model) */
|
||||
.plan-options {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.plan-option-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -6987,10 +6495,6 @@ kbd {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.plan-actions-single {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plan-skip-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
@@ -7446,11 +6950,6 @@ kbd {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mode-changed {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Plan warnings and gaps display */
|
||||
#planWarnings {
|
||||
margin-bottom: 0.75rem;
|
||||
@@ -7762,55 +7261,6 @@ kbd {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Verification criteria tooltip expansion */
|
||||
.plan-item-verify-expanded {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(100% + 4px);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
z-index: 10;
|
||||
box-shadow: 0 -2px 8px rgba(0,0,0,0.2);
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.plan-item-verify-expanded::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--border);
|
||||
}
|
||||
|
||||
/* Plan toolbar improvements */
|
||||
.plan-editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.plan-toolbar-left {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.plan-toolbar-right {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Plan item inline actions */
|
||||
.plan-item-actions {
|
||||
display: flex;
|
||||
@@ -7909,36 +7359,6 @@ kbd {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Plan version indicator */
|
||||
.plan-version-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--bg-input);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.plan-version-badge {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.plan-version-history {
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.plan-version-history:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Plan history dropdown */
|
||||
.plan-history-dropdown {
|
||||
position: absolute;
|
||||
@@ -8061,27 +7481,6 @@ kbd {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Phrase Input Group */
|
||||
.phrase-input-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.phrase-input-group input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.phrase-input-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Iteration Presets */
|
||||
.iteration-presets {
|
||||
display: flex;
|
||||
|
||||
+1
-12
@@ -8,12 +8,10 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { SAFE_PATH_PATTERN } from '../utils/regex-patterns.js';
|
||||
|
||||
// ========== Path Validation ==========
|
||||
|
||||
/** Regex to validate working directory paths (no shell metacharacters) — matches tmux-manager.ts */
|
||||
const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/;
|
||||
|
||||
/** Validate a path string: no shell metacharacters, no traversal, must be absolute */
|
||||
export function isValidWorkingDir(p: string): boolean {
|
||||
if (!p || !p.startsWith('/')) return false;
|
||||
@@ -80,15 +78,6 @@ export const RunPromptSchema = z.object({
|
||||
prompt: z.string().min(1).max(100000),
|
||||
});
|
||||
|
||||
/**
|
||||
* Schema for POST /api/sessions/:id/input
|
||||
* Sends input to an interactive session.
|
||||
*/
|
||||
export const SessionInputSchema = z.object({
|
||||
input: z.string(),
|
||||
useMux: z.boolean().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Schema for POST /api/sessions/:id/resize
|
||||
* Resizes a session's terminal.
|
||||
|
||||
+11
-11
@@ -13,7 +13,7 @@
|
||||
import Fastify, { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import fastifyCompress from '@fastify/compress';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import path, { join, dirname, resolve, relative, isAbsolute } from 'node:path';
|
||||
import { join, dirname, resolve, relative, isAbsolute } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync, statSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
RespawnEnableSchema,
|
||||
} from './schemas.js';
|
||||
import { StaleExpirationMap } from '../utils/index.js';
|
||||
import { MAX_CONCURRENT_SESSIONS } from '../config/map-limits.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -127,8 +128,6 @@ const SESSIONS_LIST_CACHE_TTL = 1000;
|
||||
const SCHEDULED_CLEANUP_INTERVAL = 5 * 60 * 1000;
|
||||
// Completed scheduled runs max age (1 hour)
|
||||
const SCHEDULED_RUN_MAX_AGE = 60 * 60 * 1000;
|
||||
// Maximum concurrent sessions to prevent resource exhaustion
|
||||
const MAX_CONCURRENT_SESSIONS = 50;
|
||||
// SSE client health check interval (every 30 seconds)
|
||||
const SSE_HEALTH_CHECK_INTERVAL = 30 * 1000;
|
||||
// Maximum allowed input length for session write (64KB)
|
||||
@@ -1461,7 +1460,7 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
const content = session.ralphTracker.generateFixPlanMarkdown();
|
||||
const filePath = path.join(workingDir, '@fix_plan.md');
|
||||
const filePath = join(workingDir, '@fix_plan.md');
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
@@ -1491,7 +1490,7 @@ export class WebServer extends EventEmitter {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Session has no working directory');
|
||||
}
|
||||
|
||||
const filePath = path.join(workingDir, '@fix_plan.md');
|
||||
const filePath = join(workingDir, '@fix_plan.md');
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
@@ -1534,7 +1533,7 @@ export class WebServer extends EventEmitter {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Session has no working directory');
|
||||
}
|
||||
|
||||
const filePath = path.join(workingDir, '@ralph_prompt.md');
|
||||
const filePath = join(workingDir, '@ralph_prompt.md');
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
@@ -5335,12 +5334,13 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
// Start stats collection for mux sessions
|
||||
this.mux.startStatsCollection(STATS_COLLECTION_INTERVAL_MS);
|
||||
}
|
||||
|
||||
// Start mouse mode sync (tmux only) — toggles mouse on/off based on pane count.
|
||||
// Mouse off = native xterm.js selection; mouse on = tmux pane clicking (split layouts).
|
||||
if ('startMouseModeSync' in this.mux) {
|
||||
(this.mux as { startMouseModeSync: (ms?: number) => void }).startMouseModeSync();
|
||||
}
|
||||
// Start mouse mode sync (tmux only) — toggles mouse on/off based on pane count.
|
||||
// Mouse off = native xterm.js selection; mouse on = tmux pane clicking (split layouts).
|
||||
// Always start, even with no sessions — new sessions may be created later.
|
||||
if ('startMouseModeSync' in this.mux) {
|
||||
(this.mux as { startMouseModeSync: (ms?: number) => void }).startMouseModeSync();
|
||||
}
|
||||
|
||||
if (dead.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user