Files
MATTERMOST_OPENCLAW_LIVESTATUS/src/status-formatter.js
sol b255283724 Wrap status output in code block for visual distinction
Status posts now render inside triple-backtick code blocks
so they look different from normal chat replies.
2026-03-07 19:13:40 +00:00

148 lines
4.3 KiB
JavaScript

'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<string>} sessionState.lines - Status lines (most recent activity)
* @param {Array<object>} [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} ${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 code block at top level so it looks distinct from normal messages
var body = lines.join('\n');
if (depth === 0) {
body = '```\n' + 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]';
}
}
/**
* 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 };