'use strict'; /** * status-formatter.js — Pure function: SessionState -> formatted Mattermost text. * * Output format: * [ACTIVE] main | 38s * Reading live-status source code... * exec: ls /agents/sessions [OK] * Analyzing agent configurations... * Sub-agent: proj035-planner * Reading protocol... * [DONE] 28s * [DONE] 53s | 12.4k tokens */ const MAX_STATUS_LINES = parseInt(process.env.MAX_STATUS_LINES, 10) || 20; const MAX_LINE_CHARS = 120; /** * Format a SessionState into a Mattermost text string. * * @param {object} sessionState * @param {string} sessionState.sessionKey * @param {string} sessionState.status - 'active' | 'done' | 'error' | 'interrupted' * @param {number} sessionState.startTime - ms since epoch * @param {Array} sessionState.lines - Status lines (most recent activity) * @param {Array} [sessionState.children] - Child session states * @param {number} [sessionState.tokenCount] - Token count if available * @param {string} [sessionState.agentId] - Agent ID (e.g. "main") * @param {number} [sessionState.depth] - Nesting depth (0 = top-level) * @returns {string} */ function format(sessionState, opts = {}) { const maxLines = opts.maxLines || MAX_STATUS_LINES; const depth = sessionState.depth || 0; const indent = ' '.repeat(depth); const lines = []; // Header line const elapsed = formatElapsed(Date.now() - sessionState.startTime); const agentId = sessionState.agentId || extractAgentId(sessionState.sessionKey); const statusPrefix = statusIcon(sessionState.status); lines.push(`${indent}**${statusPrefix}** \`${agentId}\` | ${elapsed}`); // Status lines (trimmed to maxLines, most recent) const statusLines = (sessionState.lines || []).slice(-maxLines); for (const line of statusLines) { lines.push(`${indent}${formatStatusLine(truncateLine(line))}`); } // Child sessions (sub-agents) if (sessionState.children && sessionState.children.length > 0) { for (const child of sessionState.children) { const childLines = format(child, { maxLines: Math.floor(maxLines / 2), ...opts }).split('\n'); lines.push(...childLines); } } // Footer line (only for done/error/interrupted) if (sessionState.status !== 'active') { const tokenStr = sessionState.tokenCount ? ` | ${formatTokens(sessionState.tokenCount)} tokens` : ''; lines.push(`${indent}**[${sessionState.status.toUpperCase()}]** ${elapsed}${tokenStr}`); } // Wrap in blockquote at top level — visually distinct (left border), // never collapses like code blocks do, supports inline markdown var body = lines.join('\n'); if (depth === 0) { body = body .split('\n') .map(function (l) { return '> ' + l; }) .join('\n'); // Append invisible session marker for restart recovery (search by marker) body += '\n'; } return body; } /** * Format elapsed milliseconds as human-readable string. * @param {number} ms * @returns {string} */ function formatElapsed(ms) { if (ms < 0) ms = 0; const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); const h = Math.floor(m / 60); if (h > 0) return `${h}h${m % 60}m`; if (m > 0) return `${m}m${s % 60}s`; return `${s}s`; } /** * Format token count as compact string (e.g. 12400 -> "12.4k"). * @param {number} count * @returns {string} */ function formatTokens(count) { if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; if (count >= 1000) return `${(count / 1000).toFixed(1)}k`; return String(count); } /** * Get status icon prefix. * @param {string} status * @returns {string} */ function statusIcon(status) { switch (status) { case 'active': return '[ACTIVE]'; case 'done': return '[DONE]'; case 'error': return '[ERROR]'; case 'interrupted': return '[INTERRUPTED]'; default: return '[UNKNOWN]'; } } /** * Format a status line with inline markdown. * Tool calls get inline code formatting; thinking text stays plain. * @param {string} line * @returns {string} */ function formatStatusLine(line) { // Tool call lines: "toolName: arguments [OK]" or "toolName: label" var match = line.match(/^(\S+?): (.+?)( \[OK\]| \[ERR\])?$/); if (match) { var marker = match[3] || ''; return '`' + match[1] + ':` ' + match[2] + marker; } // Thinking text — use a unicode marker to distinguish from tool calls // Avoid markdown italic (*) since it breaks with special characters return '\u2502 ' + line; } /** * Truncate a line to MAX_LINE_CHARS. * @param {string} line * @returns {string} */ function truncateLine(line) { if (line.length <= MAX_LINE_CHARS) return line; return line.slice(0, MAX_LINE_CHARS - 3) + '...'; } /** * Extract agent ID from session key. * Session key format: "agent:main:mattermost:channel:abc123:thread:xyz" * @param {string} sessionKey * @returns {string} */ function extractAgentId(sessionKey) { if (!sessionKey) return 'unknown'; const parts = sessionKey.split(':'); // "agent:main:..." -> "main" if (parts[0] === 'agent' && parts[1]) return parts[1]; return sessionKey.split(':')[0] || 'unknown'; } module.exports = { format, formatElapsed, formatTokens, statusIcon, truncateLine, extractAgentId };