diff --git a/CLAUDE.md b/CLAUDE.md index 04c3e8aa..ac7de25e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs. -**Version**: 0.1355 +**Version**: 0.1356 **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty diff --git a/package.json b/package.json index be8ccccc..861b1c9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1355", + "version": "0.1356", "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", diff --git a/src/subagent-watcher.ts b/src/subagent-watcher.ts index fe4ba8b1..ea6f4d69 100644 --- a/src/subagent-watcher.ts +++ b/src/subagent-watcher.ts @@ -404,6 +404,64 @@ export class SubagentWatcher extends EventEmitter { return workingDir.replace(/\//g, '-'); } + /** + * Extract a smart, concise title from a task prompt + * Aims for ~40-50 chars that convey what the agent is doing + */ + private extractSmartTitle(text: string): string { + const MAX_LEN = 45; + + // Get first line/sentence + let title = text.split('\n')[0].trim(); + + // If already short enough, use it + if (title.length <= MAX_LEN) { + return title.replace(/[.!?,\s]+$/, ''); + } + + // Remove common filler phrases to condense + const fillers = [ + /^(please |i need you to |i want you to |can you |could you )/i, + / (the|a|an) /gi, + / (in|at|on|to|for|of|with|from|by) the /gi, + / (including|related to|regarding|about) /gi, + /[""]/g, + / +/g, // multiple spaces to single + ]; + + let condensed = title; + for (const filler of fillers) { + condensed = condensed.replace(filler, (match) => { + // Keep single space for word boundaries + if (match.trim() === '') return ' '; + if (/^(the|a|an)$/i.test(match.trim())) return ' '; + if (/including|related to|regarding|about/i.test(match)) return ': '; + return ' '; + }); + } + condensed = condensed.replace(/ +/g, ' ').trim(); + + // If condensed version is short enough, use it + if (condensed.length <= MAX_LEN) { + return condensed.replace(/[.!?,:\s]+$/, ''); + } + + // Try to cut at a natural boundary (colon, dash, comma) + const boundaryMatch = condensed.substring(0, MAX_LEN + 5).match(/^(.{20,}?)[:\-,]/); + if (boundaryMatch && boundaryMatch[1].length <= MAX_LEN) { + return boundaryMatch[1].trim(); + } + + // Last resort: truncate at word boundary + const truncated = condensed.substring(0, MAX_LEN); + const lastSpace = truncated.lastIndexOf(' '); + if (lastSpace > 20) { + return truncated.substring(0, lastSpace).replace(/[.!?,:\s]+$/, ''); + } + + return truncated.replace(/[.!?,:\s]+$/, ''); + } + /** * Scan for all subagent directories */ @@ -518,24 +576,7 @@ export class SubagentWatcher extends EventEmitter { } if (text) { - // Extract a useful title: first line, first sentence, or first 80 chars - // Split on newline first - const firstLine = text.split('\n')[0].trim(); - // If still long, try to find a sentence boundary - let title = firstLine; - if (title.length > 80) { - // Look for sentence end (. ! ?) within first 100 chars - const sentenceEnd = title.substring(0, 100).search(/[.!?]/); - if (sentenceEnd > 10) { - title = title.substring(0, sentenceEnd + 1); - } else { - // Truncate at word boundary - const truncated = title.substring(0, 80); - const lastSpace = truncated.lastIndexOf(' '); - title = lastSpace > 40 ? truncated.substring(0, lastSpace) + '...' : truncated + '...'; - } - } - description = title; + description = this.extractSmartTitle(text); break; } }