mirror of
https://fastgit.cc/github.com/Yeachan-Heo/oh-my-claudecode
synced 2026-04-20 21:00:50 +08:00
493 lines
18 KiB
JavaScript
Generated
493 lines
18 KiB
JavaScript
Generated
/**
|
|
* OMC HUD - Agents Element
|
|
*
|
|
* Renders active agent count display with multiple format options:
|
|
* - count: agents:2
|
|
* - codes: agents:Oes (type-coded with model tier casing)
|
|
* - detailed: agents:[architect(2m),explore,exec]
|
|
*/
|
|
import { dim, RESET, getModelTierColor, getDurationColor } from '../colors.js';
|
|
import { truncateToWidth } from '../../utils/string-width.js';
|
|
const CYAN = '\x1b[36m';
|
|
// ============================================================================
|
|
// Agent Type Codes
|
|
// ============================================================================
|
|
/**
|
|
* Single-character codes for each agent type.
|
|
* Case indicates model tier: Uppercase = Opus, lowercase = Sonnet/Haiku
|
|
*/
|
|
const AGENT_TYPE_CODES = {
|
|
// ============================================================
|
|
// BUILD/ANALYSIS LANE
|
|
// ============================================================
|
|
// Explore - 'E' for Explore (haiku)
|
|
explore: 'e',
|
|
// Analyst - 'T' for aTalyst (A taken by Architect)
|
|
analyst: 'T', // opus
|
|
// Planner - 'P' for Planner
|
|
planner: 'P', // opus
|
|
// Architect - 'A' for Architect
|
|
architect: 'A', // opus
|
|
// Debugger - 'g' for debuGger (d taken by designer)
|
|
debugger: 'g', // sonnet
|
|
// Executor - 'X' for eXecutor
|
|
executor: 'x', // sonnet
|
|
// Deep Executor - 'X' (same family as executor, opus tier)
|
|
'deep-executor': 'X', // opus
|
|
// Verifier - 'V' for Verifier (but vision uses 'v'... use uppercase 'V' for governance role)
|
|
verifier: 'V', // sonnet
|
|
// ============================================================
|
|
// REVIEW LANE
|
|
// ============================================================
|
|
// Style Reviewer - 'Y' for stYle
|
|
'style-reviewer': 'y', // haiku
|
|
// Quality Reviewer - 'Qr' for Quality Reviewer (disambiguated from quality-strategist)
|
|
'quality-reviewer': 'Qr', // sonnet
|
|
// API Reviewer - 'I' for Interface/API
|
|
'api-reviewer': 'i', // sonnet
|
|
// Security Reviewer - 'K' for Security (S taken by Scientist)
|
|
'security-reviewer': 'K', // sonnet
|
|
// Performance Reviewer - 'O' for perfOrmance
|
|
'performance-reviewer': 'o', // sonnet
|
|
// Code Reviewer - 'R' for Review (uppercase, opus tier)
|
|
'code-reviewer': 'R', // opus
|
|
// ============================================================
|
|
// DOMAIN SPECIALISTS
|
|
// ============================================================
|
|
// Dependency Expert - 'L' for Library expert
|
|
'dependency-expert': 'l', // sonnet
|
|
// Test Engineer - 'T' (but analyst uses 'T'... use uppercase 'T')
|
|
'test-engineer': 't', // sonnet
|
|
// Quality Strategist - 'Qs' for Quality Strategist (disambiguated from quality-reviewer)
|
|
'quality-strategist': 'Qs', // sonnet
|
|
// Build Fixer - 'B' for Build
|
|
'build-fixer': 'b', // sonnet
|
|
// Designer - 'd' for Designer
|
|
designer: 'd', // sonnet
|
|
// Writer - 'W' for Writer
|
|
writer: 'w', // haiku
|
|
// QA Tester - 'Q' for QA
|
|
'qa-tester': 'q', // sonnet
|
|
// Scientist - 'S' for Scientist
|
|
scientist: 's', // sonnet
|
|
// Git Master - 'M' for Master
|
|
'git-master': 'm', // sonnet
|
|
// ============================================================
|
|
// PRODUCT LANE
|
|
// ============================================================
|
|
// Product Manager - 'Pm' for Product Manager (disambiguated from planner)
|
|
'product-manager': 'Pm', // sonnet
|
|
// UX Researcher - 'u' for Ux
|
|
'ux-researcher': 'u', // sonnet
|
|
// Information Architect - 'Ia' for Information Architect (disambiguated from api-reviewer)
|
|
'information-architect': 'Ia', // sonnet
|
|
// Product Analyst - 'a' for analyst
|
|
'product-analyst': 'a', // sonnet
|
|
// ============================================================
|
|
// COORDINATION
|
|
// ============================================================
|
|
// Critic - 'C' for Critic
|
|
critic: 'C', // opus
|
|
// Vision - 'V' for Vision (lowercase since sonnet)
|
|
vision: 'v', // sonnet
|
|
// Document Specialist - 'D' for Document
|
|
'document-specialist': 'D', // sonnet
|
|
// ============================================================
|
|
// BACKWARD COMPATIBILITY (Deprecated)
|
|
// ============================================================
|
|
// Researcher - 'r' for Researcher (deprecated, points to document-specialist)
|
|
researcher: 'r', // sonnet
|
|
};
|
|
/**
|
|
* Get single-character code for an agent type.
|
|
*/
|
|
function getAgentCode(agentType, model) {
|
|
// Extract the short name from full type (e.g., "oh-my-claudecode:architect" -> "architect")
|
|
const parts = agentType.split(':');
|
|
const shortName = parts[parts.length - 1] || agentType;
|
|
// Look up the code
|
|
let code = AGENT_TYPE_CODES[shortName];
|
|
if (!code) {
|
|
// Unknown agent - use first letter
|
|
code = shortName.charAt(0).toUpperCase();
|
|
}
|
|
// Determine case based on model tier
|
|
// For single-char codes, the whole code changes case
|
|
// For multi-char codes, only the first character indicates tier
|
|
if (model) {
|
|
const tier = model.toLowerCase();
|
|
if (code.length === 1) {
|
|
code = tier.includes('opus') ? code.toUpperCase() : code.toLowerCase();
|
|
}
|
|
else {
|
|
const first = tier.includes('opus') ? code[0].toUpperCase() : code[0].toLowerCase();
|
|
code = first + code.slice(1);
|
|
}
|
|
}
|
|
return code;
|
|
}
|
|
/**
|
|
* Format duration for display.
|
|
* <10s: no suffix, 10s-59s: (Xs), 1m-9m: (Xm), >=10m: !
|
|
*/
|
|
function formatDuration(durationMs) {
|
|
const seconds = Math.floor(durationMs / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (seconds < 10) {
|
|
return ''; // No suffix for very short durations
|
|
}
|
|
else if (seconds < 60) {
|
|
return `(${seconds}s)`;
|
|
}
|
|
else if (minutes < 10) {
|
|
return `(${minutes}m)`;
|
|
}
|
|
else {
|
|
return '!'; // Alert for very long durations
|
|
}
|
|
}
|
|
// ============================================================================
|
|
// Render Functions
|
|
// ============================================================================
|
|
/**
|
|
* Render active agent count.
|
|
* Returns null if no agents are running.
|
|
*
|
|
* Format: agents:2
|
|
*/
|
|
export function renderAgents(agents) {
|
|
const running = agents.filter((a) => a.status === 'running').length;
|
|
if (running === 0) {
|
|
return null;
|
|
}
|
|
return `agents:${CYAN}${running}${RESET}`;
|
|
}
|
|
/**
|
|
* Sort agents by start time (freshest first, oldest last)
|
|
*/
|
|
function sortByFreshest(agents) {
|
|
return [...agents].sort((a, b) => b.startTime.getTime() - a.startTime.getTime());
|
|
}
|
|
/**
|
|
* Render agents with single-character type codes.
|
|
* Uppercase = Opus tier, lowercase = Sonnet/Haiku.
|
|
* Color-coded by model tier.
|
|
*
|
|
* Format: agents:Oes
|
|
*/
|
|
export function renderAgentsCoded(agents) {
|
|
const running = sortByFreshest(agents.filter((a) => a.status === 'running'));
|
|
if (running.length === 0) {
|
|
return null;
|
|
}
|
|
// Build coded string with colors
|
|
const codes = running.map((a) => {
|
|
const code = getAgentCode(a.type, a.model);
|
|
const color = getModelTierColor(a.model);
|
|
return `${color}${code}${RESET}`;
|
|
});
|
|
return `agents:${codes.join('')}`;
|
|
}
|
|
/**
|
|
* Render agents with codes and duration indicators.
|
|
* Shows how long each agent has been running.
|
|
*
|
|
* Format: agents:O(2m)es
|
|
*/
|
|
export function renderAgentsCodedWithDuration(agents) {
|
|
const running = sortByFreshest(agents.filter((a) => a.status === 'running'));
|
|
if (running.length === 0) {
|
|
return null;
|
|
}
|
|
const now = Date.now();
|
|
// Build coded string with colors and durations
|
|
const codes = running.map((a) => {
|
|
const code = getAgentCode(a.type, a.model);
|
|
const durationMs = now - a.startTime.getTime();
|
|
const duration = formatDuration(durationMs);
|
|
// Color the code by model tier
|
|
const modelColor = getModelTierColor(a.model);
|
|
if (duration === '!') {
|
|
// Alert case - show exclamation in duration color
|
|
const durationColor = getDurationColor(durationMs);
|
|
return `${modelColor}${code}${durationColor}!${RESET}`;
|
|
}
|
|
else if (duration) {
|
|
// Normal duration - dim the time portion
|
|
return `${modelColor}${code}${dim(duration)}${RESET}`;
|
|
}
|
|
else {
|
|
// No duration suffix
|
|
return `${modelColor}${code}${RESET}`;
|
|
}
|
|
});
|
|
return `agents:${codes.join('')}`;
|
|
}
|
|
/**
|
|
* Render detailed agent list (for full mode).
|
|
*
|
|
* Format: agents:[architect(2m),explore,exec]
|
|
*/
|
|
export function renderAgentsDetailed(agents) {
|
|
const running = sortByFreshest(agents.filter((a) => a.status === 'running'));
|
|
if (running.length === 0) {
|
|
return null;
|
|
}
|
|
const now = Date.now();
|
|
// Extract short agent type names with duration
|
|
const names = running.map((a) => {
|
|
// Extract last part of agent type (e.g., "oh-my-claudecode:explore" -> "explore")
|
|
const parts = a.type.split(':');
|
|
let name = parts[parts.length - 1] || a.type;
|
|
// Abbreviate common names
|
|
if (name === 'executor')
|
|
name = 'exec';
|
|
if (name === 'deep-executor')
|
|
name = 'deep-x';
|
|
if (name === 'designer')
|
|
name = 'design';
|
|
if (name === 'qa-tester')
|
|
name = 'qa';
|
|
if (name === 'scientist')
|
|
name = 'sci';
|
|
if (name === 'security-reviewer')
|
|
name = 'sec';
|
|
if (name === 'build-fixer')
|
|
name = 'build';
|
|
if (name === 'code-reviewer')
|
|
name = 'review';
|
|
if (name === 'git-master')
|
|
name = 'git';
|
|
if (name === 'style-reviewer')
|
|
name = 'style';
|
|
if (name === 'quality-reviewer')
|
|
name = 'quality';
|
|
if (name === 'api-reviewer')
|
|
name = 'api-rev';
|
|
if (name === 'performance-reviewer')
|
|
name = 'perf';
|
|
if (name === 'dependency-expert')
|
|
name = 'dep-exp';
|
|
if (name === 'document-specialist')
|
|
name = 'doc-spec';
|
|
if (name === 'test-engineer')
|
|
name = 'test-eng';
|
|
if (name === 'quality-strategist')
|
|
name = 'qs';
|
|
if (name === 'debugger')
|
|
name = 'debug';
|
|
if (name === 'verifier')
|
|
name = 'verify';
|
|
if (name === 'product-manager')
|
|
name = 'pm';
|
|
if (name === 'ux-researcher')
|
|
name = 'uxr';
|
|
if (name === 'information-architect')
|
|
name = 'ia';
|
|
if (name === 'product-analyst')
|
|
name = 'pa';
|
|
// Add duration if significant
|
|
const durationMs = now - a.startTime.getTime();
|
|
const duration = formatDuration(durationMs);
|
|
return duration ? `${name}${duration}` : name;
|
|
});
|
|
return `agents:[${CYAN}${names.join(',')}${RESET}]`;
|
|
}
|
|
/**
|
|
* Truncate description to fit in statusline.
|
|
* CJK-aware: accounts for double-width characters.
|
|
*/
|
|
function truncateDescription(desc, maxWidth = 20) {
|
|
if (!desc)
|
|
return '...';
|
|
// Use CJK-aware truncation (maxWidth is visual columns, not character count)
|
|
return truncateToWidth(desc, maxWidth);
|
|
}
|
|
/**
|
|
* Get short agent type name.
|
|
*/
|
|
function getShortAgentName(agentType) {
|
|
const parts = agentType.split(':');
|
|
let name = parts[parts.length - 1] || agentType;
|
|
// Abbreviate common names
|
|
const abbrevs = {
|
|
// Build/Analysis Lane
|
|
'executor': 'exec',
|
|
'deep-executor': 'deep-x',
|
|
'debugger': 'debug',
|
|
'verifier': 'verify',
|
|
// Review Lane
|
|
'style-reviewer': 'style',
|
|
'quality-reviewer': 'quality',
|
|
'api-reviewer': 'api-rev',
|
|
'security-reviewer': 'sec',
|
|
'performance-reviewer': 'perf',
|
|
'code-reviewer': 'review',
|
|
// Domain Specialists
|
|
'dependency-expert': 'dep-exp',
|
|
'document-specialist': 'doc-spec',
|
|
'test-engineer': 'test-eng',
|
|
'quality-strategist': 'qs',
|
|
'build-fixer': 'build',
|
|
'designer': 'design',
|
|
'qa-tester': 'qa',
|
|
'scientist': 'sci',
|
|
'git-master': 'git',
|
|
// Product Lane
|
|
'product-manager': 'pm',
|
|
'ux-researcher': 'uxr',
|
|
'information-architect': 'ia',
|
|
'product-analyst': 'pa',
|
|
// Backward compat
|
|
'researcher': 'dep-exp',
|
|
};
|
|
return abbrevs[name] || name;
|
|
}
|
|
/**
|
|
* Render agents with descriptions - most informative format.
|
|
* Shows what each agent is actually doing.
|
|
*
|
|
* Format: O:analyzing code | e:searching files
|
|
*/
|
|
export function renderAgentsWithDescriptions(agents) {
|
|
const running = sortByFreshest(agents.filter((a) => a.status === 'running'));
|
|
if (running.length === 0) {
|
|
return null;
|
|
}
|
|
const now = Date.now();
|
|
// Build agent entries with descriptions
|
|
const entries = running.map((a) => {
|
|
const code = getAgentCode(a.type, a.model);
|
|
const color = getModelTierColor(a.model);
|
|
const desc = truncateDescription(a.description, 25);
|
|
const durationMs = now - a.startTime.getTime();
|
|
const duration = formatDuration(durationMs);
|
|
// Format: O:description or O:description(2m)
|
|
let entry = `${color}${code}${RESET}:${dim(desc)}`;
|
|
if (duration && duration !== '!') {
|
|
entry += dim(duration);
|
|
}
|
|
else if (duration === '!') {
|
|
const durationColor = getDurationColor(durationMs);
|
|
entry += `${durationColor}!${RESET}`;
|
|
}
|
|
return entry;
|
|
});
|
|
return entries.join(dim(' | '));
|
|
}
|
|
/**
|
|
* Render agents showing descriptions only (no codes).
|
|
* Maximum clarity about what's running.
|
|
*
|
|
* Format: [analyzing code, searching files]
|
|
*/
|
|
export function renderAgentsDescOnly(agents) {
|
|
const running = sortByFreshest(agents.filter((a) => a.status === 'running'));
|
|
if (running.length === 0) {
|
|
return null;
|
|
}
|
|
const now = Date.now();
|
|
// Build descriptions
|
|
const descriptions = running.map((a) => {
|
|
const color = getModelTierColor(a.model);
|
|
const shortName = getShortAgentName(a.type);
|
|
const desc = a.description ? truncateDescription(a.description, 20) : shortName;
|
|
const durationMs = now - a.startTime.getTime();
|
|
const duration = formatDuration(durationMs);
|
|
if (duration === '!') {
|
|
const durationColor = getDurationColor(durationMs);
|
|
return `${color}${desc}${durationColor}!${RESET}`;
|
|
}
|
|
else if (duration) {
|
|
return `${color}${desc}${dim(duration)}${RESET}`;
|
|
}
|
|
return `${color}${desc}${RESET}`;
|
|
});
|
|
return `[${descriptions.join(dim(', '))}]`;
|
|
}
|
|
/**
|
|
* Format duration with padding for alignment.
|
|
*/
|
|
function formatDurationPadded(durationMs) {
|
|
const seconds = Math.floor(durationMs / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (seconds < 10) {
|
|
return ' '; // No duration for very short
|
|
}
|
|
else if (seconds < 60) {
|
|
return `${seconds}s`.padStart(4);
|
|
}
|
|
else if (minutes < 10) {
|
|
return `${minutes}m`.padStart(4);
|
|
}
|
|
else {
|
|
return `${minutes}m`.padStart(4);
|
|
}
|
|
}
|
|
/**
|
|
* Render agents as multi-line display for maximum clarity.
|
|
* Returns header addition + multiple detail lines.
|
|
*
|
|
* Format:
|
|
* ├─ O architect 2m analyzing architecture patterns...
|
|
* ├─ e explore 45s searching for test files
|
|
* └─ x exec 1m implementing validation logic
|
|
*/
|
|
export function renderAgentsMultiLine(agents, maxLines = 5) {
|
|
const running = sortByFreshest(agents.filter((a) => a.status === 'running'));
|
|
if (running.length === 0) {
|
|
return { headerPart: null, detailLines: [] };
|
|
}
|
|
// Header part shows count for awareness
|
|
const headerPart = `agents:${CYAN}${running.length}${RESET}`;
|
|
// Build detail lines
|
|
const now = Date.now();
|
|
const detailLines = [];
|
|
const displayCount = Math.min(running.length, maxLines);
|
|
running.slice(0, maxLines).forEach((a, index) => {
|
|
const isLast = index === displayCount - 1 && running.length <= maxLines;
|
|
const prefix = isLast ? '└─' : '├─';
|
|
const code = getAgentCode(a.type, a.model);
|
|
const color = getModelTierColor(a.model);
|
|
const shortName = getShortAgentName(a.type).padEnd(12);
|
|
const durationMs = now - a.startTime.getTime();
|
|
const duration = formatDurationPadded(durationMs);
|
|
const durationColor = getDurationColor(durationMs);
|
|
const desc = a.description || '...';
|
|
// Use CJK-aware truncation (45 visual columns)
|
|
const truncatedDesc = truncateToWidth(desc, 45);
|
|
detailLines.push(`${dim(prefix)} ${color}${code}${RESET} ${dim(shortName)}${durationColor}${duration}${RESET} ${truncatedDesc}`);
|
|
});
|
|
// Add overflow indicator if needed
|
|
if (running.length > maxLines) {
|
|
const remaining = running.length - maxLines;
|
|
detailLines.push(`${dim(`└─ +${remaining} more agents...`)}`);
|
|
}
|
|
return { headerPart, detailLines };
|
|
}
|
|
/**
|
|
* Render agents based on format configuration.
|
|
*/
|
|
export function renderAgentsByFormat(agents, format) {
|
|
switch (format) {
|
|
case 'count':
|
|
return renderAgents(agents);
|
|
case 'codes':
|
|
return renderAgentsCoded(agents);
|
|
case 'codes-duration':
|
|
return renderAgentsCodedWithDuration(agents);
|
|
case 'detailed':
|
|
return renderAgentsDetailed(agents);
|
|
case 'descriptions':
|
|
return renderAgentsWithDescriptions(agents);
|
|
case 'tasks':
|
|
return renderAgentsDescOnly(agents);
|
|
case 'multiline':
|
|
// For backward compatibility, return just the header part
|
|
// The render.ts will handle the full multi-line output
|
|
return renderAgentsMultiLine(agents).headerPart;
|
|
default:
|
|
return renderAgentsCoded(agents);
|
|
}
|
|
}
|
|
//# sourceMappingURL=agents.js.map
|